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
2 changes: 2 additions & 0 deletions .github/workflows/js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ jobs:
run: npm run lint
- name: Build
run: npm run build
- name: Crypto Tests
run: npm run crypto-tests
- name: Static Tests (TS ESM)
run: npm run ts-test-static
- name: Static Tests (JS CJS)
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ Actively maintained, typed, and safe SDK for the Binance REST APIs and Websocket

### Features
- Spot, Margin, Futures and Delivery API
- Portfolio Margin API *\*soon*\*
- Testnet support
- Proxy support (REST and WS)
- Customizable HTTP headers
- Customizable request parameters
- RSA/ECDSA support *\*soon*\*
- RSA/ECDSA support
- Portfolio Margin API *\*soon*\*
- Websocket handling with automatic reconnection
- RecvWindow and automatic timestamps generation
- Ability to call any endpoint, even if not supported directly by the library
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
"ts-test-live": "mocha ./tests/binance-class-live.test.ts",
"ts-test-static": "mocha ./tests/binance-class-static.test.ts",
"test-cjs": "node ./tests/cjs-test.cjs",
"crypto-tests": "mocha ./tests/crypto.test.ts",
"ws-tests": "mocha ./tests/binance-class-ws.test.ts",
"ws-tests-spot": "mocha ./tests/binance-ws-spot.test.ts --exit",
"ws-tests-futures": "mocha ./tests/binance-ws-futures.test.ts --exit",
"test-debug": "mocha --inspect-brk",
Expand Down
49 changes: 47 additions & 2 deletions src/node-binance-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { HttpsProxyAgent } from 'https-proxy-agent';
import { SocksProxyAgent } from 'socks-proxy-agent';
// @ts-ignore
import nodeFetch from 'node-fetch';

