Skip to content

Commit 7784099

Browse files
committed
chore: add encrypt function
1 parent 23a06cc commit 7784099

File tree

1 file changed

+109
-0
lines changed

1 file changed

+109
-0
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
/* eslint-disable @typescript-eslint/no-unused-vars */
3+
4+
import { NestedWriteVisitor, type PrismaWriteActionType } from '../../cross';
5+
import { DbClientContract } from '../../types';
6+
import { InternalEnhancementOptions } from './create-enhancement';
7+
import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy';
8+
9+
/**
10+
* Gets an enhanced Prisma client that supports `@encrypted` attribute.
11+
*
12+
* @private
13+
*/
14+
export function withEncrypted<DbClient extends object = any>(
15+
prisma: DbClient,
16+
options: InternalEnhancementOptions
17+
): DbClient {
18+
return makeProxy(
19+
prisma,
20+
options.modelMeta,
21+
(_prisma, model) => new EncryptedHandler(_prisma as DbClientContract, model, options),
22+
'encrypted'
23+
);
24+
}
25+
26+
const encoder = new TextEncoder();
27+
const decoder = new TextDecoder();
28+
29+
const getKey = async (secret: string): Promise<CryptoKey> => {
30+
return crypto.subtle.importKey('raw', encoder.encode(secret).slice(0, 32), 'AES-GCM', false, [
31+
'encrypt',
32+
'decrypt',
33+
]);
34+
};
35+
const encryptFunc = async (data: string, secret: string): Promise<string> => {
36+
const key = await getKey(secret);
37+
const iv = crypto.getRandomValues(new Uint8Array(12));
38+
39+
const encrypted = await crypto.subtle.encrypt(
40+
{
41+
name: 'AES-GCM',
42+
iv,
43+
},
44+
key,
45+
encoder.encode(data)
46+
);
47+
48+
// Combine IV and encrypted data into a single array of bytes
49+
const bytes = [...iv, ...new Uint8Array(encrypted)];
50+
51+
// Convert bytes to base64 string
52+
return btoa(String.fromCharCode(...bytes));
53+
};
54+
55+
const decryptFunc = async (encryptedData: string, secret: string): Promise<string> => {
56+
const key = await getKey(secret);
57+
58+
// Convert base64 back to bytes
59+
const bytes = Uint8Array.from(atob(encryptedData), (c) => c.charCodeAt(0));
60+
61+
// First 12 bytes are IV, rest is encrypted data
62+
const decrypted = await crypto.subtle.decrypt(
63+
{
64+
name: 'AES-GCM',
65+
iv: bytes.slice(0, 12),
66+
},
67+
key,
68+
bytes.slice(12)
69+
);
70+
71+
return decoder.decode(decrypted);
72+
};
73+
74+
class EncryptedHandler extends DefaultPrismaProxyHandler {
75+
constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) {
76+
super(prisma, model, options);
77+
}
78+
79+
// base override
80+
protected async preprocessArgs(action: PrismaProxyActions, args: any) {
81+
const actionsOfInterest: PrismaProxyActions[] = ['create', 'createMany', 'update', 'updateMany', 'upsert'];
82+
if (args && args.data && actionsOfInterest.includes(action)) {
83+
await this.preprocessWritePayload(this.model, action as PrismaWriteActionType, args);
84+
}
85+
return args;
86+
}
87+
88+
// base override
89+
protected async processResultEntity(action: PrismaProxyActions, args: any) {
90+
return args;
91+
}
92+
93+
private async preprocessWritePayload(model: string, action: PrismaWriteActionType, args: any) {
94+
const visitor = new NestedWriteVisitor(this.options.modelMeta, {
95+
field: async (field, _action, data, context) => {
96+
const encAttr = field.attributes?.find((attr) => attr.name === '@encrypted');
97+
if (encAttr && field.type === 'String') {
98+
// encrypt value
99+
100+
let secret: string = encAttr.args.find((arg) => arg.name === 'secret')?.value as string;
101+
102+
context.parent[field.name] = await encryptFunc(data, secret);
103+
}
104+
},
105+
});
106+
107+
await visitor.visit(model, action, args);
108+
}
109+
}

0 commit comments

Comments
 (0)