@@ -263,6 +263,122 @@ describe('signTxRequest:', function () {
263263 nockPromises [ 2 ] . isDone ( ) . should . be . true ( ) ;
264264 } ) ;
265265
266+ describe ( 'v2 encryption (offline rounds with adata)' , function ( ) {
267+ it ( 'e2e: 3-round offline signing with v2 encrypted keys preserves adata context binding' , async function ( ) {
268+ const walletPassphrase = 'testpassphrase' ;
269+ const userShare = fs . readFileSync ( shareFiles [ vector . party1 ] ) ;
270+ const userPrvBase64 = Buffer . from ( userShare ) . toString ( 'base64' ) ;
271+
272+ // Encrypt the prv with v2 to trigger the v2 path
273+ const encryptedPrv = await bitgo . encryptAsync ( {
274+ input : userPrvBase64 ,
275+ password : walletPassphrase ,
276+ encryptionVersion : 2 ,
277+ } ) ;
278+ JSON . parse ( encryptedPrv ) . v . should . equal ( 2 ) ;
279+
280+ // Round 1: encrypt session + GPG key with v2 + adata (purely local, no server call)
281+ const round1Result = await tssUtils . createOfflineRound1Share ( {
282+ txRequest,
283+ prv : userPrvBase64 ,
284+ walletPassphrase,
285+ encryptedPrv,
286+ } ) ;
287+
288+ // Verify round 1 output has v2 envelopes with adata
289+ const r1SessionEnvelope = JSON . parse ( round1Result . encryptedRound1Session ) ;
290+ r1SessionEnvelope . v . should . equal ( 2 ) ;
291+ r1SessionEnvelope . should . have . property ( 'adata' ) ;
292+ r1SessionEnvelope . should . have . property ( 'hkdfSalt' ) ;
293+
294+ const r1GpgEnvelope = JSON . parse ( round1Result . encryptedUserGpgPrvKey ) ;
295+ r1GpgEnvelope . v . should . equal ( 2 ) ;
296+ r1GpgEnvelope . should . have . property ( 'adata' ) ;
297+ r1SessionEnvelope . adata . should . equal ( r1GpgEnvelope . adata ) ;
298+
299+ // Nock BitGo round 1 response and submit
300+ await nockTxRequestResponseSignatureShareRoundOne ( bitgoParty , txRequest , bitgoGpgKey ) ;
301+ const transactions = getRoute ( 'ecdsa' ) ;
302+ const round1TxRequestResponse = await bitgo
303+ . post ( bitgo . url ( `/wallet/${ txRequest . walletId } /txrequests/${ txRequest . txRequestId + transactions } /sign` , 2 ) )
304+ . send ( {
305+ signatureShares : [ round1Result . signatureShareRound1 ] ,
306+ signerGpgPublicKey : round1Result . userGpgPubKey ,
307+ } )
308+ . result ( ) ;
309+
310+ // Merge server response with original txRequest (server only returns signatureShares)
311+ const round1TxReq : TxRequest = {
312+ ...txRequest ,
313+ transactions : [
314+ {
315+ ...txRequest . transactions ! [ 0 ] ,
316+ signatureShares : round1TxRequestResponse . transactions [ 0 ] . signatureShares ,
317+ } ,
318+ ] ,
319+ } ;
320+
321+ // Round 2: decrypt v2 round 1 session (validates adata), encrypt round 2 session
322+ const round2Result = await tssUtils . createOfflineRound2Share ( {
323+ txRequest : round1TxReq ,
324+ prv : userPrvBase64 ,
325+ walletPassphrase,
326+ bitgoPublicGpgKey : bitgoGpgKey . publicKey ,
327+ encryptedUserGpgPrvKey : round1Result . encryptedUserGpgPrvKey ,
328+ encryptedRound1Session : round1Result . encryptedRound1Session ,
329+ } ) ;
330+
331+ // Verify round 2 output has v2 envelope with adata
332+ const r2Envelope = JSON . parse ( round2Result . encryptedRound2Session ) ;
333+ r2Envelope . v . should . equal ( 2 ) ;
334+ r2Envelope . should . have . property ( 'adata' ) ;
335+ r2Envelope . adata . should . equal ( r1SessionEnvelope . adata ) ;
336+
337+ // Nock BitGo round 2 response and submit
338+ await nockTxRequestResponseSignatureShareRoundTwo ( bitgoParty , txRequest , bitgoGpgKey ) ;
339+ const round2TxRequestResponse = await bitgo
340+ . post ( bitgo . url ( `/wallet/${ txRequest . walletId } /txrequests/${ txRequest . txRequestId + transactions } /sign` , 2 ) )
341+ . send ( {
342+ signatureShares : [ round2Result . signatureShareRound2 ] ,
343+ signerGpgPublicKey : round1Result . userGpgPubKey ,
344+ } )
345+ . result ( ) ;
346+
347+ const round2TxReq : TxRequest = {
348+ ...txRequest ,
349+ transactions : [
350+ {
351+ ...txRequest . transactions ! [ 0 ] ,
352+ signatureShares : round2TxRequestResponse . transactions [ 0 ] . signatureShares ,
353+ } ,
354+ ] ,
355+ } ;
356+
357+ // Round 3: decrypt v2 round 2 session (validates adata), produce final signature share
358+ const round3Result = await tssUtils . createOfflineRound3Share ( {
359+ txRequest : round2TxReq ,
360+ prv : userPrvBase64 ,
361+ walletPassphrase,
362+ bitgoPublicGpgKey : bitgoGpgKey . publicKey ,
363+ encryptedUserGpgPrvKey : round1Result . encryptedUserGpgPrvKey ,
364+ encryptedRound2Session : round2Result . encryptedRound2Session ,
365+ } ) ;
366+
367+ round3Result . should . have . property ( 'signatureShareRound3' ) ;
368+ } ) ;
369+
370+ it ( 'validateAdata rejects v2 envelopes with mismatched adata' , async function ( ) {
371+ const ct = await bitgo . encryptAsync ( {
372+ input : 'test-data' ,
373+ password : 'testpass' ,
374+ encryptionVersion : 2 ,
375+ adata : 'context-A' ,
376+ } ) ;
377+
378+ ( ( ) => ( tssUtils as any ) . validateAdata ( 'context-B' , ct ) ) . should . throw ( / A d a t a d o e s n o t m a t c h / ) ;
379+ } ) ;
380+ } ) ;
381+
266382 it ( 'fails to signs a txRequest for a dkls hot wallet after receiving over 3 429 errors' , async function ( ) {
267383 const nockPromises = [
268384 await nockTxRequestResponseSignatureShareRoundOne ( bitgoParty , txRequest , bitgoGpgKey ) ,
0 commit comments