Skip to content

Commit 918f1e4

Browse files
committed
feat: Add Buffer support for HMAC generation
Ticket: ANT-1033
1 parent 062be3d commit 918f1e4

File tree

4 files changed

+101
-15
lines changed

4 files changed

+101
-15
lines changed

modules/sdk-api/src/bitgoAPI.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,7 @@ export class BitGoAPI implements BitGoBase {
522522
* @param method request method
523523
* @returns {string}
524524
*/
525-
calculateHMACSubject(params: CalculateHmacSubjectOptions): string {
525+
calculateHMACSubject(params: CalculateHmacSubjectOptions): string | Buffer {
526526
return sdkHmac.calculateHMACSubject({ ...params, authVersion: this._authVersion });
527527
}
528528

modules/sdk-hmac/src/hmac.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ 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
*/
3233
export function calculateHMACSubject({
3334
urlPath,
@@ -36,23 +37,33 @@ export function calculateHMACSubject({
3637
statusCode,
3738
method,
3839
authVersion,
39-
}: CalculateHmacSubjectOptions): string {
40+
}: CalculateHmacSubjectOptions): string | Buffer {
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]);
5465
}
55-
return [timestamp, queryPath, text].join('|');
66+
return prefixedText + text;
5667
}
5768

5869
/**

modules/sdk-hmac/src/types.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export type AuthVersion = 2 | 3;
44

55
export interface CalculateHmacSubjectOptions {
66
urlPath: string;
7-
text: string;
7+
text: string | Buffer;
88
timestamp: number;
99
method: (typeof supportedRequestMethods)[number];
1010
statusCode?: number;
@@ -13,7 +13,7 @@ export interface CalculateHmacSubjectOptions {
1313

1414
export interface CalculateRequestHmacOptions {
1515
url: string;
16-
text: string;
16+
text: string | Buffer;
1717
timestamp: number;
1818
token: string;
1919
method: (typeof supportedRequestMethods)[number];
@@ -22,7 +22,7 @@ export interface CalculateRequestHmacOptions {
2222

2323
export interface CalculateRequestHeadersOptions {
2424
url: string;
25-
text: string;
25+
text: string | Buffer;
2626
token: string;
2727
method: (typeof supportedRequestMethods)[number];
2828
authVersion: AuthVersion;
@@ -37,7 +37,7 @@ export interface RequestHeaders {
3737
export interface VerifyResponseOptions extends CalculateRequestHeadersOptions {
3838
hmac: string;
3939
url: string;
40-
text: string;
40+
text: string | Buffer;
4141
timestamp: number;
4242
method: (typeof supportedRequestMethods)[number];
4343
statusCode?: number;
@@ -47,7 +47,7 @@ export interface VerifyResponseOptions extends CalculateRequestHeadersOptions {
4747
export interface VerifyResponseInfo {
4848
isValid: boolean;
4949
expectedHmac: string;
50-
signatureSubject: string;
50+
signatureSubject: string | Buffer;
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)