From 1f947ff1480c522f89fa1f547581b55e2378d920 Mon Sep 17 00:00:00 2001 From: Vladislav Hristov Date: Thu, 17 May 2018 18:03:59 +0300 Subject: [PATCH] initial commit --- .editorconfig | 32 +++++ .env | 4 + .eslintignore | 6 + .eslintrc | 34 +++++ .gitignore | 14 ++ README.md | 20 +++ index.js | 4 + lib/BankLetter.js | 66 +++++++++ lib/Client.js | 233 +++++++++++++++++++++++++++++++ lib/Key.js | 38 +++++ lib/Response.js | 57 ++++++++ lib/Signer.js | 103 ++++++++++++++ lib/consts.js | 12 ++ lib/ini.hbs | 164 ++++++++++++++++++++++ lib/middleware/ParseResponse.js | 22 +++ lib/middleware/XMLSign.js | 22 +++ lib/orders/C52.js | 50 +++++++ lib/orders/GenericOrder.js | 143 +++++++++++++++++++ lib/orders/GenericUploadOrder.js | 63 +++++++++ lib/orders/HAA.js | 43 ++++++ lib/orders/HAC.js | 54 +++++++ lib/orders/HIA.js | 85 +++++++++++ lib/orders/HKD.js | 43 ++++++ lib/orders/HPB.js | 34 +++++ lib/orders/HTD.js | 43 ++++++ lib/orders/INI.js | 77 ++++++++++ package.json | 36 +++++ 27 files changed, 1502 insertions(+) create mode 100644 .editorconfig create mode 100644 .env create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.js create mode 100644 lib/BankLetter.js create mode 100644 lib/Client.js create mode 100644 lib/Key.js create mode 100644 lib/Response.js create mode 100644 lib/Signer.js create mode 100644 lib/consts.js create mode 100644 lib/ini.hbs create mode 100644 lib/middleware/ParseResponse.js create mode 100644 lib/middleware/XMLSign.js create mode 100644 lib/orders/C52.js create mode 100644 lib/orders/GenericOrder.js create mode 100644 lib/orders/GenericUploadOrder.js create mode 100644 lib/orders/HAA.js create mode 100644 lib/orders/HAC.js create mode 100644 lib/orders/HIA.js create mode 100644 lib/orders/HKD.js create mode 100644 lib/orders/HPB.js create mode 100644 lib/orders/HTD.js create mode 100644 lib/orders/INI.js create mode 100644 package.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1a8cf57 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.env b/.env new file mode 100644 index 0000000..26f0ea1 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +SERVER_PORT=3003 + +LOG_LEVEL=info +LOG_ENABLED=true \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..efdbb4d --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +node_modules/ +build/ +static/ +config/ +src/vue-router-custom-components +src/directives/clickAway diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..72b2042 --- /dev/null +++ b/.eslintrc @@ -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" + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e419de --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..eaf5aee --- /dev/null +++ b/README.md @@ -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. diff --git a/index.js b/index.js new file mode 100644 index 0000000..395b4e4 --- /dev/null +++ b/index.js @@ -0,0 +1,4 @@ +'use strict'; + +const Client = require('./lib/Client'); +module.exports = Client; \ No newline at end of file diff --git a/lib/BankLetter.js b/lib/BankLetter.js new file mode 100644 index 0000000..cc495e9 --- /dev/null +++ b/lib/BankLetter.js @@ -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); + } +} diff --git a/lib/Client.js b/lib/Client.js new file mode 100644 index 0000000..9df7a60 --- /dev/null +++ b/lib/Client.js @@ -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)); + } +}; diff --git a/lib/Key.js b/lib/Key.js new file mode 100644 index 0000000..bd48a06 --- /dev/null +++ b/lib/Key.js @@ -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"); + } +}; diff --git a/lib/Response.js b/lib/Response.js new file mode 100644 index 0000000..325c304 --- /dev/null +++ b/lib/Response.js @@ -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); + } +}; diff --git a/lib/Signer.js b/lib/Signer.js new file mode 100644 index 0000000..7cb4ec3 --- /dev/null +++ b/lib/Signer.js @@ -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); + } + +}; diff --git a/lib/consts.js b/lib/consts.js new file mode 100644 index 0000000..50fdd0d --- /dev/null +++ b/lib/consts.js @@ -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}`, +}; \ No newline at end of file diff --git a/lib/ini.hbs b/lib/ini.hbs new file mode 100644 index 0000000..7b38faf --- /dev/null +++ b/lib/ini.hbs @@ -0,0 +1,164 @@ + + + + + + + EBICS ini + + + +
+

EBICS-Initialisierungsbrief (INI)

+ + + + + + + + + + + + + + + + + + + + + +
Datum{{ today }}
Uhrzeit{{ now }}
Empfänger{{ bankName }}
User-ID{{ userId }}
Kunden-ID{{ partnerId }}
+

Öffentlicher Schlüssel für die elektronische Unterschrift (A006)

+

Exponent ({{ keyExponentBits A006 }} Bit):

+

+ {{ keyExponent A006 }} +

+

Modulus ({{ keyModulusBits A006 }} Bit):

+

+ {{ keyModulus A006 }} +

+

Hash (SHA-256):

+

+ {{ sha256 A006 }} +

+

Ich bestätige hiermit den obigen öffentlichen Schlüssel für meine elektronische Unterschrift.

+
+
+
+
+ + + + + + + + + + + +
___________________________________________________________________________
Ort/DatumName/FirmaUnterschrift
+
+
+

EBICS-Initialisierungsbrief (HIA) - Seite 1/2

+ + + + + + + + + + + + + + + + + + + + + +
Datum{{ today }}
Uhrzeit{{ now }}
Empfänger{{ bankName }}
User-ID{{ userId }}
Kunden-ID{{ partnerId }}
+
+

Öffentlicher Authentifikationsschlüssel (X002)

+

Exponent ({{ keyExponentBits X002 }} Bit):

+

+ {{ keyExponent X002 }} +

+

Modulus ({{ keyModulusBits X002 }} Bit):

+

+ {{ keyModulus X002 }} +

+

Hash (SHA-256):

+

+ {{ sha256 X002 }} +

+

Fortsetzung auf Seite 2 ...

+
+

EBICS-Initialisierungsbrief (HIA) - Seite 2/2

+ + + + + + + + + + + + + + + + + + + + + +
Datum{{ today }}
Uhrzeit{{ now }}
Empfänger{{ bankName }}
User-ID{{ userId }}
Kunden-ID{{ partnerId }}
+
+
+

Öffentlicher Verschlüsselungsschlüssel (E002)

+

Exponent ({{ keyExponentBits E002 }} Bit):

+

+ {{ keyExponent E002 }} +

+

Modulus ({{ keyModulusBits E002 }} Bit):

+

+ {{ keyModulus E002 }} +

+

Hash (SHA-256):

+

+ {{ sha256 E002 }} +

+

Ich bestätige hiermit die obigen öffentlichen Schlüssel für meinen EBICS-Zugang.

+
+
+
+
+ + + + + + + + + + + +
___________________________________________________________________________
Ort/DatumName/FirmaUnterschrift
+
+ + + diff --git a/lib/middleware/ParseResponse.js b/lib/middleware/ParseResponse.js new file mode 100644 index 0000000..fc20336 --- /dev/null +++ b/lib/middleware/ParseResponse.js @@ -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; + } +}; diff --git a/lib/middleware/XMLSign.js b/lib/middleware/XMLSign.js new file mode 100644 index 0000000..c88abfe --- /dev/null +++ b/lib/middleware/XMLSign.js @@ -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; + } +}; diff --git a/lib/orders/C52.js b/lib/orders/C52.js new file mode 100644 index 0000000..05ab511 --- /dev/null +++ b/lib/orders/C52.js @@ -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" + } + }; + }; +}; diff --git a/lib/orders/GenericOrder.js b/lib/orders/GenericOrder.js new file mode 100644 index 0000000..0f07a44 --- /dev/null +++ b/lib/orders/GenericOrder.js @@ -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); + } +}; diff --git a/lib/orders/GenericUploadOrder.js b/lib/orders/GenericUploadOrder.js new file mode 100644 index 0000000..4882132 --- /dev/null +++ b/lib/orders/GenericUploadOrder.js @@ -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'); + }; +}; diff --git a/lib/orders/HAA.js b/lib/orders/HAA.js new file mode 100644 index 0000000..cb101b3 --- /dev/null +++ b/lib/orders/HAA.js @@ -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" + } + }; + }; +}; diff --git a/lib/orders/HAC.js b/lib/orders/HAC.js new file mode 100644 index 0000000..68f2e57 --- /dev/null +++ b/lib/orders/HAC.js @@ -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; + } +}; diff --git a/lib/orders/HIA.js b/lib/orders/HIA.js new file mode 100644 index 0000000..b3866fb --- /dev/null +++ b/lib/orders/HIA.js @@ -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); + }; +}; diff --git a/lib/orders/HKD.js b/lib/orders/HKD.js new file mode 100644 index 0000000..d33acab --- /dev/null +++ b/lib/orders/HKD.js @@ -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" + } + }; + }; +}; diff --git a/lib/orders/HPB.js b/lib/orders/HPB.js new file mode 100644 index 0000000..b0f8bc7 --- /dev/null +++ b/lib/orders/HPB.js @@ -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"; + }; +}; diff --git a/lib/orders/HTD.js b/lib/orders/HTD.js new file mode 100644 index 0000000..68a1510 --- /dev/null +++ b/lib/orders/HTD.js @@ -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" + } + }; + }; +}; diff --git a/lib/orders/INI.js b/lib/orders/INI.js new file mode 100644 index 0000000..8d9d434 --- /dev/null +++ b/lib/orders/INI.js @@ -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); + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..f44a3fb --- /dev/null +++ b/package.json @@ -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" + } +}