feat(): use web crypto API by default for sign, expose param to inject custom sign function

This commit is contained in:
tiagosiebler
2025-01-21 16:47:14 +00:00
parent 13cc5dd702
commit 13cd799e7c
10 changed files with 317 additions and 55 deletions

View File

@@ -11,7 +11,7 @@ import {
parseRateLimitHeaders,
serializeParams,
} from './requestUtils';
import { signMessage } from './node-support';
import { SignAlgorithm, SignEncodeMethod, signMessage } from './webCryptoAPI';
const ENABLE_HTTP_TRACE =
typeof process === 'object' &&
@@ -394,6 +394,18 @@ export default abstract class BaseRestClient {
};
}
private async signMessage(
paramsStr: string,
secret: string,
method: SignEncodeMethod,
algorithm: SignAlgorithm,
): Promise<string> {
if (typeof this.options.customSignMessageFn === 'function') {
return this.options.customSignMessageFn(paramsStr, secret);
}
return await signMessage(paramsStr, secret, method, algorithm);
}
/**
* @private sign request and set recv window
*/
@@ -441,7 +453,13 @@ export default abstract class BaseRestClient {
const paramsStr = timestamp + key + recvWindow + signRequestParams;
res.sign = await signMessage(paramsStr, this.secret);
res.sign = await this.signMessage(
paramsStr,
this.secret,
'hex',
'SHA-256',
);
res.serializedParams = signRequestParams;
// console.log('sign req: ', {
@@ -473,7 +491,12 @@ export default abstract class BaseRestClient {
sortProperties,
encodeValues,
);
res.sign = await signMessage(res.serializedParams, this.secret);
res.sign = await this.signMessage(
res.serializedParams,
this.secret,
'hex',
'SHA-256',
);
// @ts-ignore
res.paramsWithSign = {

View File

@@ -167,8 +167,6 @@ export abstract class BaseWebsocketClient<
authPrivateConnectionsOnConnect: true,
// Individual requests do not require a signature, so this is disabled.
authPrivateRequests: false,
// Automatically re-authenticate the WS API connection, if previously authenticated. TODO:
reauthWSAPIOnReconnect: true,
...options,
};

View File

@@ -1,29 +0,0 @@
export async function signMessage(
message: string,
secret: string,
): Promise<string> {
const encoder = new TextEncoder();
// eslint-disable-next-line no-undef
const key = await window.crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: { name: 'SHA-256' } },
false,
['sign'],
);
// eslint-disable-next-line no-undef
const signature = await window.crypto.subtle.sign(
'HMAC',
key,
encoder.encode(message),
);
return Array.prototype.map
.call(
new Uint8Array(signature),
(x: { toString: (arg0: number) => string }) =>
('00' + x.toString(16)).slice(-2),
)
.join('');
}

View File

@@ -1,9 +0,0 @@
import { createHmac } from 'crypto';
/** This is async because the browser version uses a promise (browser-support) */
export async function signMessage(
message: string,
secret: string,
): Promise<string> {
return createHmac('sha256', secret).update(message).digest('hex');
}

View File

@@ -77,6 +77,13 @@ export interface RestClientOptions {
/** Default: false. Enable to throw error if rate limit parser fails */
throwOnFailedRateLimitParse?: boolean;
/**
* Allows you to provide a custom "signMessage" function, e.g. to use node's much faster createHmac method
*
* Look in the examples folder for a demonstration on using node's createHmac instead.
*/
customSignMessageFn?: (message: string, secret: string) => Promise<string>;
}
/**

84
src/util/webCryptoAPI.ts Normal file
View File

@@ -0,0 +1,84 @@
import { neverGuard } from './websockets';
function bufferToB64(buffer: ArrayBuffer): string {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return globalThis.btoa(binary);
}
export type SignEncodeMethod = 'hex' | 'base64';
export type SignAlgorithm = 'SHA-256' | 'SHA-512';
/**
* Similar to node crypto's `createHash()` function
*/
export async function hashMessage(
message: string,
method: SignEncodeMethod,
algorithm: SignAlgorithm,
): Promise<string> {
const encoder = new TextEncoder();
const buffer = await globalThis.crypto.subtle.digest(
algorithm,
encoder.encode(message),
);
switch (method) {
case 'hex': {
return Array.from(new Uint8Array(buffer))
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
}
case 'base64': {
return bufferToB64(buffer);
}
default: {
throw neverGuard(method, `Unhandled sign method: "${method}"`);
}
}
}
/**
* Sign a message, with a secret, using the Web Crypto API
*/
export async function signMessage(
message: string,
secret: string,
method: SignEncodeMethod,
algorithm: SignAlgorithm,
): Promise<string> {
const encoder = new TextEncoder();
const key = await globalThis.crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: algorithm },
false,
['sign'],
);
const buffer = await globalThis.crypto.subtle.sign(
'HMAC',
key,
encoder.encode(message),
);
switch (method) {
case 'hex': {
return Array.from(new Uint8Array(buffer))
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
}
case 'base64': {
return bufferToB64(buffer);
}
default: {
throw neverGuard(method, `Unhandled sign method: "${method}"`);
}
}
}