Skip to content

Commit 2828138

Browse files
authored
Merge pull request #7288 from BitGo/ANT-1033
feat: add Buffer support for HMAC generation
2 parents ef69e42 + ecabc9c commit 2828138

File tree

4 files changed

+114
-28
lines changed

4 files changed

+114
-28
lines changed

modules/sdk-api/src/bitgoAPI.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -520,9 +520,9 @@ export class BitGoAPI implements BitGoBase {
520520
* @param timestamp request timestamp from `Date.now()`
521521
* @param statusCode Only set for HTTP responses, leave blank for requests
522522
* @param method request method
523-
* @returns {string}
523+
* @returns {string | Buffer}
524524
*/
525-
calculateHMACSubject(params: CalculateHmacSubjectOptions): string {
525+
calculateHMACSubject<T extends string | Buffer = string>(params: CalculateHmacSubjectOptions<T>): T {
526526
return sdkHmac.calculateHMACSubject({ ...params, authVersion: this._authVersion });
527527
}
528528

modules/sdk-hmac/src/hmac.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,45 +27,56 @@ export function calculateHMAC(key: string | BinaryLike | KeyObject, message: str
2727
* @param timestamp request timestamp from `Date.now()`
2828
* @param statusCode Only set for HTTP responses, leave blank for requests
2929
* @param method request method
30-
* @returns {string}
30+
* @param authVersion authentication version (2 or 3)
31+
* @returns {string | Buffer}
3132
*/
32-
export function calculateHMACSubject({
33+
export function calculateHMACSubject<T extends string | Buffer = string>({
3334
urlPath,
3435
text,
3536
timestamp,
3637
statusCode,
3738
method,
3839
authVersion,
39-
}: CalculateHmacSubjectOptions): string {
40+
}: CalculateHmacSubjectOptions<T>): T {
4041
/* Normalize legacy 'del' to 'delete' for backward compatibility */
4142
if (method === 'del') {
4243
method = 'delete';
4344
}
4445
const urlDetails = urlLib.parse(urlPath);
4546
const queryPath = urlDetails.query && urlDetails.query.length > 0 ? urlDetails.path : urlDetails.pathname;
47+
48+
let prefixedText: string;
4649
if (statusCode !== undefined && isFinite(statusCode) && Number.isInteger(statusCode)) {
47-
if (authVersion === 3) {
48-
return [method.toUpperCase(), timestamp, queryPath, statusCode, text].join('|');
49-
}
50-
return [timestamp, queryPath, statusCode, text].join('|');
50+
prefixedText =
51+
authVersion === 3
52+
? [method.toUpperCase(), timestamp, queryPath, statusCode].join('|')
53+
: [timestamp, queryPath, statusCode].join('|');
54+
} else {
55+
prefixedText =
56+
authVersion === 3
57+
? [method.toUpperCase(), timestamp, '3.0', queryPath].join('|')
58+
: [timestamp, queryPath].join('|');
5159
}
52-
if (authVersion === 3) {
53-
return [method.toUpperCase(), timestamp, '3.0', queryPath, text].join('|');
60+
prefixedText += '|';
61+
62+
const isBuffer = Buffer.isBuffer(text);
63+
if (isBuffer) {
64+
return Buffer.concat([Buffer.from(prefixedText, 'utf-8'), text]) as T;
5465
}
55-
return [timestamp, queryPath, text].join('|');
66+
return (prefixedText + text) as T;
5667
}
5768

5869
/**
5970
* Calculate the HMAC for an HTTP request
6071
*/
61-
export function calculateRequestHMAC({
72+
export function calculateRequestHMAC<T extends string | Buffer = string>({
6273
url: urlPath,
6374
text,
6475
timestamp,
6576
token,
6677
method,
6778
authVersion,
68-
}: CalculateRequestHmacOptions): string {
79+
}: CalculateRequestHmacOptions<T>): string {
6980
const signatureSubject = calculateHMACSubject({ urlPath, text, timestamp, method, authVersion });
7081

7182
// calculate the HMAC
@@ -75,13 +86,13 @@ export function calculateRequestHMAC({
7586
/**
7687
* Calculate request headers with HMAC
7788
*/
78-
export function calculateRequestHeaders({
89+
export function calculateRequestHeaders<T extends string | Buffer = string>({
7990
url,
8091
text,
8192
token,
8293
method,
8394
authVersion,
84-
}: CalculateRequestHeadersOptions): RequestHeaders {
95+
}: CalculateRequestHeadersOptions<T>): RequestHeaders {
8596
const timestamp = Date.now();
8697
const hmac = calculateRequestHMAC({ url, text, timestamp, token, method, authVersion });
8798

@@ -98,7 +109,7 @@ export function calculateRequestHeaders({
98109
/**
99110
* Verify the HMAC for an HTTP response
100111
*/
101-
export function verifyResponse({
112+
export function verifyResponse<T extends string | Buffer = string>({
102113
url: urlPath,
103114
statusCode,
104115
text,
@@ -107,7 +118,7 @@ export function verifyResponse({
107118
hmac,
108119
method,
109120
authVersion,
110-
}: VerifyResponseOptions): VerifyResponseInfo {
121+
}: VerifyResponseOptions<T>): VerifyResponseInfo<T> {
111122
const signatureSubject = calculateHMACSubject({
112123
urlPath,
113124
text,

modules/sdk-hmac/src/types.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,27 @@ export const supportedRequestMethods = ['get', 'post', 'put', 'del', 'patch', 'o
22

33
export type AuthVersion = 2 | 3;
44

5-
export interface CalculateHmacSubjectOptions {
5+
export interface CalculateHmacSubjectOptions<T> {
66
urlPath: string;
7-
text: string;
7+
text: T;
88
timestamp: number;
99
method: (typeof supportedRequestMethods)[number];
1010
statusCode?: number;
1111
authVersion: AuthVersion;
1212
}
1313

14-
export interface CalculateRequestHmacOptions {
14+
export interface CalculateRequestHmacOptions<T extends string | Buffer = string> {
1515
url: string;
16-
text: string;
16+
text: T;
1717
timestamp: number;
1818
token: string;
1919
method: (typeof supportedRequestMethods)[number];
2020
authVersion: AuthVersion;
2121
}
2222

23-
export interface CalculateRequestHeadersOptions {
23+
export interface CalculateRequestHeadersOptions<T extends string | Buffer = string> {
2424
url: string;
25-
text: string;
25+
text: T;
2626
token: string;
2727
method: (typeof supportedRequestMethods)[number];
2828
authVersion: AuthVersion;
@@ -34,20 +34,20 @@ export interface RequestHeaders {
3434
tokenHash: string;
3535
}
3636

37-
export interface VerifyResponseOptions extends CalculateRequestHeadersOptions {
37+
export interface VerifyResponseOptions<T extends string | Buffer = string> extends CalculateRequestHeadersOptions<T> {
3838
hmac: string;
3939
url: string;
40-
text: string;
40+
text: T;
4141
timestamp: number;
4242
method: (typeof supportedRequestMethods)[number];
4343
statusCode?: number;
4444
authVersion: AuthVersion;
4545
}
4646

47-
export interface VerifyResponseInfo {
47+
export interface VerifyResponseInfo<T extends string | Buffer = string> {
4848
isValid: boolean;
4949
expectedHmac: string;
50-
signatureSubject: string;
50+
signatureSubject: T;
5151
isInResponseValidityWindow: boolean;
5252
verificationTime: number;
5353
}

modules/sdk-hmac/test/hmac.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,49 @@ describe('HMAC Utility Functions', () => {
7474
})
7575
).to.equal(expectedSubject);
7676
});
77+
78+
it('should handle Buffer text input and return a Buffer for requests', () => {
79+
const buffer = Buffer.from('binary-data-content');
80+
const result = calculateHMACSubject({
81+
urlPath: '/api/test',
82+
text: buffer,
83+
timestamp: MOCK_TIMESTAMP,
84+
method: 'get',
85+
authVersion: 3,
86+
});
87+
88+
expect(Buffer.isBuffer(result)).to.be.true;
89+
90+
// Check the content structure
91+
const expectedPrefix = 'GET|1672531200000|3.0|/api/test|';
92+
const prefixBuffer = Buffer.from(expectedPrefix, 'utf8');
93+
94+
// Manually reconstruct the expected buffer to compare
95+
const expectedBuffer = Buffer.concat([prefixBuffer, buffer]);
96+
expect(result).to.deep.equal(expectedBuffer);
97+
});
98+
99+
it('should handle Buffer text input and return a Buffer for responses', () => {
100+
const buffer = Buffer.from('binary-response-data');
101+
const result = calculateHMACSubject({
102+
urlPath: '/api/test',
103+
text: buffer,
104+
timestamp: MOCK_TIMESTAMP,
105+
statusCode: 200,
106+
method: 'get',
107+
authVersion: 3,
108+
});
109+
110+
expect(Buffer.isBuffer(result)).to.be.true;
111+
112+
// Check the content structure
113+
const expectedPrefix = 'GET|1672531200000|/api/test|200|';
114+
const prefixBuffer = Buffer.from(expectedPrefix, 'utf8');
115+
116+
// Manually reconstruct the expected buffer to compare
117+
const expectedBuffer = Buffer.concat([prefixBuffer, buffer]);
118+
expect(result).to.deep.equal(expectedBuffer);
119+
});
77120
});
78121

79122
describe('calculateRequestHMAC', () => {
@@ -161,5 +204,37 @@ describe('HMAC Utility Functions', () => {
161204

162205
expect(result.isInResponseValidityWindow).to.be.false;
163206
});
207+
208+
it('should verify response with Buffer data', () => {
209+
const responseData = Buffer.from('binary-response-data');
210+
211+
// First create an HMAC for this binary data
212+
const signatureSubject = calculateHMACSubject({
213+
urlPath: '/api/test',
214+
text: responseData,
215+
timestamp: MOCK_TIMESTAMP,
216+
statusCode: 200,
217+
method: 'post',
218+
authVersion: 3,
219+
});
220+
221+
const token = 'test-token';
222+
const expectedHmac = calculateHMAC(token, signatureSubject);
223+
224+
// Now verify using the generated HMAC
225+
const result = verifyResponse({
226+
url: '/api/test',
227+
statusCode: 200,
228+
text: responseData, // Use binary data here
229+
timestamp: MOCK_TIMESTAMP,
230+
token: token,
231+
hmac: expectedHmac,
232+
method: 'post',
233+
authVersion: 3,
234+
});
235+
expect(result.isValid).to.be.true;
236+
expect(result.expectedHmac).to.equal(expectedHmac);
237+
expect(Buffer.isBuffer(result.signatureSubject)).to.be.true;
238+
});
164239
});
165240
});

0 commit comments

Comments
 (0)