From ecabc9c8f6c7cff6bf6c82fd9158534befb699f1 Mon Sep 17 00:00:00 2001 From: Bahaa Desoky Date: Tue, 21 Oct 2025 11:40:08 -0400 Subject: [PATCH] feat(sdk-hmac): add Buffer support for HMAC generation Ticket: ANT-1033 --- modules/sdk-api/src/bitgoAPI.ts | 4 +- modules/sdk-hmac/src/hmac.ts | 43 ++++++++++++------- modules/sdk-hmac/src/types.ts | 20 ++++----- modules/sdk-hmac/test/hmac.ts | 75 +++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 28 deletions(-) diff --git a/modules/sdk-api/src/bitgoAPI.ts b/modules/sdk-api/src/bitgoAPI.ts index 339f89fc5a..e947001cae 100644 --- a/modules/sdk-api/src/bitgoAPI.ts +++ b/modules/sdk-api/src/bitgoAPI.ts @@ -520,9 +520,9 @@ export class BitGoAPI implements BitGoBase { * @param timestamp request timestamp from `Date.now()` * @param statusCode Only set for HTTP responses, leave blank for requests * @param method request method - * @returns {string} + * @returns {string | Buffer} */ - calculateHMACSubject(params: CalculateHmacSubjectOptions): string { + calculateHMACSubject(params: CalculateHmacSubjectOptions): T { return sdkHmac.calculateHMACSubject({ ...params, authVersion: this._authVersion }); } diff --git a/modules/sdk-hmac/src/hmac.ts b/modules/sdk-hmac/src/hmac.ts index 8be11f396d..d77e33cba5 100644 --- a/modules/sdk-hmac/src/hmac.ts +++ b/modules/sdk-hmac/src/hmac.ts @@ -27,45 +27,56 @@ export function calculateHMAC(key: string | BinaryLike | KeyObject, message: str * @param timestamp request timestamp from `Date.now()` * @param statusCode Only set for HTTP responses, leave blank for requests * @param method request method - * @returns {string} + * @param authVersion authentication version (2 or 3) + * @returns {string | Buffer} */ -export function calculateHMACSubject({ +export function calculateHMACSubject({ urlPath, text, timestamp, statusCode, method, authVersion, -}: CalculateHmacSubjectOptions): string { +}: CalculateHmacSubjectOptions): T { /* Normalize legacy 'del' to 'delete' for backward compatibility */ if (method === 'del') { method = 'delete'; } const urlDetails = urlLib.parse(urlPath); const queryPath = urlDetails.query && urlDetails.query.length > 0 ? urlDetails.path : urlDetails.pathname; + + let prefixedText: string; if (statusCode !== undefined && isFinite(statusCode) && Number.isInteger(statusCode)) { - if (authVersion === 3) { - return [method.toUpperCase(), timestamp, queryPath, statusCode, text].join('|'); - } - return [timestamp, queryPath, statusCode, text].join('|'); + prefixedText = + authVersion === 3 + ? [method.toUpperCase(), timestamp, queryPath, statusCode].join('|') + : [timestamp, queryPath, statusCode].join('|'); + } else { + prefixedText = + authVersion === 3 + ? [method.toUpperCase(), timestamp, '3.0', queryPath].join('|') + : [timestamp, queryPath].join('|'); } - if (authVersion === 3) { - return [method.toUpperCase(), timestamp, '3.0', queryPath, text].join('|'); + prefixedText += '|'; + + const isBuffer = Buffer.isBuffer(text); + if (isBuffer) { + return Buffer.concat([Buffer.from(prefixedText, 'utf-8'), text]) as T; } - return [timestamp, queryPath, text].join('|'); + return (prefixedText + text) as T; } /** * Calculate the HMAC for an HTTP request */ -export function calculateRequestHMAC({ +export function calculateRequestHMAC({ url: urlPath, text, timestamp, token, method, authVersion, -}: CalculateRequestHmacOptions): string { +}: CalculateRequestHmacOptions): string { const signatureSubject = calculateHMACSubject({ urlPath, text, timestamp, method, authVersion }); // calculate the HMAC @@ -75,13 +86,13 @@ export function calculateRequestHMAC({ /** * Calculate request headers with HMAC */ -export function calculateRequestHeaders({ +export function calculateRequestHeaders({ url, text, token, method, authVersion, -}: CalculateRequestHeadersOptions): RequestHeaders { +}: CalculateRequestHeadersOptions): RequestHeaders { const timestamp = Date.now(); const hmac = calculateRequestHMAC({ url, text, timestamp, token, method, authVersion }); @@ -98,7 +109,7 @@ export function calculateRequestHeaders({ /** * Verify the HMAC for an HTTP response */ -export function verifyResponse({ +export function verifyResponse({ url: urlPath, statusCode, text, @@ -107,7 +118,7 @@ export function verifyResponse({ hmac, method, authVersion, -}: VerifyResponseOptions): VerifyResponseInfo { +}: VerifyResponseOptions): VerifyResponseInfo { const signatureSubject = calculateHMACSubject({ urlPath, text, diff --git a/modules/sdk-hmac/src/types.ts b/modules/sdk-hmac/src/types.ts index b90304983c..5d9db2066f 100644 --- a/modules/sdk-hmac/src/types.ts +++ b/modules/sdk-hmac/src/types.ts @@ -2,27 +2,27 @@ export const supportedRequestMethods = ['get', 'post', 'put', 'del', 'patch', 'o export type AuthVersion = 2 | 3; -export interface CalculateHmacSubjectOptions { +export interface CalculateHmacSubjectOptions { urlPath: string; - text: string; + text: T; timestamp: number; method: (typeof supportedRequestMethods)[number]; statusCode?: number; authVersion: AuthVersion; } -export interface CalculateRequestHmacOptions { +export interface CalculateRequestHmacOptions { url: string; - text: string; + text: T; timestamp: number; token: string; method: (typeof supportedRequestMethods)[number]; authVersion: AuthVersion; } -export interface CalculateRequestHeadersOptions { +export interface CalculateRequestHeadersOptions { url: string; - text: string; + text: T; token: string; method: (typeof supportedRequestMethods)[number]; authVersion: AuthVersion; @@ -34,20 +34,20 @@ export interface RequestHeaders { tokenHash: string; } -export interface VerifyResponseOptions extends CalculateRequestHeadersOptions { +export interface VerifyResponseOptions extends CalculateRequestHeadersOptions { hmac: string; url: string; - text: string; + text: T; timestamp: number; method: (typeof supportedRequestMethods)[number]; statusCode?: number; authVersion: AuthVersion; } -export interface VerifyResponseInfo { +export interface VerifyResponseInfo { isValid: boolean; expectedHmac: string; - signatureSubject: string; + signatureSubject: T; isInResponseValidityWindow: boolean; verificationTime: number; } diff --git a/modules/sdk-hmac/test/hmac.ts b/modules/sdk-hmac/test/hmac.ts index 75f669cd27..c803ceb95c 100644 --- a/modules/sdk-hmac/test/hmac.ts +++ b/modules/sdk-hmac/test/hmac.ts @@ -74,6 +74,49 @@ describe('HMAC Utility Functions', () => { }) ).to.equal(expectedSubject); }); + + it('should handle Buffer text input and return a Buffer for requests', () => { + const buffer = Buffer.from('binary-data-content'); + const result = calculateHMACSubject({ + urlPath: '/api/test', + text: buffer, + timestamp: MOCK_TIMESTAMP, + method: 'get', + authVersion: 3, + }); + + expect(Buffer.isBuffer(result)).to.be.true; + + // Check the content structure + const expectedPrefix = 'GET|1672531200000|3.0|/api/test|'; + const prefixBuffer = Buffer.from(expectedPrefix, 'utf8'); + + // Manually reconstruct the expected buffer to compare + const expectedBuffer = Buffer.concat([prefixBuffer, buffer]); + expect(result).to.deep.equal(expectedBuffer); + }); + + it('should handle Buffer text input and return a Buffer for responses', () => { + const buffer = Buffer.from('binary-response-data'); + const result = calculateHMACSubject({ + urlPath: '/api/test', + text: buffer, + timestamp: MOCK_TIMESTAMP, + statusCode: 200, + method: 'get', + authVersion: 3, + }); + + expect(Buffer.isBuffer(result)).to.be.true; + + // Check the content structure + const expectedPrefix = 'GET|1672531200000|/api/test|200|'; + const prefixBuffer = Buffer.from(expectedPrefix, 'utf8'); + + // Manually reconstruct the expected buffer to compare + const expectedBuffer = Buffer.concat([prefixBuffer, buffer]); + expect(result).to.deep.equal(expectedBuffer); + }); }); describe('calculateRequestHMAC', () => { @@ -161,5 +204,37 @@ describe('HMAC Utility Functions', () => { expect(result.isInResponseValidityWindow).to.be.false; }); + + it('should verify response with Buffer data', () => { + const responseData = Buffer.from('binary-response-data'); + + // First create an HMAC for this binary data + const signatureSubject = calculateHMACSubject({ + urlPath: '/api/test', + text: responseData, + timestamp: MOCK_TIMESTAMP, + statusCode: 200, + method: 'post', + authVersion: 3, + }); + + const token = 'test-token'; + const expectedHmac = calculateHMAC(token, signatureSubject); + + // Now verify using the generated HMAC + const result = verifyResponse({ + url: '/api/test', + statusCode: 200, + text: responseData, // Use binary data here + timestamp: MOCK_TIMESTAMP, + token: token, + hmac: expectedHmac, + method: 'post', + authVersion: 3, + }); + expect(result.isValid).to.be.true; + expect(result.expectedHmac).to.equal(expectedHmac); + expect(Buffer.isBuffer(result.signatureSubject)).to.be.true; + }); }); });