feat(): use web crypto API by default for sign, expose param to inject custom sign function
This commit is contained in:
@@ -138,12 +138,6 @@ export interface WSClientConfigurableOptions {
|
||||
* Look in the examples folder for a demonstration on using node's createHmac instead.
|
||||
*/
|
||||
customSignMessageFn?: (message: string, secret: string) => Promise<string>;
|
||||
|
||||
/**
|
||||
* If you authenticated the WS API before, automatically try to
|
||||
* re-authenticate the WS API if you're disconnected/reconnected for any reason.
|
||||
*/
|
||||
reauthWSAPIOnReconnect?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,5 +152,4 @@ export interface WebsocketClientOptions extends WSClientConfigurableOptions {
|
||||
recvWindow: number;
|
||||
authPrivateConnectionsOnConnect: boolean;
|
||||
authPrivateRequests: boolean;
|
||||
reauthWSAPIOnReconnect: boolean;
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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('');
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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
84
src/util/webCryptoAPI.ts
Normal 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}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
isWsPong,
|
||||
neverGuard,
|
||||
} from './util';
|
||||
import { signMessage } from './util/node-support';
|
||||
import {
|
||||
BaseWebsocketClient,
|
||||
EmittableEvent,
|
||||
@@ -42,6 +41,7 @@ import {
|
||||
WsOperation,
|
||||
WsRequestOperationBybit,
|
||||
} from './types/websockets/ws-api';
|
||||
import { SignAlgorithm, signMessage } from './util/webCryptoAPI';
|
||||
|
||||
const WS_LOGGER_CATEGORY = { category: 'bybit-ws' };
|
||||
|
||||
@@ -527,6 +527,18 @@ export class WebsocketClient extends BaseWebsocketClient<
|
||||
return '';
|
||||
}
|
||||
|
||||
private async signMessage(
|
||||
paramsStr: string,
|
||||
secret: string,
|
||||
method: 'hex' | 'base64',
|
||||
algorithm: SignAlgorithm,
|
||||
): Promise<string> {
|
||||
if (typeof this.options.customSignMessageFn === 'function') {
|
||||
return this.options.customSignMessageFn(paramsStr, secret);
|
||||
}
|
||||
return await signMessage(paramsStr, secret, method, algorithm);
|
||||
}
|
||||
|
||||
protected async getWsAuthRequestEvent(wsKey: WsKey): Promise<any> {
|
||||
try {
|
||||
const { signature, expiresAt } = await this.getWsAuthSignature(wsKey);
|
||||
@@ -566,9 +578,11 @@ export class WebsocketClient extends BaseWebsocketClient<
|
||||
|
||||
const signatureExpiresAt = Date.now() + this.getTimeOffsetMs() + recvWindow;
|
||||
|
||||
const signature = await signMessage(
|
||||
const signature = await this.signMessage(
|
||||
'GET/realtime' + signatureExpiresAt,
|
||||
secret,
|
||||
'hex',
|
||||
'SHA-256',
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -951,7 +965,7 @@ export class WebsocketClient extends BaseWebsocketClient<
|
||||
*
|
||||
* Returned promise is rejected if an exception is detected in the reply OR the connection disconnects for any reason (even if automatic reconnect will happen).
|
||||
*
|
||||
* After a fresh connection, you should always send a login request first.
|
||||
* Authentication is automatic. If you didn't request authentication yourself, there might be a small delay after your first request, while the SDK automatically authenticates.
|
||||
*
|
||||
* If you authenticated once and you're reconnected later (e.g. connection temporarily lost), the SDK will by default automatically:
|
||||
* - Detect you were authenticated to the WS API before
|
||||
|
||||
Reference in New Issue
Block a user