@@ -212,6 +212,61 @@ describe('V2 Keychains', function () {
212212 ) ;
213213 } ) ;
214214
215+ it ( 'to update the password for a single keychain (async)' , async function ( ) {
216+ await keychains
217+ . updateSingleKeychainPasswordAsync ( { newPassword : '5678' } )
218+ . should . be . rejectedWith ( 'expected old password to be a string' ) ;
219+
220+ await keychains
221+ . updateSingleKeychainPasswordAsync ( { oldPassword : 1234 , newPassword : '5678' } )
222+ . should . be . rejectedWith ( 'expected old password to be a string' ) ;
223+
224+ await keychains
225+ . updateSingleKeychainPasswordAsync ( { oldPassword : '1234' } )
226+ . should . be . rejectedWith ( 'expected new password to be a string' ) ;
227+
228+ await keychains
229+ . updateSingleKeychainPasswordAsync ( { oldPassword : '1234' , newPassword : 5678 } )
230+ . should . be . rejectedWith ( 'expected new password to be a string' ) ;
231+
232+ await keychains
233+ . updateSingleKeychainPasswordAsync ( { oldPassword : '1234' , newPassword : '5678' } )
234+ . should . be . rejectedWith ( 'expected keychain to be an object with an encryptedPrv property' ) ;
235+
236+ await keychains
237+ . updateSingleKeychainPasswordAsync ( { oldPassword : '1234' , newPassword : '5678' , keychain : { } } )
238+ . should . be . rejectedWith ( 'expected keychain to be an object with an encryptedPrv property' ) ;
239+
240+ await keychains
241+ . updateSingleKeychainPasswordAsync ( {
242+ oldPassword : '1234' ,
243+ newPassword : '5678' ,
244+ keychain : { encryptedPrv : 123 } ,
245+ } )
246+ . should . be . rejectedWith ( 'expected keychain to be an object with an encryptedPrv property' ) ;
247+
248+ // wrong password — decrypt fails
249+ const keychain = { encryptedPrv : bitgo . encrypt ( { input : 'xprv1' , password : otherPassword } ) } ;
250+ await keychains
251+ . updateSingleKeychainPasswordAsync ( { oldPassword, newPassword, keychain } )
252+ . should . be . rejectedWith ( 'failed to update keychain password: incorrect password' ) ;
253+
254+ // invalid JSON in encryptedPrv
255+ await keychains
256+ . updateSingleKeychainPasswordAsync ( { oldPassword, newPassword, keychain : { encryptedPrv : 'not-valid-json' } } )
257+ . should . be . rejectedWith ( 'failed to update keychain password: decrypt: ciphertext is not valid JSON' ) ;
258+
259+ // unknown envelope version
260+ const unknownVersionEnvelope = JSON . stringify ( { v : 99 , ct : 'abc' } ) ;
261+ await keychains
262+ . updateSingleKeychainPasswordAsync ( {
263+ oldPassword,
264+ newPassword,
265+ keychain : { encryptedPrv : unknownVersionEnvelope } ,
266+ } )
267+ . should . be . rejectedWith ( 'failed to update keychain password: decrypt: unknown envelope version 99' ) ;
268+ } ) ;
269+
215270 it ( 'on any other error' , async function ( ) {
216271 nock ( bgUrl )
217272 . get ( '/api/v2/tltc/key' )
@@ -225,7 +280,7 @@ describe('V2 Keychains', function () {
225280 ] ,
226281 } ) ;
227282
228- sandbox . stub ( keychains , 'updateSingleKeychainPassword ' ) . throws ( 'error' , 'some random error' ) ;
283+ sandbox . stub ( keychains , 'updateSingleKeychainPasswordAsync ' ) . throws ( 'error' , 'some random error' ) ;
229284
230285 await keychains . updatePassword ( { oldPassword, newPassword } ) . should . be . rejectedWith ( 'some random error' ) ;
231286 } ) ;
@@ -313,19 +368,78 @@ describe('V2 Keychains', function () {
313368 validateKeys ( keys , newPassword , 3 ) ;
314369 } ) ;
315370
316- it ( 'single keychain password update' , ( ) => {
371+ it ( 'single keychain password update' , async ( ) => {
317372 const prv = 'xprvtest' ;
318373 const keychain = {
319374 xpub : 'xpub123' ,
320375 encryptedPrv : bitgo . encrypt ( { input : prv , password : oldPassword } ) ,
321376 } ;
322377
323- const newKeychain = keychains . updateSingleKeychainPassword ( { keychain, oldPassword, newPassword } ) ;
378+ const newKeychain = await keychains . updateSingleKeychainPassword ( { keychain, oldPassword, newPassword } ) ;
324379
325380 const decryptedPrv = bitgo . decrypt ( { input : newKeychain . encryptedPrv , password : newPassword } ) ;
326381 decryptedPrv . should . equal ( prv ) ;
327382 } ) ;
328383
384+ it ( 'single keychain password update preserves v2 (Argon2id) envelope' , async ( ) => {
385+ const prv = 'xprvtest-v2' ;
386+ const encryptedPrv = await bitgo . encryptAsync ( { input : prv , password : oldPassword , encryptionVersion : 2 } ) ;
387+ const envelope = JSON . parse ( encryptedPrv ) ;
388+ envelope . v . should . equal ( 2 , 'pre-condition: keychain must be v2-encrypted' ) ;
389+
390+ const keychain = { xpub : 'xpub123' , encryptedPrv } ;
391+ const newKeychain = await keychains . updateSingleKeychainPasswordAsync ( { keychain, oldPassword, newPassword } ) ;
392+
393+ const newEnvelope = JSON . parse ( newKeychain . encryptedPrv ) ;
394+ newEnvelope . v . should . equal ( 2 , 're-encrypted keychain must still be v2' ) ;
395+
396+ const decryptedPrv = await bitgo . decryptAsync ( { input : newKeychain . encryptedPrv , password : newPassword } ) ;
397+ decryptedPrv . should . equal ( prv , 'new password must decrypt to original prv' ) ;
398+
399+ await bitgo . decryptAsync ( { input : newKeychain . encryptedPrv , password : oldPassword } ) . should . be . rejected ( ) ;
400+ } ) ;
401+
402+ it ( 'updatePassword handles a mix of v1 and v2 keychains' , async function ( ) {
403+ const v1Prv = 'xprv-v1' ;
404+ const v2Prv = 'xprv-v2' ;
405+
406+ nock ( bgUrl )
407+ . get ( '/api/v2/tltc/key' )
408+ . query ( true )
409+ . reply ( 200 , {
410+ keys : [
411+ {
412+ pub : 'xpub-v1' ,
413+ encryptedPrv : bitgo . encrypt ( { input : v1Prv , password : oldPassword } ) ,
414+ } ,
415+ {
416+ pub : 'xpub-v2' ,
417+ encryptedPrv : await bitgo . encryptAsync ( { input : v2Prv , password : oldPassword , encryptionVersion : 2 } ) ,
418+ } ,
419+ {
420+ pub : 'xpub-other' ,
421+ encryptedPrv : bitgo . encrypt ( { input : 'xprv-other' , password : 'different-password' } ) ,
422+ } ,
423+ ] ,
424+ } ) ;
425+
426+ const updatedKeys = await keychains . updatePassword ( { oldPassword, newPassword } ) ;
427+
428+ assert . strictEqual ( Object . keys ( updatedKeys ) . length , 2 , 'only the two matching keychains should be updated' ) ;
429+
430+ const updatedV1 = updatedKeys [ 'xpub-v1' ] ;
431+ const updatedV2 = updatedKeys [ 'xpub-v2' ] ;
432+ assert . ok ( updatedV1 , 'v1 keychain must be in the result' ) ;
433+ assert . ok ( updatedV2 , 'v2 keychain must be in the result' ) ;
434+
435+ bitgo . decrypt ( { input : updatedV1 , password : newPassword } ) . should . equal ( v1Prv ) ;
436+
437+ const updatedV2Envelope = JSON . parse ( updatedV2 ) ;
438+ updatedV2Envelope . v . should . equal ( 2 , 'v2 keychain must remain v2 after password change' ) ;
439+ const decryptedV2 = await bitgo . decryptAsync ( { input : updatedV2 , password : newPassword } ) ;
440+ decryptedV2 . should . equal ( v2Prv ) ;
441+ } ) ;
442+
329443 it ( 'should return the updated keys with ids' , async function ( ) {
330444 nock ( bgUrl )
331445 . get ( '/api/v2/tltc/key' )
@@ -502,7 +616,7 @@ describe('V2 Keychains', function () {
502616 } ,
503617 } ) ;
504618
505- sandbox . stub ( BitGo . prototype , 'decrypt ' ) . returns ( decryptResult ) ;
619+ sandbox . stub ( bitgo , 'decryptAsync ' ) . resolves ( decryptResult ) ;
506620 } ) ;
507621
508622 afterEach ( function ( ) {
0 commit comments