11/* eslint-disable @typescript-eslint/no-explicit-any */
22/* eslint-disable @typescript-eslint/no-unused-vars */
33
4- import { z } from 'zod' ;
54import { ACTIONS_WITH_WRITE_PAYLOAD } from '../../constants' ;
65import {
76 FieldInfo ,
@@ -11,6 +10,7 @@ import {
1110 resolveField ,
1211 type PrismaWriteActionType ,
1312} from '../../cross' ;
13+ import { Decrypter , Encrypter } from '../../encryption' ;
1414import { CustomEncryption , DbClientContract , SimpleEncryption } from '../../types' ;
1515import { InternalEnhancementOptions } from './create-enhancement' ;
1616import { Logger } from './logger' ;
@@ -36,27 +36,12 @@ export function withEncrypted<DbClient extends object = any>(
3636
3737class EncryptedHandler extends DefaultPrismaProxyHandler {
3838 private queryUtils : QueryUtils ;
39- private encoder = new TextEncoder ( ) ;
40- private decoder = new TextDecoder ( ) ;
4139 private logger : Logger ;
4240 private encryptionKey : CryptoKey | undefined ;
4341 private encryptionKeyDigest : string | undefined ;
4442 private decryptionKeys : Array < { key : CryptoKey ; digest : string } > = [ ] ;
45- private encryptionMetaSchema = z . object ( {
46- // version
47- v : z . number ( ) ,
48- // algorithm
49- a : z . string ( ) ,
50- // key digest
51- k : z . string ( ) ,
52- } ) ;
53-
54- // constants
55- private readonly ENCRYPTION_KEY_BYTES = 32 ;
56- private readonly IV_BYTES = 12 ;
57- private readonly ALGORITHM = 'AES-GCM' ;
58- private readonly ENCRYPTER_VERSION = 1 ;
59- private readonly KEY_DIGEST_BYTES = 8 ;
43+ private encrypter : Encrypter | undefined ;
44+ private decrypter : Decrypter | undefined ;
6045
6146 constructor ( prisma : DbClientContract , model : string , options : InternalEnhancementOptions ) {
6247 super ( prisma , model , options ) ;
@@ -76,138 +61,33 @@ class EncryptedHandler extends DefaultPrismaProxyHandler {
7661 if ( ! options . encryption . encryptionKey ) {
7762 throw this . queryUtils . unknownError ( 'Encryption key must be provided' ) ;
7863 }
79- if ( options . encryption . encryptionKey . length !== this . ENCRYPTION_KEY_BYTES ) {
80- throw this . queryUtils . unknownError ( `Encryption key must be ${ this . ENCRYPTION_KEY_BYTES } bytes` ) ;
81- }
64+
65+ this . encrypter = new Encrypter ( options . encryption . encryptionKey ) ;
66+ this . decrypter = new Decrypter ( [
67+ options . encryption . encryptionKey ,
68+ ...( options . encryption . decryptionKeys || [ ] ) ,
69+ ] ) ;
8270 }
8371 }
8472
8573 private isCustomEncryption ( encryption : CustomEncryption | SimpleEncryption ) : encryption is CustomEncryption {
8674 return 'encrypt' in encryption && 'decrypt' in encryption ;
8775 }
8876
89- private async loadKey ( key : Uint8Array , keyUsages : KeyUsage [ ] ) : Promise < CryptoKey > {
90- return crypto . subtle . importKey ( 'raw' , key , this . ALGORITHM , false , keyUsages ) ;
91- }
92-
93- private async computeKeyDigest ( key : Uint8Array ) {
94- const rawDigest = await crypto . subtle . digest ( 'SHA-256' , key ) ;
95- return new Uint8Array ( rawDigest . slice ( 0 , this . KEY_DIGEST_BYTES ) ) . reduce (
96- ( acc , byte ) => acc + byte . toString ( 16 ) . padStart ( 2 , '0' ) ,
97- ''
98- ) ;
99- }
100-
101- private async getEncryptionKey ( ) : Promise < CryptoKey > {
102- if ( this . isCustomEncryption ( this . options . encryption ! ) ) {
103- throw new Error ( 'Unexpected custom encryption settings' ) ;
104- }
105- if ( ! this . encryptionKey ) {
106- this . encryptionKey = await this . loadKey ( this . options . encryption ! . encryptionKey , [ 'encrypt' , 'decrypt' ] ) ;
107- }
108- return this . encryptionKey ;
109- }
110-
111- private async getEncryptionKeyDigest ( ) {
112- if ( this . isCustomEncryption ( this . options . encryption ! ) ) {
113- throw new Error ( 'Unexpected custom encryption settings' ) ;
114- }
115- if ( ! this . encryptionKeyDigest ) {
116- this . encryptionKeyDigest = await this . computeKeyDigest ( this . options . encryption ! . encryptionKey ) ;
117- }
118- return this . encryptionKeyDigest ;
119- }
120-
121- private async findDecryptionKeys ( keyDigest : string ) : Promise < CryptoKey [ ] > {
122- if ( this . isCustomEncryption ( this . options . encryption ! ) ) {
123- throw new Error ( 'Unexpected custom encryption settings' ) ;
124- }
125-
126- if ( this . decryptionKeys . length === 0 ) {
127- const keys = [ this . options . encryption ! . encryptionKey , ...( this . options . encryption ! . decryptionKeys || [ ] ) ] ;
128- this . decryptionKeys = await Promise . all (
129- keys . map ( async ( key ) => ( {
130- key : await this . loadKey ( key , [ 'decrypt' ] ) ,
131- digest : await this . computeKeyDigest ( key ) ,
132- } ) )
133- ) ;
134- }
135-
136- return this . decryptionKeys . filter ( ( entry ) => entry . digest === keyDigest ) . map ( ( entry ) => entry . key ) ;
137- }
138-
13977 private async encrypt ( field : FieldInfo , data : string ) : Promise < string > {
14078 if ( this . isCustomEncryption ( this . options . encryption ! ) ) {
14179 return this . options . encryption . encrypt ( this . model , field , data ) ;
14280 }
14381
144- const key = await this . getEncryptionKey ( ) ;
145- const iv = crypto . getRandomValues ( new Uint8Array ( this . IV_BYTES ) ) ;
146- const encrypted = await crypto . subtle . encrypt (
147- {
148- name : this . ALGORITHM ,
149- iv,
150- } ,
151- key ,
152- this . encoder . encode ( data )
153- ) ;
154-
155- // combine IV and encrypted data into a single array of bytes
156- const cipherBytes = [ ...iv , ...new Uint8Array ( encrypted ) ] ;
157-
158- // encryption metadata
159- const meta = { v : this . ENCRYPTER_VERSION , a : this . ALGORITHM , k : await this . getEncryptionKeyDigest ( ) } ;
160-
161- // convert concatenated result to base64 string
162- return `${ btoa ( JSON . stringify ( meta ) ) } .${ btoa ( String . fromCharCode ( ...cipherBytes ) ) } ` ;
82+ return this . encrypter ! . encrypt ( data ) ;
16383 }
16484
16585 private async decrypt ( field : FieldInfo , data : string ) : Promise < string > {
16686 if ( this . isCustomEncryption ( this . options . encryption ! ) ) {
16787 return this . options . encryption . decrypt ( this . model , field , data ) ;
16888 }
16989
170- const [ metaText , cipherText ] = data . split ( '.' ) ;
171- if ( ! metaText || ! cipherText ) {
172- throw new Error ( 'Malformed encrypted data' ) ;
173- }
174-
175- let metaObj : unknown ;
176- try {
177- metaObj = JSON . parse ( atob ( metaText ) ) ;
178- } catch ( error ) {
179- throw new Error ( 'Malformed metadata' ) ;
180- }
181-
182- // parse meta
183- const { a : algorithm , k : keyDigest } = this . encryptionMetaSchema . parse ( metaObj ) ;
184-
185- // find a matching decryption key
186- const keys = await this . findDecryptionKeys ( keyDigest ) ;
187- if ( keys . length === 0 ) {
188- throw new Error ( 'No matching decryption key found' ) ;
189- }
190-
191- // convert base64 back to bytes
192- const bytes = Uint8Array . from ( atob ( cipherText ) , ( c ) => c . charCodeAt ( 0 ) ) ;
193-
194- // extract IV from the head
195- const iv = bytes . slice ( 0 , this . IV_BYTES ) ;
196- const cipher = bytes . slice ( this . IV_BYTES ) ;
197- let lastError : unknown ;
198-
199- for ( const key of keys ) {
200- let decrypted : ArrayBuffer ;
201- try {
202- decrypted = await crypto . subtle . decrypt ( { name : algorithm , iv } , key , cipher ) ;
203- } catch ( err ) {
204- lastError = err ;
205- continue ;
206- }
207- return this . decoder . decode ( decrypted ) ;
208- }
209-
210- throw lastError ;
90+ return this . decrypter ! . decrypt ( data ) ;
21191 }
21292
21393 // base override
0 commit comments