1
1
/* eslint-disable @typescript-eslint/no-explicit-any */
2
2
/* eslint-disable @typescript-eslint/no-unused-vars */
3
3
4
- import { z } from 'zod' ;
5
4
import { ACTIONS_WITH_WRITE_PAYLOAD } from '../../constants' ;
6
5
import {
7
6
FieldInfo ,
@@ -11,6 +10,7 @@ import {
11
10
resolveField ,
12
11
type PrismaWriteActionType ,
13
12
} from '../../cross' ;
13
+ import { Decrypter , Encrypter } from '../../encryption' ;
14
14
import { CustomEncryption , DbClientContract , SimpleEncryption } from '../../types' ;
15
15
import { InternalEnhancementOptions } from './create-enhancement' ;
16
16
import { Logger } from './logger' ;
@@ -36,27 +36,12 @@ export function withEncrypted<DbClient extends object = any>(
36
36
37
37
class EncryptedHandler extends DefaultPrismaProxyHandler {
38
38
private queryUtils : QueryUtils ;
39
- private encoder = new TextEncoder ( ) ;
40
- private decoder = new TextDecoder ( ) ;
41
39
private logger : Logger ;
42
40
private encryptionKey : CryptoKey | undefined ;
43
41
private encryptionKeyDigest : string | undefined ;
44
42
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 ;
60
45
61
46
constructor ( prisma : DbClientContract , model : string , options : InternalEnhancementOptions ) {
62
47
super ( prisma , model , options ) ;
@@ -76,138 +61,33 @@ class EncryptedHandler extends DefaultPrismaProxyHandler {
76
61
if ( ! options . encryption . encryptionKey ) {
77
62
throw this . queryUtils . unknownError ( 'Encryption key must be provided' ) ;
78
63
}
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
+ ] ) ;
82
70
}
83
71
}
84
72
85
73
private isCustomEncryption ( encryption : CustomEncryption | SimpleEncryption ) : encryption is CustomEncryption {
86
74
return 'encrypt' in encryption && 'decrypt' in encryption ;
87
75
}
88
76
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
-
139
77
private async encrypt ( field : FieldInfo , data : string ) : Promise < string > {
140
78
if ( this . isCustomEncryption ( this . options . encryption ! ) ) {
141
79
return this . options . encryption . encrypt ( this . model , field , data ) ;
142
80
}
143
81
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 ) ;
163
83
}
164
84
165
85
private async decrypt ( field : FieldInfo , data : string ) : Promise < string > {
166
86
if ( this . isCustomEncryption ( this . options . encryption ! ) ) {
167
87
return this . options . encryption . decrypt ( this . model , field , data ) ;
168
88
}
169
89
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 ) ;
211
91
}
212
92
213
93
// base override
0 commit comments