Skip to content

Commit 190a64b

Browse files
authored
Merge pull request #927 from ccxt/ed-encryption
feat: add RSA and ed22519 support
2 parents 58f4d53 + 51f77b2 commit 190a64b

6 files changed

Lines changed: 105 additions & 4 deletions

File tree

.github/workflows/js.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ jobs:
2727
run: npm run lint
2828
- name: Build
2929
run: npm run build
30+
- name: Crypto Tests
31+
run: npm run crypto-tests
3032
- name: Static Tests (TS ESM)
3133
run: npm run ts-test-static
3234
- name: Static Tests (JS CJS)

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@ Actively maintained, typed, and safe SDK for the Binance REST APIs and Websocket
5757

5858
### Features
5959
- Spot, Margin, Futures and Delivery API
60-
- Portfolio Margin API *\*soon*\*
6160
- Testnet support
6261
- Proxy support (REST and WS)
6362
- Customizable HTTP headers
6463
- Customizable request parameters
65-
- RSA/ECDSA support *\*soon*\*
64+
- RSA/ECDSA support
65+
- Portfolio Margin API *\*soon*\*
6666
- Websocket handling with automatic reconnection
6767
- RecvWindow and automatic timestamps generation
6868
- Ability to call any endpoint, even if not supported directly by the library

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
"ts-test-live": "mocha ./tests/binance-class-live.test.ts",
2828
"ts-test-static": "mocha ./tests/binance-class-static.test.ts",
2929
"test-cjs": "node ./tests/cjs-test.cjs",
30+
"crypto-tests": "mocha ./tests/crypto.test.ts",
31+
"ws-tests": "mocha ./tests/binance-class-ws.test.ts",
3032
"ws-tests-spot": "mocha ./tests/binance-ws-spot.test.ts --exit",
3133
"ws-tests-futures": "mocha ./tests/binance-ws-futures.test.ts --exit",
3234
"test-debug": "mocha --inspect-brk",

