Skip to content

Commit

Permalink
Merge pull request #31 from tiagosiebler/hmacauth
Browse files Browse the repository at this point in the history
fix(): auth & hmac handling for international and exchange APIs. fix(): auth headers for prime
  • Loading branch information
tiagosiebler authored Sep 19, 2024
2 parents 653e5a6 + 4c45cee commit 89df598
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 25 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
52 changes: 28 additions & 24 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 @@ -385,6 +386,7 @@ export abstract class BaseRestClient {

const apiKey = this.apiKey;
const apiSecret = this.apiSecret;
const apiPassphrase = this.apiPassphrase;
const jwtExpiresSeconds = this.options.jwtExpiresSeconds || 120;

if (!apiKey) {
Expand Down Expand Up @@ -448,9 +450,12 @@ export abstract class BaseRestClient {
// See: https://github.com/tiagosiebler/coinbase-api/issues/24
}

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

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

const signInput =
timestampInSeconds + method + endpoint + requestBodyString;
Expand All @@ -459,7 +464,7 @@ export abstract class BaseRestClient {
throw new Error(`No API secret provided, cannot sign request.`);
}

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

Expand All @@ -468,13 +473,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,
'CB-ACCESS-PASSPHRASE': apiPassphrase,
};

return {
Expand All @@ -486,16 +492,16 @@ export abstract class BaseRestClient {
},
};

// TODO: is there demand for FIX
// For CB Exchange, is there demand for FIX
// Docs, FIX: https://docs.cdp.coinbase.com/exchange/docs/fix-connectivity
}

// Docs: https://docs.cdp.coinbase.com/intx/docs/rest-auth
case REST_CLIENT_TYPE_ENUM.international:
// 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 timestampInSeconds = Math.floor(timestampInMs / 1000); // decimal not allowed

const signInput =
timestampInSeconds + method + endpoint + requestBodyString;
Expand All @@ -504,7 +510,7 @@ export abstract class BaseRestClient {
throw new Error(`No API secret provided, cannot sign request.`);
}

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

Expand All @@ -513,15 +519,19 @@ export abstract class BaseRestClient {
apiSecret,
'base64',
'SHA-256',
'base64:web',
);

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

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

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

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

// For CB Prime, is there demand for FIX
// Docs, FIX: https://docs.cdp.coinbase.com/prime/docs/fix-connectivity
}
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 89df598

Please sign in to comment.