Skip to content

Commit

Permalink
feat(): working international hmac auth
Browse files Browse the repository at this point in the history
  • Loading branch information
tiagosiebler committed Sep 19, 2024
1 parent 7b7b77d commit 62d176f
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 11 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ coverage
ts-adv-trade-test-private.ts
examples/ts-app-priv.ts
examples/ts-commerce.ts
ts-exchange-priv.ts
2 changes: 2 additions & 0 deletions src/CBAppClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export class CBAppClient extends BaseRestClient {
super(restClientOptions, {
...requestOptions,
headers: {
// Some endpoints return a warning if a version header isn't included: https://docs.cdp.coinbase.com/coinbase-app/docs/versioning
// Currently set to a date from the changelog: https://docs.cdp.coinbase.com/coinbase-app/docs/changelog
'CB-VERSION': '2024-09-13',
...requestOptions.headers,
},
Expand Down
64 changes: 54 additions & 10 deletions src/lib/BaseRestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,8 +351,9 @@ export abstract class BaseRestClient {
requestOptions: {
...this.options,
// Prevent credentials from leaking into error messages
apiKeyName: 'omittedFromError',
apiPrivateKey: 'omittedFromError',
apiKey: 'omittedFromError',
apiSecret: 'omittedFromError',
apiPassphrase: 'omittedFromError',
cdpApiKey: 'omittedFromError',
},
requestParams,
Expand Down Expand Up @@ -450,7 +451,7 @@ export abstract class BaseRestClient {

case REST_CLIENT_TYPE_ENUM.exchange: {
// Docs: https://docs.cdp.coinbase.com/exchange/docs/rest-auth
const timestampInSeconds = timestampInMs / 1000;
const timestampInSeconds = timestampInMs / 1000; // decimals are OK

const signInput =
timestampInSeconds + method + endpoint + requestBodyString;
Expand All @@ -468,13 +469,14 @@ export abstract class BaseRestClient {
apiSecret,
'base64',
'SHA-256',
'base64:web',
);

const headers = {
'CB-ACCESS-KEY': apiKey,
'CB-ACCESS-SIGN': sign,
'CB-ACCESS-TIMESTAMP': timestampInSeconds,
'CB-ACCESS-PASSPHRASE': this.apiPassphrase,
'CB-ACCESS-KEY': apiKey,
};

return {
Expand All @@ -491,10 +493,7 @@ export abstract class BaseRestClient {
}

// Docs: https://docs.cdp.coinbase.com/intx/docs/rest-auth
case REST_CLIENT_TYPE_ENUM.international:

// Docs: https://docs.cdp.coinbase.com/prime/docs/rest-authentication
case REST_CLIENT_TYPE_ENUM.prime: {
case REST_CLIENT_TYPE_ENUM.international: {
const timestampInSeconds = String(Math.floor(timestampInMs / 1000));

const signInput =
Expand All @@ -513,6 +512,7 @@ export abstract class BaseRestClient {
apiSecret,
'base64',
'SHA-256',
'base64:web',
);

const headers = {
Expand All @@ -522,6 +522,9 @@ export abstract class BaseRestClient {
'CB-ACCESS-KEY': apiKey,
};

// For CB International, is there demand for FIX
// Docs, FIX: https://docs.cdp.coinbase.com/intx/docs/fix-overview

return {
...res,
sign: sign,
Expand All @@ -530,12 +533,53 @@ export abstract class BaseRestClient {
...headers,
},
};
}

// For CB International, is there demand for FIX
// Docs, FIX: https://docs.cdp.coinbase.com/intx/docs/fix-overview
// Docs: https://docs.cdp.coinbase.com/prime/docs/rest-authentication
case REST_CLIENT_TYPE_ENUM.prime: {
const timestampInSeconds = String(Math.floor(timestampInMs / 1000));

const signInput =
timestampInSeconds + method + endpoint + requestBodyString;

if (!apiSecret) {
throw new Error(`No API secret provided, cannot sign request.`);
}

if (!this.apiPassphrase) {
throw new Error(`No API passphrase provided, cannot sign request.`);
}

const sign = await signMessage(
signInput,
apiSecret,
'base64',
'SHA-256',
'utf',
);

const headers = {
// 'CB-ACCESS-TIMESTAMP': timestampInSeconds,
// 'CB-ACCESS-SIGN': sign,
// 'CB-ACCESS-PASSPHRASE': this.apiPassphrase,
// 'CB-ACCESS-KEY': apiKey,
'X-CB-ACCESS-TIMESTAMP': timestampInSeconds,
'X-CB-ACCESS-SIGNATURE': sign,
'X-CB-ACCESS-PASSPHRASE': this.apiPassphrase,
'X-CB-ACCESS-KEY': apiKey,
};

// For CB Prime, is there demand for FIX
// Docs, FIX: https://docs.cdp.coinbase.com/prime/docs/fix-connectivity

return {
...res,
sign: sign,
queryParamsWithSign: signRequestParams,
headers: {
...headers,
},
};
}
case REST_CLIENT_TYPE_ENUM.commerce: {
// https://docs.cdp.coinbase.com/commerce-onchain/docs/getting-started
Expand Down
34 changes: 33 additions & 1 deletion src/lib/webCryptoAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ function bufferToB64(buffer: ArrayBuffer): string {
return globalThis.btoa(binary);
}

function b64StringToBuffer(input: string): ArrayBuffer {
const binaryString = atob(input); // Decode base64 string
const buffer = new Uint8Array(binaryString.length);

// Convert binary string to a Uint8Array
for (let i = 0; i < binaryString.length; i++) {
buffer[i] = binaryString.charCodeAt(i);
}
return buffer;
}

export type SignEncodeMethod = 'hex' | 'base64';
export type SignAlgorithm = 'SHA-256' | 'SHA-512';

Expand Down Expand Up @@ -51,12 +62,33 @@ export async function signMessage(
secret: string,
method: SignEncodeMethod,
algorithm: SignAlgorithm,
secretEncodeMethod: 'base64:web' | 'utf',
): Promise<string> {
const encoder = new TextEncoder();

let encodedSecret;

switch (secretEncodeMethod) {
// case 'base64:node': {
// encodedSecret = Buffer.from(secret, 'base64');
// break;
// }
case 'base64:web': {
encodedSecret = b64StringToBuffer(secret);
break;
}
case 'utf': {
encodedSecret = encoder.encode(secret);
break;
}
default: {
throw new Error(`Unhandled encoding: "${secretEncodeMethod}"`);
}
}

const key = await globalThis.crypto.subtle.importKey(
'raw',
encoder.encode(secret),
encodedSecret,
{ name: 'HMAC', hash: algorithm },
false,
['sign'],
Expand Down

0 comments on commit 62d176f

Please sign in to comment.