Skip to content

Commit aedbd93

Browse files
committed
feat: implement custom encryption handling in EncryptedHandler
1 parent 688d92d commit aedbd93

File tree

1 file changed

+61
-43
lines changed

1 file changed

+61
-43
lines changed

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

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
/* eslint-disable @typescript-eslint/no-unused-vars */
33

4-
import { NestedWriteVisitor, enumerate, getModelFields, resolveField, type PrismaWriteActionType } from '../../cross';
5-
import { DbClientContract } from '../../types';
4+
import {
5+
FieldInfo,
6+
NestedWriteVisitor,
7+
enumerate,
8+
getModelFields,
9+
resolveField,
10+
type PrismaWriteActionType,
11+
} from '../../cross';
12+
import { DbClientContract, CustomEncryption, SimpleEncryption } from '../../types';
613
import { InternalEnhancementOptions } from './create-enhancement';
714
import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy';
815
import { QueryUtils } from './query-utils';
@@ -33,52 +40,65 @@ const getKey = async (secret: string): Promise<CryptoKey> => {
3340
'decrypt',
3441
]);
3542
};
36-
const encryptFunc = async (data: string, secret: string): Promise<string> => {
37-
const key = await getKey(secret);
38-
const iv = crypto.getRandomValues(new Uint8Array(12));
39-
40-
const encrypted = await crypto.subtle.encrypt(
41-
{
42-
name: 'AES-GCM',
43-
iv,
44-
},
45-
key,
46-
encoder.encode(data)
47-
);
4843

49-
// Combine IV and encrypted data into a single array of bytes
50-
const bytes = [...iv, ...new Uint8Array(encrypted)];
44+
class EncryptedHandler extends DefaultPrismaProxyHandler {
45+
private queryUtils: QueryUtils;
5146

52-
// Convert bytes to base64 string
53-
return btoa(String.fromCharCode(...bytes));
54-
};
47+
constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) {
48+
super(prisma, model, options);
5549

56-
const decryptFunc = async (encryptedData: string, secret: string): Promise<string> => {
57-
const key = await getKey(secret);
50+
this.queryUtils = new QueryUtils(prisma, options);
51+
}
5852

59-
// Convert base64 back to bytes
60-
const bytes = Uint8Array.from(atob(encryptedData), (c) => c.charCodeAt(0));
53+
private isCustomEncryption(encryption: CustomEncryption | SimpleEncryption): encryption is CustomEncryption {
54+
return 'encrypt' in encryption && 'decrypt' in encryption;
55+
}
6156

62-
// First 12 bytes are IV, rest is encrypted data
63-
const decrypted = await crypto.subtle.decrypt(
64-
{
65-
name: 'AES-GCM',
66-
iv: bytes.slice(0, 12),
67-
},
68-
key,
69-
bytes.slice(12)
70-
);
57+
private async encrypt(field: FieldInfo, data: string): Promise<string> {
58+
if (this.isCustomEncryption(this.options.encryption!)) {
59+
return this.options.encryption.encrypt(this.model, field, data);
60+
}
7161

72-
return decoder.decode(decrypted);
73-
};
62+
const key = await getKey(this.options.encryption!.encryptionKey);
63+
const iv = crypto.getRandomValues(new Uint8Array(12));
7464

75-
class EncryptedHandler extends DefaultPrismaProxyHandler {
76-
private queryUtils: QueryUtils;
65+
const encrypted = await crypto.subtle.encrypt(
66+
{
67+
name: 'AES-GCM',
68+
iv,
69+
},
70+
key,
71+
encoder.encode(data)
72+
);
7773

78-
constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) {
79-
super(prisma, model, options);
74+
// Combine IV and encrypted data into a single array of bytes
75+
const bytes = [...iv, ...new Uint8Array(encrypted)];
8076

81-
this.queryUtils = new QueryUtils(prisma, options);
77+
// Convert bytes to base64 string
78+
return btoa(String.fromCharCode(...bytes));
79+
}
80+
81+
private async decrypt(field: FieldInfo, data: string): Promise<string> {
82+
if (this.isCustomEncryption(this.options.encryption!)) {
83+
return this.options.encryption.decrypt(this.model, field, data);
84+
}
85+
86+
const key = await getKey(this.options.encryption!.encryptionKey);
87+
88+
// Convert base64 back to bytes
89+
const bytes = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
90+
91+
// First 12 bytes are IV, rest is encrypted data
92+
const decrypted = await crypto.subtle.decrypt(
93+
{
94+
name: 'AES-GCM',
95+
iv: bytes.slice(0, 12),
96+
},
97+
key,
98+
bytes.slice(12)
99+
);
100+
101+
return decoder.decode(decrypted);
82102
}
83103

84104
// base override
@@ -115,9 +135,7 @@ class EncryptedHandler extends DefaultPrismaProxyHandler {
115135

116136
const shouldDecrypt = fieldInfo.attributes?.find((attr) => attr.name === '@encrypted');
117137
if (shouldDecrypt) {
118-
const descryptSecret = shouldDecrypt.args.find((arg) => arg.name === 'secret')?.value as string;
119-
120-
entityData[field] = await decryptFunc(entityData[field], descryptSecret);
138+
entityData[field] = await this.decrypt(fieldInfo, entityData[field]);
121139
}
122140
}
123141
}
@@ -131,7 +149,7 @@ class EncryptedHandler extends DefaultPrismaProxyHandler {
131149

132150
const secret: string = encAttr.args.find((arg) => arg.name === 'secret')?.value as string;
133151

134-
context.parent[field.name] = await encryptFunc(data, secret);
152+
context.parent[field.name] = await this.encrypt(field, data);
135153
}
136154
},
137155
});

0 commit comments

Comments
 (0)