// @ts-ignore
import zip from 'lodash.zipobject';
import stringHash from 'string-hash';
Expand Down Expand Up @@ -63,6 +64,8 @@ export default class Binance {

APIKEY: string = undefined;
APISECRET: string = undefined;
PRIVATEKEY: string = undefined;
PRIVATEKEYPASSWORD: string = undefined;
test = false;

timeOffset: number = 0;
Expand Down Expand Up @@ -209,6 +212,8 @@ export default class Binance {

if (this.Options.APIKEY) this.APIKEY = this.Options.APIKEY;
if (this.Options.APISECRET) this.APISECRET = this.Options.APISECRET;
if (this.Options.PRIVATEKEY) this.PRIVATEKEY = this.Options.PRIVATEKEY;
if (this.Options.PRIVATEKEYPASSWORD) this.PRIVATEKEYPASSWORD = this.Options.PRIVATEKEYPASSWORD;
if (this.Options.test) this.test = true;
if (this.Options.headers) this.headers = this.Options.Headers;
if (this.Options.domain) this.domain = this.Options.domain;
Expand Down Expand Up @@ -535,7 +540,7 @@ export default class Binance {
data.timestamp += this.timeOffset;
}
query = this.makeQueryString(data);
data.signature = crypto.createHmac('sha256', this.APISECRET).update(query).digest('hex'); // HMAC hash header
data.signature = this.generateSignature(query);
opt.url = `${url}?${query}&signature=${data.signature}`;
}
(opt as any).qs = data;
Expand Down Expand Up @@ -647,7 +652,9 @@ export default class Binance {

if (!data.recvWindow) data.recvWindow = this.Options.recvWindow;
const query = method === 'POST' && noDataInSignature ? '' : this.makeQueryString(data);
const signature = crypto.createHmac('sha256', this.Options.APISECRET).update(query).digest('hex'); // set the HMAC hash header

const signature = this.generateSignature(query);

if (method === 'POST') {
const opt = this.reqObjPOST(
url,
Expand All @@ -670,6 +677,44 @@ export default class Binance {
}
}

generateSignature(query: string, encode = true) {
const secret = this.APISECRET || this.PRIVATEKEY;
let signature = '';
if (secret.includes ('PRIVATE KEY')) {
// if less than the below length, then it can't be RSA key
let keyObject: crypto.KeyObject;
try {
const privateKeyObj: crypto.PrivateKeyInput = { key: secret };

if (this.PRIVATEKEYPASSWORD) {
privateKeyObj.passphrase = this.PRIVATEKEYPASSWORD;
}

keyObject = crypto.createPrivateKey(privateKeyObj);

} catch (e){
throw new Error(
'Invalid private key. Please provide a valid RSA or ED25519 private key. ' + e.toString()
);
}

if (secret.length > 120) {
// RSA key
signature = crypto
.sign('RSA-SHA256', Buffer.from(query), keyObject)
.toString('base64');
if (encode) signature = encodeURIComponent (signature);
return signature;
} else {
// Ed25519 key
signature = crypto.sign(null, Buffer.from(query), keyObject).toString('base64');
}
} else {
signature = crypto.createHmac('sha256', this.Options.APISECRET).update(query).digest('hex'); // set the HMAC hash header
}
return signature;
}

// --- ENDPOINTS --- //

/**
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ export type Callback = (...args: any) => any;
export interface IConstructorArgs {
APIKEY: string;
APISECRET: string;
PRIVATEKEY: string; // when using RSA/EDCSA keys
PRIVATEKEYPASSWORD: string; // when using RSA/EDCSA keys
recvWindow: number;
useServerTime: boolean;
reconnect: boolean;
Expand Down
50 changes: 50 additions & 0 deletions tests/crypto.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Binance from '../src/node-binance-api';
import { assert } from 'chai';


const testCases = [
{
"description": "Unencrypted PKCS8 ed22519 private key",
"private_key": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIPQmzwVKJETqVd7L9E/DFbkvrOigy1tLL+9QF0mSn6dV\n-----END PRIVATE KEY-----\n",
"password": undefined,
"expected_signature": "a4Pm3p02D2HXtNfo3DBaVCe9Ov7kledewgYtGjekotFmZ5wXa3mC5AtLB7CpAphyNjeyovIuDP+9fyjYmsojCw==",
},
{
"description": "Unencrypted PKCS8 ed22519 private key in bytes",
"private_key": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIPQmzwVKJETqVd7L9E/DFbkvrOigy1tLL+9QF0mSn6dV\n-----END PRIVATE KEY-----\n",
"password": undefined,
"expected_signature": "a4Pm3p02D2HXtNfo3DBaVCe9Ov7kledewgYtGjekotFmZ5wXa3mC5AtLB7CpAphyNjeyovIuDP+9fyjYmsojCw==",
},
{
"description": "Encrypted PKCS8 RSA private key",
"private_key": "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQWW+iEMYYCPUntrPq\nZ2RCMAICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEIw3ViSuTp8JeN43\n5VGlHt0EggTQBvEzd2w2F561CzU+MDouZDOPj4RTIStC471z0/bxTgYqH3gYchoe\nOfi2lsLuD8B+ivIRuXB8GT66BIseIOMV8t/tiMe97rFI/cV4h6DrBO1xlmSrBG97\nvFF9qPA5yPRlrHtWKkGxhXteNVsT3w/7Y7KsulO/gA2KpsOElMElOhUP462Yd0Wl\nOxAIV3+knl2niozws2Kq3EdzTF3N6hlavUPryiU/w4RRsPN5qgjchVVLq/sYRYhx\nN8uWJbkjhCcHsULkD5KkdgddR0VOhpQPXIdY+gPkSBJq1ltRWy/TYdXiU2fEBNZW\nhFUVrxnS76+u2R3vukY2IAX8zTC6h2AbCBG+r4XXzgk/l/4peySKHsPQRzQ0in39\na9o5sctOmUNeD4uJ6cClXDdqyEwXhnPmRKZjJ8qeH4D9wl7HOG7iQsYiyfJe/igi\nFEXVRZOtLBdbwX45rU6wiWWjxzY+mDnw4BXE31ZBPwgtoh+CLTyK8NI8LnCV/CgO\nzOY4sm/KDWmbfTTZjLSdYRFj7wEpOdUWjZ13viDFZqnmy/o1auvLmBcqbRrCyW+B\nOMI7aHE0mZ/52vEFQYU1tH0BxMmRfWXUCJj0TjwxDY6BQmmW4YlhsrgGNekLFDo1\n6phFd0pA4UPqGXfNLzHp1dtLhUEb4YzcpDn+HMzMf1gfez7qeqU28nNFg/AwwqHZ\nTWdGclCFjiah7SfvOslob4vdLGwkUhgCBKQUQoU1DltX2GOgIv9SNY3q6X0NwdZG\nL5gqk225WVUwIRzmi5nfUEXlbaTvyHg3BuGedUKJ91IhRCW1ZjvU8GQcfVsu8bse\nTCKMdr7wi/zEZXSldCza6vL4m3tmBLtWkHVOW8bcDWvoVwRswbFHfleHzckl7EeC\n9C4TRa66gA5UOv14SrpC8noQUNpSegg+1KI4BSNvwaheiSUqjQbisb0qYCxML0ZP\nmQodwVsXG6LYo+Y6y6CpHbT7UYkfa59q/CGOZByL1bEzzgd98ZHwjihOjHVaV6sY\nBW018AvGxr7kjEU4LNqIteydTp0o31ZJN/qK78w5EQFfJxfImrx/E4nYKtg4higj\nKOQCgJALKIveidqQEFsbGWsulYrMXwnu0nPThofR1D8eCJZpdTxvOh2nIrNrAeY8\nZMAwG1uQos5A0yEZ1auHxz+rb4errnk92OnVlWnElf1TwwlkFFNLdNDl8VpiMP40\n6en9VtlOfgH8AwB03WsoeuEQsxYTIcRKWZZPRsLx3hd0BsOw0FcYDSX2XIGPkVVW\niYf9hzFSQsWV3d6utloIm4nG8XONfNaRimGECbUSZyHZimrO1m4Gga5pE3LKuDri\nJKR2lR7b6XPR7+FS+lG1zq5KY7onAVQY1oABfTjpJRju6pQGWt70hairo6EaVC3u\nrBy8UkLwBbfDuigSvsVk+sF2+Ic0IzX6IniU0F5kMe+MKqGB4aicXP6FFGBpPFTe\nv6yHD+DYAu1rnlXrqmFL50CfutTF78uPPJ9D2Sm0DcGPFj+6IrCigj48uxoHR9Qb\nFeNzfsmVwoFAWWq/MpkPbX6Aql8ddCbpMxDUUkybwVV9rJmEMTLil44FrxKAKFhP\n0Av7JeFvdz15pfnf/IQ3IOvVhHGFChFS13sbYSvFHMQF3P0BiyvjhBI=\n-----END ENCRYPTED PRIVATE KEY-----\n",
"password": "testpwd",
"expected_signature": "S4l9IONXGHIdt4NjwmpCIhawDTitjUQls73d+mi0HJTSbTGyn95NabX5hC9+n6HsTqLcWPvxKgTvLFMnTaf6Jxl+xwQMbu9/6mw88KF7i1pEQizerKcr91rPUPVBQ4OY10Q018QEamIAymRgo/eoRYSm7CqCdeibGyO0XfXZBaJnVGFJ9hgrPIwSKHgeUnfK8qMenULvL0qKMEJ6ziYPiqh7k9xX3xIV7lGIpokk+ekqlFd01f/Lov45osJCFuccJO4xuUUZewZnVGF7Uw6Rim3UsKhXKZUN9WZWa5RT+dpBIJ5DTBIXBSvowwj3GZC3j+XvWw8Sn0Ls9836l89BXw==",
},
{
"description": "Encrypted PKCS8 RSA private key in bytes",
"private_key": "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQWW+iEMYYCPUntrPq\nZ2RCMAICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEIw3ViSuTp8JeN43\n5VGlHt0EggTQBvEzd2w2F561CzU+MDouZDOPj4RTIStC471z0/bxTgYqH3gYchoe\nOfi2lsLuD8B+ivIRuXB8GT66BIseIOMV8t/tiMe97rFI/cV4h6DrBO1xlmSrBG97\nvFF9qPA5yPRlrHtWKkGxhXteNVsT3w/7Y7KsulO/gA2KpsOElMElOhUP462Yd0Wl\nOxAIV3+knl2niozws2Kq3EdzTF3N6hlavUPryiU/w4RRsPN5qgjchVVLq/sYRYhx\nN8uWJbkjhCcHsULkD5KkdgddR0VOhpQPXIdY+gPkSBJq1ltRWy/TYdXiU2fEBNZW\nhFUVrxnS76+u2R3vukY2IAX8zTC6h2AbCBG+r4XXzgk/l/4peySKHsPQRzQ0in39\na9o5sctOmUNeD4uJ6cClXDdqyEwXhnPmRKZjJ8qeH4D9wl7HOG7iQsYiyfJe/igi\nFEXVRZOtLBdbwX45rU6wiWWjxzY+mDnw4BXE31ZBPwgtoh+CLTyK8NI8LnCV/CgO\nzOY4sm/KDWmbfTTZjLSdYRFj7wEpOdUWjZ13viDFZqnmy/o1auvLmBcqbRrCyW+B\nOMI7aHE0mZ/52vEFQYU1tH0BxMmRfWXUCJj0TjwxDY6BQmmW4YlhsrgGNekLFDo1\n6phFd0pA4UPqGXfNLzHp1dtLhUEb4YzcpDn+HMzMf1gfez7qeqU28nNFg/AwwqHZ\nTWdGclCFjiah7SfvOslob4vdLGwkUhgCBKQUQoU1DltX2GOgIv9SNY3q6X0NwdZG\nL5gqk225WVUwIRzmi5nfUEXlbaTvyHg3BuGedUKJ91IhRCW1ZjvU8GQcfVsu8bse\nTCKMdr7wi/zEZXSldCza6vL4m3tmBLtWkHVOW8bcDWvoVwRswbFHfleHzckl7EeC\n9C4TRa66gA5UOv14SrpC8noQUNpSegg+1KI4BSNvwaheiSUqjQbisb0qYCxML0ZP\nmQodwVsXG6LYo+Y6y6CpHbT7UYkfa59q/CGOZByL1bEzzgd98ZHwjihOjHVaV6sY\nBW018AvGxr7kjEU4LNqIteydTp0o31ZJN/qK78w5EQFfJxfImrx/E4nYKtg4higj\nKOQCgJALKIveidqQEFsbGWsulYrMXwnu0nPThofR1D8eCJZpdTxvOh2nIrNrAeY8\nZMAwG1uQos5A0yEZ1auHxz+rb4errnk92OnVlWnElf1TwwlkFFNLdNDl8VpiMP40\n6en9VtlOfgH8AwB03WsoeuEQsxYTIcRKWZZPRsLx3hd0BsOw0FcYDSX2XIGPkVVW\niYf9hzFSQsWV3d6utloIm4nG8XONfNaRimGECbUSZyHZimrO1m4Gga5pE3LKuDri\nJKR2lR7b6XPR7+FS+lG1zq5KY7onAVQY1oABfTjpJRju6pQGWt70hairo6EaVC3u\nrBy8UkLwBbfDuigSvsVk+sF2+Ic0IzX6IniU0F5kMe+MKqGB4aicXP6FFGBpPFTe\nv6yHD+DYAu1rnlXrqmFL50CfutTF78uPPJ9D2Sm0DcGPFj+6IrCigj48uxoHR9Qb\nFeNzfsmVwoFAWWq/MpkPbX6Aql8ddCbpMxDUUkybwVV9rJmEMTLil44FrxKAKFhP\n0Av7JeFvdz15pfnf/IQ3IOvVhHGFChFS13sbYSvFHMQF3P0BiyvjhBI=\n-----END ENCRYPTED PRIVATE KEY-----\n",
"password": "testpwd",
"expected_signature": "S4l9IONXGHIdt4NjwmpCIhawDTitjUQls73d+mi0HJTSbTGyn95NabX5hC9+n6HsTqLcWPvxKgTvLFMnTaf6Jxl+xwQMbu9/6mw88KF7i1pEQizerKcr91rPUPVBQ4OY10Q018QEamIAymRgo/eoRYSm7CqCdeibGyO0XfXZBaJnVGFJ9hgrPIwSKHgeUnfK8qMenULvL0qKMEJ6ziYPiqh7k9xX3xIV7lGIpokk+ekqlFd01f/Lov45osJCFuccJO4xuUUZewZnVGF7Uw6Rim3UsKhXKZUN9WZWa5RT+dpBIJ5DTBIXBSvowwj3GZC3j+XvWw8Sn0Ls9836l89BXw==",
},
]

describe('Test crypto signature', function () {

it('RSA and ed22519 tests ', function () {

const dataQuery = 'price=50000&quantity=1&side=BUY&symbol=BTCUSDT&timestamp=1631234567890&type=LIMIT'

for (const testCase of testCases) {
const binance = new Binance({
APISECRET: testCase.private_key,
PRIVATEKEYPASSWORD: testCase.password,
})

const signature = binance.generateSignature(dataQuery, false);
assert.equal(signature, testCase.expected_signature, testCase.description);
}

});

});
Loading