Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions modules/sdk-api/src/bitgoAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends string | Buffer = string>(params: CalculateHmacSubjectOptions<T>): T {
return sdkHmac.calculateHMACSubject({ ...params, authVersion: this._authVersion });
}

Expand Down
43 changes: 27 additions & 16 deletions modules/sdk-hmac/src/hmac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends string | Buffer = string>({
urlPath,
text,
timestamp,
statusCode,
method,
authVersion,
}: CalculateHmacSubjectOptions): string {
}: CalculateHmacSubjectOptions<T>): 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<T extends string | Buffer = string>({
url: urlPath,
text,
timestamp,
token,
method,
authVersion,
}: CalculateRequestHmacOptions): string {
}: CalculateRequestHmacOptions<T>): string {
const signatureSubject = calculateHMACSubject({ urlPath, text, timestamp, method, authVersion });

// calculate the HMAC
Expand All @@ -75,13 +86,13 @@ export function calculateRequestHMAC({
/**
* Calculate request headers with HMAC
*/
export function calculateRequestHeaders({
export function calculateRequestHeaders<T extends string | Buffer = string>({
url,
text,
token,
method,
authVersion,
}: CalculateRequestHeadersOptions): RequestHeaders {
}: CalculateRequestHeadersOptions<T>): RequestHeaders {
const timestamp = Date.now();
const hmac = calculateRequestHMAC({ url, text, timestamp, token, method, authVersion });

Expand All @@ -98,7 +109,7 @@ export function calculateRequestHeaders({
/**
* Verify the HMAC for an HTTP response
*/
export function verifyResponse({
export function verifyResponse<T extends string | Buffer = string>({
url: urlPath,
statusCode,
text,
Expand All @@ -107,7 +118,7 @@ export function verifyResponse({
hmac,
method,
authVersion,
}: VerifyResponseOptions): VerifyResponseInfo {
}: VerifyResponseOptions<T>): VerifyResponseInfo<T> {
const signatureSubject = calculateHMACSubject({
urlPath,
text,
Expand Down
20 changes: 10 additions & 10 deletions modules/sdk-hmac/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@ export const supportedRequestMethods = ['get', 'post', 'put', 'del', 'patch', 'o

export type AuthVersion = 2 | 3;

export interface CalculateHmacSubjectOptions {
export interface CalculateHmacSubjectOptions<T> {
urlPath: string;
text: string;
text: T;
timestamp: number;
method: (typeof supportedRequestMethods)[number];
statusCode?: number;
authVersion: AuthVersion;
}

export interface CalculateRequestHmacOptions {
export interface CalculateRequestHmacOptions<T extends string | Buffer = string> {
url: string;
text: string;
text: T;
timestamp: number;
token: string;
method: (typeof supportedRequestMethods)[number];
authVersion: AuthVersion;
}

export interface CalculateRequestHeadersOptions {
export interface CalculateRequestHeadersOptions<T extends string | Buffer = string> {
url: string;
text: string;
text: T;
token: string;
method: (typeof supportedRequestMethods)[number];
authVersion: AuthVersion;
Expand All @@ -34,20 +34,20 @@ export interface RequestHeaders {
tokenHash: string;
}

export interface VerifyResponseOptions extends CalculateRequestHeadersOptions {
export interface VerifyResponseOptions<T extends string | Buffer = string> extends CalculateRequestHeadersOptions<T> {
hmac: string;
url: string;
text: string;
text: T;
timestamp: number;
method: (typeof supportedRequestMethods)[number];
statusCode?: number;
authVersion: AuthVersion;
}

export interface VerifyResponseInfo {
export interface VerifyResponseInfo<T extends string | Buffer = string> {
isValid: boolean;
expectedHmac: string;
signatureSubject: string;
signatureSubject: T;
isInResponseValidityWindow: boolean;
verificationTime: number;
}
75 changes: 75 additions & 0 deletions modules/sdk-hmac/test/hmac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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;
});
});
});