Skip to content

Commit 8c5559e

Browse files
authored
Merge pull request #5336 from BitGo/aloe/hmac
feat: move hmac fns to own package
2 parents 03402e3 + 589dd29 commit 8c5559e

23 files changed

+536
-123
lines changed

Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ COPY --from=builder /tmp/bitgo/modules/abstract-lightning /var/modules/abstract-
5252
COPY --from=builder /tmp/bitgo/modules/abstract-utxo /var/modules/abstract-utxo/
5353
COPY --from=builder /tmp/bitgo/modules/blockapis /var/modules/blockapis/
5454
COPY --from=builder /tmp/bitgo/modules/sdk-api /var/modules/sdk-api/
55+
COPY --from=builder /tmp/bitgo/modules/sdk-hmac /var/modules/sdk-hmac/
5556
COPY --from=builder /tmp/bitgo/modules/unspents /var/modules/unspents/
5657
COPY --from=builder /tmp/bitgo/modules/account-lib /var/modules/account-lib/
5758
COPY --from=builder /tmp/bitgo/modules/sdk-coin-algo /var/modules/sdk-coin-algo/
@@ -126,6 +127,7 @@ cd /var/modules/abstract-lightning && yarn link && \
126127
cd /var/modules/abstract-utxo && yarn link && \
127128
cd /var/modules/blockapis && yarn link && \
128129
cd /var/modules/sdk-api && yarn link && \
130+
cd /var/modules/sdk-hmac && yarn link && \
129131
cd /var/modules/unspents && yarn link && \
130132
cd /var/modules/account-lib && yarn link && \
131133
cd /var/modules/sdk-coin-algo && yarn link && \
@@ -203,6 +205,7 @@ RUN cd /var/bitgo-express && \
203205
yarn link @bitgo/abstract-utxo && \
204206
yarn link @bitgo/blockapis && \
205207
yarn link @bitgo/sdk-api && \
208+
yarn link @bitgo/sdk-hmac && \
206209
yarn link @bitgo/unspents && \
207210
yarn link @bitgo/account-lib && \
208211
yarn link @bitgo/sdk-coin-algo && \

modules/bitgo/test/v2/unit/auth.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ import * as sinon from 'sinon';
55
import { BitGo } from '../../../src';
66

