Skip to content

Commit 04eecad

Browse files
committed
feat: move hmac fns to own package
- define hmac related functions in separate module Ticket: CE-5010
1 parent 6bcb1c6 commit 04eecad

File tree

19 files changed

+498
-120
lines changed

19 files changed

+498
-120
lines changed

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-hmac/.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist/
2+
src/opensslbytes.ts

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/

modules/sdk-hmac/.mocharc.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
require: 'ts-node/register'
2+
timeout: '60000'
3+
reporter: 'min'
4+
reporter-option:
5+
- 'cdn=true'
6+
- 'json=false'
7+
exit: true
8+
spec: ['test/**/*.ts']

modules/sdk-hmac/.npmignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
!dist/
2+
.idea/
3+
.prettierrc.yml
4+
tsconfig.json
5+
src/
6+
test/
7+
scripts/
8+
.nyc_output
9+
CODEOWNERS
10+
node_modules/
11+
.prettierignore
12+
.mocharc.js

modules/sdk-hmac/.prettierignore

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

modules/sdk-hmac/.prettierrc.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
printWidth: 120
2+
singleQuote: true
3+
trailingComma: 'es5'

0 commit comments

Comments
 (0)