initial commit

This commit is contained in:
Vladislav Hristov 2018-05-17 18:03:59 +03:00
parent cd37de3895
commit 1f947ff148
27 changed files with 1502 additions and 0 deletions

32
.editorconfig Normal file
View File

@ -0,0 +1,32 @@
# @see editorconfig.org
root = true
[*]
indent_style = tab
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.json]
indent_size = 2
[*.js]
indent_size = 4
[*.tag]
indent_size = 2
[*.yml]
indent_size = 2
# Trailing whitespace is significant in markdown files.
[*.md]
trim_trailing_whitespace = false
max_line_length = 80
[Makefile]
indent_style = tab
indent_size = 4

4
.env Normal file
View File

@ -0,0 +1,4 @@
SERVER_PORT=3003
LOG_LEVEL=info
LOG_ENABLED=true

6
.eslintignore Normal file
View File

@ -0,0 +1,6 @@
node_modules/
build/
static/
config/
src/vue-router-custom-components
src/directives/clickAway

34
.eslintrc Normal file
View File

@ -0,0 +1,34 @@
{
"extends": "airbnb",
"env": {
"node": true
},
"parserOptions": {
"ecmaVersion": 8,
"sourceType": "script",
"ecmaFeatures": {
"modules": false
}
},
"rules": {
"max-len": 0,
"linebreak-style": 0,
"no-plusplus": [
2,
{
"allowForLoopAfterthoughts": true
}
],
"no-continue": 0,
"indent": [2, "tab"],
"no-tabs": 0,
"strict": [2, "safe"],
"curly": [2, "multi", "consistent"],
"import/no-extraneous-dependencies": 0,
"import/no-unresolved": 0,
"no-underscore-dangle": 0,
"no-param-reassign": 0,
"generator-star-spacing": 0,
"jsx-a11y/href-no-hash": "off"
}
}

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
npm-debug.log
node_modules
.DS_Store
*.local.json5
yarn.lock
/project.sublime-workspace
/public/css/style.css.map
/.idea
/.vscode
*.pid
/coverage
package-lock.json
*.key
*.html

20
README.md Normal file
View File