77
describe('Auth', () => {
8+
let sandbox;
9+
beforeEach(() => {
10+
sandbox = sinon.createSandbox();
11+
});
12+
afterEach(() => {
13+
sandbox.restore();
14+
});
815
describe('Auth V3', () => {
916
it('should set auth version to 3 when initializing a bitgo object with explicit auth version 3', () => {
1017
const bitgo = new BitGo({ authVersion: 3 });
@@ -74,7 +81,10 @@ describe('Auth', () => {
7481
const accessToken = `v2x${'0'.repeat(64)}`;
7582
const bitgo = new BitGo({ authVersion: 3, accessToken });
7683

77-
const calculateHMACSpy = sinon.spy(bitgo, 'calculateHMAC');
84+
const crypto = require('crypto');
85+
const createHmacSpy = sinon.spy(crypto, 'createHmac');
86+
const updateSpy = sinon.spy(crypto.Hmac.prototype, 'update');
87+
7888
const verifyResponseStub = sinon.stub(bitgo, 'verifyResponse').returns({
7989
isValid: true,
8090
isInResponseValidityWindow: true,
@@ -86,8 +96,10 @@ describe('Auth', () => {
8696
const scope = nock(url).get('/').reply(200);
8797

8898
await bitgo.get(url).should.eventually.have.property('status', 200);
89-
calculateHMACSpy.firstCall.calledWith(accessToken, sinon.match('3.0')).should.be.true();
90-
calculateHMACSpy.restore();
99+
100+
createHmacSpy.firstCall.calledWith('sha256', accessToken).should.be.true();
101+
updateSpy.firstCall.calledWith(sinon.match('3.0')).should.be.true();
102+
createHmacSpy.restore();
91103
verifyResponseStub.restore();
92104
scope.done();
93105
});

modules/bitgo/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@
6060
{
6161
"path": "../sdk-api"
6262
},
63+
{
64+
"path": "../sdk-hmac"
65+
},
6366
{
6467
"path": "../sdk-coin-ada"
6568
},

modules/sdk-api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
},
4242
"dependencies": {
4343
"@bitgo/sdk-core": "^28.18.0",
44+
"@bitgo/sdk-hmac": "^1.0.0",
4445
"@bitgo/sjcl": "^1.0.1",
4546
"@bitgo/unspents": "^0.47.17",
4647
"@bitgo/utxo-lib": "^11.2.1",

modules/sdk-api/src/api.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import querystring from 'querystring';
1111

1212
import { ApiResponseError, BitGoRequest } from '@bitgo/sdk-core';
1313

14-
import { VerifyResponseOptions } from './types';
14+
import { AuthVersion, VerifyResponseOptions } from './types';
1515
import { BitGoAPI } from './bitgoAPI';
1616

1717
const debug = Debug('bitgo:api');
@@ -178,7 +178,8 @@ export function verifyResponse(
178178
token: string | undefined,
179179
method: VerifyResponseOptions['method'],
180180
req: superagent.SuperAgentRequest,
181-
response: superagent.Response
181+
response: superagent.Response,
182+
authVersion: AuthVersion
182183
): superagent.Response {
183184
// we can't verify the response if we're not authenticated
184185
if (!req.isV2Authenticated || !req.authenticationToken) {
@@ -193,6 +194,7 @@ export function verifyResponse(
193194
timestamp: response.header.timestamp,
194195
token: req.authenticationToken,
195196
method,
197+
authVersion,
196198
});
197199

198200
if (!verificationResponse.isValid) {

modules/sdk-api/src/bitgoAPI.ts

Lines changed: 14 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,15 @@ import {
2121
makeRandomKey,
2222
sanitizeLegacyPath,
2323
} from '@bitgo/sdk-core';
24-
import * as sjcl from '@bitgo/sjcl';
24+
import * as sdkHmac from '@bitgo/sdk-hmac';
2525
import * as utxolib from '@bitgo/utxo-lib';
2626
import { bip32, ECPairInterface } from '@bitgo/utxo-lib';
2727
import * as bitcoinMessage from 'bitcoinjs-message';
28-
import { createHmac } from 'crypto';
2928
import { type Agent } from 'http';
3029
import debugLib from 'debug';
3130
import * as _ from 'lodash';
3231
import * as secp256k1 from 'secp256k1';
3332
import * as superagent from 'superagent';
34-
import * as urlLib from 'url';
3533
import {
3634
handleResponseError,
3735
handleResponseResult,
@@ -396,6 +394,7 @@ export class BitGoAPI implements BitGoBase {
396394
token: this._token,
397395
method,
398396
text: data || '',
397+
authVersion: this._authVersion,
399398
});
400399
req.set('Auth-Timestamp', requestProperties.timestamp.toString());
401400

@@ -420,7 +419,7 @@ export class BitGoAPI implements BitGoBase {
420419
return onfulfilled(response);
421420
}
422421

423-
const verifiedResponse = verifyResponse(this, this._token, method, req, response);
422+
const verifiedResponse = verifyResponse(this, this._token, method, req, response, this._authVersion);
424423
return onfulfilled(verifiedResponse);
425424
}
426425
: null;
@@ -455,7 +454,7 @@ export class BitGoAPI implements BitGoBase {
455454
* @returns {*} - the result of the HMAC operation
456455
*/
457456
calculateHMAC(key: string, message: string): string {
458-
return createHmac('sha256', key).update(message).digest('hex');
457+
return sdkHmac.calculateHMAC(key, message);
459458
}
460459

461460
/**
@@ -467,83 +466,29 @@ export class BitGoAPI implements BitGoBase {
467466
* @param method request method
468467
* @returns {string}
469468
*/
470-
calculateHMACSubject({ urlPath, text, timestamp, statusCode, method }: CalculateHmacSubjectOptions): string {
471-
const urlDetails = urlLib.parse(urlPath);
472-
const queryPath = urlDetails.query && urlDetails.query.length > 0 ? urlDetails.path : urlDetails.pathname;
473-
if (!_.isUndefined(statusCode) && _.isInteger(statusCode) && _.isFinite(statusCode)) {
474-
if (this._authVersion === 3) {
475-
return [method.toUpperCase(), timestamp, queryPath, statusCode, text].join('|');
476-
}
477-
return [timestamp, queryPath, statusCode, text].join('|');
478-
}
479-
if (this._authVersion === 3) {
480-
return [method.toUpperCase(), timestamp, '3.0', queryPath, text].join('|');
481-
}
482-
return [timestamp, queryPath, text].join('|');
469+
calculateHMACSubject(params: CalculateHmacSubjectOptions): string {
470+
return sdkHmac.calculateHMACSubject({ ...params, authVersion: this._authVersion });
483471
}
484472

485473
/**
486474
* Calculate the HMAC for an HTTP request
487475
*/
488-
calculateRequestHMAC({ url: urlPath, text, timestamp, token, method }: CalculateRequestHmacOptions): string {
489-
const signatureSubject = this.calculateHMACSubject({ urlPath, text, timestamp, method });
490-
491-
// calculate the HMAC
492-
return this.calculateHMAC(token, signatureSubject);
476+
calculateRequestHMAC(params: CalculateRequestHmacOptions): string {
477+
return sdkHmac.calculateRequestHMAC({ ...params, authVersion: this._authVersion });
493478
}
494479

495480
/**
496481
* Calculate request headers with HMAC
497482
*/
498-
calculateRequestHeaders({ url, text, token, method }: CalculateRequestHeadersOptions): RequestHeaders {
499-
const timestamp = Date.now();
500-
const hmac = this.calculateRequestHMAC({ url, text, timestamp, token, method });
501-
502-
// calculate the SHA256 hash of the token
503-
const hashDigest = sjcl.hash.sha256.hash(token);
504-
const tokenHash = sjcl.codec.hex.fromBits(hashDigest);
505-
return {
506-
hmac,
507-
timestamp,
508-
tokenHash,
509-
};
483+
calculateRequestHeaders(params: CalculateRequestHeadersOptions): RequestHeaders {
484+
return sdkHmac.calculateRequestHeaders({ ...params, authVersion: this._authVersion });
510485
}
511486

512487
/**
513488
* Verify the HMAC for an HTTP response
514489
*/
515-
verifyResponse({
516-
url: urlPath,
517-
statusCode,
518-
text,
519-
timestamp,
520-
token,
521-
hmac,
522-
method,
523-
}: VerifyResponseOptions): VerifyResponseInfo {
524-
const signatureSubject = this.calculateHMACSubject({
525-
urlPath,
526-
text,
527-
timestamp,
528-
statusCode,
529-
method,
530-
});
531-
532-
// calculate the HMAC
533-
const expectedHmac = this.calculateHMAC(token, signatureSubject);
534-
535-
// determine if the response is still within the validity window (5 minute window)
536-
const now = Date.now();
537-
const isInResponseValidityWindow = timestamp >= now - 1000 * 60 * 5 && timestamp <= now;
538-
539-
// verify the HMAC and timestamp
540-
return {
541-
isValid: expectedHmac === hmac,
542-
expectedHmac,
543-
signatureSubject,
544-
isInResponseValidityWindow,
545-
verificationTime: now,
546-
};
490+
verifyResponse(params: VerifyResponseOptions): VerifyResponseInfo {
491+
return sdkHmac.verifyResponse({ ...params, authVersion: this._authVersion });
547492
}
548493

549494
/**
@@ -904,7 +849,7 @@ export class BitGoAPI implements BitGoBase {
904849
this._ecdhXprv = responseDetails.ecdhXprv;
905850

906851
// verify the response's authenticity
907-
verifyResponse(this, responseDetails.token, 'post', request, response);
852+
verifyResponse(this, responseDetails.token, 'post', request, response, this._authVersion);
908853

909854
// add the remaining component for easier access
910855
response.body.access_token = this._token;
@@ -1186,7 +1131,7 @@ export class BitGoAPI implements BitGoBase {
11861131
}
11871132

11881133
// verify the authenticity of the server's response before proceeding any further
1189-
verifyResponse(this, this._token, 'post', request, response);
1134+
verifyResponse(this, this._token, 'post', request, response, this._authVersion);
11901135

11911136
const responseDetails = this.handleTokenIssuance(response.body);
11921137
response.body.token = responseDetails.token;

modules/sdk-api/src/types.ts

Lines changed: 10 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import { EnvironmentName, IRequestTracer, V1Network } from '@bitgo/sdk-core';
22
import { ECPairInterface } from '@bitgo/utxo-lib';
33
import { type Agent } from 'http';
4-
4+
export {
5+
supportedRequestMethods,
6+
AuthVersion,
7+
CalculateHmacSubjectOptions,
8+
CalculateRequestHmacOptions,
9+
CalculateRequestHeadersOptions,
10+
RequestHeaders,
11+
VerifyResponseOptions,
12+
VerifyResponseInfo,
13+
} from '@bitgo/sdk-hmac';
514
export interface BitGoAPIOptions {
615
accessToken?: string;
716
authVersion?: 2 | 3;
@@ -36,54 +45,6 @@ export interface PingOptions {
3645
reqId?: IRequestTracer;
3746
}
3847

39-
export const supportedRequestMethods = ['get', 'post', 'put', 'del', 'patch', 'options'] as const;
40-
41-
export interface CalculateHmacSubjectOptions {
42-
urlPath: string;
43-
text: string;
44-
timestamp: number;
45-
method: (typeof supportedRequestMethods)[number];
46-
statusCode?: number;
47-
}
48-
49-
export interface CalculateRequestHmacOptions {
50-
url: string;
51-
text: string;
52-
timestamp: number;
53-
token: string;
54-
method: (typeof supportedRequestMethods)[number];
55-
}
56-
57-
export interface RequestHeaders {
58-
hmac: string;
59-
timestamp: number;
60-
tokenHash: string;
61-
}
62-
63-
export interface CalculateRequestHeadersOptions {
64-
url: string;
65-
text: string;
66-
token: string;
67-
method: (typeof supportedRequestMethods)[number];
68-
}
69-
70-
export interface VerifyResponseOptions extends CalculateRequestHeadersOptions {
71-
hmac: string;
72-
url: string;
73-
text: string;
74-
timestamp: number;
75-
method: (typeof supportedRequestMethods)[number];
76-
statusCode?: number;
77-
}
78-
79-
export interface VerifyResponseInfo {
80-
isValid: boolean;
81-
expectedHmac: string;
82-
signatureSubject: string;
83-
isInResponseValidityWindow: boolean;
84-
verificationTime: number;
85-
}
86-
8748
export interface AuthenticateOptions {
8849
username: string;
8950
password: string;

modules/sdk-api/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
{
1313
"path": "../sdk-core"
1414
},
15+
{
16+
"path": "../sdk-hmac"
17+
},
1518
{
1619
"path": "../utxo-lib"
1720
},

modules/sdk-hmac/.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist/

modules/sdk-hmac/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
.idea/
3+
dist/

0 commit comments

Comments
 (0)