Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add @encrypted enhancer #1922

Merged
merged 20 commits into from
Dec 30, 2024
Merged

Conversation

genu
Copy link
Contributor

@genu genu commented Dec 20, 2024

Resolves #1643

Copy link
Contributor

coderabbitai bot commented Dec 20, 2024

📝 Walkthrough

Walkthrough

This pull request introduces a new @encrypted attribute for ZenStack, enabling field-level encryption in Prisma models. The implementation spans multiple files across the runtime and schema packages, adding support for both simple and custom encryption methods. The feature allows developers to mark sensitive string fields for automatic encryption when storing in the database and decryption when retrieving, utilizing the Web Crypto API for secure data handling.

Changes

File Change Summary
packages/runtime/src/enhancements/edge/encrypted.ts New file implementing edge runtime encryption handler
packages/runtime/src/enhancements/node/encrypted.ts New file implementing node runtime encryption handler
packages/runtime/src/enhancements/node/create-enhancement.ts Added 'encrypted' enhancement support, updated enhancement kinds
packages/runtime/src/types.ts Added encryption-related type definitions, extended enhancement kinds
packages/schema/src/res/stdlib.zmodel Introduced @encrypted() attribute for string fields
tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts Added integration tests for encryption functionality

Assessment against linked issues

Objective Addressed Explanation
Add @encrypted enhancer
Support field-level encryption
Work with ZenStack generator
Encrypt fields before database storage
Decrypt fields when reading

Possibly related PRs

Tip

CodeRabbit's docstrings feature is now available as part of our Early Access Program! Simply use the command @coderabbitai generate docstrings to have CodeRabbit automatically generate docstrings for your pull request. We would love to hear your feedback on Discord.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

‼️ IMPORTANT
Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.

  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@ymc9 ymc9 changed the base branch from main to dev December 23, 2024 15:02
Copy link
Member

@ymc9 ymc9 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @genu , thanks a lot for contributing this feature! And I appreciate that you incorporated our latest discussions into it 😄.

Overall the changes look great to me. I've left a few comments. Please take a look. Thanks!

packages/runtime/src/enhancements/edge/encrypted.ts Outdated Show resolved Hide resolved
packages/runtime/src/enhancements/edge/encrypted.ts Outdated Show resolved Hide resolved
packages/runtime/src/types.ts Outdated Show resolved Hide resolved
packages/runtime/src/enhancements/node/encrypted.ts Outdated Show resolved Hide resolved
@ymc9 ymc9 marked this pull request as ready for review December 24, 2024 05:33
@ymc9 ymc9 changed the title (WIP) feat: Add @encrypted enhancer feat: Add @encrypted enhancer Dec 24, 2024
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
packages/runtime/src/enhancements/node/encrypted.ts (2)

57-80: Consider handling encryption errors more gracefully.
If an error occurs in the “await crypto.subtle.encrypt(...)” call (e.g., invalid key length), the caller receives a raw error. Depending on your architecture, you might consider catching and throwing a more descriptive error.


107-107: Use optional chaining for readability.
Static analysis suggests changing “if (args && args.data && actionsOfInterest.includes(action))” to optional chaining:

