@@ -6846,4 +6846,238 @@ describe('V2 Wallet:', function () {
68466846 } ) ;
68476847 } ) ;
68486848 } ) ;
6849+
6850+ describe ( 'upgradeEncryption' , function ( ) {
6851+ const walletId = walletData . id ;
6852+ const [ userKeyId , backupKeyId , bitgoKeyId ] = walletData . keys ;
6853+ const passphrase = 'walletPassphrase' ;
6854+
6855+ beforeEach ( function ( ) {
6856+ nock . cleanAll ( ) ;
6857+ } ) ;
6858+
6859+ afterEach ( function ( ) {
6860+ // Prevent leftover interceptors from bleeding into other suites in the file.
6861+ nock . cleanAll ( ) ;
6862+ } ) ;
6863+
6864+ function nockUnlock ( times = 1 ) {
6865+ return nock ( bgUrl ) . post ( '/api/v1/user/unlock' ) . times ( times ) . reply ( 200 , { } ) ;
6866+ }
6867+
6868+ function nockKeychain ( id : string , keychain : Record < string , unknown > ) {
6869+ return nock ( bgUrl )
6870+ . get ( `/api/v2/tbtc/key/${ id } ` )
6871+ . reply ( 200 , { id, pub : 'pub' , type : 'independent' , ...keychain } ) ;
6872+ }
6873+
6874+ function nockPecFetch ( pec : string ) {
6875+ return nock ( bgUrl )
6876+ . post ( `/api/v2/tbtc/wallet/${ walletId } /passcoderecovery` )
6877+ . reply ( 200 , { recoveryInfo : { passcodeEncryptionCode : pec } } ) ;
6878+ }
6879+
6880+ it ( 're-encrypts both user and backup keys and PUTs them as v2' , async function ( ) {
6881+ const userEnc = await bitgo . encrypt ( { input : 'userPrv' , password : passphrase , encryptionVersion : 1 } ) ;
6882+ const backupEnc = await bitgo . encrypt ( { input : 'backupPrv' , password : passphrase , encryptionVersion : 1 } ) ;
6883+
6884+ nockUnlock ( ) ;
6885+ nockKeychain ( userKeyId , { encryptedPrv : userEnc } ) ;
6886+ nockKeychain ( backupKeyId , { encryptedPrv : backupEnc } ) ;
6887+ nockKeychain ( bitgoKeyId , { } ) ;
6888+
6889+ const puts : Array < { url : string ; body : Record < string , unknown > } > = [ ] ;
6890+ nock ( bgUrl )
6891+ . put ( `/api/v2/tbtc/key/${ userKeyId } ` , ( body ) => {
6892+ puts . push ( { url : userKeyId , body } ) ;
6893+ return true ;
6894+ } )
6895+ . reply ( 200 , { } ) ;
6896+ nock ( bgUrl )
6897+ . put ( `/api/v2/tbtc/key/${ backupKeyId } ` , ( body ) => {
6898+ puts . push ( { url : backupKeyId , body } ) ;
6899+ return true ;
6900+ } )
6901+ . reply ( 200 , { } ) ;
6902+
6903+ const result = await wallet . upgradeEncryption ( { passphrase, passcodeEncryptionCode : 'pec-xyz' } ) ;
6904+
6905+ assert . ok ( result === undefined , 'no PDF generator → returns undefined' ) ;
6906+ puts . should . have . length ( 2 ) ;
6907+ const userPut = puts . find ( ( p ) => p . url === userKeyId ) ! ;
6908+ JSON . parse ( userPut . body . encryptedPrv as string ) . v . should . equal ( 2 ) ;
6909+ JSON . parse ( userPut . body . originalEncryptedPrv as string ) . v . should . equal ( 2 ) ;
6910+ } ) ;
6911+
6912+ it ( 'throws when neither passphrase nor boxD is provided' , async function ( ) {
6913+ await wallet . upgradeEncryption ( { } ) . should . be . rejectedWith ( / p a s s p h r a s e .* b o x D / ) ;
6914+ } ) ;
6915+
6916+ it ( 'throws for an MPCv2 wallet when boxA or boxB are missing' , async function ( ) {
6917+ const mpcWalletData = { ...walletData , multisigTypeVersion : 'MPCv2' as const } ;
6918+ const mpcWallet = new Wallet ( bitgo , basecoin , mpcWalletData ) ;
6919+
6920+ nockUnlock ( ) ;
6921+ // PEC fetch happens before the MPCv2 validation short-circuits — allow the call to noop
6922+ nockPecFetch ( 'pec' ) ;
6923+
6924+ await mpcWallet . upgradeEncryption ( { passphrase, passcodeEncryptionCode : 'pec' } ) . should . be . rejectedWith ( / M P C v 2 / ) ;
6925+ } ) ;
6926+
6927+ it ( 're-encrypts boxA and boxB as reducedEncryptedPrv on the keychains for MPCv2 wallets' , async function ( ) {
6928+ const mpcWalletData = { ...walletData , multisigTypeVersion : 'MPCv2' as const } ;
6929+ const mpcWallet = new Wallet ( bitgo , basecoin , mpcWalletData ) ;
6930+ const userEnc = await bitgo . encrypt ( { input : 'userPrv' , password : passphrase , encryptionVersion : 1 } ) ;
6931+ const backupEnc = await bitgo . encrypt ( { input : 'backupPrv' , password : passphrase , encryptionVersion : 1 } ) ;
6932+ const boxA = await bitgo . encrypt ( { input : 'userReducedPrv' , password : passphrase , encryptionVersion : 1 } ) ;
6933+ const boxB = await bitgo . encrypt ( { input : 'backupReducedPrv' , password : passphrase , encryptionVersion : 1 } ) ;
6934+
6935+ nockUnlock ( ) ;
6936+ nockKeychain ( userKeyId , { encryptedPrv : userEnc } ) ;
6937+ nockKeychain ( backupKeyId , { encryptedPrv : backupEnc } ) ;
6938+ nockKeychain ( bitgoKeyId , { } ) ;
6939+ nock ( bgUrl ) . put ( `/api/v2/tbtc/key/${ userKeyId } ` ) . reply ( 200 , { } ) ;
6940+ nock ( bgUrl ) . put ( `/api/v2/tbtc/key/${ backupKeyId } ` ) . reply ( 200 , { } ) ;
6941+
6942+ const capturedPdfParams : Array < Record < string , unknown > > = [ ] ;
6943+ const generatePdf = async ( params : Record < string , unknown > ) => {
6944+ capturedPdfParams . push ( params ) ;
6945+ return { output : ( ) => new ArrayBuffer ( 0 ) } ;
6946+ } ;
6947+
6948+ await mpcWallet . upgradeEncryption ( {
6949+ passphrase,
6950+ boxA,
6951+ boxB,
6952+ passcodeEncryptionCode : 'pec' ,
6953+ generatePdf,
6954+ } ) ;
6955+
6956+ capturedPdfParams . should . have . length ( 1 ) ;
6957+ const { userKeychain, backupKeychain } = capturedPdfParams [ 0 ] as {
6958+ userKeychain : { reducedEncryptedPrv ?: string } ;
6959+ backupKeychain : { reducedEncryptedPrv ?: string } ;
6960+ } ;
6961+ assert . ok ( userKeychain . reducedEncryptedPrv , 'user reducedEncryptedPrv must be set' ) ;
6962+ JSON . parse ( userKeychain . reducedEncryptedPrv ! ) . v . should . equal ( 2 ) ;
6963+ assert . ok ( backupKeychain . reducedEncryptedPrv , 'backup reducedEncryptedPrv must be set' ) ;
6964+ JSON . parse ( backupKeychain . reducedEncryptedPrv ! ) . v . should . equal ( 2 ) ;
6965+ } ) ;
6966+
6967+ it ( 'skips keychains that are already v2' , async function ( ) {
6968+ const userV2 = await bitgo . encrypt ( { input : 'userPrv' , password : passphrase , encryptionVersion : 2 } ) ;
6969+ const backupV1 = await bitgo . encrypt ( { input : 'backupPrv' , password : passphrase , encryptionVersion : 1 } ) ;
6970+
6971+ nockUnlock ( ) ;
6972+ nockKeychain ( userKeyId , { encryptedPrv : userV2 } ) ;
6973+ nockKeychain ( backupKeyId , { encryptedPrv : backupV1 } ) ;
6974+ nockKeychain ( bitgoKeyId , { } ) ;
6975+
6976+ const puts : string [ ] = [ ] ;
6977+ // Only the backup key should be PUT — user is already v2.
6978+ nock ( bgUrl )
6979+ . put ( new RegExp ( `/api/v2/tbtc/key/(${ userKeyId } |${ backupKeyId } )` ) )
6980+ . reply ( 200 , function ( uri ) {
6981+ puts . push ( uri ) ;
6982+ return { } ;
6983+ } ) ;
6984+
6985+ await wallet . upgradeEncryption ( { passphrase, passcodeEncryptionCode : 'pec' } ) ;
6986+
6987+ puts . should . have . length ( 1 ) ;
6988+ puts [ 0 ] . should . containEql ( backupKeyId ) ;
6989+ } ) ;
6990+
6991+ it ( 'handles backup keychain with no encryptedPrv (public-key-only) without PUTing' , async function ( ) {
6992+ const userV1 = await bitgo . encrypt ( { input : 'userPrv' , password : passphrase , encryptionVersion : 1 } ) ;
6993+
6994+ nockUnlock ( ) ;
6995+ nockKeychain ( userKeyId , { encryptedPrv : userV1 } ) ;
6996+ nockKeychain ( backupKeyId , { } ) ; // public key only
6997+ nockKeychain ( bitgoKeyId , { } ) ;
6998+ nock ( bgUrl ) . put ( `/api/v2/tbtc/key/${ userKeyId } ` ) . reply ( 200 , { } ) ;
6999+
7000+ await wallet . upgradeEncryption ( { passphrase, passcodeEncryptionCode : 'pec' } ) ;
7001+ // If we reached here without unhandled nock error, the flow completed without PUTing the backup.
7002+ } ) ;
7003+
7004+ it ( 're-encrypts a backup key from boxB without PUTing to the server' , async function ( ) {
7005+ const userV1 = await bitgo . encrypt ( { input : 'userPrv' , password : passphrase , encryptionVersion : 1 } ) ;
7006+ const boxB = await bitgo . encrypt ( { input : 'backupPrv' , password : passphrase , encryptionVersion : 1 } ) ;
7007+
7008+ nockUnlock ( ) ;
7009+ nockKeychain ( userKeyId , { encryptedPrv : userV1 } ) ;
7010+ nockKeychain ( backupKeyId , { } ) ; // no encryptedPrv server-side
7011+ nockKeychain ( bitgoKeyId , { } ) ;
7012+ nock ( bgUrl ) . put ( `/api/v2/tbtc/key/${ userKeyId } ` ) . reply ( 200 , { } ) ;
7013+
7014+ await wallet . upgradeEncryption ( { passphrase, boxB, passcodeEncryptionCode : 'pec' } ) ;
7015+ } ) ;
7016+
7017+ it ( 'derives the passphrase from boxD when passphrase is omitted' , async function ( ) {
7018+ const pec = 'pec-derive' ;
7019+ const derivedPass = 'derivedFromBoxD' ;
7020+ const boxD = await bitgo . encrypt ( { input : derivedPass , password : pec , encryptionVersion : 1 } ) ;
7021+ const userV1 = await bitgo . encrypt ( { input : 'userPrv' , password : derivedPass , encryptionVersion : 1 } ) ;
7022+ const backupV1 = await bitgo . encrypt ( { input : 'backupPrv' , password : derivedPass , encryptionVersion : 1 } ) ;
7023+
7024+ nockUnlock ( ) ;
7025+ nockPecFetch ( pec ) ;
7026+ nockKeychain ( userKeyId , { encryptedPrv : userV1 } ) ;
7027+ nockKeychain ( backupKeyId , { encryptedPrv : backupV1 } ) ;
7028+ nockKeychain ( bitgoKeyId , { } ) ;
7029+ nock ( bgUrl ) . put ( new RegExp ( `/api/v2/tbtc/key/` ) ) . twice ( ) . reply ( 200 , { } ) ;
7030+
7031+ // No passphrase — must be derived from boxD.
7032+ await wallet . upgradeEncryption ( { boxD } ) ;
7033+ } ) ;
7034+
7035+ it ( 'makes no PUT calls in dry-run mode and returns undefined' , async function ( ) {
7036+ const userV1 = await bitgo . encrypt ( { input : 'userPrv' , password : passphrase , encryptionVersion : 1 } ) ;
7037+ const backupV1 = await bitgo . encrypt ( { input : 'backupPrv' , password : passphrase , encryptionVersion : 1 } ) ;
7038+
7039+ // No unlock, no PEC, no PUTs expected.
7040+ nockKeychain ( userKeyId , { encryptedPrv : userV1 } ) ;
7041+ nockKeychain ( backupKeyId , { encryptedPrv : backupV1 } ) ;
7042+ nockKeychain ( bitgoKeyId , { } ) ;
7043+
7044+ const result = await wallet . upgradeEncryption ( { passphrase, dryRun : true } ) ;
7045+ assert . strictEqual ( result , undefined ) ;
7046+ } ) ;
7047+
7048+ it ( 'throws when unlock rejects with an error unrelated to session duration' , async function ( ) {
7049+ nock ( bgUrl ) . post ( '/api/v1/user/unlock' ) . replyWithError ( 'invalid OTP' ) ;
7050+
7051+ await wallet
7052+ . upgradeEncryption ( { passphrase, passcodeEncryptionCode : 'pec' } )
7053+ . should . be . rejectedWith ( / i n v a l i d O T P / ) ;
7054+ } ) ;
7055+
7056+ it ( 'proceeds when unlock reports "already unlocked longer"' , async function ( ) {
7057+ const userV1 = await bitgo . encrypt ( { input : 'userPrv' , password : passphrase , encryptionVersion : 1 } ) ;
7058+ const backupV1 = await bitgo . encrypt ( { input : 'backupPrv' , password : passphrase , encryptionVersion : 1 } ) ;
7059+
7060+ nock ( bgUrl ) . post ( '/api/v1/user/unlock' ) . reply ( 401 , { error : 'Session already unlocked longer' } ) ;
7061+ nockKeychain ( userKeyId , { encryptedPrv : userV1 } ) ;
7062+ nockKeychain ( backupKeyId , { encryptedPrv : backupV1 } ) ;
7063+ nockKeychain ( bitgoKeyId , { } ) ;
7064+ nock ( bgUrl ) . put ( new RegExp ( `/api/v2/tbtc/key/` ) ) . twice ( ) . reply ( 200 , { } ) ;
7065+
7066+ await wallet . upgradeEncryption ( { passphrase, passcodeEncryptionCode : 'pec' } ) ;
7067+ } ) ;
7068+
7069+ it ( 'uses the supplied passcodeEncryptionCode and skips the passcoderecovery fetch' , async function ( ) {
7070+ const userV1 = await bitgo . encrypt ( { input : 'userPrv' , password : passphrase , encryptionVersion : 1 } ) ;
7071+ const backupV1 = await bitgo . encrypt ( { input : 'backupPrv' , password : passphrase , encryptionVersion : 1 } ) ;
7072+
7073+ nockUnlock ( ) ;
7074+ // Intentionally do NOT nock passcoderecovery — the test fails if the code tries to call it.
7075+ nockKeychain ( userKeyId , { encryptedPrv : userV1 } ) ;
7076+ nockKeychain ( backupKeyId , { encryptedPrv : backupV1 } ) ;
7077+ nockKeychain ( bitgoKeyId , { } ) ;
7078+ nock ( bgUrl ) . put ( new RegExp ( `/api/v2/tbtc/key/` ) ) . twice ( ) . reply ( 200 , { } ) ;
7079+
7080+ await wallet . upgradeEncryption ( { passphrase, passcodeEncryptionCode : 'pec-supplied' } ) ;
7081+ } ) ;
7082+ } ) ;
68497083} ) ;
0 commit comments