Skip to content

Commit 3e4ea41

Browse files
authored
Merge pull request #96 from leMaik/misc-updates
Improve readme, update xml-crypto, fix ci, replace deprecated crypto methods and make add compatibility with NodeJS 22.
2 parents 1fbe178 + 8dd8444 commit 3e4ea41

12 files changed

Lines changed: 349 additions & 593 deletions

File tree

.github/workflows/CI.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
node: [ 18, 19, 20 ]
14+
node: [ 18, 20, 22 ]
1515
name: Node.js ${{ matrix.node }}
1616
steps:
17-
- uses: actions/checkout@v3
17+
- uses: actions/checkout@v4
1818
- name: Setup node
19-
uses: actions/setup-node@v3
19+
uses: actions/setup-node@v4
2020
with:
2121
node-version: ${{ matrix.node }}
2222
- run: npm ci

README.md

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,55 @@
1515
<a href='https://coveralls.io/github/eCollect/node-ebics-client?branch=master' title="Coverage Status"><img src='https://coveralls.io/repos/github/eCollect/node-ebics-client/badge.svg?branch=master' alt='Coverage Status' /></a>
1616
</p>
1717

18-
Pure node.js ( >=8 ) implementation of [EBICS](https://en.wikipedia.org/wiki/Electronic_Banking_Internet_Communication_Standard) ( Electronic Banking Internet Communication ).
18+
Pure Node.js (>= 16) implementation of [EBICS](https://en.wikipedia.org/wiki/Electronic_Banking_Internet_Communication_Standard) (Electronic Banking Internet Communication).
1919

20-
The client is aimed to be 100% [ISO 20022](https://www.iso20022.org) compliant, and supports the complete initializations process ( INI, HIA, HPB orders ) and HTML letter generation.
20+
The client is aimed to be 100% [ISO 20022](https://www.iso20022.org) compliant, and supports the complete initializations process (INI, HIA, HPB orders) and HTML letter generation.
2121

22+
## Usage
23+
24+
For examples on how to use this library, take a look at the [examples](https://github.com/node-ebics/node-ebics-client/tree/master/examples).
25+
26+
### A note on recent Node.js versions
27+
28+
The latest Node.js versions don't support `RSA_PKCS1_PADDING` for private decryption for security reasons, throwing an error like _TypeError: RSA_PKCS1_PADDING is no longer supported for private decryption, this can be reverted with --security-revert=CVE-2023-46809_.
29+
30+
EBICS requires this mode, so in order for this library to work, add the following parameter when starting Node.js: `--security-revert=CVE-2023-46809`
31+
32+
### Initialization
33+
34+
1. Create a configuration (see [example configs](https://github.com/node-ebics/node-ebics-client/tree/master/examples/config)) with the EBICS credentials you received from your bank and name it in this schema: `config.<environment>.<bank>[.<entity>].json` (the entity is optional).
35+
36+
- The fields `url`, `partnerId`, `userId`, `hostId` are provided by your bank.
37+
- The `passphrase` is used to encrypt the keys file, which will be stored at the `storageLocation`.
38+
- The `bankName` and `bankShortName` are used internally for creating files and identifying the bank to you.
39+
- The `languageCode` is used when creating the Initialization Letter and can be either `de`, `en`, or `fr`.
40+
- You can chose any environment, bank and, optionally, entity name. Entities are useful if you have multiple EBICS users for the same bank account.
41+
42+
2. Run `node examples/initialize.js <environment> <bank> [entity]` to generate your key pair and perform the INI and HIA orders (ie. send the public keys to your bank)
43+
The generated keys are stored in the file specified in your config and encrypted with the specified passphrase.
44+
3. Run `node examples/bankLetter.js <environment> <bank> [entity]` to generate the Initialization Letter
45+
4. Print the letter, sign it and send it to your bank. Wait for them to activate your EBICS account.
46+
5. Download the bank keys by running `node examples/save-bank-keys.js <environment> <bank> [entity]`
47+
48+
If all these steps were executed successfully, you can now do all things EBICS, like fetching bank statements by running `node examples/send-sta-order.js <environment> <bank> [entity]`, or actually use this library in your custom banking applications.
2249

2350
## Supported Banks
24-
The client is currently tested and verified to work with the following banks:
2551

26-
* [Credit Suisse (Schweiz) AG](https://www.credit-suisse.com/ch/en.html)
27-
* [Zürcher Kantonalbank](https://www.zkb.ch/en/lg/ew.html)
28-
* [Raiffeisen Schweiz](https://www.raiffeisen.ch/rch/de.html)
29-
* [BW Bank](https://www.bw-bank.de/de/home.html)
30-
* [Bank GPB International S.A.](https://gazprombank.lu/e-banking)
31-
* [Bank GPB AO](https://gazprombank.ru/)
32-
* [J.P. Morgan](https://www.jpmorgan.com/)
52+
The client is currently tested and verified to work with the following banks:
3353

54+
- [Credit Suisse (Schweiz) AG](https://www.credit-suisse.com/ch/en.html)
55+
- [Zürcher Kantonalbank](https://www.zkb.ch/en/lg/ew.html)
56+
- [Raiffeisen Schweiz](https://www.raiffeisen.ch/rch/de.html)
57+
- [BW Bank](https://www.bw-bank.de/de/home.html)
58+
- [Bank GPB International S.A.](https://gazprombank.lu/e-banking)
59+
- [Bank GPB AO](https://gazprombank.ru/)
60+
- [J.P. Morgan](https://www.jpmorgan.com/)
3461

3562
## Inspiration
3663

3764
The basic concept of this library was inspired by the [EPICS](https://github.com/railslove/epics) library from the Railslove Team.
3865

39-
4066
## Copyright
4167

4268
Copyright: Dimitar Nanov, 2019-2022.
4369
Licensed under the [MIT](LICENSE) license.
44-

examples/bankLetter.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const os = require('os');
1010
const config = require('./loadConfig')();
1111
const client = require('./getClient')(config);
1212
const bankName = client.bankName;
13-
const template = fs.readFileSync("../templates/ini_"+client.languageCode+".hbs", { encoding: 'utf8'});
13+
const template = fs.readFileSync("./templates/ini_"+client.languageCode+".hbs", { encoding: 'utf8'});
1414
const bankLetterFile = path.join("./", "bankLetter_"+client.bankShortName+"_"+client.languageCode+".html");
1515

1616
const letter = new ebics.BankLetter({ client, bankName, template });

examples/getClient.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ module.exports = ({
1010
userId,
1111
hostId,
1212
passphrase,
13+
iv,
1314
keyStoragePath,
1415
} = loadConfig()) => new Client({
1516
url,
1617
partnerId,
1718
userId,
1819
hostId,
1920
passphrase,
21+
iv,
2022
keyStorage: fsKeysStorage(keyStoragePath),
2123
});

lib/Client.js

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,15 @@ const stringifyKeys = (keys) => {
4343
* @property {string} partnerId - PARTNERID provided by the bank
4444
* @property {string} hostId - HOSTID provided by the bank
4545
* @property {string} userId - USERID provided by the bank
46-
* @property {string} passphrase - passphrase for keys encryption
46+
* @property {string|Buffer} passphrase - passphrase or key for keys encryption
47+
* @property {string|Buffer} iv - Initialization Vector for keys encryption
4748
* @property {KeyStorage} keyStorage - keyStorage implementation
4849
* @property {object} [tracesStorage] - traces (logs) storage implementation
4950
* @property {string} bankName - Full name of the bank to be used in the bank INI letters.
5051
* @property {string} bankShortName - Short name of the bank to be used in folders, filenames etc.
5152
* @property {string} languageCode - Language code to be used in the bank INI letters ("de", "en" and "fr" are currently supported).
5253
* @property {string} storageLocation - Location where to store the files that are downloaded. This can be a network share for example.
5354
*/
54-
5555
module.exports = class Client {
5656
/**
5757
*Creates an instance of Client.
@@ -63,6 +63,7 @@ module.exports = class Client {
6363
userId,
6464
hostId,
6565
passphrase,
66+
iv,
6667
keyStorage,
6768
tracesStorage,
6869
bankName,
@@ -88,7 +89,7 @@ module.exports = class Client {
8889
this.userId = userId;
8990
this.hostId = hostId;
9091
this.keyStorage = keyStorage;
91-
this.keyEncryptor = defaultKeyEncryptor({ passphrase });
92+
this.keyEncryptor = defaultKeyEncryptor({ passphrase, iv });
9293
this.tracesStorage = tracesStorage || null;
9394
this.bankName = bankName || 'Dummy Bank Full Name';
9495
this.bankShortName = bankShortName || 'BANKSHORTCODE';
@@ -216,26 +217,25 @@ module.exports = class Client {
216217
.persist();
217218

218219
rock({
219-
method: 'POST',
220-
url: this.url,
221-
body: r,
222-
headers: { 'content-type': 'text/xml;charset=UTF-8' },
223-
},
224-
(err, res, data) => {
225-
if (err) reject(err);
226-
227-
const ebicsResponse = response.version(version)(data.toString('utf-8'), keys);
228-
229-
if (this.tracesStorage)
230-
this.tracesStorage
231-
.label(`RESPONSE.${order.orderDetails.OrderType}`)
232-
.connect()
233-
.data(ebicsResponse.toXML())
234-
.persist();
235-
236-
resolve(ebicsResponse);
237-
},
238-
);
220+
method: 'POST',
221+
url: this.url,
222+
body: r,
223+
headers: { 'content-type': 'text/xml;charset=UTF-8' },
224+
},
225+
(err, res, data) => {
226+
if (err) reject(err);
227+
228+
const ebicsResponse = response.version(version)(data.toString('utf-8'), keys);
229+
230+
if (this.tracesStorage)
231+
this.tracesStorage
232+
.label(`RESPONSE.${order.orderDetails.OrderType}`)
233+
.connect()
234+
.data(ebicsResponse.toXML())
235+
.persist();
236+
237+
resolve(ebicsResponse);
238+
});
239239
});
240240
}
241241

@@ -250,7 +250,6 @@ module.exports = class Client {
250250
async keys() {
251251
try {
252252
const keysString = await this._readKeys();
253-
254253
return new Keys(JSON.parse(this.keyEncryptor.decrypt(keysString)));
255254
} catch (err) {
256255
return null;

lib/crypto/Crypto.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const crypto = require('crypto');
4+
const NodeRSA = require('node-rsa');
45

56
const BigNumber = require('./BigNumber.js');
67
const mgf1 = require('./MGF1');
@@ -54,10 +55,14 @@ module.exports = class Crypto {
5455
}
5556

5657
static privateDecrypt(key, data) {
57-
return crypto.privateDecrypt({
58-
key: key.toPem(),
59-
padding: crypto.constants.RSA_PKCS1_PADDING,
60-
}, data);
58+
const keyRSA = new NodeRSA(
59+
key.toPem(),
60+
'pkcs1-private-pem', {
61+
encryptionScheme: 'pkcs1',
62+
environment: 'browser', // would use the crypto module by default, which blocks pkcs1
63+
},
64+
);
65+
return keyRSA.decrypt(data);
6166
}
6267

6368
static privateSign(key, data, outputEncoding = 'base64') {

lib/crypto/encryptDecrypt.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use strict';
2+
3+
const crypto = require('crypto');
4+
5+
const createKeyAndIv = (passphrase) => {
6+
// this generates a 256-bit key and a 128-bit iv for aes-256-cbc
7+
// just like nodejs's deprecated/removed crypto.createCipher would
8+
const a = crypto.createHash('md5').update(passphrase).digest();
9+
const b = crypto
10+
.createHash('md5')
11+
.update(Buffer.concat([a, Buffer.from(passphrase)]))
12+
.digest();
13+
const c = crypto
14+
.createHash('md5')
15+
.update(Buffer.concat([b, Buffer.from(passphrase)]))
16+
.digest();
17+
const bytes = Buffer.concat([a, b, c]);
18+
const key = bytes.subarray(0, 32);
19+
const iv = bytes.subarray(32, 48);
20+
return { key, iv };
21+
};
22+
23+
const encrypt = (data, algorithm, passphrase, iv) => {
24+
let cipher;
25+
if (iv) {
26+
cipher = crypto.createCipheriv(algorithm, passphrase, iv);
27+
} else {
28+
console.warn(
29+
'[Deprecation notice] No IV provided, falling back to legacy key derivation.\n'
30+
+ 'This will be removed in a future major release. You should encrypt your keys with a proper key and IV.',
31+
);
32+
if (crypto.createCipher) {
33+
// nodejs < 22
34+
cipher = crypto.createCipher(algorithm, passphrase);
35+
} else {
36+
const { key, iv: generatedIv } = createKeyAndIv(passphrase);
37+
cipher = crypto.createCipheriv(algorithm, key, generatedIv);
38+
}
39+
}
40+
const encrypted = cipher.update(data, 'utf8', 'hex') + cipher.final('hex');
41+
return Buffer.from(encrypted).toString('base64');
42+
};
43+
44+
const decrypt = (data, algorithm, passphrase, iv) => {
45+
data = Buffer.from(data, 'base64').toString();
46+
let decipher;
47+
if (iv) {
48+
decipher = crypto.createDecipheriv(algorithm, passphrase, iv);
49+
} else {
50+
console.warn(
51+
'[Deprecation notice] No IV provided, falling back to legacy key derivation.\n'
52+
+ 'This will be removed in a future major release. You should re-encrypt your keys with a proper key and IV.',
53+
);
54+
if (crypto.createDecipher) {
55+
// nodejs < 22
56+
decipher = crypto.createDecipher(algorithm, passphrase);
57+
} else {
58+
const { key, iv: generatedIv } = createKeyAndIv(passphrase);
59+
decipher = crypto.createDecipheriv(algorithm, key, generatedIv);
60+
}
61+
}
62+
const decrypted = decipher.update(data, 'hex', 'utf8') + decipher.final('utf8');
63+
return decrypted;
64+
};
65+
66+
module.exports = { encrypt, decrypt };

lib/keymanagers/KeysManager.js

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,8 @@
11
'use strict';
22

3-
const crypto = require('crypto');
4-
3+
const { encrypt, decrypt } = require('../crypto/encryptDecrypt');
54
const Keys = require('./Keys');
65

7-
const encrypt = (data, algorithm, passphrase) => {
8-
const cipher = crypto.createCipher(algorithm, passphrase);
9-
const encrypted = cipher.update(data, 'utf8', 'hex') + cipher.final('hex');
10-
11-
return Buffer.from(encrypted).toString('base64');
12-
};
13-
const decrypt = (data, algorithm, passphrase) => {
14-
data = (Buffer.from(data, 'base64')).toString();
15-
16-
const decipher = crypto.createDecipher(algorithm, passphrase);
17-
const decrypted = decipher.update(data, 'hex', 'utf8') + decipher.final('utf8');
18-
19-
return decrypted;
20-
};
21-
226
module.exports = (keysStorage, passphrase, algorithm = 'aes-256-cbc') => {
237
const storage = keysStorage;
248
const pass = passphrase;
Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,9 @@
11
'use strict';
22

3-
const crypto = require('crypto');
3+
const { encrypt, decrypt } = require('../crypto/encryptDecrypt');
44

5-
const encrypt = (data, algorithm, passphrase) => {
6-
const cipher = crypto.createCipher(algorithm, passphrase);
7-
const encrypted = cipher.update(data, 'utf8', 'hex') + cipher.final('hex');
8-
return Buffer.from(encrypted).toString('base64');
9-
};
10-
const decrypt = (data, algorithm, passphrase) => {
11-
data = (Buffer.from(data, 'base64')).toString();
12-
const decipher = crypto.createDecipher(algorithm, passphrase);
13-
const decrypted = decipher.update(data, 'hex', 'utf8') + decipher.final('utf8');
145

15-
return decrypted;
16-
};
17-
18-
module.exports = ({
19-
passphrase,
20-
algorithm = 'aes-256-cbc',
21-
}) => ({
22-
encrypt: data => encrypt(data, algorithm, passphrase),
6+
module.exports = ({ passphrase, iv, algorithm = 'aes-256-cbc' }) => ({
7+
encrypt: data => encrypt(data, algorithm, passphrase, iv),
238
decrypt: data => decrypt(data, algorithm, passphrase),
249
});

0 commit comments

Comments
 (0)