@ -0,0 +1,20 @@
# node-ebics-client v0.0.3
---
Pure node.js ( >=8 ) implementation of the [EBICS](https://en.wikipedia.org/wiki/Electronic_Banking_Internet_Communication_Standard) (Electronic Banking Internet Communication).
The client is aimed to be 100% ISO 20022 complient, and supports complete initilizations process ( INI, HIA, HPB orders ) and HTML letter generation.
## Supported Banks
The client is tested and verified to work with the following banks:
* Credit Suisse
* Zürcher Kantonalbank
* Raiffeisenbank
## Inspiration
A lot of the concepts in this library are inspired from the [EPICS](https://github.com/railslove/epics) library.
## Copyright
Copyright (c) 2017 eCollect.

4
index.js Normal file
View File

@ -0,0 +1,4 @@
'use strict';
const Client = require('./lib/Client');
module.exports = Client;

66
lib/BankLetter.js Normal file
View File

@ -0,0 +1,66 @@
'use strict';
const fs = require('fs');
const moment = require('moment');
const handlebars = require('handlebars');
const BN = require("bn.js");
module.exports = class BankLetter {
constructor(client, bankName) {
this.client = client;
this.bankName = bankName;
this.pathToTemplate = './app/ebics/ini.hbs';
};
_registerHelpers() {
handlebars.registerHelper("today", () => {
return moment().format('DD.MM.YYYY');
});
handlebars.registerHelper("now", () => {
return moment().format('HH:mm:ss');
});
handlebars.registerHelper("keyExponentBits", (k) => {
return Buffer.byteLength(new BN(k.key.keyPair.e).toBuffer()) * 8;
});
handlebars.registerHelper("keyModulusBits", (k) => {
return k.key.getKeySize();
// return Buffer.byteLength(new BN(k.key.keyPair.e).toBuffer()) * 8;
});
handlebars.registerHelper("keyExponent", (k) => {
return k.e();
});
handlebars.registerHelper("keyModulus", (k) => {
return k.n().toUpperCase().match(/.{1,2}/g).join(' ');
});
handlebars.registerHelper("sha256", (k) => {
const digest = Buffer(k.publicDigest(), 'base64').toString('HEX');
return digest.toUpperCase().match(/.{1,2}/g).join(' ');
});
};
generate() {
this._registerHelpers();
const str = fs.readFileSync(this.pathToTemplate).toString();
const templ = handlebars.compile(str);
const data = {
bankName : this.bankName,
userId : this.client.userId,
partnerId: this.client.partnerId,
A006 : this.client.a(),
X002 : this.client.x(),
E002 : this.client.e(),
};
return templ(data);
}
}

233
lib/Client.js Normal file
View File

@ -0,0 +1,233 @@
'use strict';
const fs = require("fs");
const crypto = require("crypto");
const $request = require("request");
const BN = require('bn.js');
const xpath = require("xpath");
const NodeRSA = require("node-rsa");
const Key = require('./Key');
const XMLSign = require('./middleware/XMLSign');
const ParseResponse = require('./middleware/ParseResponse');
const BankLetter = require('./BankLetter');
const EBICSINI = require('./orders/INI');
const EBICSHIA = require('./orders/HIA');
const EBICSHPB = require('./orders/HPB');
const EBICSHKD = require('./orders/HKD');
const EBICSHAA = require('./orders/HAA');
const EBICSHAC = require('./orders/HAC');
const EBICSHTD = require('./orders/HTD');
const EBICSC52 = require('./orders/C52');
const utils = {
exponent: {
// str = 65537 => AQAB
toBase64(str) {
return new BN(str).toBuffer().toString('base64');
},
// str = AQAB => 65537
fromBase64(str) {
return new BN(Buffer.from(str, 'base64'), 2).toNumber();
}
}
}
module.exports = class Client {
constructor(keysContent, passphrase, url, hostId, userId, partnerId) {
this.keysContent = keysContent;
this.passphrase = passphrase;
this.url = url;
this.hostId = hostId;
this.userId = userId;
this.partnerId = partnerId;
this.encryptAlgorithm = 'aes-256-cbc';
this.keys = keysContent ? this.extractKeys() : {};
};
a() {
return this.keys["A006"];
};
e() {
return this.keys["E002"];
};
x() {
return this.keys["X002"];
}
bankX() {
return this.keys[`${this.hostId}.X002`];
}
bankE() {
return this.keys[`${this.hostId}.E002`];
}
encrypt(data) {
const cipher = crypto.createCipher(this.encryptAlgorithm, this.passphrase);
const encrypted = cipher.update(data, 'utf8', 'hex') + cipher.final('hex');
return Buffer.from(encrypted).toString('base64');
};
decrypt(data) {
data = (new Buffer(data, 'base64')).toString();
const decipher = crypto.createDecipher(this.encryptAlgorithm, this.passphrase);
const decrypted = decipher.update(data, 'hex', 'utf8') + decipher.final('utf8');
return decrypted;
};
static setup(passphrase, url, hostId, userId, partnerId, keysize = 2048) {
const client = new Client(null, passphrase, url, hostId, userId, partnerId);
for (let key in {A006: '', X002: '', E002: ''}) {
client.keys[key] = new Key(new NodeRSA({ b: keysize }));
}
return client;
};
saveIniLetter(bankName, path) {
const letter = new BankLetter(this, bankName);
try {
fs.writeFileSync(path, letter.generate());
console.log("Data written to file");
} catch (error) {
console.log(error);
throw error;
}
};
saveKeys(path) {
const data = {};
for (let key in this.keys) {
data[key] = this.encrypt(this.keys[key].toPem());
};
try {
fs.writeFileSync(path, JSON.stringify(data));
console.log("Data written to file");
} catch(error) {
console.log(error);
throw error;
}
};
extractKeys() {
const keys = {};
const jsonData = JSON.parse(this.keysContent);
for (let key in jsonData) {
keys[key] = new Key(this.decrypt(jsonData[key]));
}
return keys;
}
async download(order) {
const res = await this.ebicsRequest(order.toXML());
const ttt = res.toXML(); // keep this for debugging purposes
order.transactionId = res.transactionId();
if (res.isSegmented() && res.isLastSegment()) {
const receipt = await this.ebicsRequest(order.toReceiptXML());
const receiptXML = order.toReceiptXML(); // keep this for debugging purposes
const rX = receipt.toXML(); // keep this for debugging purposes
}
return res.orderData();
};
async downloadAndUnzip(order) {
}
ebicsRequest(order) {
return new Promise((resolve, reject) => {
const bbb = XMLSign.go(this, order);
$request.post({
url : this.url,
body : bbb,
headers: { 'Content-Type': 'text/xml' }
}, (err, res, data) => {
const b = data; // keep this for debugging purposes
const r = ParseResponse.go(this, data); // keep this for debugging purposes
const rXML = r.toXML(); // keep this for debugging purposes
return err ? reject(err): resolve(ParseResponse.go(this, data));
});
});
};
async INI() {
return this.ebicsRequest((new EBICSINI(this)).toXML());
};
async HIA() {
return this.ebicsRequest((new EBICSHIA(this)).toXML());
};
async HPB() {
const data = await this.download(new EBICSHPB(this));
const doc = new DOMParser().parseFromString(data, 'text/xml');
const sel = xpath.useNamespaces({'xmlns': "urn:org:ebics:H004"});
const keyNodes = sel("//xmlns:PubKeyValue", doc);
// console.log(keyNodes);
function xmlLastChild (node) {
let y = node.lastChild;
while (y.nodeType != 1) y = y.previousSibling;
return y;
};
for (let i = 0; i < keyNodes.length; i++) {
const type = xmlLastChild(keyNodes[i].parentNode).textContent;
const modulus = xpath.select("//*[local-name(.)='Modulus']", keyNodes[i])[0].textContent;
const exponent = xpath.select("//*[local-name(.)='Exponent']", keyNodes[i])[0].textContent;
const mod = new BN(Buffer.from(modulus, 'base64'), 2).toBuffer();
const exp = utils.exponent.fromBase64(exponent);
const bank = new NodeRSA();
bank.importKey({ n: mod, e: exp }, 'components-public');
this.keys[`${this.hostId}.${type}`] = new Key(bank);
}
return [this.bankX(), this.bankE()];
};
HKD() {
return this.download(new EBICSHKD(this));
};
HAA() {
return this.download(new EBICSHAA(this));
};
HTD() {
return this.download(new EBICSHTD(this));
};
HAC(from = null, to = null) {
return this.download(new EBICSHAC(this, from, to));
};
C52(from, to) {
return this.downloadAndUnzip(new EBICSC52(this, from, to));
}
};

38
lib/Key.js Normal file
View File

@ -0,0 +1,38 @@
'use strict';
const crypto = require('crypto');
const NodeRSA = require("node-rsa");
const BN = require('bn.js');
module.exports = class Key {
constructor(encodedKey, passphrase = null) {
if (encodedKey instanceof NodeRSA) {
this.key = encodedKey
} else {
this.key = new NodeRSA(encodedKey);
}
};
publicDigest() {
const str = [this.e().replace(/^(0+)/g, ''), this.n().replace(/^(0+)/g, '')].map((x) => x.toLowerCase()).join(' ');
return crypto.createHash('sha256').update(str).digest('base64').trim();
};
publicEncrypt(str) {
return this.key.encrypt(str);
}
n() {
return this.key.exportKey("components-public").n.toString("hex", 1);
};
e() {
return new BN(this.key.exportKey("components-public").e).toBuffer().toString('hex');
};
toPem() {
return this.key.isPrivate() ? this.key.exportKey("pkcs1-private-pem") : this.key.exportKey("pkcs8-public-pem");
}
};

57
lib/Response.js Normal file
View File

@ -0,0 +1,57 @@
'use strict';
const zlib = require('zlib');
const crypto = require("crypto");
const DOMParser = require("xmldom").DOMParser;
const XMLSerializer = require("xmldom").XMLSerializer;
const xpath = require("xpath");
module.exports = class Response {
constructor(client, data) {
this.client = client;
this.doc = new DOMParser().parseFromString(data, 'text/xml');
};
isSegmented() {
const select = xpath.useNamespaces({'xmlns': "urn:org:ebics:H004"});
const node = select("//xmlns:header/xmlns:mutable/xmlns:SegmentNumber", this.doc);
return node.length ? true: false;
}
isLastSegment() {
const select = xpath.useNamespaces({'xmlns': "urn:org:ebics:H004"});
const node = select("//xmlns:header/xmlns:mutable/*[@lastSegment='true']", this.doc);
return node.length ? true: false;
}
orderData() {
const orderData = this.doc.getElementsByTagNameNS("urn:org:ebics:H004", "OrderData")[0].textContent;
const decipher = crypto.createDecipheriv('aes-128-cbc', this.transactionKey(), Buffer.from([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,])).setAutoPadding(false);
const data = Buffer.from(decipher.update(orderData, 'base64', 'binary') + decipher.final('binary'), 'binary');
return zlib.inflateSync(data).toString();
}
transactionKey() {
const keyNodeText = this.doc.getElementsByTagNameNS("urn:org:ebics:H004", "TransactionKey")[0].textContent;
const tkEncrypted = Buffer.from(keyNodeText, 'base64');
this.client.e().key.setOptions({encryptionScheme: 'pkcs1'});
return this.client.e().key.decrypt(tkEncrypted);
}
transactionId() {
const select = xpath.useNamespaces({'xmlns': "urn:org:ebics:H004"});
const node = select("//xmlns:header/xmlns:static/xmlns:TransactionID", this.doc);
return node.length ? node[0].textContent : '';
}
toXML() {
return new XMLSerializer().serializeToString(this.doc);
}
};

103
lib/Signer.js Normal file
View File

@ -0,0 +1,103 @@
'use strict';
const crypto = require("crypto");
const DOMParser = require("xmldom").DOMParser;
const XMLSerializer = require("xmldom").XMLSerializer;
const select = require("xpath.js");
const c14n = require('xml-crypto/lib/c14n-canonicalization').C14nCanonicalization;
module.exports = class Signer {
/**
* Contructor.
*
* @param {Client} client
* @param {String} data
*/
constructor(client, data) {
/**
* The main client
*
* @type {Signer}
*/
this.client = client;
/**
* Request data - generated xml
*
* @type {...}
*/
this.doc = new DOMParser().parseFromString(data, 'text/xml');
}
_junk() {
this.digest();
this.sign();
// console.log(this.toXML());
/* const headerSet = select(this.doc, "//*[@authenticate='true']").map(x => {
// x.setAttribute('xmlns:ds', 'http://www.w3.org/2000/09/xmldsig#');
return new c14n().process(x);
}).join();
const can = headerSet.replace('xmlns="urn:org:ebics:H004"', 'xmlns="urn:org:ebics:H004" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"');
const hash = crypto.createHash('sha256');
hash.update(can);
const digester = hash.digest('base64').trim();
if ( this.doc.getElementsByTagName("ds:DigestValue")[0] )
this.doc.getElementsByTagName("ds:DigestValue")[0].textContent = digester; */
/* const nodeSet = select(this.doc, "//ds:SignedInfo");
const canonicalized = nodeSet.map(x => {
const g = x.toString();
const res = new c14n().process(x);
return res;
}).join();
const canonicalizedString = canonicalized.replace('xmlns:ds="http://www.w3.org/2000/09/xmldsig#"', 'xmlns="urn:org:ebics:H004" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"');
// const SIGN = crypto.createSign('RSA-SHA256');
// SIGN.update(canonicalizedString);
// const key = SIGN.sign(this.client.x().key.exportKey("pkcs1-private-pem"), 'base64');
const f = this.client.x().key.sign(canonicalizedString, 'base64');
if ( this.doc.getElementsByTagName("ds:SignatureValue")[0] ) {
this.doc.getElementsByTagName("ds:SignatureValue")[0].textContent = f;
} */
}
digest() {
// get the xml node, where the digested value is supposed to be
const nodeDigestValue = this.doc.getElementsByTagName("ds:DigestValue")[0];
const nodes = select(this.doc, "//*[@authenticate='true']");
// canonicalize the node that has authenticate='true' attribute
const contentToDigest = select(this.doc, "//*[@authenticate='true']")
.map(x => {
const aaaa = x.toString();
return new c14n().process(x)
}).join("");
// fix the canonicalization
const fixedContent = contentToDigest.replace(/xmlns="urn:org:ebics:H004"/g, 'xmlns="urn:org:ebics:H004" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"');
if (nodeDigestValue) {
nodeDigestValue.textContent = crypto.createHash('sha256').update(fixedContent).digest('base64').trim();
}
};
sign() {
const nodeSignatureValue = this.doc.getElementsByTagName("ds:SignatureValue")[0];
if (nodeSignatureValue) {
const contentToSign = (new c14n().process(select(this.doc, "//ds:SignedInfo")[0])).replace('xmlns:ds="http://www.w3.org/2000/09/xmldsig#"', 'xmlns="urn:org:ebics:H004" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"');
nodeSignatureValue.textContent = this.client.x().key.sign(contentToSign, 'base64');
}
}
toXML() {
return new XMLSerializer().serializeToString(this.doc);
}
};

12
lib/consts.js Normal file
View File

@ -0,0 +1,12 @@
'use strict';
const packageJson = require('../package.json');
const name = 'eCollect Node Ebics Client';
const version = packageJson.version;
module.exports = {
name,
version,
productString: `${name} ${version}`,
};

164
lib/ini.hbs Normal file
View File

@ -0,0 +1,164 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta charset="UTF-8" />
<title>EBICS ini</title>
</head>
<body>
<div>
<h2>EBICS-Initialisierungsbrief (INI)</h2>
<table>
<tr>
<td>Datum</td>
<td>{{ today }}</td>
</tr>
<tr>
<td>Uhrzeit</td>
<td>{{ now }}</td>
</tr>
<tr>
<td>Empfänger</td>
<td>{{ bankName }}</td>
</tr>
<tr>
<td>User-ID</td>
<td>{{ userId }}</td>
</tr>
<tr>
<td>Kunden-ID</td>
<td>{{ partnerId }}</td>
</tr>
</table>
<p>Öffentlicher Schlüssel für die elektronische Unterschrift (A006)</p>
<p>Exponent ({{ keyExponentBits A006 }} Bit):</p>
<p>
<code>{{ keyExponent A006 }}</code>
</p>
<p>Modulus ({{ keyModulusBits A006 }} Bit):</p>
<p>
<code>{{ keyModulus A006 }}</code>
</p>
<p>Hash (SHA-256):</p>
<p>
<code>{{ sha256 A006 }}</code>
</p>
<p>Ich bestätige hiermit den obigen öffentlichen Schlüssel für meine elektronische Unterschrift.</p>
<br/>
<br/>
<br/>
<br/>
<table>
<tr>
<td>_________________________</td>
<td>_________________________</td>
<td>_________________________</td>
</tr>
<tr>
<td>Ort/Datum</td>
<td>Name/Firma</td>
<td>Unterschrift</td>
</tr>
</table>
</div>
<div style="page-break-after:always"></div>
<h2>EBICS-Initialisierungsbrief (HIA) - Seite 1/2</h2>
<table>
<tr>
<td>Datum</td>
<td>{{ today }}</td>
</tr>
<tr>
<td>Uhrzeit</td>
<td>{{ now }}</td>
</tr>
<tr>
<td>Empfänger</td>
<td>{{ bankName }}</td>
</tr>
<tr>
<td>User-ID</td>
<td>{{ userId }}</td>
</tr>
<tr>
<td>Kunden-ID</td>
<td>{{ partnerId }}</td>
</tr>
</table>
<div>
<p>Öffentlicher Authentifikationsschlüssel (X002)</p>
<p>Exponent ({{ keyExponentBits X002 }} Bit):</p>
<p>
<code>{{ keyExponent X002 }}</code>
</p>
<p>Modulus ({{ keyModulusBits X002 }} Bit):</p>
<p>
<code>{{ keyModulus X002 }}</code>
</p>
<p>Hash (SHA-256):</p>
<p>
<code>{{ sha256 X002 }}</code>
</p>
<p> Fortsetzung auf Seite 2 ...</p>
<div style="page-break-after:always"></div>
<h2>EBICS-Initialisierungsbrief (HIA) - Seite 2/2</h2>
<table>
<tr>
<td>Datum</td>
<td>{{ today }}</td>
</tr>
<tr>
<td>Uhrzeit</td>
<td>{{ now }}</td>
</tr>
<tr>
<td>Empfänger</td>
<td>{{ bankName }}</td>
</tr>
<tr>
<td>User-ID</td>
<td>{{ userId }}</td>
</tr>
<tr>
<td>Kunden-ID</td>
<td>{{ partnerId }}</td>
</tr>
</table>
</div>
<div>
<p>Öffentlicher Verschlüsselungsschlüssel (E002)</p>
<p>Exponent ({{ keyExponentBits E002 }} Bit):</p>
<p>
<code>{{ keyExponent E002 }}</code>
</p>
<p>Modulus ({{ keyModulusBits E002 }} Bit):</p>
<p>
<code>{{ keyModulus E002 }}</code>
</p>
<p>Hash (SHA-256):</p>
<p>
<code>{{ sha256 E002 }}</code>
</p>
<p>Ich bestätige hiermit die obigen öffentlichen Schlüssel für meinen EBICS-Zugang.</p>
<br/>
<br/>
<br/>
<br/>
<table>
<tr>
<td>_________________________</td>
<td>_________________________</td>
<td>_________________________</td>
</tr>
<tr>
<td>Ort/Datum</td>
<td>Name/Firma</td>
<td>Unterschrift</td>
</tr>
</table>
</div>
</body>
</html>

View File

@ -0,0 +1,22 @@
'use strict';
const Response = require('../Response');
module.exports = class ParseResponse {
constructor(client, data) {
this.client = client;
this.data = data;
};
static go (client, data) {
const parseRensponse = new ParseResponse(client, data);
const response = new Response(client, data);
// TODO:
// raise error if any
this.data = response.doc;
return response;
}
};

22
lib/middleware/XMLSign.js Normal file
View File

@ -0,0 +1,22 @@
'use strict';
const Signer = require('../Signer');
module.exports = class XMLSign {
constructor(client, data) {
this.client = client;
this.data = data;
};
static go (client, data) {
const xmlSigner = new XMLSign(client, data);
const signer = new Signer(client, data);
signer.digest();
signer.sign();
this.data = signer.toXML();
return this.data;
}
};

50
lib/orders/C52.js Normal file
View File

@ -0,0 +1,50 @@
'use strict';
const GenericOrder = require('./GenericOrder');
module.exports = class C52 extends GenericOrder {
constructor (client, from, to) {
super(client);
this._from = from;
this._to = to;
this._schema.header = {
"@" : { authenticate: true },
static: {
HostID : this.hostId,
Nonce : this.nonce(),
Timestamp: this.timestamp(),
PartnerID: this.partnerId,
UserID : this.userId,
Product : {
"@": { Language: "de" },
"#": this.productString,
},
OrderDetails: {
OrderType : "C52",
OrderAttribute : "DZHNN",
StandardOrderParams: {
DateRange: {
Start: this._from,
End : this._to
}
},
},
BankPubKeyDigests: {
Authentication: {
"@": { Version: "X002", Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256" },
"#": this.client.bankX().publicDigest()
},
Encryption: {
"@": { Version: "E002", Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256" },
"#": this.client.bankE().publicDigest()
}
},
SecurityMedium: "0000"
},
mutable: {
TransactionPhase: "Initialisation"
}
};
};
};

143
lib/orders/GenericOrder.js Normal file
View File

@ -0,0 +1,143 @@
'use strict';
// const randHex = require('../../lib/utils').randHex;
const crypto = require("crypto");
const js2xmlparser = require('js2xmlparser');
const consts = require('../consts');
module.exports = class GenericOrder {
constructor(client) {
this.client = client;
this.hostId = client.hostId;
this.userId = client.userId;
this.partnerId = client.partnerId;
this.transactionId = '';
this.xmlOptions = {
declaration: {
include: true,
encoding: "utf-8"
},
format: {
doubleQuotes: true,
indent: '',
newline: '',
// indent: "\t",
// newline: "\r\n",
pretty: true
}
};
this._schema = {
"@": {
"xmlns:ds": "http://www.w3.org/2000/09/xmldsig#",
xmlns: "urn:org:ebics:H004",
Version: "H004",
Revision: "1"
},
header: {},
AuthSignature: this.authSignature(),
body: {}
};
}
authSignature() {
return {
"ds:SignedInfo": {
"ds:CanonicalizationMethod": {
"@": {
Algorithm:
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
}
},
"ds:SignatureMethod": {
"@": {
Algorithm:
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
}
},
"ds:Reference": {
"@": { URI: "#xpointer(//*[@authenticate='true'])" },
"ds:Transforms": {
"ds:Transform": {
"@": {
Algorithm:
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
}
}
},
"ds:DigestMethod": {
"@": {
Algorithm:
"http://www.w3.org/2001/04/xmlenc#sha256"
}
},
"ds:DigestValue": {}
}
},
"ds:SignatureValue": {}
};
}
get schema() {
return this._schema;
}
get productString() {
return consts.productString;
}
nonce() {
return crypto.randomBytes(16).toString('hex');
}
timestamp() {
return new Date().toISOString();
}
root() {
return "ebicsRequest";
}
toReceiptXML() {
const xmlObj = {
"@": {
"xmlns:ds": "http://www.w3.org/2000/09/xmldsig#",
xmlns: "urn:org:ebics:H004",
Version: "H004",
Revision: "1"
},
header: {
"@": { authenticate: true },
static: {
HostID: this.hostId,
TransactionID: this.transactionId
},
mutable: {
TransactionPhase: 'Receipt',
}
},
AuthSignature: this.authSignature(),
body: {
TransferReceipt: {
"@": { authenticate: true },
ReceiptCode: 0
}
}
};
return js2xmlparser.parse(this.root(), xmlObj, this.xmlOptions);
}
toXML() {
return js2xmlparser.parse(this.root(), this._schema, this.xmlOptions);
}
};

View File

@ -0,0 +1,63 @@
'use strict';
const zlib = require('zlib');
const crypto = require("crypto");
const js2xmlparser = require('js2xmlparser');
const GenericOrder = require('./GenericOrder');
module.exports = class GenericUploadOrder extends GenericOrder {
constructor(client, document) {
super(client);
this._document = document;
this._key = crypto.randomBytes(16);
this._schema.body = {
DataTransfer: {
DataEncryptionInfo: {
"@": { authenticate: true },
EncryptionPubKeyDigest: {
"@": { Version: "E002", Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256" },
"#": this.client.bankE().publicDigest()
},
TransactionKey: Buffer.from(this.client.bankE().publicEncrypt(this._key)).toString('base64'),
},
SignatureData: {
"@": { authenticate: true },
"#": this.encryptedOrderSignature()
}
}
};
};
orderSignature() {
const xmlObj = {
"@": {
xmlns: "http://www.ebics.org/S001",
"xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
"xsi:schemaLocation": "http://www.ebics.org/S001 http://www.ebics.org/S001/ebics_signature.xsd"
},
OrderSignatureData: {
SignatureVersion: "A006",
SignatureValue: this.signatureValue(),
PartnerID: this.partnerId,
UserID: this.userId
}
};
return js2xmlparser.parse('UserSignatureData', xmlObj, this.xmlOptions);
};
signatureValue() {
const digested = crypto.createHash('sha256').update(this._document).digest();
};
encryptedOrderSignature() {
const dst = zlib.deflateSync(this.orderSignature());
const cipher = crypto.createCipheriv('aes-128-cbc', this._key, Buffer.from([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,])).setAutoPadding(false);
const encrypted = cipher.update(dst) + cipher.final();
return Buffer.from(encrypted).toString('base64');
};
};

43
lib/orders/HAA.js Normal file
View File

@ -0,0 +1,43 @@
'use strict';
const GenericOrder = require('./GenericOrder');
module.exports = class HAA extends GenericOrder {
constructor (client) {
super(client);
this._schema.header = {
"@": { authenticate: true },
static: {
HostID: this.hostId,
Nonce: this.nonce(),
Timestamp: this.timestamp(),
PartnerID: this.partnerId,
UserID: this.userId,
Product: {
"@": { Language: "de" },
"#": this.productString
},
OrderDetails: {
OrderType: "HAA",
OrderAttribute: "DZHNN",
StandardOrderParams: ""
},
BankPubKeyDigests: {
Authentication: {
"@": { Version: "X002", Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256" },
"#": this.client.bankX().publicDigest()
},
Encryption: {
"@": { Version: "E002", Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256" },
"#": this.client.bankE().publicDigest()
}
},
SecurityMedium: "0000"
},
mutable: {
TransactionPhase: "Initialisation"
}
};
};
};

54
lib/orders/HAC.js Normal file
View File

@ -0,0 +1,54 @@
'use strict';
const GenericOrder = require('./GenericOrder');
module.exports = class HAC extends GenericOrder {
constructor (client, from = null, to = null) {
super(client);
this._from = from;
this._to = to;
this._schema.header = {
"@" : { authenticate: true },
static: {
HostID : this.hostId,
Nonce : this.nonce(),
Timestamp: this.timestamp(),
PartnerID: this.partnerId,
UserID : this.userId,
Product : {
"@": { Language: "de" },
"#": this.productString,
},
OrderDetails: {
OrderType : "HAC",
OrderAttribute : "DZHNN",
StandardOrderParams: this._hasDateRange() ? {
DateRange: {
Start: this._from,
End : this._to
}
} : ""
},
BankPubKeyDigests: {
Authentication: {
"@": { Version: "X002", Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256" },
"#": this.client.bankX().publicDigest()
},
Encryption: {
"@": { Version: "E002", Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256" },
"#": this.client.bankE().publicDigest()
}
},
SecurityMedium: "0000"
},
mutable: {
TransactionPhase: "Initialisation"
}
};
};
_hasDateRange() {
return this._from && this._to;
}
};

85
lib/orders/HIA.js Normal file
View File

@ -0,0 +1,85 @@
'use strict';
const zlib = require('zlib');
const js2xmlparser = require('js2xmlparser');
const GenericOrder = require('./GenericOrder');
module.exports = class HIA extends GenericOrder {
constructor(client) {
super(client);
this._schema = {
"@": {
"xmlns:ds": "http://www.w3.org/2000/09/xmldsig#",
xmlns: "urn:org:ebics:H004",
Version: "H004",
Revision: "1"
},
header: {
"@": { authenticate: true },
static: {
HostID: this.hostId,
PartnerID: this.partnerId,
UserID: this.userId,
Product: {
"@": { Language: "de" },
"#": this.productString,
},
OrderDetails: {
OrderType: "HIA",
OrderAttribute: "DZNNN"
},
SecurityMedium: "0000"
},
mutable: {}
},
body: {
DataTransfer: {
OrderData: Buffer.from(zlib.deflateSync(this.orderData())).toString('base64')
}
}
};
}
root() {
return "ebicsUnsecuredRequest";
};
orderData() {
const xmlOrderData = {
"@": {
"xmlns:ds": "http://www.w3.org/2000/09/xmldsig#",
xmlns: "urn:org:ebics:H004"
},
AuthenticationPubKeyInfo: {
PubKeyValue: {
"ds:RSAKeyValue": {
"ds:Modulus": Buffer.from(this.client.x().n(), 'HEX').toString('base64'),
"ds:Exponent": "AQAB"
},
},
AuthenticationVersion: "X002"
},
EncryptionPubKeyInfo: {
PubKeyValue: {
"ds:RSAKeyValue": {
"ds:Modulus": Buffer.from(this.client.e().n(), 'HEX').toString('base64'),
"ds:Exponent": "AQAB"
},
},
EncryptionVersion: "E002"
},
PartnerID: this.partnerId,
UserID: this.userId
};
return js2xmlparser.parse("HIARequestOrderData", xmlOrderData, this.xmlOptions);
};
toXML() {
return js2xmlparser.parse(this.root(), this._schema, this.xmlOptions);
};
};

43
lib/orders/HKD.js Normal file
View File

@ -0,0 +1,43 @@
'use strict';
const GenericOrder = require('./GenericOrder');
module.exports = class HKD extends GenericOrder {
constructor (client) {
super(client);
this._schema.header = {
"@": { authenticate: true },
static: {
HostID: this.hostId,
Nonce: this.nonce(),
Timestamp: this.timestamp(),
PartnerID: this.partnerId,
UserID: this.userId,
Product: {
"@": { Language: "de" },
"#": this.productString,
},
OrderDetails: {
OrderType: "HKD",
OrderAttribute: "DZHNN",
StandardOrderParams: ""
},
BankPubKeyDigests: {
Authentication: {
"@": { Version: "X002", Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256" },
"#": this.client.bankX().publicDigest()
},
Encryption: {
"@": { Version: "E002", Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256" },
"#": this.client.bankE().publicDigest()
}
},
SecurityMedium: "0000"
},
mutable: {
TransactionPhase: "Initialisation"
}
};
};
};

34
lib/orders/HPB.js Normal file
View File

@ -0,0 +1,34 @@
'use strict';
const GenericOrder = require('./GenericOrder');
module.exports = class HPB extends GenericOrder {
constructor (client) {
super(client);
this._schema.header = {
"@": { authenticate: true },
static: {
HostID: this.hostId,
Nonce: this.nonce(),
Timestamp: this.timestamp(),
PartnerID: this.partnerId,
UserID: this.userId,
Product: {
"@": { Language: "de" },
"#": this.productString,
},
OrderDetails: {
OrderType: "HPB",
OrderAttribute: "DZHNN"
},
SecurityMedium: "0000"
},
mutable: {}
};
};
root() {
return "ebicsNoPubKeyDigestsRequest";
};
};

43
lib/orders/HTD.js Normal file
View File

@ -0,0 +1,43 @@
'use strict';
const GenericOrder = require('./GenericOrder');
module.exports = class HTD extends GenericOrder {
constructor (client) {
super(client);
this._schema.header = {
"@": { authenticate: true },
static: {
HostID: this.hostId,
Nonce: this.nonce(),
Timestamp: this.timestamp(),
PartnerID: this.partnerId,
UserID: this.userId,
Product: {
"@": { Language: "de" },
"#": this.productString,
},
OrderDetails: {
OrderType: "HTD",
OrderAttribute: "DZHNN",
StandardOrderParams: ""
},
BankPubKeyDigests: {
Authentication: {
"@": { Version: "X002", Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256" },
"#": this.client.bankX().publicDigest()
},
Encryption: {
"@": { Version: "E002", Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256" },
"#": this.client.bankE().publicDigest()
}
},
SecurityMedium: "0000"
},
mutable: {
TransactionPhase: "Initialisation"
}
};
};
};

77
lib/orders/INI.js Normal file
View File

@ -0,0 +1,77 @@
'use strict';
const zlib = require('zlib');
const js2xmlparser = require('js2xmlparser');
const GenericOrder = require('./GenericOrder');
module.exports = class INI extends GenericOrder {
constructor (client) {
super(client);
this._schema = {
"@": {
"xmlns:ds": "http://www.w3.org/2000/09/xmldsig#",
xmlns: "urn:org:ebics:H004",
Version: "H004",
Revision: "1"
},
header: {
"@": { authenticate: true },
static: {
HostID: this.hostId,
PartnerID: this.partnerId,
UserID: this.userId,
Product: {
"@": { Language: "de" },
"#": this.productString,
},
OrderDetails: {
OrderType: "INI",
OrderAttribute: "DZNNN"
},
SecurityMedium: "0000"
},
mutable: {}
},
body: {
DataTransfer: {
OrderData: Buffer.from(zlib.deflateSync(this.keySignature())).toString('base64')
}
}
};
}
root() {
return "ebicsUnsecuredRequest";
};
keySignature() {
const xmlOrderData = {
"@": {
"xmlns:ds": "http://www.w3.org/2000/09/xmldsig#",
xmlns: "http://www.ebics.org/S001"
},
SignaturePubKeyInfo: {
PubKeyValue: {
"ds:RSAKeyValue": {
"ds:Modulus": Buffer.from(this.client.a().n(), 'HEX').toString('base64'),
"ds:Exponent": "AQAB"
},
TimeStamp: this.timestamp()
},
SignatureVersion: "A006"
},
PartnerID: this.partnerId,
UserID: this.userId
};
return js2xmlparser.parse("SignaturePubKeyOrderData", xmlOrderData, this.xmlOptions);
};
toXML() {
return js2xmlparser.parse(this.root(), this._schema, this.xmlOptions);
}
};

36
package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "node-ebics-client",
"version": "0.0.3",
"description": "Node.js ISO 20022 Compliant EBICS Client",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://github.com/eCollect/node-ebics-client"
},
"keywords": [
"EBICS",
"ISO20022",
"nodejs",
"api"
],
"author": "eCollect Sofia Tech Team",
"license": "MIT",
"dependencies": {
"bn.js": "^4.11.8",
"handlebars": "^4.0.11",
"js2xmlparser": "^3.0.0",
"moment": "^2.22.1",
"node-rsa": "^0.4.2",
"xml-c14n": "0.0.6",
"xmldom": "^0.1.27",
"xpath": "0.0.27"
},
"devDependencies": {
"eslint": "^4.19.1",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-plugin-import": "^2.12.0"
}
}