@@ -21,28 +21,78 @@ class ProtectDynamoDBErrorImpl extends Error implements ProtectDynamoDBError {
2121 }
2222}
2323
24+ function deepClone < T > ( obj : T ) : T {
25+ if ( obj === null || typeof obj !== 'object' ) {
26+ return obj
27+ }
28+
29+ if ( Array . isArray ( obj ) ) {
30+ return obj . map ( ( item ) => deepClone ( item ) ) as unknown as T
31+ }
32+
33+ return Object . entries ( obj as Record < string , unknown > ) . reduce (
34+ ( acc , [ key , value ] ) => ( {
35+ // biome-ignore lint/performance/noAccumulatingSpread: TODO later
36+ ...acc ,
37+ [ key ] : deepClone ( value ) ,
38+ } ) ,
39+ { } as T ,
40+ )
41+ }
42+
2443function toEncryptedDynamoItem (
2544 encrypted : Record < string , unknown > ,
2645 encryptedAttrs : string [ ] ,
2746) : Record < string , unknown > {
28- return Object . entries ( encrypted ) . reduce (
29- ( putItem , [ attrName , attrValue ] ) => {
30- if ( encryptedAttrs . includes ( attrName ) ) {
31- if ( attrValue === null || attrValue === undefined ) {
32- putItem [ attrName ] = attrValue
33- } else {
34- const encryptPayload = attrValue as EncryptedPayload
35- if ( encryptPayload ?. c ) {
36- if ( encryptPayload . hm ) {
37- putItem [ `${ attrName } ${ searchTermAttrSuffix } ` ] = encryptPayload . hm
38- }
39- putItem [ `${ attrName } ${ ciphertextAttrSuffix } ` ] = encryptPayload . c
40- }
47+ function processValue (
48+ attrName : string ,
49+ attrValue : unknown ,
50+ isNested : boolean ,
51+ ) : Record < string , unknown > {
52+ if ( attrValue === null || attrValue === undefined ) {
53+ return { [ attrName ] : attrValue }
54+ }
55+
56+ // Handle encrypted payload
57+ if (
58+ encryptedAttrs . includes ( attrName ) ||
59+ ( isNested &&
60+ typeof attrValue === 'object' &&
61+ 'c' in ( attrValue as object ) )
62+ ) {
63+ const encryptPayload = attrValue as EncryptedPayload
64+ if ( encryptPayload ?. c ) {
65+ const result : Record < string , unknown > = { }
66+ if ( encryptPayload . hm ) {
67+ result [ `${ attrName } ${ searchTermAttrSuffix } ` ] = encryptPayload . hm
4168 }
42- } else {
43- putItem [ attrName ] = attrValue
69+ result [ ` ${ attrName } ${ ciphertextAttrSuffix } ` ] = encryptPayload . c
70+ return result
4471 }
45- return putItem
72+ }
73+
74+ // Handle nested objects recursively
75+ if ( typeof attrValue === 'object' && ! Array . isArray ( attrValue ) ) {
76+ const nestedResult = Object . entries (
77+ attrValue as Record < string , unknown > ,
78+ ) . reduce (
79+ ( acc , [ key , val ] ) => {
80+ const processed = processValue ( key , val , true )
81+ return Object . assign ( { } , acc , processed )
82+ } ,
83+ { } as Record < string , unknown > ,
84+ )
85+ return { [ attrName ] : nestedResult }
86+ }
87+
88+ // Handle non-encrypted values
89+ return { [ attrName ] : attrValue }
90+ }
91+
92+ return Object . entries ( encrypted ) . reduce (
93+ ( putItem , [ attrName , attrValue ] ) => {
94+ const processed = processValue ( attrName , attrValue , false )
95+ return Object . assign ( { } , putItem , processed )
4696 } ,
4797 { } as Record < string , unknown > ,
4898 )
@@ -52,27 +102,64 @@ function toItemWithEqlPayloads(
52102 decrypted : Record < string , EncryptedPayload | unknown > ,
53103 encryptedAttrs : string [ ] ,
54104) : Record < string , unknown > {
55- return Object . entries ( decrypted ) . reduce (
56- ( formattedItem , [ attrName , attrValue ] ) => {
57- if (
58- attrName . endsWith ( ciphertextAttrSuffix ) &&
59- encryptedAttrs . includes ( attrName . slice ( 0 , - ciphertextAttrSuffix . length ) )
60- ) {
61- formattedItem [ attrName . slice ( 0 , - ciphertextAttrSuffix . length ) ] = {
105+ function processValue (
106+ attrName : string ,
107+ attrValue : unknown ,
108+ isNested : boolean ,
109+ ) : Record < string , unknown > {
110+ if ( attrValue === null || attrValue === undefined ) {
111+ return { [ attrName ] : attrValue }
112+ }
113+
114+ // Skip HMAC fields
115+ if ( attrName . endsWith ( searchTermAttrSuffix ) ) {
116+ return { }
117+ }
118+
119+ // Handle encrypted payload
120+ if (
121+ attrName . endsWith ( ciphertextAttrSuffix ) &&
122+ ( encryptedAttrs . includes (
123+ attrName . slice ( 0 , - ciphertextAttrSuffix . length ) ,
124+ ) ||
125+ isNested )
126+ ) {
127+ const baseName = attrName . slice ( 0 , - ciphertextAttrSuffix . length )
128+ return {
129+ [ baseName ] : {
62130 c : attrValue ,
63131 bf : null ,
64132 hm : null ,
65133 i : { c : 'notUsed' , t : 'notUsed' } ,
66134 k : 'notUsed' ,
67135 ob : null ,
68136 v : 2 ,
69- }
70- } else if ( attrName . endsWith ( searchTermAttrSuffix ) ) {
71- // skip HMAC attrs since we don't need those for decryption
72- } else {
73- formattedItem [ attrName ] = attrValue
137+ } ,
74138 }
75- return formattedItem
139+ }
140+
141+ // Handle nested objects recursively
142+ if ( typeof attrValue === 'object' && ! Array . isArray ( attrValue ) ) {
143+ const nestedResult = Object . entries (
144+ attrValue as Record < string , unknown > ,
145+ ) . reduce (
146+ ( acc , [ key , val ] ) => {
147+ const processed = processValue ( key , val , true )
148+ return Object . assign ( { } , acc , processed )
149+ } ,
150+ { } as Record < string , unknown > ,
151+ )
152+ return { [ attrName ] : nestedResult }
153+ }
154+
155+ // Handle non-encrypted values
156+ return { [ attrName ] : attrValue }
157+ }
158+
159+ return Object . entries ( decrypted ) . reduce (
160+ ( formattedItem , [ attrName , attrValue ] ) => {
161+ const processed = processValue ( attrName , attrValue , false )
162+ return Object . assign ( { } , formattedItem , processed )
76163 } ,
77164 { } as Record < string , unknown > ,
78165 )
@@ -110,7 +197,7 @@ export function protectDynamoDB(
110197 return await withResult (
111198 async ( ) => {
112199 const encryptResult = await protectClient . encryptModel (
113- item ,
200+ deepClone ( item ) ,
114201 protectTable ,
115202 )
116203
@@ -120,10 +207,10 @@ export function protectDynamoDB(
120207 )
121208 }
122209
123- const data = encryptResult . data
210+ const data = deepClone ( encryptResult . data )
124211 const encryptedAttrs = Object . keys ( protectTable . build ( ) . columns )
125212
126- return toEncryptedDynamoItem ( data , encryptedAttrs )
213+ return toEncryptedDynamoItem ( data , encryptedAttrs ) as T
127214 } ,
128215 ( error ) => handleError ( error , 'encryptModel' ) ,
129216 )
@@ -136,7 +223,7 @@ export function protectDynamoDB(
136223 return await withResult (
137224 async ( ) => {
138225 const encryptResult = await protectClient . bulkEncryptModels (
139- items ,
226+ items . map ( ( item ) => deepClone ( item ) ) ,
140227 protectTable ,
141228 )
142229
@@ -146,11 +233,12 @@ export function protectDynamoDB(
146233 )
147234 }
148235
149- const data = encryptResult . data
236+ const data = encryptResult . data . map ( ( item ) => deepClone ( item ) )
150237 const encryptedAttrs = Object . keys ( protectTable . build ( ) . columns )
151238
152- return data . map ( ( encrypted ) =>
153- toEncryptedDynamoItem ( encrypted , encryptedAttrs ) ,
239+ return data . map (
240+ ( encrypted ) =>
241+ toEncryptedDynamoItem ( encrypted , encryptedAttrs ) as T ,
154242 )
155243 } ,
156244 ( error ) => handleError ( error , 'bulkEncryptModels' ) ,
0 commit comments