@@ -2313,6 +2313,284 @@ describe('V2 Wallets:', function () {
23132313 } ) ;
23142314 } ) ;
23152315
2316+ describe ( 'acceptShare with webauthnInfo' , ( ) => {
2317+ const sandbox = sinon . createSandbox ( ) ;
2318+
2319+ afterEach ( function ( ) {
2320+ sandbox . verifyAndRestore ( ) ;
2321+ } ) ;
2322+
2323+ it ( 'should include webauthnInfo in updateShare when provided (ECDH branch)' , async function ( ) {
2324+ const shareId = 'test_webauthn_ecdh_1' ;
2325+ const userPassword = 'test_password_123' ;
2326+ const webauthnPassphrase = 'prf-derived-secret' ;
2327+
2328+ const toKeychain = utxoLib . bip32 . fromSeed ( Buffer . from ( 'deadbeef02deadbeef02deadbeef02deadbeef02' , 'hex' ) ) ;
2329+ const path = 'm/999999/1/1' ;
2330+ const pubkey = toKeychain . derivePath ( path ) . publicKey . toString ( 'hex' ) ;
2331+
2332+ const eckey = makeRandomKey ( ) ;
2333+ const secret = getSharedSecret ( eckey , Buffer . from ( pubkey , 'hex' ) ) . toString ( 'hex' ) ;
2334+ const walletPrv = 'wallet-private-key-for-test' ;
2335+ const encryptedPrvFromShare = bitgo . encrypt ( { password : secret , input : walletPrv } ) ;
2336+
2337+ const myEcdhKeychain = await bitgo . keychains ( ) . create ( ) ;
2338+
2339+ const walletShareNock = nock ( bgUrl )
2340+ . get ( `/api/v2/tbtc/walletshare/${ shareId } ` )
2341+ . reply ( 200 , {
2342+ id : shareId ,
2343+ keychain : {
2344+ path : path ,
2345+ fromPubKey : eckey . publicKey . toString ( 'hex' ) ,
2346+ encryptedPrv : encryptedPrvFromShare ,
2347+ toPubKey : pubkey ,
2348+ pub : pubkey ,
2349+ } ,
2350+ } ) ;
2351+
2352+ sandbox . stub ( bitgo , 'getECDHKeychain' ) . resolves ( {
2353+ encryptedXprv : bitgo . encrypt ( { input : myEcdhKeychain . xprv , password : userPassword } ) ,
2354+ } ) ;
2355+
2356+ const prvKey = bitgo . decrypt ( {
2357+ password : userPassword ,
2358+ input : bitgo . encrypt ( { input : myEcdhKeychain . xprv , password : userPassword } ) ,
2359+ } ) ;
2360+ sandbox . stub ( bitgo , 'decrypt' ) . returns ( prvKey ) ;
2361+ sandbox . stub ( moduleBitgo , 'getSharedSecret' ) . returns ( Buffer . from ( secret , 'hex' ) ) ;
2362+
2363+ let capturedBody : any ;
2364+ const acceptShareNock = nock ( bgUrl )
2365+ . post ( `/api/v2/tbtc/walletshare/${ shareId } ` , ( body : any ) => {
2366+ capturedBody = body ;
2367+ return true ;
2368+ } )
2369+ . reply ( 200 , { changed : true , state : 'accepted' } ) ;
2370+
2371+ await wallets . acceptShare ( {
2372+ walletShareId : shareId ,
2373+ userPassword,
2374+ webauthnInfo : {
2375+ otpDeviceId : 'device-001' ,
2376+ prfSalt : 'salt-abc' ,
2377+ passphrase : webauthnPassphrase ,
2378+ } ,
2379+ } ) ;
2380+
2381+ should . exist ( capturedBody . encryptedPrv ) ;
2382+ should . exist ( capturedBody . webauthnInfo ) ;
2383+ should . equal ( capturedBody . webauthnInfo . otpDeviceId , 'device-001' ) ;
2384+ should . equal ( capturedBody . webauthnInfo . prfSalt , 'salt-abc' ) ;
2385+ should . exist ( capturedBody . webauthnInfo . encryptedPrv ) ;
2386+ should . not . exist ( capturedBody . webauthnInfo . passphrase ) ;
2387+
2388+ walletShareNock . done ( ) ;
2389+ acceptShareNock . done ( ) ;
2390+ } ) ;
2391+
2392+ it ( 'should NOT include webauthnInfo when not provided (ECDH backward compat)' , async function ( ) {
2393+ const shareId = 'test_webauthn_ecdh_2' ;
2394+ const userPassword = 'test_password_123' ;
2395+
2396+ const toKeychain = utxoLib . bip32 . fromSeed ( Buffer . from ( 'deadbeef02deadbeef02deadbeef02deadbeef02' , 'hex' ) ) ;
2397+ const path = 'm/999999/1/1' ;
2398+ const pubkey = toKeychain . derivePath ( path ) . publicKey . toString ( 'hex' ) ;
2399+
2400+ const eckey = makeRandomKey ( ) ;
2401+ const secret = getSharedSecret ( eckey , Buffer . from ( pubkey , 'hex' ) ) . toString ( 'hex' ) ;
2402+ const walletPrv = 'wallet-private-key-for-test' ;
2403+ const encryptedPrvFromShare = bitgo . encrypt ( { password : secret , input : walletPrv } ) ;
2404+
2405+ const myEcdhKeychain = await bitgo . keychains ( ) . create ( ) ;
2406+
2407+ const walletShareNock = nock ( bgUrl )
2408+ . get ( `/api/v2/tbtc/walletshare/${ shareId } ` )
2409+ . reply ( 200 , {
2410+ id : shareId ,
2411+ keychain : {
2412+ path : path ,
2413+ fromPubKey : eckey . publicKey . toString ( 'hex' ) ,
2414+ encryptedPrv : encryptedPrvFromShare ,
2415+ toPubKey : pubkey ,
2416+ pub : pubkey ,
2417+ } ,
2418+ } ) ;
2419+
2420+ sandbox . stub ( bitgo , 'getECDHKeychain' ) . resolves ( {
2421+ encryptedXprv : bitgo . encrypt ( { input : myEcdhKeychain . xprv , password : userPassword } ) ,
2422+ } ) ;
2423+
2424+ const prvKey = bitgo . decrypt ( {
2425+ password : userPassword ,
2426+ input : bitgo . encrypt ( { input : myEcdhKeychain . xprv , password : userPassword } ) ,
2427+ } ) ;
2428+ sandbox . stub ( bitgo , 'decrypt' ) . returns ( prvKey ) ;
2429+ sandbox . stub ( moduleBitgo , 'getSharedSecret' ) . returns ( Buffer . from ( secret , 'hex' ) ) ;
2430+
2431+ let capturedBody : any ;
2432+ const acceptShareNock = nock ( bgUrl )
2433+ . post ( `/api/v2/tbtc/walletshare/${ shareId } ` , ( body : any ) => {
2434+ capturedBody = body ;
2435+ return true ;
2436+ } )
2437+ . reply ( 200 , { changed : true , state : 'accepted' } ) ;
2438+
2439+ await wallets . acceptShare ( {
2440+ walletShareId : shareId ,
2441+ userPassword,
2442+ } ) ;
2443+
2444+ should . exist ( capturedBody . encryptedPrv ) ;
2445+ should . not . exist ( capturedBody . webauthnInfo ) ;
2446+
2447+ walletShareNock . done ( ) ;
2448+ acceptShareNock . done ( ) ;
2449+ } ) ;
2450+
2451+ it ( 'should include webauthnInfo in updateShare when provided (userMultiKeyRotationRequired branch)' , async function ( ) {
2452+ const shareId = 'test_webauthn_multi_1' ;
2453+ const userPassword = 'test_password_123' ;
2454+ const webauthnPassphrase = 'prf-derived-secret' ;
2455+ const walletId = 'test_wallet_123' ;
2456+
2457+ const walletShareNock = nock ( bgUrl )
2458+ . get ( `/api/v2/ofc/walletshare/${ shareId } ` )
2459+ . reply ( 200 , {
2460+ userMultiKeyRotationRequired : true ,
2461+ permissions : [ 'admin' , 'spend' , 'view' ] ,
2462+ wallet : walletId ,
2463+ } ) ;
2464+
2465+ const testKeychain = bitgo . coin ( 'ofc' ) . keychains ( ) . create ( ) ;
2466+ const keychain = {
2467+ prv : testKeychain . prv ,
2468+ pub : testKeychain . pub ,
2469+ } ;
2470+
2471+ const keychainsInstance = ofcWallets . baseCoin . keychains ( ) ;
2472+ sandbox . stub ( keychainsInstance , 'create' ) . returns ( keychain ) ;
2473+
2474+ let capturedBody : any ;
2475+ const acceptShareNock = nock ( bgUrl )
2476+ . post ( `/api/v2/ofc/walletshare/${ shareId } ` , ( body : any ) => {
2477+ capturedBody = body ;
2478+ return true ;
2479+ } )
2480+ . reply ( 200 , { changed : true , state : 'accepted' } ) ;
2481+
2482+ await ofcWallets . acceptShare ( {
2483+ walletShareId : shareId ,
2484+ userPassword,
2485+ webauthnInfo : {
2486+ otpDeviceId : 'device-002' ,
2487+ prfSalt : 'salt-def' ,
2488+ passphrase : webauthnPassphrase ,
2489+ } ,
2490+ } ) ;
2491+
2492+ should . exist ( capturedBody . encryptedPrv ) ;
2493+ should . exist ( capturedBody . pub ) ;
2494+ should . exist ( capturedBody . webauthnInfo ) ;
2495+ should . equal ( capturedBody . webauthnInfo . otpDeviceId , 'device-002' ) ;
2496+ should . equal ( capturedBody . webauthnInfo . prfSalt , 'salt-def' ) ;
2497+ should . exist ( capturedBody . webauthnInfo . encryptedPrv ) ;
2498+ should . not . exist ( capturedBody . webauthnInfo . passphrase ) ;
2499+
2500+ walletShareNock . done ( ) ;
2501+ acceptShareNock . done ( ) ;
2502+ } ) ;
2503+
2504+ it ( 'should NOT include webauthnInfo when not provided (userMultiKeyRotationRequired backward compat)' , async function ( ) {
2505+ const shareId = 'test_webauthn_multi_2' ;
2506+ const userPassword = 'test_password_123' ;
2507+ const walletId = 'test_wallet_123' ;
2508+
2509+ const walletShareNock = nock ( bgUrl )
2510+ . get ( `/api/v2/ofc/walletshare/${ shareId } ` )
2511+ . reply ( 200 , {
2512+ userMultiKeyRotationRequired : true ,
2513+ permissions : [ 'admin' , 'spend' , 'view' ] ,
2514+ wallet : walletId ,
2515+ } ) ;
2516+
2517+ const testKeychain = bitgo . coin ( 'ofc' ) . keychains ( ) . create ( ) ;
2518+ const keychain = {
2519+ prv : testKeychain . prv ,
2520+ pub : testKeychain . pub ,
2521+ } ;
2522+
2523+ const keychainsInstance = ofcWallets . baseCoin . keychains ( ) ;
2524+ sandbox . stub ( keychainsInstance , 'create' ) . returns ( keychain ) ;
2525+
2526+ const encryptedPrv = bitgo . encrypt ( { input : keychain . prv , password : userPassword } ) ;
2527+ sandbox . stub ( bitgo , 'encrypt' ) . returns ( encryptedPrv ) ;
2528+
2529+ let capturedBody : any ;
2530+ const acceptShareNock = nock ( bgUrl )
2531+ . post ( `/api/v2/ofc/walletshare/${ shareId } ` , ( body : any ) => {
2532+ capturedBody = body ;
2533+ return true ;
2534+ } )
2535+ . reply ( 200 , { changed : true , state : 'accepted' } ) ;
2536+
2537+ await ofcWallets . acceptShare ( {
2538+ walletShareId : shareId ,
2539+ userPassword,
2540+ } ) ;
2541+
2542+ should . exist ( capturedBody . encryptedPrv ) ;
2543+ should . exist ( capturedBody . pub ) ;
2544+ should . not . exist ( capturedBody . webauthnInfo ) ;
2545+
2546+ walletShareNock . done ( ) ;
2547+ acceptShareNock . done ( ) ;
2548+ } ) ;
2549+
2550+ it ( 'should NOT include webauthnInfo for keychainOverrideRequired case even when provided' , async function ( ) {
2551+ const shareId = 'test_webauthn_override_1' ;
2552+ const userPassword = 'test_password_123' ;
2553+ const keychainId = 'test_keychain_id' ;
2554+
2555+ const keyChainNock = nock ( bgUrl )
2556+ . post ( '/api/v2/ofc/key' , _ . conforms ( { pub : ( p ) => p . startsWith ( 'xpub' ) } ) )
2557+ . reply ( 200 , ( uri , requestBody ) => {
2558+ return { id : keychainId , encryptedPrv : requestBody [ 'encryptedPrv' ] , pub : requestBody [ 'pub' ] } ;
2559+ } ) ;
2560+
2561+ const walletShareNock = nock ( bgUrl )
2562+ . get ( `/api/v2/ofc/walletshare/${ shareId } ` )
2563+ . reply ( 200 , {
2564+ keychainOverrideRequired : true ,
2565+ permissions : [ 'admin' , 'spend' , 'view' ] ,
2566+ } ) ;
2567+
2568+ let capturedBody : any ;
2569+ const acceptShareNock = nock ( bgUrl )
2570+ . post ( `/api/v2/ofc/walletshare/${ shareId } ` , ( body : any ) => {
2571+ capturedBody = body ;
2572+ return true ;
2573+ } )
2574+ . reply ( 200 , { changed : false } ) ;
2575+
2576+ await ofcWallets . acceptShare ( {
2577+ walletShareId : shareId ,
2578+ userPassword,
2579+ webauthnInfo : {
2580+ otpDeviceId : 'device-003' ,
2581+ prfSalt : 'salt-ghi' ,
2582+ passphrase : 'prf-derived-secret' ,
2583+ } ,
2584+ } ) ;
2585+
2586+ should . not . exist ( capturedBody . webauthnInfo ) ;
2587+
2588+ walletShareNock . done ( ) ;
2589+ keyChainNock . done ( ) ;
2590+ acceptShareNock . done ( ) ;
2591+ } ) ;
2592+ } ) ;
2593+
23162594 it ( 'should share a wallet to viewer' , async function ( ) {
23172595 const shareId = '12311' ;
23182596
0 commit comments