@@ -23,6 +23,7 @@ import {
2323 sanitizeLegacyPath ,
2424} from '@bitgo/sdk-core' ;
2525import * as sdkHmac from '@bitgo/sdk-hmac' ;
26+ import { DefaultHmacAuthStrategy , type IHmacAuthStrategy } from '@bitgo/sdk-hmac' ;
2627import * as utxolib from '@bitgo/utxo-lib' ;
2728import { bip32 , ECPairInterface } from '@bitgo/utxo-lib' ;
2829import * as bitcoinMessage from 'bitcoinjs-message' ;
@@ -37,7 +38,7 @@ import {
3738 serializeRequestData ,
3839 setRequestQueryString ,
3940 toBitgoRequest ,
40- verifyResponse ,
41+ verifyResponseAsync ,
4142} from './api' ;
4243import { decrypt , encrypt } from './encrypt' ;
4344import { verifyAddress } from './v1/verifyAddress' ;
@@ -134,6 +135,7 @@ export class BitGoAPI implements BitGoBase {
134135 private _customProxyAgent ?: Agent ;
135136 private _requestIdPrefix ?: string ;
136137 private getAdditionalHeadersCb ?: AdditionalHeadersCallback ;
138+ protected _hmacAuthStrategy : IHmacAuthStrategy ;
137139
138140 constructor ( params : BitGoAPIOptions = { } ) {
139141 this . getAdditionalHeadersCb = params . getAdditionalHeadersCb ;
@@ -309,6 +311,7 @@ export class BitGoAPI implements BitGoBase {
309311 }
310312
311313 this . _customProxyAgent = params . customProxyAgent ;
314+ this . _hmacAuthStrategy = params . hmacAuthStrategy ?? new DefaultHmacAuthStrategy ( ) ;
312315
313316 // Only fetch constants from constructor if clientConstants was not provided
314317 if ( ! clientConstants ) {
@@ -423,9 +426,12 @@ export class BitGoAPI implements BitGoBase {
423426 // Set the request timeout to just above 5 minutes by default
424427 req . timeout ( ( process . env . BITGO_TIMEOUT as any ) * 1000 || 305 * 1000 ) ;
425428
426- // if there is no token, and we're not logged in, the request cannot be v2 authenticated
429+ // The strategy may have its own signing material (e.g. a CryptoKey
430+ // restored from IndexedDB) independent of this._token.
431+ const strategyAuthenticated = this . _hmacAuthStrategy . isAuthenticated ?.( ) ?? false ;
432+
427433 req . isV2Authenticated = true ;
428- req . authenticationToken = this . _token ;
434+ req . authenticationToken = this . _token ?? ( strategyAuthenticated ? 'strategy-authenticated' : undefined ) ;
429435 // some of the older tokens appear to be only 40 characters long
430436 if ( ( this . _token && this . _token . length !== 67 && this . _token . indexOf ( 'v2x' ) !== 0 ) || req . forceV1Auth ) {
431437 // use the old method
@@ -439,51 +445,66 @@ export class BitGoAPI implements BitGoBase {
439445 req . set ( 'BitGo-Auth-Version' , this . _authVersion === 3 ? '3.0' : '2.0' ) ;
440446
441447 const data = serializeRequestData ( req ) ;
442- if ( this . _token ) {
443- setRequestQueryString ( req ) ;
444-
445- const requestProperties = this . calculateRequestHeaders ( {
446- url : req . url ,
447- token : this . _token ,
448- method,
449- text : data || '' ,
450- authVersion : this . _authVersion ,
451- } ) ;
452- req . set ( 'Auth-Timestamp' , requestProperties . timestamp . toString ( ) ) ;
453-
454- // we're not sending the actual token, but only its hash
455- req . set ( 'Authorization' , 'Bearer ' + requestProperties . tokenHash ) ;
456- debug ( 'sending v2 %s request to %s with token %s' , method , url , this . _token ?. substr ( 0 , 8 ) ) ;
457448
458- // set the HMAC
459- req . set ( 'HMAC' , requestProperties . hmac ) ;
460- }
449+ const sendWithHmac = ( async ( ) => {
450+ if ( this . _token || strategyAuthenticated ) {
451+ setRequestQueryString ( req ) ;
452+
453+ const requestProperties = await this . _hmacAuthStrategy . calculateRequestHeaders ( {
454+ url : req . url ,
455+ token : this . _token ?? '' ,
456+ method,
457+ text : data || '' ,
458+ authVersion : this . _authVersion ,
459+ } ) ;
460+ req . set ( 'Auth-Timestamp' , requestProperties . timestamp . toString ( ) ) ;
461+
462+ req . set ( 'Authorization' , 'Bearer ' + requestProperties . tokenHash ) ;
463+ debug (
464+ 'sending v2 %s request to %s with token %s' ,
465+ method ,
466+ url ,
467+ this . _token ?. substr ( 0 , 8 ) ?? '(strategy-managed)'
468+ ) ;
469+
470+ req . set ( 'HMAC' , requestProperties . hmac ) ;
471+ }
461472
462- if ( this . getAdditionalHeadersCb ) {
463- const additionalHeaders = this . getAdditionalHeadersCb ( method , url , data ) ;
464- for ( const { key, value } of additionalHeaders ) {
465- req . set ( key , value ) ;
473+ if ( this . getAdditionalHeadersCb ) {
474+ const additionalHeaders = this . getAdditionalHeadersCb ( method , url , data ) ;
475+ for ( const { key, value } of additionalHeaders ) {
476+ req . set ( key , value ) ;
477+ }
466478 }
467- }
468479
469- /**
470- * Verify the response before calling the original onfulfilled handler,
471- * and make sure onrejected is called if a verification error is encountered
472- */
473- const newOnFulfilled = onfulfilled
474- ? ( response : superagent . Response ) => {
475- // HMAC verification is only allowed to be skipped in certain environments.
476- // This is checked in the constructor, but checking it again at request time
477- // will help prevent against tampering of this property after the object is created
478- if ( ! this . _hmacVerification && ! common . Environments [ this . getEnv ( ) ] . hmacVerificationEnforced ) {
479- return onfulfilled ( response ) ;
480+ /**
481+ * Verify the response before calling the original onfulfilled handler,
482+ * and make sure onrejected is called if a verification error is encountered
483+ */
484+ const newOnFulfilled = onfulfilled
485+ ? async ( response : superagent . Response ) => {
486+ // HMAC verification is only allowed to be skipped in certain environments.
487+ // This is checked in the constructor, but checking it again at request time
488+ // will help prevent against tampering of this property after the object is created
489+ if ( ! this . _hmacVerification && ! common . Environments [ this . getEnv ( ) ] . hmacVerificationEnforced ) {
490+ return onfulfilled ( response ) ;
491+ }
492+
493+ const verifiedResponse = await verifyResponseAsync (
494+ this ,
495+ this . _token ,
496+ method ,
497+ req ,
498+ response ,
499+ this . _authVersion
500+ ) ;
501+ return onfulfilled ( verifiedResponse ) ;
480502 }
503+ : null ;
504+ return originalThen ( newOnFulfilled ) ;
505+ } ) ( ) ;
481506
482- const verifiedResponse = verifyResponse ( this , this . _token , method , req , response , this . _authVersion ) ;
483- return onfulfilled ( verifiedResponse ) ;
484- }
485- : null ;
486- return originalThen ( newOnFulfilled ) . catch ( onrejected ) ;
507+ return sendWithHmac . catch ( onrejected ) ;
487508 } ;
488509 return toBitgoRequest ( req ) ;
489510 }
@@ -545,12 +566,21 @@ export class BitGoAPI implements BitGoBase {
545566 }
546567
547568 /**
548- * Verify the HMAC for an HTTP response
569+ * Verify the HMAC for an HTTP response (synchronous, uses sdk-hmac directly).
570+ * Kept for backward compatibility with external callers.
549571 */
550572 verifyResponse ( params : VerifyResponseOptions ) : VerifyResponseInfo {
551573 return sdkHmac . verifyResponse ( { ...params , authVersion : this . _authVersion } ) ;
552574 }
553575
576+ /**
577+ * Verify the HMAC for an HTTP response via the configured strategy (async).
578+ * Used internally by the request pipeline.
579+ */
580+ verifyResponseAsync ( params : VerifyResponseOptions ) : Promise < VerifyResponseInfo > {
581+ return this . _hmacAuthStrategy . verifyResponse ( { ...params , authVersion : this . _authVersion } ) ;
582+ }
583+
554584 /**
555585 * Fetch useful constant values from the BitGo server.
556586 * These values do change infrequently, so they need to be fetched,
@@ -772,7 +802,7 @@ export class BitGoAPI implements BitGoBase {
772802 * Process the username, password and otp into an object containing the username and hashed password, ready to
773803 * send to bitgo for authentication.
774804 */
775- preprocessAuthenticationParams ( {
805+ async preprocessAuthenticationParams ( {
776806 username,
777807 password,
778808 otp,
@@ -782,7 +812,7 @@ export class BitGoAPI implements BitGoBase {
782812 forReset2FA,
783813 initialHash,
784814 fingerprintHash,
785- } : AuthenticateOptions ) : ProcessedAuthenticationOptions {
815+ } : AuthenticateOptions ) : Promise < ProcessedAuthenticationOptions > {
786816 if ( ! _ . isString ( username ) ) {
787817 throw new Error ( 'expected string username' ) ;
788818 }
@@ -793,7 +823,7 @@ export class BitGoAPI implements BitGoBase {
793823
794824 const lowerName = username . toLowerCase ( ) ;
795825 // Calculate the password HMAC so we don't send clear-text passwords
796- const hmacPassword = this . calculateHMAC ( lowerName , password ) ;
826+ const hmacPassword = await this . _hmacAuthStrategy . calculateHMAC ( lowerName , password ) ;
797827
798828 const authParams : ProcessedAuthenticationOptions = {
799829 email : lowerName ,
@@ -944,7 +974,7 @@ export class BitGoAPI implements BitGoBase {
944974 }
945975
946976 const forceV1Auth = ! ! params . forceV1Auth ;
947- const authParams = this . preprocessAuthenticationParams ( params ) ;
977+ const authParams = await this . preprocessAuthenticationParams ( params ) ;
948978 const password = params . password ;
949979
950980 if ( this . _token ) {
@@ -981,7 +1011,7 @@ export class BitGoAPI implements BitGoBase {
9811011 this . _ecdhXprv = responseDetails . ecdhXprv ;
9821012
9831013 // verify the response's authenticity
984- verifyResponse ( this , responseDetails . token , 'post' , request , response , this . _authVersion ) ;
1014+ await verifyResponseAsync ( this , responseDetails . token , 'post' , request , response , this . _authVersion ) ;
9851015
9861016 // add the remaining component for easier access
9871017 response . body . access_token = this . _token ;
@@ -1111,15 +1141,15 @@ export class BitGoAPI implements BitGoBase {
11111141
11121142 /**
11131143 */
1114- verifyPassword ( params : VerifyPasswordOptions = { } ) : Promise < any > {
1144+ async verifyPassword ( params : VerifyPasswordOptions = { } ) : Promise < any > {
11151145 if ( ! _ . isString ( params . password ) ) {
11161146 throw new Error ( 'missing required string password' ) ;
11171147 }
11181148
11191149 if ( ! this . _user || ! this . _user . username ) {
11201150 throw new Error ( 'no current user' ) ;
11211151 }
1122- const hmacPassword = this . calculateHMAC ( this . _user . username , params . password ) ;
1152+ const hmacPassword = await this . _hmacAuthStrategy . calculateHMAC ( this . _user . username , params . password ) ;
11231153
11241154 return this . post ( this . url ( '/user/verifypassword' ) ) . send ( { password : hmacPassword } ) . result ( 'valid' ) ;
11251155 }
@@ -1269,7 +1299,7 @@ export class BitGoAPI implements BitGoBase {
12691299 }
12701300
12711301 // verify the authenticity of the server's response before proceeding any further
1272- verifyResponse ( this , this . _token , 'post' , request , response , this . _authVersion ) ;
1302+ await verifyResponseAsync ( this , this . _token , 'post' , request , response , this . _authVersion ) ;
12731303
12741304 const responseDetails = this . handleTokenIssuance ( response . body ) ;
12751305 response . body . token = responseDetails . token ;
@@ -1924,12 +1954,17 @@ export class BitGoAPI implements BitGoBase {
19241954 const v1KeychainUpdatePWResult = await this . keychains ( ) . updatePassword ( updateKeychainPasswordParams ) ;
19251955 const v2Keychains = await this . coin ( coin ) . keychains ( ) . updatePassword ( updateKeychainPasswordParams ) ;
19261956
1957+ const [ hmacOldPassword , hmacNewPassword ] = await Promise . all ( [
1958+ this . _hmacAuthStrategy . calculateHMAC ( user . username , oldPassword ) ,
1959+ this . _hmacAuthStrategy . calculateHMAC ( user . username , newPassword ) ,
1960+ ] ) ;
1961+
19271962 const updatePasswordParams = {
19281963 keychains : v1KeychainUpdatePWResult . keychains ,
19291964 v2_keychains : v2Keychains ,
19301965 version : v1KeychainUpdatePWResult . version ,
1931- oldPassword : this . calculateHMAC ( user . username , oldPassword ) ,
1932- password : this . calculateHMAC ( user . username , newPassword ) ,
1966+ oldPassword : hmacOldPassword ,
1967+ password : hmacNewPassword ,
19331968 } ;
19341969
19351970 // Calculate payload size in KB
0 commit comments