src/node-binance-api.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { HttpsProxyAgent } from 'https-proxy-agent';
1111
import { SocksProxyAgent } from 'socks-proxy-agent';
1212
// @ts-ignore
1313
import nodeFetch from 'node-fetch';
14+
1415
// @ts-ignore
1516
import zip from 'lodash.zipobject';
1617
import stringHash from 'string-hash';
@@ -63,6 +64,8 @@ export default class Binance {
6364

6465
APIKEY: string = undefined;
6566
APISECRET: string = undefined;
67+
PRIVATEKEY: string = undefined;
68+
PRIVATEKEYPASSWORD: string = undefined;
6669
test = false;
6770

6871
timeOffset: number = 0;
@@ -209,6 +212,8 @@ export default class Binance {
209212

210213
if (this.Options.APIKEY) this.APIKEY = this.Options.APIKEY;
211214
if (this.Options.APISECRET) this.APISECRET = this.Options.APISECRET;
215+
if (this.Options.PRIVATEKEY) this.PRIVATEKEY = this.Options.PRIVATEKEY;
216+
if (this.Options.PRIVATEKEYPASSWORD) this.PRIVATEKEYPASSWORD = this.Options.PRIVATEKEYPASSWORD;
212217
if (this.Options.test) this.test = true;
213218
if (this.Options.headers) this.headers = this.Options.Headers;
214219
if (this.Options.domain) this.domain = this.Options.domain;
@@ -535,7 +540,7 @@ export default class Binance {
535540
data.timestamp += this.timeOffset;
536541
}
537542
query = this.makeQueryString(data);
538-
data.signature = crypto.createHmac('sha256', this.APISECRET).update(query).digest('hex'); // HMAC hash header
543+
data.signature = this.generateSignature(query);
539544
opt.url = `${url}?${query}&signature=${data.signature}`;
540545
}
541546
(opt as any).qs = data;
@@ -647,7 +652,9 @@ export default class Binance {
647652

648653
if (!data.recvWindow) data.recvWindow = this.Options.recvWindow;
649654
const query = method === 'POST' && noDataInSignature ? '' : this.makeQueryString(data);
650-
const signature = crypto.createHmac('sha256', this.Options.APISECRET).update(query).digest('hex'); // set the HMAC hash header
655+
656+
const signature = this.generateSignature(query);
657+
651658
if (method === 'POST') {
652659
const opt = this.reqObjPOST(
653660
url,
@@ -670,6 +677,44 @@ export default class Binance {
670677
}
671678
}
672679

680+
generateSignature(query: string, encode = true) {
681+
const secret = this.APISECRET || this.PRIVATEKEY;
682+
let signature = '';
683+
if (secret.includes ('PRIVATE KEY')) {
684+
// if less than the below length, then it can't be RSA key
685+
let keyObject: crypto.KeyObject;
686+
try {
687+
const privateKeyObj: crypto.PrivateKeyInput = { key: secret };
688+
689+
if (this.PRIVATEKEYPASSWORD) {
690+
privateKeyObj.passphrase = this.PRIVATEKEYPASSWORD;
691+
}
692+
693+
keyObject = crypto.createPrivateKey(privateKeyObj);
694+
695+
} catch (e){
696+
throw new Error(
697+
'Invalid private key. Please provide a valid RSA or ED25519 private key. ' + e.toString()
698+
);
699+
}
700+
701+
if (secret.length > 120) {
702+
// RSA key
703+
signature = crypto
704+
.sign('RSA-SHA256', Buffer.from(query), keyObject)
705+
.toString('base64');
706+
if (encode) signature = encodeURIComponent (signature);
707+
return signature;
708+
} else {
709+
// Ed25519 key
710+
signature = crypto.sign(null, Buffer.from(query), keyObject).toString('base64');
711+
}
712+
} else {
713+
signature = crypto.createHmac('sha256', this.Options.APISECRET).update(query).digest('hex'); // set the HMAC hash header
714+
}
715+
return signature;
716+
}
717+
673718
// --- ENDPOINTS --- //
674719

675720
/**

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ export type Callback = (...args: any) => any;
125125
export interface IConstructorArgs {
126126
APIKEY: string;
127127
APISECRET: string;
128+
PRIVATEKEY: string; // when using RSA/EDCSA keys
129+
PRIVATEKEYPASSWORD: string; // when using RSA/EDCSA keys
128130
recvWindow: number;
129131
useServerTime: boolean;
130132
reconnect: boolean;

tests/crypto.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Binance from '../src/node-binance-api';
2+
import { assert } from 'chai';
3+
4+
5+
const testCases = [
6+
{
7+
"description": "Unencrypted PKCS8 ed22519 private key",
8+
"private_key": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIPQmzwVKJETqVd7L9E/DFbkvrOigy1tLL+9QF0mSn6dV\n-----END PRIVATE KEY-----\n",
9+
"password": undefined,
10+
"expected_signature": "a4Pm3p02D2HXtNfo3DBaVCe9Ov7kledewgYtGjekotFmZ5wXa3mC5AtLB7CpAphyNjeyovIuDP+9fyjYmsojCw==",
11+
},
12+
{
13+
"description": "Unencrypted PKCS8 ed22519 private key in bytes",
14+
"private_key": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIPQmzwVKJETqVd7L9E/DFbkvrOigy1tLL+9QF0mSn6dV\n-----END PRIVATE KEY-----\n",
15+
"password": undefined,
16+
"expected_signature": "a4Pm3p02D2HXtNfo3DBaVCe9Ov7kledewgYtGjekotFmZ5wXa3mC5AtLB7CpAphyNjeyovIuDP+9fyjYmsojCw==",
17+
},
18+
{
19+
"description": "Encrypted PKCS8 RSA private key",
20+
"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",
21+
"password": "testpwd",
22+
"expected_signature": "S4l9IONXGHIdt4NjwmpCIhawDTitjUQls73d+mi0HJTSbTGyn95NabX5hC9+n6HsTqLcWPvxKgTvLFMnTaf6Jxl+xwQMbu9/6mw88KF7i1pEQizerKcr91rPUPVBQ4OY10Q018QEamIAymRgo/eoRYSm7CqCdeibGyO0XfXZBaJnVGFJ9hgrPIwSKHgeUnfK8qMenULvL0qKMEJ6ziYPiqh7k9xX3xIV7lGIpokk+ekqlFd01f/Lov45osJCFuccJO4xuUUZewZnVGF7Uw6Rim3UsKhXKZUN9WZWa5RT+dpBIJ5DTBIXBSvowwj3GZC3j+XvWw8Sn0Ls9836l89BXw==",
23+
},
24+
{
25+
"description": "Encrypted PKCS8 RSA private key in bytes",
26+
"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",
27+
"password": "testpwd",
28+
"expected_signature": "S4l9IONXGHIdt4NjwmpCIhawDTitjUQls73d+mi0HJTSbTGyn95NabX5hC9+n6HsTqLcWPvxKgTvLFMnTaf6Jxl+xwQMbu9/6mw88KF7i1pEQizerKcr91rPUPVBQ4OY10Q018QEamIAymRgo/eoRYSm7CqCdeibGyO0XfXZBaJnVGFJ9hgrPIwSKHgeUnfK8qMenULvL0qKMEJ6ziYPiqh7k9xX3xIV7lGIpokk+ekqlFd01f/Lov45osJCFuccJO4xuUUZewZnVGF7Uw6Rim3UsKhXKZUN9WZWa5RT+dpBIJ5DTBIXBSvowwj3GZC3j+XvWw8Sn0Ls9836l89BXw==",
29+
},
30+
]
31+
32+
describe('Test crypto signature', function () {
33+
34+
it('RSA and ed22519 tests ', function () {
35+
36+
const dataQuery = 'price=50000&quantity=1&side=BUY&symbol=BTCUSDT&timestamp=1631234567890&type=LIMIT'
37+
38+
for (const testCase of testCases) {
39+
const binance = new Binance({
40+
APISECRET: testCase.private_key,
41+
PRIVATEKEYPASSWORD: testCase.password,
42+
})
43+
44+
const signature = binance.generateSignature(dataQuery, false);
45+
assert.equal(signature, testCase.expected_signature, testCase.description);
46+
}
47+
48+
});
49+
50+
});

0 commit comments

Comments
 (0)