@@ -2451,6 +2451,86 @@ describe('V2 Wallets:', function () {
24512451 acceptShareNock . done ( ) ;
24522452 } ) ;
24532453
2454+ describe ( 'v2 envelope support in acceptShare' , ( ) => {
2455+ const sandbox = sinon . createSandbox ( ) ;
2456+ afterEach ( ( ) => sandbox . verifyAndRestore ( ) ) ;
2457+
2458+ it ( 'should accept share when ECDH sharing keychain encryptedXprv is v2-encrypted' , async function ( ) {
2459+ const shareId = 'v2-ecdh-xprv-1' ;
2460+ const userPassword = 'test_password' ;
2461+
2462+ // Simulate a v2 envelope for the ECDH root key
2463+ const v2EncryptedXprv = JSON . stringify ( { v : 2 , iv : 'aabbcc' , ct : 'ddeeff' , adata : '00' } ) ;
2464+ const fakeXprv = 'xprvSomeBase58Value' ;
2465+ const fakeSecret = 'fakeEcdhSharedSecret' ;
2466+ const decryptedWalletPrv = 'walletPrivKeyPlaintext' ;
2467+
2468+ const eckey = makeRandomKey ( ) ;
2469+ const v2EncryptedPrv = bitgo . encrypt ( { password : fakeSecret , input : decryptedWalletPrv } ) ;
2470+
2471+ nock ( bgUrl )
2472+ . get ( `/api/v2/tbtc/walletshare/${ shareId } ` )
2473+ . reply ( 200 , {
2474+ keychain : {
2475+ path : 'm/0/0/1' ,
2476+ fromPubKey : eckey . publicKey . toString ( 'hex' ) ,
2477+ toPubKey : eckey . publicKey . toString ( 'hex' ) ,
2478+ encryptedPrv : v2EncryptedPrv ,
2479+ pub : eckey . publicKey . toString ( 'hex' ) ,
2480+ } ,
2481+ } ) ;
2482+ nock ( bgUrl ) . post ( `/api/v2/tbtc/walletshare/${ shareId } ` ) . reply ( 200 , { changed : true , state : 'accepted' } ) ;
2483+
2484+ sandbox . stub ( bitgo , 'getECDHKeychain' ) . resolves ( { encryptedXprv : v2EncryptedXprv } ) ;
2485+ const decryptAsyncStub = sandbox . stub ( bitgo , 'decryptAsync' ) ;
2486+ // First call decrypts the ECDH keychain (v2), second decrypts the shared wallet prv
2487+ decryptAsyncStub . onFirstCall ( ) . resolves ( fakeXprv ) ;
2488+ decryptAsyncStub . onSecondCall ( ) . resolves ( decryptedWalletPrv ) ;
2489+ sandbox . stub ( moduleBitgo , 'getSharedSecret' ) . returns ( Buffer . from ( fakeSecret ) ) ;
2490+
2491+ const res = await wallets . acceptShare ( { walletShareId : shareId , userPassword } ) ;
2492+ should . equal ( res . changed , true ) ;
2493+ should . equal ( res . state , 'accepted' ) ;
2494+ // Must have called decryptAsync (not the sync decrypt) for both the ECDH key and the wallet prv
2495+ assert . equal ( decryptAsyncStub . callCount , 2 ) ;
2496+ } ) ;
2497+
2498+ it ( 'should accept share when wallet keychain encryptedPrv is v2-encrypted' , async function ( ) {
2499+ const shareId = 'v2-wallet-prv-1' ;
2500+ const userPassword = 'test_password' ;
2501+
2502+ const v2EncryptedXprv = JSON . stringify ( { v : 2 , iv : 'aabbcc' , ct : 'ddeeff' , adata : '00' } ) ;
2503+ const fakeXprv = 'xprvAnotherKey' ;
2504+ const fakeSecret = 'ecdhSharedSecret2' ;
2505+ const decryptedWalletPrv = 'walletPrivKey2' ;
2506+ const v2EncryptedPrv = JSON . stringify ( { v : 2 , iv : '112233' , ct : '445566' , adata : '77' } ) ;
2507+ const eckey = makeRandomKey ( ) ;
2508+
2509+ nock ( bgUrl )
2510+ . get ( `/api/v2/tbtc/walletshare/${ shareId } ` )
2511+ . reply ( 200 , {
2512+ keychain : {
2513+ path : 'm/0/0/2' ,
2514+ fromPubKey : eckey . publicKey . toString ( 'hex' ) ,
2515+ toPubKey : eckey . publicKey . toString ( 'hex' ) ,
2516+ encryptedPrv : v2EncryptedPrv ,
2517+ pub : eckey . publicKey . toString ( 'hex' ) ,
2518+ } ,
2519+ } ) ;
2520+ nock ( bgUrl ) . post ( `/api/v2/tbtc/walletshare/${ shareId } ` ) . reply ( 200 , { changed : true , state : 'accepted' } ) ;
2521+
2522+ sandbox . stub ( bitgo , 'getECDHKeychain' ) . resolves ( { encryptedXprv : v2EncryptedXprv } ) ;
2523+ const decryptAsyncStub = sandbox . stub ( bitgo , 'decryptAsync' ) ;
2524+ decryptAsyncStub . onFirstCall ( ) . resolves ( fakeXprv ) ;
2525+ decryptAsyncStub . onSecondCall ( ) . resolves ( decryptedWalletPrv ) ;
2526+ sandbox . stub ( moduleBitgo , 'getSharedSecret' ) . returns ( Buffer . from ( fakeSecret ) ) ;
2527+
2528+ const res = await wallets . acceptShare ( { walletShareId : shareId , userPassword } ) ;
2529+ should . equal ( res . changed , true ) ;
2530+ should . equal ( res . state , 'accepted' ) ;
2531+ } ) ;
2532+ } ) ;
2533+
24542534 describe ( 'bulkAcceptShare' , function ( ) {
24552535 afterEach ( function ( ) {
24562536 nock . cleanAll ( ) ;
@@ -2614,7 +2694,7 @@ describe('V2 Wallets:', function () {
26142694 password : walletPassphrase ,
26152695 input : bitgo . encrypt ( { input : myEcdhKeychain . xprv , password : walletPassphrase } ) ,
26162696 } ) ;
2617- sinon . stub ( bitgo , 'decrypt ' ) . returns ( prvKey ) ;
2697+ sinon . stub ( bitgo , 'decryptAsync ' ) . resolves ( prvKey ) ;
26182698 sinon . stub ( moduleBitgo , 'getSharedSecret' ) . resolves ( 'fakeSharedSecret' ) ;
26192699
26202700 const share = await wallets . bulkAcceptShare ( {
@@ -2630,6 +2710,107 @@ describe('V2 Wallets:', function () {
26302710 } ) ;
26312711 } ) ;
26322712
2713+ it ( 'should accept share when ECDH sharing keychain is v2-encrypted' , async ( ) => {
2714+ const walletPassphrase = 'bitgo1234' ;
2715+ const shareId = '66a229dbdccdcfb95b44fc2745a60bd4' ;
2716+
2717+ const toKeychain = utxoLib . bip32 . fromSeed ( Buffer . from ( 'deadbeef02deadbeef02deadbeef02deadbeef02' , 'hex' ) ) ;
2718+ const path = 'm/999999/1/1' ;
2719+ const pubkey = toKeychain . derivePath ( path ) . publicKey . toString ( 'hex' ) ;
2720+ const eckey = makeRandomKey ( ) ;
2721+ const secret = getSharedSecret ( eckey , Buffer . from ( pubkey , 'hex' ) ) . toString ( 'hex' ) ;
2722+ const decryptedPrv = 'someWalletPrivateKey' ;
2723+ const newEncryptedPrv = bitgo . encrypt ( { password : secret , input : decryptedPrv } ) ;
2724+
2725+ // Simulate v2-encrypted ECDH keychain xprv (envelope with v:2 marker)
2726+ const v2EncryptedXprv = JSON . stringify ( { v : 2 , iv : 'aaa' , ct : 'bbb' , adata : 'ccc' } ) ;
2727+ const myEcdhXprv = 'xprvSomeBase58Key' ;
2728+
2729+ nock ( bgUrl )
2730+ . get ( '/api/v2/walletshares' )
2731+ . reply ( 200 , {
2732+ incoming : [
2733+ {
2734+ id : shareId ,
2735+ isUMSInitiated : true ,
2736+ keychain : {
2737+ path,
2738+ fromPubKey : eckey . publicKey . toString ( 'hex' ) ,
2739+ encryptedPrv : newEncryptedPrv ,
2740+ toPubKey : pubkey ,
2741+ pub : pubkey ,
2742+ } ,
2743+ } ,
2744+ ] ,
2745+ } ) ;
2746+ nock ( bgUrl )
2747+ . put ( '/api/v2/walletshares/accept' )
2748+ . reply ( 200 , { acceptedWalletShares : [ { walletShareId : shareId } ] } ) ;
2749+
2750+ sinon . stub ( bitgo , 'getECDHKeychain' ) . resolves ( { encryptedXprv : v2EncryptedXprv } ) ;
2751+ // decryptAsync resolves with the xprv regardless of whether the envelope is v1 or v2
2752+ sinon . stub ( bitgo , 'decryptAsync' ) . resolves ( myEcdhXprv ) ;
2753+ sinon . stub ( moduleBitgo , 'getSharedSecret' ) . resolves ( 'fakeSharedSecret' ) ;
2754+
2755+ const share = await wallets . bulkAcceptShare ( {
2756+ walletShareIds : [ shareId ] ,
2757+ userLoginPassword : walletPassphrase ,
2758+ } ) ;
2759+ assert . deepEqual ( share , { acceptedWalletShares : [ { walletShareId : shareId } ] } ) ;
2760+ } ) ;
2761+
2762+ it ( 'should accept share when wallet keychain encryptedPrv is v2-encrypted' , async ( ) => {
2763+ const walletPassphrase = 'bitgo1234' ;
2764+ const shareId = 'v2-prv-share-1' ;
2765+
2766+ const toKeychain = utxoLib . bip32 . fromSeed ( Buffer . from ( 'deadbeef02deadbeef02deadbeef02deadbeef02' , 'hex' ) ) ;
2767+ const path = 'm/999999/1/1' ;
2768+ const pubkey = toKeychain . derivePath ( path ) . publicKey . toString ( 'hex' ) ;
2769+ const eckey = makeRandomKey ( ) ;
2770+
2771+ // Simulate v2-encrypted encryptedPrv in the wallet share keychain
2772+ const v2EncryptedPrv = JSON . stringify ( { v : 2 , iv : 'xxx' , ct : 'yyy' , adata : 'zzz' } ) ;
2773+ const v2EncryptedXprv = JSON . stringify ( { v : 2 , iv : 'aaa' , ct : 'bbb' , adata : 'ccc' } ) ;
2774+ const myEcdhXprv = 'xprvSomeBase58Key' ;
2775+ const decryptedWalletPrv = 'decryptedWalletPrivKey' ;
2776+
2777+ nock ( bgUrl )
2778+ . get ( '/api/v2/walletshares' )
2779+ . reply ( 200 , {
2780+ incoming : [
2781+ {
2782+ id : shareId ,
2783+ isUMSInitiated : true ,
2784+ keychain : {
2785+ path,
2786+ fromPubKey : eckey . publicKey . toString ( 'hex' ) ,
2787+ encryptedPrv : v2EncryptedPrv ,
2788+ toPubKey : pubkey ,
2789+ pub : pubkey ,
2790+ } ,
2791+ } ,
2792+ ] ,
2793+ } ) ;
2794+ nock ( bgUrl )
2795+ . put ( '/api/v2/walletshares/accept' )
2796+ . reply ( 200 , { acceptedWalletShares : [ { walletShareId : shareId } ] } ) ;
2797+
2798+ sinon . stub ( bitgo , 'getECDHKeychain' ) . resolves ( { encryptedXprv : v2EncryptedXprv } ) ;
2799+ // First call: decrypt ECDH keychain xprv; second call: decrypt v2 wallet share prv
2800+ const decryptAsyncStub = sinon . stub ( bitgo , 'decryptAsync' ) ;
2801+ decryptAsyncStub . onFirstCall ( ) . resolves ( myEcdhXprv ) ;
2802+ decryptAsyncStub . onSecondCall ( ) . resolves ( decryptedWalletPrv ) ;
2803+ sinon . stub ( moduleBitgo , 'getSharedSecret' ) . resolves ( 'fakeSharedSecret' ) ;
2804+
2805+ const share = await wallets . bulkAcceptShare ( {
2806+ walletShareIds : [ shareId ] ,
2807+ userLoginPassword : walletPassphrase ,
2808+ } ) ;
2809+ assert . deepEqual ( share , { acceptedWalletShares : [ { walletShareId : shareId } ] } ) ;
2810+ // Both decrypt calls must use decryptAsync (not the sync decrypt)
2811+ assert . equal ( decryptAsyncStub . callCount , 2 ) ;
2812+ } ) ;
2813+
26332814 it ( 'should include webauthnInfo in request when provided (ECDH branch)' , async ( ) => {
26342815 const fromUserPrv = Math . random ( ) ;
26352816 const walletPassphrase = 'bitgo1234' ;
@@ -2687,7 +2868,7 @@ describe('V2 Wallets:', function () {
26872868 password : walletPassphrase ,
26882869 input : bitgo . encrypt ( { input : myEcdhKeychain . xprv , password : walletPassphrase } ) ,
26892870 } ) ;
2690- sinon . stub ( bitgo , 'decrypt ' ) . returns ( prvKey ) ;
2871+ sinon . stub ( bitgo , 'decryptAsync ' ) . resolves ( prvKey ) ;
26912872 sinon . stub ( moduleBitgo , 'getSharedSecret' ) . resolves ( 'fakeSharedSecret' ) ;
26922873
26932874 await wallets . bulkAcceptShare ( {
@@ -2765,7 +2946,7 @@ describe('V2 Wallets:', function () {
27652946 password : walletPassphrase ,
27662947 input : bitgo . encrypt ( { input : myEcdhKeychain . xprv , password : walletPassphrase } ) ,
27672948 } ) ;
2768- sinon . stub ( bitgo , 'decrypt ' ) . returns ( prvKey ) ;
2949+ sinon . stub ( bitgo , 'decryptAsync ' ) . resolves ( prvKey ) ;
27692950 sinon . stub ( moduleBitgo , 'getSharedSecret' ) . resolves ( 'fakeSharedSecret' ) ;
27702951
27712952 await wallets . bulkAcceptShare ( {
@@ -2834,7 +3015,7 @@ describe('V2 Wallets:', function () {
28343015 password : walletPassphrase ,
28353016 input : bitgo . encrypt ( { input : myEcdhKeychain . xprv , password : walletPassphrase } ) ,
28363017 } ) ;
2837- sinon . stub ( bitgo , 'decrypt ' ) . returns ( prvKey ) ;
3018+ sinon . stub ( bitgo , 'decryptAsync ' ) . resolves ( prvKey ) ;
28383019 sinon . stub ( bitgo , 'encrypt' ) . returns ( userPrv + 'X' . repeat ( 100000 ) ) ;
28393020 sinon . stub ( moduleBitgo , 'getSharedSecret' ) . resolves ( 'fakeSharedSecret' ) ;
28403021
@@ -2934,7 +3115,7 @@ describe('V2 Wallets:', function () {
29343115 password : walletPassphrase ,
29353116 input : bitgo . encrypt ( { input : myEcdhKeychain . xprv , password : walletPassphrase } ) ,
29363117 } ) ;
2937- sinon . stub ( bitgo , 'decrypt ' ) . returns ( prvKey ) ;
3118+ sinon . stub ( bitgo , 'decryptAsync ' ) . resolves ( prvKey ) ;
29383119 sinon . stub ( bitgo , 'encrypt' ) . returns ( userPrv + 'X' . repeat ( 100000 ) ) ;
29393120 sinon . stub ( moduleBitgo , 'getSharedSecret' ) . resolves ( 'fakeSharedSecret' ) ;
29403121
@@ -3036,7 +3217,7 @@ describe('V2 Wallets:', function () {
30363217 password : walletPassphrase ,
30373218 input : bitgo . encrypt ( { input : myEcdhKeychain . xprv , password : walletPassphrase } ) ,
30383219 } ) ;
3039- sinon . stub ( bitgo , 'decrypt ' ) . returns ( prvKey ) ;
3220+ sinon . stub ( bitgo , 'decryptAsync ' ) . resolves ( prvKey ) ;
30403221 sinon . stub ( moduleBitgo , 'getSharedSecret' ) . resolves ( 'fakeSharedSecret' ) ;
30413222
30423223 // Always throw 413 error, even for batch size 1
@@ -3284,7 +3465,7 @@ describe('V2 Wallets:', function () {
32843465 . resolves ( userKeychain ) ;
32853466
32863467 // Mock decrypt and signMessage
3287- sinon . stub ( bitgo , 'decrypt ' ) . returns ( 'decryptedPrivateKey' ) ;
3468+ sinon . stub ( bitgo , 'decryptAsync ' ) . resolves ( 'decryptedPrivateKey' ) ;
32883469 sinon . stub ( wallets . baseCoin , 'signMessage' ) . resolves ( Buffer . from ( 'signature' ) ) ;
32893470
32903471 // Mock bulkUpdateWalletShareRequest
@@ -3348,7 +3529,7 @@ describe('V2 Wallets:', function () {
33483529 sinon . stub ( ofcWallets . baseCoin . keychains ( ) , 'createUserKeychain' ) . resolves ( userKeychain ) ;
33493530
33503531 // Mock decrypt and signMessage
3351- sinon . stub ( bitgo , 'decrypt ' ) . returns ( 'decryptedPrivateKey' ) ;
3532+ sinon . stub ( bitgo , 'decryptAsync ' ) . resolves ( 'decryptedPrivateKey' ) ;
33523533 sinon . stub ( ofcWallets . baseCoin , 'signMessage' ) . resolves ( Buffer . from ( 'signature' ) ) ;
33533534
33543535 // Mock getECDHKeychain
@@ -3491,9 +3672,9 @@ describe('V2 Wallets:', function () {
34913672 } ) ;
34923673
34933674 // Setup decrypt and encrypt stubs
3494- const decryptStub = sinon . stub ( bitgo , 'decrypt ' ) ;
3495- decryptStub . onFirstCall ( ) . returns ( myEcdhKeychain . xprv ) ; // For sharing keychain
3496- decryptStub . onSecondCall ( ) . returns ( originalPrivKey ) ; // For wallet keychain
3675+ const decryptStub = sinon . stub ( bitgo , 'decryptAsync ' ) ;
3676+ decryptStub . onFirstCall ( ) . resolves ( myEcdhKeychain . xprv ) ; // For sharing keychain
3677+ decryptStub . onSecondCall ( ) . resolves ( originalPrivKey ) ; // For wallet keychain
34973678
34983679 const encryptStub = sinon . stub ( bitgo , 'encrypt' ) . returns ( 'newEncryptedPrv' ) ;
34993680
@@ -3625,6 +3806,75 @@ describe('V2 Wallets:', function () {
36253806 result . walletShareUpdateErrors [ 0 ] . should . have . property ( 'walletShareId' , 'share2' ) ;
36263807 result . walletShareUpdateErrors [ 0 ] . should . have . property ( 'reason' , 'Failed to process share2' ) ;
36273808 } ) ;
3809+
3810+ it ( 'should accept share with v2-encrypted ECDH keychain and wallet prv' , async ( ) => {
3811+ const walletPassphrase = 'bitgo1234' ;
3812+ const path = 'm/999999/1/1' ;
3813+
3814+ const fromKeychain = utxoLib . bip32 . fromSeed ( Buffer . from ( 'deadbeef01deadbeef01deadbeef01deadbeef01' , 'hex' ) ) ;
3815+ const toKeychain = utxoLib . bip32 . fromSeed ( Buffer . from ( 'deadbeef02deadbeef02deadbeef02deadbeef02' , 'hex' ) ) ;
3816+ const toPubKey = toKeychain . derivePath ( path ) . publicKey . toString ( 'hex' ) ;
3817+ const fromPubKey = fromKeychain . publicKey . toString ( 'hex' ) ;
3818+
3819+ const originalPrivKey = 'originalPrivateKey' ;
3820+ const sharedSecret = getSharedSecret ( fromKeychain , Buffer . from ( toPubKey , 'hex' ) ) . toString ( 'hex' ) ;
3821+ const encryptedPrv = bitgo . encrypt ( { password : sharedSecret , input : originalPrivKey } ) ;
3822+
3823+ // Both the ECDH keychain and share prv are v2-encrypted in this scenario
3824+ const v2EncryptedXprv = JSON . stringify ( { v : 2 , iv : 'aabbcc' , ct : 'ddeeff' , adata : '00' } ) ;
3825+
3826+ sinon . stub ( Wallets . prototype , 'listSharesV2' ) . resolves ( {
3827+ incoming : [
3828+ {
3829+ id : 'share1' ,
3830+ coin : 'tsol' ,
3831+ walletLabel : 'testing' ,
3832+ fromUser : 'dummyFromUser' ,
3833+ toUser : 'dummyToUser' ,
3834+ wallet : 'wallet1' ,
3835+ permissions : [ 'spend' ] ,
3836+ state : 'active' ,
3837+ keychain : { pub : toPubKey , toPubKey, fromPubKey, encryptedPrv, path } ,
3838+ } ,
3839+ ] ,
3840+ outgoing : [ ] ,
3841+ } ) ;
3842+
3843+ const myEcdhKeychain = await bitgo . keychains ( ) . create ( ) ;
3844+ sinon . stub ( bitgo , 'getECDHKeychain' ) . resolves ( { encryptedXprv : v2EncryptedXprv } ) ;
3845+
3846+ // decryptAsync handles v2 transparently; stub to return expected values
3847+ const decryptAsyncStub = sinon . stub ( bitgo , 'decryptAsync' ) ;
3848+ decryptAsyncStub . onFirstCall ( ) . resolves ( myEcdhKeychain . xprv ) ; // ECDH keychain
3849+ decryptAsyncStub . onSecondCall ( ) . resolves ( originalPrivKey ) ; // wallet share prv
3850+
3851+ const encryptStub = sinon . stub ( bitgo , 'encrypt' ) . returns ( 'newEncryptedPrv' ) ;
3852+ sinon . stub ( moduleBitgo , 'getSharedSecret' ) . returns ( Buffer . from ( sharedSecret ) ) ;
3853+
3854+ const bulkUpdateStub = sinon . stub ( Wallets . prototype , 'bulkUpdateWalletShareRequest' ) . resolves ( {
3855+ acceptedWalletShares : [ 'share1' ] ,
3856+ rejectedWalletShares : [ ] ,
3857+ walletShareUpdateErrors : [ ] ,
3858+ } ) ;
3859+
3860+ const result = await wallets . bulkUpdateWalletShare ( {
3861+ shares : [ { walletShareId : 'share1' , status : 'accept' } ] ,
3862+ userLoginPassword : walletPassphrase ,
3863+ newWalletPassphrase : 'newPassphrase' ,
3864+ } ) ;
3865+
3866+ assert . deepEqual ( result , {
3867+ acceptedWalletShares : [ 'share1' ] ,
3868+ rejectedWalletShares : [ ] ,
3869+ walletShareUpdateErrors : [ ] ,
3870+ } ) ;
3871+
3872+ // Both decrypt calls must have gone through decryptAsync
3873+ assert . equal ( decryptAsyncStub . callCount , 2 ) ;
3874+ bulkUpdateStub . calledOnce . should . be . true ( ) ;
3875+ encryptStub . calledOnce . should . be . true ( ) ;
3876+ encryptStub . firstCall . args [ 0 ] . should . have . property ( 'input' , originalPrivKey ) ;
3877+ } ) ;
36283878 } ) ;
36293879 } ) ;
36303880
0 commit comments