- if (args && args.data && actionsOfInterest.includes(action)) {
+ if (args?.data && actionsOfInterest.includes(action)) {
🧰 Tools
🪛 Biome (1.9.4)

[error] 107-107: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

packages/runtime/src/enhancements/edge/encrypted.ts (2)

17-32: Identical logic to node environment is acceptable.
If the edge environment is truly the same, you might symlink as suggested before. Otherwise, duplicating logic is fine if separate platform-specific changes are expected.


107-107: Consider optional chaining for args.
Similar to the node implementation, adopting optional chaining is a minor readability improvement.

- if (args && args.data && actionsOfInterest.includes(action)) {
+ if (args?.data && actionsOfInterest.includes(action)) {
🧰 Tools
🪛 Biome (1.9.4)

[error] 107-107: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

packages/runtime/src/types.ts (1)

181-181: Brief note: immutable encryption key.
When using a simple string-based key, confirm that future expansions won’t require key rotation logic or length checks.

packages/schema/src/res/stdlib.zmodel (1)

556-562: Allow configuring encryption secrets.

Currently, the new @Encrypted() attribute does not provide a mechanism for specifying an encryption secret (or mentioning how it is fetched). Storing secrets directly in code or schema is generally discouraged. Consider adding a parameter (e.g., secret: String?) to allow retrieving the key from an environment variable using env("KEY"), or any other configuration system, to enable safer key management.

Would you like me to propose a code snippet that extends @Encrypted() to accept a secret parameter?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b41fd93 and 83c242c.

📒 Files selected for processing (6)
  • packages/runtime/src/enhancements/edge/encrypted.ts (1 hunks)
  • packages/runtime/src/enhancements/node/create-enhancement.ts (3 hunks)
  • packages/runtime/src/enhancements/node/encrypted.ts (1 hunks)
  • packages/runtime/src/types.ts (4 hunks)
  • packages/schema/src/res/stdlib.zmodel (1 hunks)
  • tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts (1 hunks)
🧰 Additional context used
🪛 Gitleaks (8.21.2)
tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts

26-26: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 Biome (1.9.4)
packages/runtime/src/enhancements/edge/encrypted.ts

[error] 107-107: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

packages/runtime/src/enhancements/node/encrypted.ts

[error] 107-107: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (15)
packages/runtime/src/enhancements/node/encrypted.ts (4)

17-32: Looks good: Properly wires encryption enhancement into the Prisma proxy.
No issues found in this block.


53-55: Explicit check for custom encryption.
This type guard cleanly distinguishes between custom and simple encryption. Nicely done.


81-102: Consider adding a fallback strategy if decryption fails.
Should decryption fail (e.g., corrupted or invalid cipher), returning the original cipher text might simplify adoption, matching one of your colleague’s earlier comments.


145-145: Add a nullish check to data before encrypting.
If “data” is null or undefined, encryption calls will fail. A quick check prevents runtime errors, echoing a past suggestion.

packages/runtime/src/enhancements/edge/encrypted.ts (2)

53-55: Good check for custom vs. simple encryption.
This type guard remains consistent with the node version, ensuring a parallel architecture.


81-102: Offer a fallback for decryption step.
When an invalid cipher string is encountered, consider reusing logic from the node version about fallback to the original string.

tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts (2)

47-50: Great test verifying the encryption boundary.
Verifies that the stored value is encrypted for unauthorized read attempts yet decrypted for legitimate lookups.


98-101: Custom encryption logic test is well-structured.
This scenario effectively demonstrates custom encryption features. Nicely done.

packages/runtime/src/types.ts (3)

138-141: Encryption property inclusion is coherent.
The optional encryption field has a clear definition, making it straightforward to integrate.


154-154: Including 'encrypted' in EnhancementKind is consistent.
This ensures the new enhancement is recognized when building enhancements for the Prisma client.


176-180: Validate if a Uint8Array-based key is more suitable.
A prior comment suggested using a typed array instead of string. If you need stronger key validation, consider adopting that approach.

packages/runtime/src/enhancements/node/create-enhancement.ts (4)

17-17: Import for withEncrypted is correct.
Cleanly pulls in the new encryption functionality.


24-24: Adding 'encrypted' to ALL_ENHANCEMENTS is correct.
This ensures the new enhancement is part of the default pipeline.


104-104: Efficient detection of @Encrypted usage.
Simple check for fields with the “@Encrypted” attribute. Nothing to add.


132-139: Mandatory encryption options for @Encrypted.
Throwing an error if missing is appropriate, preventing incomplete configurations.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🔭 Outside diff range comments (1)
packages/runtime/src/enhancements/edge/encrypted.ts (1)

Line range hint 27-34: Consider enhancing crypto security measures

Several security improvements could be made:

  1. Use a proper key derivation function (KDF) instead of truncating the key
  2. Add error handling for invalid keys
  3. Consider using PBKDF2 or similar for key derivation

Example implementation:

const getKey = async (secret: string): Promise<CryptoKey> => {
    // Use PBKDF2 for key derivation
    const salt = new Uint8Array(16);
    const keyMaterial = await crypto.subtle.importKey(
        'raw',
        encoder.encode(secret),
        'PBKDF2',
        false,
        ['deriveBits', 'deriveKey']
    );
    return crypto.subtle.deriveKey(
        {
            name: 'PBKDF2',
            salt,
            iterations: 100000,
            hash: 'SHA-256'
        },
        keyMaterial,
        { name: 'AES-GCM', length: 256 },
        false,
        ['encrypt', 'decrypt']
    );
};
🧹 Nitpick comments (3)
packages/runtime/src/enhancements/edge/encrypted.ts (3)

Line range hint 15-25: Enhance function documentation

The @private tag suggests internal use, but the documentation could be more comprehensive:

  • Add @param descriptions
  • Add @returns description
  • Document encryption options and requirements
  • Add usage examples

Line range hint 36-48: Add validation for encryption options

The constructor should validate encryption options to fail fast if they're invalid.

constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) {
    super(prisma, model, options);
    
    if (!options.encryption) {
        throw new Error('Encryption options are required for @encrypted enhancement');
    }
    
    if (!this.isCustomEncryption(options.encryption) && !options.encryption.encryptionKey) {
        throw new Error('Encryption key is required for simple encryption');
    }
    
    this.queryUtils = new QueryUtils(prisma, options);
}

Line range hint 127-146: Consider performance optimization for batch operations

The doPostProcess method processes each field individually, which could be inefficient for batch operations. Consider:

  1. Batch decryption for multiple fields
  2. Caching the CryptoKey to avoid repeated key derivation

Example optimization:

private cryptoKeyCache: Map<string, Promise<CryptoKey>> = new Map();

private getCachedKey(secret: string): Promise<CryptoKey> {
    let key = this.cryptoKeyCache.get(secret);
    if (!key) {
        key = getKey(secret);
        this.cryptoKeyCache.set(secret, key);
    }
    return key;
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 83c242c and d9b95ef.

📒 Files selected for processing (1)
  • packages/runtime/src/enhancements/edge/encrypted.ts (1 hunks)
🔇 Additional comments (2)
packages/runtime/src/enhancements/edge/encrypted.ts (2)

1-1: Consider using a symlink instead of duplicating the file

Since this file is identical to ../node/encrypted.ts, consider using a symlink to avoid code duplication and maintain consistency.


Line range hint 49-124: Verify security implementation across the codebase

Let's verify the security implementation and usage patterns.

✅ Verification successful

Let me gather more information about the encryption implementation and tests.


Security implementation verified with some recommendations

The encryption implementation shows good security practices with:

  • Proper use of AES-GCM with secure IV generation
  • No encryption key logging found in the codebase
  • Support for custom encryption providers
  • Comprehensive test coverage for both simple and custom encryption

However, there are a few points that need attention:

  • The test file contains a hardcoded encryption key 'c558Gq0YQK2QcqtkMF9BGXHCQn4dMF8w'. While this is acceptable for tests, ensure this is not used in production.
  • The key derivation function getKey() uses a 32-byte slice of the input secret. Consider adding input validation to ensure the provided secret has sufficient entropy.
  • Consider adding a mechanism to rotate encryption keys for long-term data security.
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for potential security issues in the implementation

# Look for other uses of crypto APIs to ensure consistent implementation
echo "Checking for crypto API usage patterns..."
rg -A 2 "crypto\.subtle\." --type ts

# Check for potential key exposure in logs
echo "Checking for potential key logging..."
rg "console\.(log|debug|info)" -A 2 -B 2 --type ts | rg -i "key|encrypt"

# Look for test files to ensure proper security testing
echo "Checking for encryption-related tests..."
fd -e test.ts -e spec.ts | rg -i "encrypt"

Length of output: 4470


Script:

#!/bin/bash
# Check encryption implementation details and tests

# Look for encryption-related test content
echo "Checking encryption test implementation..."
cat tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts

# Check for key generation and management
echo "Checking key management implementation..."
rg -A 5 "getKey" --type ts

# Look for encryption-related type definitions
echo "Checking encryption types..."
ast-grep --pattern 'interface $_ {
  $$$
  encrypt
  $$$
}'

Length of output: 6020

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts (1)

26-29: ⚠️ Potential issue

Move encryption key to environment variable

Even in test files, encryption keys should not be hardcoded in the source code. Consider moving this to an environment variable or a test configuration file.

-            encryption: { encryptionKey: 'c558Gq0YQK2QcqtkMF9BGXHCQn4dMF8w' },
+            encryption: { encryptionKey: process.env.TEST_ENCRYPTION_KEY },
🧰 Tools
🪛 Gitleaks (8.21.2)

28-28: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

packages/runtime/src/enhancements/node/create-enhancement.ts (1)

132-139: 🛠️ Refactor suggestion

Validate encryption options structure

The encryption options validation should be more thorough:

  • Validate the structure of both SimpleEncryption and CustomEncryption
  • Check key format and length for SimpleEncryption
  • Verify custom encryption methods are properly implemented
packages/runtime/src/enhancements/node/encrypted.ts (2)

87-108: 🛠️ Refactor suggestion

Improve error handling in decrypt method.

As suggested in the previous review, consider gracefully handling decryption failures by returning the original ciphertext. This would enable gradual adoption of the @encrypted attribute.

     private async decrypt(field: FieldInfo, data: string): Promise<string> {
         if (this.isCustomEncryption(this.options.encryption!)) {
             return this.options.encryption.decrypt(this.model, field, data);
         }
 
+        try {
             const key = await this.getKey(this.options.encryption!.encryptionKey);
 
             // Convert base64 back to bytes
             const bytes = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
 
             // First 12 bytes are IV, rest is encrypted data
             const decrypted = await crypto.subtle.decrypt(
                 {
                     name: 'AES-GCM',
                     iv: bytes.slice(0, 12),
                 },
                 key,
                 bytes.slice(12)
             );
 
             return this.decoder.decode(decrypted);
+        } catch (error) {
+            console.warn(`Failed to decrypt field ${field.name} in model ${this.model}:`, error);
+            return data; // Return original ciphertext on failure
+        }
     }

132-147: 🛠️ Refactor suggestion

Add nullish checks and improve type safety.

As suggested in the previous review, add nullish checks for data. Also, consider improving type safety with type guards.

     private async doPostProcess(entityData: any, model: string) {
+        if (!entityData) return;
+
         const realModel = this.queryUtils.getDelegateConcreteModel(model, entityData);
 
         for (const field of getModelFields(entityData)) {
             const fieldInfo = await resolveField(this.options.modelMeta, realModel, field);
 
-            if (!fieldInfo) {
+            if (!fieldInfo || !entityData[field]) {
                 continue;
             }
 
             const shouldDecrypt = fieldInfo.attributes?.find((attr) => attr.name === '@encrypted');
             if (shouldDecrypt) {
                 entityData[field] = await this.decrypt(fieldInfo, entityData[field]);
             }
         }
     }
🧹 Nitpick comments (9)
tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts (2)

16-53: Enhance test coverage with edge cases

The simple encryption test should include additional scenarios:

  • Validation of encryption key format/length
  • Empty string encryption
  • Special characters in the encrypted value
  • Very long string encryption
🧰 Tools
🪛 Gitleaks (8.21.2)

28-28: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


68-79: Improve custom encryption example

The current example using string concatenation might mislead developers about security requirements. Consider using a more realistic encryption example that demonstrates:

  • Proper cryptographic methods
  • Error handling for invalid inputs
  • Secure key management practices
packages/runtime/src/types.ts (2)

137-141: Enhance encryption options documentation

The encryption options documentation should include:

  • Security requirements and recommendations
  • Key format specifications
  • Usage examples
  • Best practices for key management

176-179: Add error handling types for encryption operations

The CustomEncryption type should include error handling specifications:

  • Define expected error types for encryption/decryption failures
  • Document error scenarios (invalid input, key issues, etc.)
packages/runtime/src/enhancements/node/encrypted.ts (5)

22-32: Consider improving type safety.

The function implementation looks good, but consider these type-safety improvements:

  • Add explicit return type annotation
  • Consider constraining DbClient to a more specific type than object
-export function withEncrypted<DbClient extends object = any>(
+export function withEncrypted<DbClient extends DbClientContract>(
     prisma: DbClient,
     options: InternalEnhancementOptions
-): DbClient {
+): DbClient & { $encrypted: true } {

39-53: Enhance encryption options validation.

While the validation logic is thorough, consider these improvements:

  1. More descriptive error messages including expected formats
  2. Validation of encryption key format/encoding
  3. Consider using a validation helper function for better organization
     constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) {
         super(prisma, model, options);
 
         this.queryUtils = new QueryUtils(prisma, options);
 
-        if (!options.encryption) throw new Error('Encryption options must be provided');
+        if (!options.encryption) {
+            throw new Error('Encryption options must be provided for @encrypted enhancement');
+        }
 
         if (this.isCustomEncryption(options.encryption!)) {
             if (!options.encryption.encrypt || !options.encryption.decrypt)
-                throw new Error('Custom encryption must provide encrypt and decrypt functions');
+                throw new Error('Custom encryption requires both encrypt() and decrypt() functions');
         } else {
-            if (!options.encryption.encryptionKey) throw new Error('Encryption key must be provided');
-            if (options.encryption.encryptionKey.length !== 32) throw new Error('Encryption key must be 32 bytes');
+            if (!options.encryption.encryptionKey) {
+                throw new Error('Simple encryption requires an encryption key');
+            }
+            if (!(options.encryption.encryptionKey instanceof Uint8Array)) {
+                throw new Error('Encryption key must be a Uint8Array');
+            }
+            if (options.encryption.encryptionKey.length !== 32) {
+                throw new Error('Encryption key must be exactly 32 bytes (256 bits)');
+            }
         }

111-117: Use optional chaining and improve type safety.

As suggested by static analysis, use optional chaining for better safety.

     protected async preprocessArgs(action: PrismaProxyActions, args: any) {
         const actionsOfInterest: PrismaProxyActions[] = ['create', 'createMany', 'update', 'updateMany', 'upsert'];
-        if (args && args.data && actionsOfInterest.includes(action)) {
+        if (args?.data && actionsOfInterest.includes(action)) {
             await this.preprocessWritePayload(this.model, action as PrismaWriteActionType, args);
         }
         return args;
     }
🧰 Tools
🪛 Biome (1.9.4)

[error] 113-113: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


149-160: Consider adding input validation for write payload processing.

The preprocessWritePayload method should validate its inputs before processing.

     private async preprocessWritePayload(model: string, action: PrismaWriteActionType, args: any) {
+        if (!model || !action || !args) {
+            throw new Error('Invalid arguments for preprocessWritePayload');
+        }
+
         const visitor = new NestedWriteVisitor(this.options.modelMeta, {
             field: async (field, _action, data, context) => {
                 const encAttr = field.attributes?.find((attr) => attr.name === '@encrypted');
                 if (encAttr && field.type === 'String') {
+                    if (data === null || data === undefined) return;
                     context.parent[field.name] = await this.encrypt(field, data);
                 }
             },
         });
 
         await visitor.visit(model, action, args);
     }

1-161: Consider additional security measures.

While the current implementation is secure, consider these security improvements:

  1. Add key rotation support by including a version tag in the encrypted data
  2. Add integrity checks for custom encryption implementations
  3. Consider adding rate limiting for crypto operations

Would you like me to provide a detailed implementation for any of these security improvements?

🧰 Tools
🪛 Biome (1.9.4)

[error] 113-113: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d9b95ef and 29b7d15.

📒 Files selected for processing (4)
  • packages/runtime/src/enhancements/node/create-enhancement.ts (3 hunks)
  • packages/runtime/src/enhancements/node/encrypted.ts (1 hunks)
  • packages/runtime/src/types.ts (4 hunks)
  • tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
packages/runtime/src/enhancements/node/encrypted.ts

[error] 113-113: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🪛 Gitleaks (8.21.2)
tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts

28-28: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (1)
packages/runtime/src/enhancements/node/create-enhancement.ts (1)

125-140: LGTM! Enhancement order is correct

The encrypted enhancement is correctly placed before the policy enhancement to maintain field length consistency, similar to the password enhancement.

packages/runtime/src/types.ts Show resolved Hide resolved
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
packages/runtime/src/enhancements/node/encrypted.ts (3)

17-32: Enhance documentation for better maintainability.

While the function is well-implemented, the documentation could be more comprehensive. Consider adding:

  • @param descriptions for prisma and options
  • @returns description
  • Example usage
  • Why it's marked as @private
 /**
  * Gets an enhanced Prisma client that supports `@encrypted` attribute.
+ * 
+ * @param prisma - The Prisma client instance to enhance
+ * @param options - Enhancement options including encryption configuration
+ * @returns Enhanced Prisma client with encryption support
+ * @example
+ * ```ts
+ * const enhanced = withEncrypted(prisma, {
+ *   encryption: {
+ *     encryptionKey: new Uint8Array(32)
+ *   }
+ * });
+ * ```
  *
  * @private
  */

44-52: Enhance encryption key validation and error messages.

The validation could be more robust and provide more helpful error messages:

  1. Error messages should guide users on how to fix the issue
  2. For simple encryption, consider validating that the key isn't all zeros
-            if (!options.encryption.encryptionKey) throw new Error('Encryption key must be provided');
-            if (options.encryption.encryptionKey.length !== 32) throw new Error('Encryption key must be 32 bytes');
+            if (!options.encryption.encryptionKey) {
+                throw new Error('Encryption key must be provided. Generate a secure 32-byte key using crypto.getRandomValues().');
+            }
+            if (options.encryption.encryptionKey.length !== 32) {
+                throw new Error('Encryption key must be exactly 32 bytes. Current length: ' + 
+                    options.encryption.encryptionKey.length);
+            }
+            // Validate key isn't all zeros
+            if (options.encryption.encryptionKey.every(byte => byte === 0)) {
+                throw new Error('Encryption key must not be all zeros. Generate a secure key using crypto.getRandomValues().');
+            }

111-117: Use optional chaining as suggested by static analysis.

The static analysis tool correctly identifies an opportunity to use optional chaining.

 protected async preprocessArgs(action: PrismaProxyActions, args: any) {
     const actionsOfInterest: PrismaProxyActions[] = ['create', 'createMany', 'update', 'updateMany', 'upsert'];
-    if (args && args.data && actionsOfInterest.includes(action)) {
+    if (args?.data && actionsOfInterest.includes(action)) {
         await this.preprocessWritePayload(this.model, action as PrismaWriteActionType, args);
     }
     return args;
 }
🧰 Tools
🪛 Biome (1.9.4)

[error] 113-113: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 29b7d15 and acb2ee2.

📒 Files selected for processing (1)
  • packages/runtime/src/enhancements/node/encrypted.ts (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
packages/runtime/src/enhancements/node/encrypted.ts

[error] 113-113: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

packages/runtime/src/enhancements/node/encrypted.ts Outdated Show resolved Hide resolved
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
packages/runtime/src/enhancements/node/encrypted.ts (1)

87-108: ⚠️ Potential issue

Improve error handling in decrypt method.

As discussed in previous reviews, consider handling decryption failures gracefully to allow gradual adoption of encryption.

 private async decrypt(field: FieldInfo, data: string): Promise<string> {
     if (this.isCustomEncryption(this.options.encryption!)) {
-        return this.options.encryption.decrypt(this.model, field, data);
+        try {
+            return await this.options.encryption.decrypt(this.model, field, data);
+        } catch (error) {
+            console.warn(`Failed to decrypt field ${field.name} in model ${this.model}:`, error);
+            return data;
+        }
     }

     const key = await this.getKey(this.options.encryption!.encryptionKey);
+    try {
         // Convert base64 back to bytes
         const bytes = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));

         // First 12 bytes are IV, rest is encrypted data
         const decrypted = await crypto.subtle.decrypt(
             {
                 name: 'AES-GCM',
                 iv: bytes.slice(0, 12),
             },
             key,
             bytes.slice(12)
         );

         return this.decoder.decode(decrypted);
+    } catch (error) {
+        console.warn(`Failed to decrypt field ${field.name} in model ${this.model}:`, error);
+        return data;
+    }
 }
🧹 Nitpick comments (4)
packages/runtime/src/enhancements/node/encrypted.ts (4)

17-32: Enhance API documentation for better maintainability.

While the function is marked as @Private, consider adding more detailed documentation:

  • Document the encryption options expected
  • Add example usage
  • Document return value and possible errors
 /**
  * Gets an enhanced Prisma client that supports `@encrypted` attribute.
  *
+ * @param prisma - The Prisma client instance to enhance
+ * @param options - Enhancement options including encryption configuration
+ * @returns Enhanced Prisma client with encryption support
+ * @throws {Error} When encryption options are invalid
  * @private
  */

44-52: Improve error messages and extract magic number.

The validation logic could be improved by:

  1. Making error messages more descriptive
  2. Extracting the key length as a named constant
+const ENCRYPTION_KEY_LENGTH = 32; // AES-256 key length in bytes
+
 if (!options.encryption) throw new Error('Encryption options must be provided');

 if (this.isCustomEncryption(options.encryption!)) {
     if (!options.encryption.encrypt || !options.encryption.decrypt)
-        throw new Error('Custom encryption must provide encrypt and decrypt functions');
+        throw new Error('Custom encryption requires both encrypt and decrypt functions to be implemented');
 } else {
-    if (!options.encryption.encryptionKey) throw new Error('Encryption key must be provided');
-    if (options.encryption.encryptionKey.length !== 32) throw new Error('Encryption key must be 32 bytes');
+    if (!options.encryption.encryptionKey) throw new Error('Simple encryption requires an encryption key');
+    if (options.encryption.encryptionKey.length !== ENCRYPTION_KEY_LENGTH) 
+        throw new Error(`Encryption key must be ${ENCRYPTION_KEY_LENGTH} bytes (AES-256)`);
 }

111-117: Use optional chaining for safer property access.

The condition can be simplified using optional chaining.

-if (args && args.data && actionsOfInterest.includes(action)) {
+if (args?.data && actionsOfInterest.includes(action)) {
🧰 Tools
🪛 Biome (1.9.4)

[error] 113-113: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


1-167: Security considerations for encryption implementation.

The implementation follows cryptographic best practices but consider these security improvements:

  1. Key rotation: Add support for key rotation to handle compromised keys
  2. Key derivation: Consider using a key derivation function (KDF) instead of raw keys
  3. Logging: Ensure encryption errors are logged securely without exposing sensitive data

Would you like me to provide a detailed implementation for any of these security improvements?

🧰 Tools
🪛 Biome (1.9.4)

[error] 113-113: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between acb2ee2 and f4dda18.

📒 Files selected for processing (1)
  • packages/runtime/src/enhancements/node/encrypted.ts (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
packages/runtime/src/enhancements/node/encrypted.ts

[error] 113-113: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (1)
packages/runtime/src/enhancements/node/encrypted.ts (1)

152-166: Add defensive checks in preprocessWritePayload.

The field visitor could be more defensive against malformed data.

 private async preprocessWritePayload(model: string, action: PrismaWriteActionType, args: any) {
+    if (!args?.data) {
+        return;
+    }
     const visitor = new NestedWriteVisitor(this.options.modelMeta, {
         field: async (field, _action, data, context) => {
             // Don't encrypt null, undefined or empty string values
             if (!data) return;

             const encAttr = field.attributes?.find((attr) => attr.name === '@encrypted');
-            if (encAttr && field.type === 'String') {
+            if (encAttr && field.type === 'String' && context?.parent) {
                 context.parent[field.name] = await this.encrypt(field, data);
             }
         },
     });

     await visitor.visit(model, action, args);
 }

Let's verify the usage of context.parent:

✅ Verification successful

Let me gather more information about the NestedWriteVisitor implementation to understand the context structure better.


Let me check the NestedWriterVisitorCallback interface to understand the field callback structure better.


Let me try a different approach to find the interface definition.


Let me check the NestedWriteVisitorContext type to understand the context structure.


Based on the implementation of NestedWriteVisitor and the context structure, I can now provide the final response:

The suggested defensive checks are appropriate and necessary

The review comment's suggested changes are valid for the following reasons:

  • The args?.data check is necessary as the preprocessWritePayload method receives raw arguments that might be undefined or missing the data property
  • The context?.parent check is important because:
    • The context object is passed to field visitors and contains the parent data object that will be modified
    • The parent property is documented as "Parent data, can be used to replace fields" in NestedWriteVisitorContext
    • The field visitor uses this parent object to update encrypted field values
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for NestedWriteVisitor usage to understand context structure
rg -A 10 "class NestedWriteVisitor"

Length of output: 988


Script:

#!/bin/bash
# Get the full implementation of NestedWriteVisitor
ast-grep --pattern 'class NestedWriteVisitor {
  $$$
}'

Length of output: 24287


Script:

#!/bin/bash
# Search for NestedWriterVisitorCallback interface definition
ast-grep --pattern 'interface NestedWriterVisitorCallback {
  $$$
}'

Length of output: 70


Script:

#!/bin/bash
# Search for NestedWriterVisitorCallback type definition
rg -A 10 "type NestedWriterVisitorCallback"

Length of output: 1005


Script:

#!/bin/bash
# Search for NestedWriteVisitorContext type definition
rg -A 5 "type NestedWriteVisitorContext"

Length of output: 1125

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/runtime/src/enhancements/node/create-enhancement.ts (1)

133-135: Consider enhancing encryption options validation

While the basic existence check is good, consider adding more detailed validation of the encryption options structure to catch configuration issues early.

Example validation to consider:

 if (!options.encryption) {
     throw new Error('Encryption options are required for @encrypted enhancement');
 }
+if ('type' in options.encryption) {
+    if (!['simple', 'custom'].includes(options.encryption.type)) {
+        throw new Error('Invalid encryption type. Must be either "simple" or "custom"');
+    }
+    if (options.encryption.type === 'simple' && !options.encryption.secret) {
+        throw new Error('Secret is required for simple encryption');
+    }
+    if (options.encryption.type === 'custom' && typeof options.encryption.encrypt !== 'function') {
+        throw new Error('encrypt function is required for custom encryption');
+    }
+}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f4dda18 and 4e5a2be.

📒 Files selected for processing (1)
  • packages/runtime/src/enhancements/node/create-enhancement.ts (3 hunks)
🔇 Additional comments (4)
packages/runtime/src/enhancements/node/create-enhancement.ts (4)

17-17: LGTM: Import follows consistent style

The import of withEncrypted is correctly placed alongside other enhancement imports.


24-24: LGTM: Enhancement kind addition is appropriate

Adding 'encrypted' to ALL_ENHANCEMENTS is correct as the enhancement is only applied when @encrypted attributes are present in the model fields, as confirmed in previous discussions.


104-104: LGTM: Field detection follows established pattern

The detection of @encrypted fields follows the same pattern as other enhancements, ensuring consistency in the codebase.


125-139: LGTM: Correct enhancement ordering with clear documentation

The enhancement is correctly placed before the policy enhancement, with a clear comment explaining why this ordering is necessary (field length considerations). This follows the same pattern as the password enhancement.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
packages/runtime/src/enhancements/node/encrypted.ts (4)

17-32: Enhance function documentation and visibility.

The withEncrypted function is marked as @private but is exported. Consider either:

  1. Removing the @private tag if this is intended to be a public API
  2. Adding comprehensive documentation including:
    • Parameter descriptions
    • Return value description
    • Usage examples
    • Encryption requirements

44-52: Enhance error handling and type safety in encryption options validation.

The validation logic could be improved with:

  1. More descriptive error messages including expected vs actual values
  2. Type checking for encryptionKey being Uint8Array
-        if (!options.encryption) throw new Error('Encryption options must be provided');
+        if (!options.encryption) {
+            throw new Error('Encryption options must be provided for @encrypted enhancement');
+        }

         if (this.isCustomEncryption(options.encryption!)) {
             if (!options.encryption.encrypt || !options.encryption.decrypt)
-                throw new Error('Custom encryption must provide encrypt and decrypt functions');
+                throw new Error('Custom encryption requires both encrypt and decrypt functions to be implemented');
         } else {
-            if (!options.encryption.encryptionKey) throw new Error('Encryption key must be provided');
-            if (options.encryption.encryptionKey.length !== 32) throw new Error('Encryption key must be 32 bytes');
+            if (!options.encryption.encryptionKey) {
+                throw new Error('Encryption key must be provided for default encryption');
+            }
+            if (!(options.encryption.encryptionKey instanceof Uint8Array)) {
+                throw new Error('Encryption key must be a Uint8Array');
+            }
+            if (options.encryption.encryptionKey.length !== 32) {
+                throw new Error(`Encryption key must be 32 bytes, got ${options.encryption.encryptionKey.length} bytes`);
+            }
         }

55-57: Consider using key derivation for enhanced security.

Instead of directly using the raw key, consider implementing PBKDF2 or similar key derivation function to strengthen the encryption key.

 private async getKey(secret: Uint8Array): Promise<CryptoKey> {
-    return crypto.subtle.importKey('raw', secret, 'AES-GCM', false, ['encrypt', 'decrypt']);
+    // First derive a key using PBKDF2
+    const salt = new Uint8Array(16); // Use a consistent salt or store it with the data
+    const baseKey = await crypto.subtle.importKey(
+        'raw',
+        secret,
+        'PBKDF2',
+        false,
+        ['deriveBits', 'deriveKey']
+    );
+    return crypto.subtle.deriveKey(
+        {
+            name: 'PBKDF2',
+            salt,
+            iterations: 100000,
+            hash: 'SHA-256',
+        },
+        baseKey,
+        { name: 'AES-GCM', length: 256 },
+        false,
+        ['encrypt', 'decrypt']
+    );
 }

111-117: Improve null safety in preprocessArgs.

Use optional chaining and add type safety.

-    if (args && args.data && actionsOfInterest.includes(action)) {
+    if (args?.data && actionsOfInterest.includes(action)) {
         await this.preprocessWritePayload(this.model, action as PrismaWriteActionType, args);
     }
     return args;
🧰 Tools
🪛 Biome (1.9.4)

[error] 113-113: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e5a2be and fa5c065.

📒 Files selected for processing (2)
  • packages/runtime/src/enhancements/node/encrypted.ts (1 hunks)
  • tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts
🧰 Additional context used
🪛 Biome (1.9.4)
packages/runtime/src/enhancements/node/encrypted.ts

[error] 113-113: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

Copy link
Member

@ymc9 ymc9 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this great feature and the patience for the review process @genu 😄. I'm approving and merging it. It'll be part of the v2.11.0 release!

@ymc9 ymc9 merged commit 1b7448f into zenstackhq:dev Dec 30, 2024
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feature Request] Add a @encrypted enhancer
2 participants