From ff9a3a16b47d0a25674134c875bfd651995837e4 Mon Sep 17 00:00:00 2001 From: Vladislav Hristov Date: Mon, 11 Jun 2018 11:38:32 +0300 Subject: [PATCH] Major changes. Separating responsibilities. Orders builder, serializer. --- lib/BankLetter.js | 4 +- lib/Client.js | 240 +++++--------------- lib/Key.js | 76 ------- lib/Response.js | 79 ++++++- lib/Signer.js | 58 +---- lib/crypto/Crypto.js | 93 ++++++++ lib/{ => crypto}/MGF1.js | 0 lib/keymanagers/FsKeyStorage.js | 40 ++++ lib/keymanagers/Key.js | 50 ++++ lib/keymanagers/Keys.js | 58 +++++ lib/keymanagers/KeysManager.js | 94 ++++++++ lib/middleware/ParseResponse.js | 10 +- lib/middleware/XMLSign.js | 10 +- lib/orders/C52.js | 60 ----- lib/orders/GenericOrder.js | 181 --------------- lib/orders/GenericUploadOrder.js | 79 ------- lib/orders/H004/GenericSerializer.js | 81 +++++++ lib/orders/H004/InitializationSerializer.js | 128 +++++++++++ lib/orders/H004/OrderBuilder.js | 78 +++++++ lib/orders/H004/OrderSerializer.js | 15 ++ lib/orders/H004/PaymentSerializer.js | 131 +++++++++++ lib/orders/H004/StatusSerializer.js | 67 ++++++ 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 ------- 29 files changed, 986 insertions(+), 1025 deletions(-) delete mode 100644 lib/Key.js create mode 100644 lib/crypto/Crypto.js rename lib/{ => crypto}/MGF1.js (100%) create mode 100644 lib/keymanagers/FsKeyStorage.js create mode 100644 lib/keymanagers/Key.js create mode 100644 lib/keymanagers/Keys.js create mode 100644 lib/keymanagers/KeysManager.js delete mode 100644 lib/orders/C52.js delete mode 100644 lib/orders/GenericOrder.js delete mode 100644 lib/orders/GenericUploadOrder.js create mode 100644 lib/orders/H004/GenericSerializer.js create mode 100644 lib/orders/H004/InitializationSerializer.js create mode 100644 lib/orders/H004/OrderBuilder.js create mode 100644 lib/orders/H004/OrderSerializer.js create mode 100644 lib/orders/H004/PaymentSerializer.js create mode 100644 lib/orders/H004/StatusSerializer.js delete mode 100644 lib/orders/HAA.js delete mode 100644 lib/orders/HAC.js delete mode 100644 lib/orders/HIA.js delete mode 100644 lib/orders/HKD.js delete mode 100644 lib/orders/HPB.js delete mode 100644 lib/orders/HTD.js delete mode 100644 lib/orders/INI.js diff --git a/lib/BankLetter.js b/lib/BankLetter.js index 31d5a98..a31e404 100644 --- a/lib/BankLetter.js +++ b/lib/BankLetter.js @@ -14,9 +14,9 @@ const registerHelpers = () => { handlebars.registerHelper('keyModulusBits', k => k.key.getKeySize()); // return Buffer.byteLength(new BN(k.key.keyPair.e).toBuffer()) * 8; - handlebars.registerHelper('keyExponent', k => k.e()); + handlebars.registerHelper('keyExponent', k => k.e('hex')); - handlebars.registerHelper('keyModulus', k => k.n().toUpperCase().match(/.{1,2}/g).join(' ')); + handlebars.registerHelper('keyModulus', k => k.n('hex').toUpperCase().match(/.{1,2}/g).join(' ')); handlebars.registerHelper('sha256', (k) => { const digest = Buffer.from(k.publicDigest(), 'base64').toString('HEX'); diff --git a/lib/Client.js b/lib/Client.js index 7f1a2e4..5b9aa03 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -1,32 +1,21 @@ 'use strict'; -const fs = require('fs'); -const path = require('path'); +// const fs = require('fs'); +// const path = require('path'); -const crypto = require('crypto'); const $request = require('request'); -const BN = require('bn.js'); -const xpath = require('xpath'); -const NodeRSA = require('node-rsa'); -const { DOMParser } = require('xmldom'); - -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 BankLetter = require('./BankLetter'); -const defaultIniTemplatePath = path.join(__dirname, '../templates/ini.hbs'); +const OrderSerializer = require('./orders/H004/OrderSerializer'); + + +/* const defaultIniTemplatePath = path.join(__dirname, '../templates/ini.hbs'); const utils = { + mapObject: (o = {}, predicate = v => v) => Object.entries(o).reduce((r, [key, value]) => { r[key] = value; return r; }, o), exponent: { // str = 65537 => AQAB toBase64(str) { @@ -37,67 +26,28 @@ const utils = { 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; + /* constructor({ + url, + hostId, + userId, + partnerId, + keyManager = new FsKeyManager({ path: './keys.ebics', passphrase: 'node-ebics' }), + }) { this.url = url; this.hostId = hostId; this.userId = userId; this.partnerId = partnerId; - this.encryptAlgorithm = 'aes-256-cbc'; - this.keys = keysContent ? this.extractKeys() : {}; + this.keyManager = keyManager; + } */ + + constructor({ url }) { + this.url = url; } - 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(); // eslint-disable-line no-buffer-constructor - - 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); - - Object.keys({ A006: '', X002: '', E002: '' }).forEach((key) => { - client.keys[key] = new Key(new NodeRSA({ b: keysize })); - }); - // for (const key in Object.keys({ A006: '', X002: '', E002: '' })) - // client.keys[key] = new Key(new NodeRSA({ b: keysize })); - - - return client; - } + /* saveIniLetter(bankName, target, template) { const letter = new BankLetter({ @@ -112,137 +62,73 @@ module.exports = class Client { throw error; } } - - saveKeys(target) { - const data = {}; - - Object.keys(this.keys).forEach((key) => { - data[key] = this.encrypt(this.keys[key].toPem()); - }); - // for (const key in this.keys) - // data[key] = this.encrypt(this.keys[key].toPem()); - - try { - fs.writeFileSync(target, JSON.stringify(data)); - } catch (error) { - throw error; - } - } - - extractKeys() { - const keys = {}; - const jsonData = JSON.parse(this.keysContent); - - Object.keys(jsonData).forEach((key) => { - keys[key] = new Key(this.decrypt(jsonData[key])); - }); - // for (const key in jsonData) - // keys[key] = new Key(this.decrypt(jsonData[key])); + */ - return keys; + async initialization(order) { + const res = await this.ebicsRequest(OrderSerializer.serialize(order)); + const xml = res.orderData(); + + return { + orderData: xml, + orderId: res.orderId(), + returnCode: res.returnCode(), + reportText: res.reportText(), + bankKeys: res.bankKeys(), + }; } async download(order) { - const res = await this.ebicsRequest(order.toXML()); - - const ttt = res.toXML(); // TODO: keep this for debugging purposes + const res = await this.ebicsRequest(OrderSerializer.serialize(order)); order.transactionId = res.transactionId(); - if (res.isSegmented() && res.isLastSegment()) { - const receipt = await this.ebicsRequest(order.toReceiptXML()); + if (res.isSegmented() && res.isLastSegment()) + await this.ebicsRequest(OrderSerializer.serialize(order)); - const receiptXML = order.toReceiptXML(); // TODO: keep this for debugging purposes - const rX = receipt.toXML(); // TODO: keep this for debugging purposes - } - - return res.orderData(); + // return res.orderData(); + return { + orderData: res.orderData(), + orderId: res.orderId(), + returnCode: res.returnCode(), + reportText: res.reportText(), + }; } async upload(order) { - let res = await this.ebicsRequest(order.toXML()); - order.transactionId = res.transactionId(); - // const orderId = res.orderId(); + let res = await this.ebicsRequest(OrderSerializer.serialize(order)); + const transactionId = res.transactionId(); + const orderId = res.orderId(); - res = await this.ebicsRequest(order.toTransferXML()); + order.transactionId = transactionId; - return res.transactionId(); + res = await this.ebicsRequest(OrderSerializer.serialize(order)); + + return [transactionId, orderId]; } async downloadAndUnzip(order) { // eslint-disable-line } - ebicsRequest(order) { + ebicsRequest(serializedOrder) { + const { keys } = serializedOrder; + return new Promise((resolve, reject) => { + const s = XMLSign.go(keys, serializedOrder.toXML()); $request.post({ url: this.url, - body: XMLSign.go(this, order), + body: s, // XMLSign.go(this, serializedOrder), headers: { 'content-type': 'text/xml;charset=UTF-8' }, - }, (err, res, data) => (err ? reject(err) : resolve(ParseResponse.go(this, data)))); + }, (err, res, data) => (err ? reject(err) : resolve(ParseResponse.go(keys, data)))); }); } - async INI() { - return this.ebicsRequest((new EBICSINI(this)).toXML()); - } + request(order) { + if (order.type.toLowerCase() === 'ini') return this.initialization(order); + if (order.type.toLowerCase() === 'payment') return this.upload(order); + if (order.type.toLowerCase() === 'status') return this.download(order); - 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)); + throw Error('Invalid order type'); } }; diff --git a/lib/Key.js b/lib/Key.js deleted file mode 100644 index 78c6648..0000000 --- a/lib/Key.js +++ /dev/null @@ -1,76 +0,0 @@ -'use strict'; - -const crypto = require('crypto'); - -const BN = require('bn.js'); -const NodeRSA = require('node-rsa'); - -const mgf1 = require('./MGF1'); - -module.exports = class Key { - constructor(encodedKey/* , passphrase = null */) { - this.key = (encodedKey instanceof NodeRSA) ? encodedKey : 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(buf) { - return crypto.publicEncrypt({ - key: this.toPem(), - padding: crypto.constants.RSA_PKCS1_PADDING, - }, buf); - } - - 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'); - } - - sign(msg, salt = crypto.randomBytes(32)) { - const base = new BN(Key._emsaPSS(msg, salt)); - const power = new BN(this.key.keyPair.d.toBuffer()); - const mod = new BN(this.key.keyPair.n.toBuffer()); - - return (Key._modPow(base, power, mod)).toBuffer().toString('base64'); - } - - static _emsaPSS(msg, salt) { - const eightNullBytes = Buffer.from('\x00'.repeat(8)); - const digestedMsg = crypto.createHash('sha256').update(msg).digest(); - const mTickHash = crypto.createHash('sha256').update(Buffer.concat([eightNullBytes, digestedMsg, salt]), 'binary').digest(); - - const ps = Buffer.from('\x00'.repeat(190)); - const db = Buffer.concat([ps, Buffer.from('\x01'), salt]); - - const dbMask = mgf1.generate(mTickHash, db.length); - const maskedDb = mgf1.xor(db, dbMask); // so far so good - - let maskedDbMsb = mgf1.rjust(new BN(maskedDb.slice(0, 1), 2).toString(2), 8, '0'); - - maskedDbMsb = `0${maskedDbMsb.substr(1)}`; - maskedDb[0] = (new BN(maskedDbMsb, 2).toBuffer())[0]; // eslint-disable-line - - return Buffer.concat([maskedDb, mTickHash, Buffer.from('BC', 'hex')]); - } - - static _modPow(base, power, mod) { - let result = new BN(1); - while (power > 0) { - result = power.and(new BN(1)) === 1 ? (result.mul(base)).mod(mod) : result; - base = (base.mul(base)).mod(mod); - power = power.shrn(1); - } - return result; - } -}; diff --git a/lib/Response.js b/lib/Response.js index dfbe08d..075ef4d 100644 --- a/lib/Response.js +++ b/lib/Response.js @@ -2,14 +2,26 @@ const zlib = require('zlib'); const crypto = require('crypto'); +const BN = require('bn.js'); + +const Crypto = require('./crypto/Crypto'); const { DOMParser, XMLSerializer } = require('xmldom'); const xpath = require('xpath'); const DEFAULT_IV = Buffer.from(Array(16).fill(0, 0, 15)); + +const lastChild = (node) => { + let y = node.lastChild; + + while (y.nodeType !== 1) y = y.previousSibling; + + return y; +}; + module.exports = class Response { - constructor(client, data) { - this.client = client; + constructor(keys, data) { + this.keys = keys; this.doc = new DOMParser().parseFromString(data, 'text/xml'); } @@ -28,7 +40,11 @@ module.exports = class Response { } orderData() { - const orderData = this.doc.getElementsByTagNameNS('urn:org:ebics:H004', 'OrderData')[0].textContent; + const orderDataNode = this.doc.getElementsByTagNameNS('urn:org:ebics:H004', 'OrderData'); + + if (!orderDataNode.length) return {}; + + const orderData = orderDataNode[0].textContent; const decipher = crypto.createDecipheriv('aes-128-cbc', this.transactionKey(), DEFAULT_IV).setAutoPadding(false); const data = Buffer.from(decipher.update(orderData, 'base64', 'binary') + decipher.final('binary'), 'binary'); @@ -37,11 +53,8 @@ module.exports = class Response { 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); + return Crypto.privateDecrypt(this.keys.e(), Buffer.from(keyNodeText, 'base64')); } transactionId() { @@ -51,6 +64,58 @@ module.exports = class Response { return node.length ? node[0].textContent : ''; } + orderId() { + const select = xpath.useNamespaces({ xmlns: 'urn:org:ebics:H004' }); + const node = select('//xmlns:header/xmlns:mutable/xmlns:OrderID', this.doc); + + return node.length ? node[0].textContent : ''; + } + + returnCode() { + const select = xpath.useNamespaces({ xmlns: 'urn:org:ebics:H004' }); + const node = select('//xmlns:header/xmlns:mutable/xmlns:ReturnCode', this.doc); + + return node.length ? node[0].textContent : ''; + } + + reportText() { + const select = xpath.useNamespaces({ xmlns: 'urn:org:ebics:H004' }); + const node = select('//xmlns:header/xmlns:mutable/xmlns:ReportText', this.doc); + + return node.length ? node[0].textContent : ''; + } + + bankKeys() { + const orderData = this.orderData(); + if (!Object.keys(orderData).length) return {}; + + const doc = new DOMParser().parseFromString(orderData, 'text/xml'); + const select = xpath.useNamespaces({ xmlns: 'urn:org:ebics:H004' }); + const keyNodes = select('//xmlns:PubKeyValue', doc); + const bankKeys = {}; + + if (!keyNodes.length) return {}; + + for (let i = 0; i < keyNodes.length; i++) { + const type = lastChild(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 = new BN(Buffer.from(exponent, 'base64')).toNumber(); + + bankKeys[`bank${type}`] = { mod, exp }; + + // const bank = new NodeRSA(); + + // bank.importKey({ n: mod, e: exp }, 'components-public'); + + // this.keys[`${this.hostId}.${type}`] = new Key(bank); + } + + return bankKeys; + } + toXML() { return new XMLSerializer().serializeToString(this.doc); } diff --git a/lib/Signer.js b/lib/Signer.js index 8db089b..dca7226 100644 --- a/lib/Signer.js +++ b/lib/Signer.js @@ -1,6 +1,7 @@ 'use strict'; -const crypto = require('crypto'); +// const crypto = require('crypto'); +const Crypto = require('./crypto/Crypto'); const { DOMParser, XMLSerializer } = require('xmldom'); const xpath = require('xpath'); @@ -11,87 +12,48 @@ module.exports = class Signer { /** * Contructor. * - * @param {Client} client + * @param {Keys} keys * @param {String} data */ - constructor(client, data) { + constructor(keys, data) { /** - * The main client + * Keys to operate with * - * @type {Signer} + * @type {Keys} */ - this.client = client; + this.keys = keys; /** * Request data - generated xml * - * @type {...} + * @type {String} */ 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"]') const contentToDigest = xpath.select("//*[@authenticate='true']", this.doc) .map(x => new C14n().process(x)).join(''); - console.log('digest', 'contentToDigest', contentToDigest); // 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(); + nodeDigestValue.textContent = Crypto.digestWithHash(fixedContent).toString('base64').trim(); } sign() { const nodeSignatureValue = this.doc.getElementsByTagName('ds:SignatureValue')[0]; - console.log('sign =>'); if (nodeSignatureValue) { const select = xpath.useNamespaces({ ds: 'http://www.w3.org/2000/09/xmldsig#' }); const contentToSign = (new C14n().process(select('//ds:SignedInfo', this.doc)[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'); + nodeSignatureValue.textContent = Crypto.privateSign(this.keys.x(), contentToSign); // this.keys.x().key.sign(contentToSign, 'base64'); } } diff --git a/lib/crypto/Crypto.js b/lib/crypto/Crypto.js new file mode 100644 index 0000000..282183d --- /dev/null +++ b/lib/crypto/Crypto.js @@ -0,0 +1,93 @@ +'use strict'; + +const crypto = require('crypto'); + +const BN = require('bn.js'); + +const mgf1 = require('./MGF1'); + +const modPow = (base, power, mod) => { + let result = new BN(1); + + while (power > 0) { + result = power.and(new BN(1)) == 1 ? (result.mul(base)).mod(mod) : result; // eslint-disable-line + base = (base.mul(base)).mod(mod); + power = power.shrn(1); + } + return result; +}; + +const emsaPSS = (msg, salt) => { + const eightNullBytes = Buffer.from('\x00'.repeat(8)); + const digestedMsg = crypto.createHash('sha256').update(msg).digest(); + const mTickHash = crypto.createHash('sha256').update(Buffer.concat([eightNullBytes, digestedMsg, salt]), 'binary').digest(); + + const ps = Buffer.from('\x00'.repeat(190)); + const db = Buffer.concat([ps, Buffer.from('\x01'), salt]); + + const dbMask = mgf1.generate(mTickHash, db.length); + const maskedDb = mgf1.xor(db, dbMask); + + let maskedDbMsb = mgf1.rjust(new BN(maskedDb.slice(0, 1), 2).toString(2), 8, '0'); + + maskedDbMsb = `0${maskedDbMsb.substr(1)}`; + maskedDb[0] = (new BN(maskedDbMsb, 2).toBuffer())[0]; // eslint-disable-line + + return Buffer.concat([maskedDb, mTickHash, Buffer.from('BC', 'hex')]); +}; + + +module.exports = class Crypto { + static digestPublicKey(key) { + const str = [key.e('hex').replace(/^(0+)/g, ''), key.n('hex').replace(/^(0+)/g, '')].map(x => x.toLowerCase()).join(' '); + + return crypto.createHash('sha256').update(str).digest('base64').trim(); + } + + static publicEncrypt(key, data) { + return crypto.publicEncrypt({ + key: key.toPem(), + padding: crypto.constants.RSA_PKCS1_PADDING, + }, data); + } + + static privateDecrypt(key, data) { + return crypto.privateDecrypt({ + key: key.toPem(), + padding: crypto.constants.RSA_PKCS1_PADDING, + }, data); + } + + static privateSign(key, data, outputEncoding = 'base64') { + const signer = crypto.createSign('SHA256'); + + return signer.update(data).sign(key.toPem(), outputEncoding); + } + + static sign(key, msg, salt = crypto.randomBytes(32)) { + const base = new BN(emsaPSS(msg, salt)); + const power = new BN(key.d()); + const mod = new BN(key.n()); + + return (modPow(base, power, mod)).toBuffer().toString('base64'); + } + + static pad(d) { + const dLen = d.length; + const len = 16 * (Math.trunc(dLen / 16) + 1); + + return Buffer.concat([d, Buffer.from(Buffer.from([0]).toString().repeat(len - dLen - 1)), Buffer.from([len - dLen])]); + } + + static digestWithHash(data, algorith = 'sha256') { + return crypto.createHash(algorith).update(data).digest(); + } + + static nonce(outputEncoding = 'hex') { + return crypto.randomBytes(16).toString(outputEncoding); + } + + static timestamp() { + return new Date().toISOString(); + } +}; diff --git a/lib/MGF1.js b/lib/crypto/MGF1.js similarity index 100% rename from lib/MGF1.js rename to lib/crypto/MGF1.js diff --git a/lib/keymanagers/FsKeyStorage.js b/lib/keymanagers/FsKeyStorage.js new file mode 100644 index 0000000..e6b6911 --- /dev/null +++ b/lib/keymanagers/FsKeyStorage.js @@ -0,0 +1,40 @@ +'use strict'; + +const fs = require('fs'); +/* const extractKeys = (keysObject, encryptAlgorithm, passphrase) => Object.entries(keysObject).reduce((keys, [key, data]) => { + keys[key] = decrypt(data, encryptAlgorithm, passphrase); + return keys; +}, {}); */ + +module.exports = class FsKeyStorage { + /** + * @param {String} path - destingiton file to save the keys + */ + constructor({ path }) { + if (!path) + throw new Error('Invalid path provided'); + + this._path = path; + } + + get path() { + return this._path; + } + + read() { + return fs.readFileSync(this._path, { encoding: 'utf8' }); + // return extractKeys(JSON.parse(fs.readFileSync(this._path, { encoding: 'utf8' })), this.algorithm, this.passphrase); + } + + save(data) { + fs.writeFileSync(this._path, data, { encoding: 'utf8' }); + // fs.writeFileSync(this._path, encrypt(JSON.stringify(data), this.algorithm, this.passphrase), { encoding: 'utf8' }); + } + + hasData() { + if (fs.existsSync(this._path)) + return this.read() !== ''; + + return false; + } +}; diff --git a/lib/keymanagers/Key.js b/lib/keymanagers/Key.js new file mode 100644 index 0000000..c3c5809 --- /dev/null +++ b/lib/keymanagers/Key.js @@ -0,0 +1,50 @@ +'use strict'; + +const BN = require('bn.js'); +const NodeRSA = require('node-rsa'); + +module.exports = class Key { + constructor(encodedKey/* , passphrase = null */) { + this._key = (encodedKey instanceof NodeRSA) ? encodedKey : new NodeRSA(encodedKey); + } + + static generate(keysize = 2048) { + return new NodeRSA({ b: keysize }); + } + + static importKey({ mod, exp }) { + const key = new NodeRSA(); + + key.importKey({ n: mod, e: exp }, 'components-public'); + + return new Key(key); + } + + get key() { + return this._key; + } + + n(to = 'buff') { + const keyN = Buffer.from(this.key.exportKey('components-public').n); + + return to === 'hex' + ? keyN.toString('hex', 1) + : keyN; + } + + e(to = 'buff') { + const eKey = new BN(this.key.exportKey('components-public').e).toBuffer(); + + return to === 'hex' + ? eKey.toString('hex') + : eKey; + } + + d() { + return this.key.keyPair.d.toBuffer(); + } + + toPem() { + return this.key.isPrivate() ? this.key.exportKey('pkcs1-private-pem') : this.key.exportKey('pkcs8-public-pem'); + } +}; diff --git a/lib/keymanagers/Keys.js b/lib/keymanagers/Keys.js new file mode 100644 index 0000000..a2f3314 --- /dev/null +++ b/lib/keymanagers/Keys.js @@ -0,0 +1,58 @@ +'use strict'; + +const Key = require('./Key'); + +const keyOrNull = key => (key ? new Key(key) : null); + +module.exports = class Keys { + constructor({ + A006, + E002, + X002, + bankX002, + bankE002, + }) { + this.keys = { + A006: keyOrNull(A006), + E002: keyOrNull(E002), + X002: keyOrNull(X002), + bankX002: keyOrNull(bankX002), + bankE002: keyOrNull(bankE002), + }; + } + + static generate() { + const keys = {}; + + Object.keys({ A006: '', X002: '', E002: '' }).forEach((key) => { + keys[key] = Key.generate(); + }); + + return new Keys(keys); + } + + setBankKeys(bankKeys) { + this.keys.bankX002 = Key.importKey(bankKeys.bankX002); + this.keys.bankE002 = Key.importKey(bankKeys.bankE002); + } + + a() { + return this.keys.A006; + } + + e() { + return this.keys.E002; + } + + x() { + return this.keys.X002; + } + + bankX() { + return this.keys.bankX002; + } + + bankE() { + return this.keys.bankE002; + } +}; diff --git a/lib/keymanagers/KeysManager.js b/lib/keymanagers/KeysManager.js new file mode 100644 index 0000000..6981f6c --- /dev/null +++ b/lib/keymanagers/KeysManager.js @@ -0,0 +1,94 @@ +'use strict'; + +const crypto = require('crypto'); + +const Keys = require('./Keys'); + +const encrypt = (data, algorithm, passphrase) => { + const cipher = crypto.createCipher(algorithm, passphrase); + const encrypted = cipher.update(data, 'utf8', 'hex') + cipher.final('hex'); + + return Buffer.from(encrypted).toString('base64'); +}; +const decrypt = (data, algorithm, passphrase) => { + data = (Buffer.from(data, 'base64')).toString(); + + const decipher = crypto.createDecipher(algorithm, passphrase); + const decrypted = decipher.update(data, 'hex', 'utf8') + decipher.final('utf8'); + + return decrypted; +}; + +module.exports = class KeysManager { + constructor(keysStorage, passphrase, algorithm = 'aes-256-cbc', createIfNone = true) { + this._storage = keysStorage; + this._passphrase = passphrase; + this._algorithm = algorithm; + + if (createIfNone && !this._storage.hasData()) + this.generate(); + } + + /** + * Generates the keys to work with. Then either + * saves them to the storage or returnes the keys generated + * + * @param {Boolean} save + * @default true + * + * @returns void | Keys object + */ + generate(save = true) { + const keys = Keys.generate(); + + if (save) this.write(keys); + + return keys; + } + + /** + * Writes the keys to the storage + * + * @param {Keys} keysObject + * + * @returns void + */ + write(keysObject) { + keysObject = keysObject.keys; + + Object.keys(keysObject).map((key) => { + keysObject[key] = keysObject[key] === null ? null : keysObject[key].toPem(); + + return key; + }); + + this._storage.save(encrypt(JSON.stringify(keysObject), this._algorithm, this._passphrase)); + } + + setBankKeys(bankKeys) { + const keys = this.keys(); + + keys.setBankKeys(bankKeys); + this.write(keys); + } + + /** + * Gets the keys + * + * @returns Keys object + */ + keys() { + return this._read(); + } + + /** + * Reads the keys from the storage + * + * @returns Keys object + */ + _read() { + const keysString = this._storage.read(); + + return new Keys(JSON.parse(decrypt(keysString, this._algorithm, this._passphrase))); + } +}; diff --git a/lib/middleware/ParseResponse.js b/lib/middleware/ParseResponse.js index 51494af..60d59d9 100644 --- a/lib/middleware/ParseResponse.js +++ b/lib/middleware/ParseResponse.js @@ -3,14 +3,8 @@ 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); + static go(keys, data) { + const response = new Response(keys, data); // TODO: // raise error if any diff --git a/lib/middleware/XMLSign.js b/lib/middleware/XMLSign.js index f40955e..0827516 100644 --- a/lib/middleware/XMLSign.js +++ b/lib/middleware/XMLSign.js @@ -3,14 +3,8 @@ 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); + static go(keys, xml) { + const signer = new Signer(keys, xml); signer.digest(); signer.sign(); diff --git a/lib/orders/C52.js b/lib/orders/C52.js deleted file mode 100644 index 965358f..0000000 --- a/lib/orders/C52.js +++ /dev/null @@ -1,60 +0,0 @@ -'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 deleted file mode 100644 index e34cb76..0000000 --- a/lib/orders/GenericOrder.js +++ /dev/null @@ -1,181 +0,0 @@ -'use strict'; - -// const randHex = require('../../lib/utils').randHex; -const crypto = require('crypto'); -const js2xmlparser = require('js2xmlparser'); -const consts = require('../consts'); - -const 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': {}, - }; -}; -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: '', - pretty: true, - }, - }; - - this._schema = { - '@': { - 'xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#', - xmlns: 'urn:org:ebics:H004', - Version: 'H004', - Revision: '1', - }, - - header: {}, - - AuthSignature: authSignature(), - - body: {}, - }; - } - - get schema() { - return this._schema; - } - - static get productString() { - return consts.productString; - } - - static nonce() { - return crypto.randomBytes(16).toString('hex'); - } - - // TODO: remove eslint-disable-line - timestamp() { // eslint-disable-line - return new Date().toISOString(); - } - - root() { // eslint-disable-line class-methods-use-this - 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); - } - - toTransferXML() { - 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: 'Transfer', - SegmentNumber: { - '@': { lastSegment: true }, - '#': 1, - }, - }, - }, - - AuthSignature: this.authSignature(), - - body: { - DataTransfer: { - OrderData: this.encryptedOrderData(), - }, - }, - }; - - return js2xmlparser.parse(this.root(), xmlObj, this.xmlOptions); - } - - encryptedOrderData() { // eslint-disable-line class-methods-use-this - return null; - } - - toXML() { - return js2xmlparser.parse(this.root(), this._schema, this.xmlOptions); - } -}; diff --git a/lib/orders/GenericUploadOrder.js b/lib/orders/GenericUploadOrder.js deleted file mode 100644 index eb55f64..0000000 --- a/lib/orders/GenericUploadOrder.js +++ /dev/null @@ -1,79 +0,0 @@ -'use strict'; - -const zlib = require('zlib'); -const crypto = require('crypto'); - -const js2xmlparser = require('js2xmlparser'); - -const GenericOrder = require('./GenericOrder'); - -const pad = (d) => { - const dLen = d.length; - const len = 16 * (Math.trunc(dLen / 16) + 1); - - return Buffer.concat([d, Buffer.from(Buffer.from([0]).toString().repeat(len - dLen - 1)), Buffer.from([len - dLen])]); -}; - -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: 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.replace(/\n|\r/g, '')).digest(); - - return this.client.a().sign(digested); - } - - encryptedOrderData() { - const dst = zlib.deflateSync(this._document.replace(/\r|\n/g, '')); - 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); - - return Buffer.concat([cipher.update(pad(dst)), cipher.final()]).toString('base64'); - } - - 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); - - return Buffer.concat([cipher.update(pad(dst)), cipher.final()]).toString('base64'); - } -}; diff --git a/lib/orders/H004/GenericSerializer.js b/lib/orders/H004/GenericSerializer.js new file mode 100644 index 0000000..8a2f7c5 --- /dev/null +++ b/lib/orders/H004/GenericSerializer.js @@ -0,0 +1,81 @@ +'use strict'; + +const js2xmlparser = require('js2xmlparser'); + +const xmlOptions = { + declaration: { + include: true, + encoding: 'utf-8', + }, + format: { + doubleQuotes: true, + indent: '', + newline: '', + pretty: true, + }, +}; + +const authSignature = ({ + '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': {}, +}); + +module.exports = class GenericSerializer { + constructor(orderBuilder) { + this._order = orderBuilder; + this._orderDetails = orderBuilder.orderDetails; + this._hostId = orderBuilder.hostId; + this._partnerId = orderBuilder.partnerId; + this._userId = orderBuilder.userId; + this._keys = orderBuilder.keys; + this._transactionId = orderBuilder.transactionId; + this._rootName = orderBuilder.root.nodeName; + this._rootAttributes = orderBuilder.root.nodeAttributes; + + this._xmlOptions = xmlOptions; + this._xml = {}; + } + + static authSignature() { + return authSignature; + } + + get keys() { + return this._keys; + } + + toXML() { + return js2xmlparser.parse(this._rootName, this._xml, this._xmlOptions); + } +}; diff --git a/lib/orders/H004/InitializationSerializer.js b/lib/orders/H004/InitializationSerializer.js new file mode 100644 index 0000000..6fb880e --- /dev/null +++ b/lib/orders/H004/InitializationSerializer.js @@ -0,0 +1,128 @@ +'use strict'; + +const zlib = require('zlib'); +const js2xmlparser = require('js2xmlparser'); + +const consts = require('../../consts'); +const Crypto = require('../../crypto/Crypto'); + +const GenericSerializer = require('./GenericSerializer'); + +module.exports = class InitializationSerializer extends GenericSerializer { + constructor(order) { + super(order); + + this._xml = { + '@': this._rootAttributes, + header: { + '@': { authenticate: true }, + static: { + HostID: this._hostId, + Nonce: Crypto.nonce(), + Timestamp: Crypto.timestamp(), + PartnerID: this._partnerId, + UserID: this._userId, + Product: { + '@': { Language: 'en' }, + '#': consts.productString, + }, + OrderDetails: this._orderDetails, + SecurityMedium: '0000', + }, + mutable: {}, + }, + }; + + if (this._isINI() || this._isHIA()) { + delete this._xml.header.static.Nonce; + delete this._xml.header.static.Timestamp; + + this._xml.body = { + DataTransfer: { + OrderData: this.orderData(), + }, + }; + } else { + this._rootName = 'ebicsNoPubKeyDigestsRequest'; + this._xml.AuthSignature = GenericSerializer.authSignature(); + this._xml.body = {}; + } + } + + orderData() { + if (this._isINI()) return this._iniKeySignature(); + if (this._isHIA()) return this._hiaOrderData(); + + return ''; + } + + _iniKeySignature() { + 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._keys.a().n(), 'HEX').toString('base64'), + 'ds:Exponent': this._keys.a().e().toString('base64'), + }, + TimeStamp: Crypto.timestamp(), + }, + SignatureVersion: 'A006', + }, + PartnerID: this._partnerId, + UserID: this._userId, + }; + + const signature = js2xmlparser.parse('SignaturePubKeyOrderData', xmlOrderData, this._xmlOptions); + + return Buffer.from(zlib.deflateSync(signature)).toString('base64'); + } + + _hiaOrderData() { + 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._keys.x().n(), 'HEX').toString('base64'), + 'ds:Exponent': this._keys.x().e().toString('base64'), + }, + }, + AuthenticationVersion: 'X002', + }, + EncryptionPubKeyInfo: { + PubKeyValue: { + 'ds:RSAKeyValue': { + 'ds:Modulus': Buffer.from(this.keys.e().n(), 'HEX').toString('base64'), + 'ds:Exponent': this._keys.e().e().toString('base64'), + }, + }, + EncryptionVersion: 'E002', + }, + PartnerID: this._partnerId, + UserID: this._userId, + }; + + const order = js2xmlparser.parse('HIARequestOrderData', xmlOrderData, this._xmlOptions); + + return Buffer.from(zlib.deflateSync(order)).toString('base64'); + } + + _isINI() { + return this._orderDetails.OrderType.toUpperCase() === 'INI'; + } + + _isHIA() { + return this._orderDetails.OrderType.toUpperCase() === 'HIA'; + } + + _isHPB() { + return this._orderDetails.OrderType.toUpperCase() === 'HPB'; + } +}; diff --git a/lib/orders/H004/OrderBuilder.js b/lib/orders/H004/OrderBuilder.js new file mode 100644 index 0000000..ece3330 --- /dev/null +++ b/lib/orders/H004/OrderBuilder.js @@ -0,0 +1,78 @@ +'use strict'; + +const crypto = require('crypto'); + +// const orderTypes = ['ini', 'download', 'upload', 'zip']; + +module.exports = class OrderBuilder { + constructor() { + this._transactionKey = crypto.randomBytes(16); + this._root = { + nodeName: 'ebicsRequest', + nodeAttributes: { + 'xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#', + xmlns: 'urn:org:ebics:H004', + Version: 'H004', + Revision: '1', + }, + }; + this._body = {}; + } + + details(data) { + this._data = data; + this._data.transactionId = null; + + return this; + } + + static payment() { + const builder = new OrderBuilder(); + + builder._type = 'payment'; + + return builder; + } + + static status() { + const builder = new OrderBuilder(); + + builder._type = 'status'; + + return builder; + } + + static ini() { + const builder = new OrderBuilder(); + + builder._type = 'ini'; + builder._root.nodeName = 'ebicsUnsecuredRequest'; + + return builder; + } + + /** + * Getters + */ + get type() { return this._type; } + get root() { return this._root; } + get body() { return this._body; } + get data() { return this._data; } + get orderDetails() { return this._data.orderDetails; } + get transactionId() { return this._data.transactionId; } + get document() { return this._data.document; } + get transactionKey() { return this._transactionKey; } + get ebicsData() { return this._data.ebicsData; } + get hostId() { return this._data.ebicsData.hostId; } + get partnerId() { return this._data.ebicsData.partnerId; } + get userId() { return this._data.ebicsData.userId; } + get keys() { return this._data.ebicsData.keysManager.keys(); } + + set transactionId(tid) { + this._data.transactionId = tid === '' ? null : tid; + } + + hasTransactionId() { + return this.transactionId !== null; + } +}; diff --git a/lib/orders/H004/OrderSerializer.js b/lib/orders/H004/OrderSerializer.js new file mode 100644 index 0000000..bfce565 --- /dev/null +++ b/lib/orders/H004/OrderSerializer.js @@ -0,0 +1,15 @@ +'use strict'; + +const InitializationSerializer = require('./InitializationSerializer'); +const StatusSerializer = require('./StatusSerializer'); +const PaymentSerializer = require('./PaymentSerializer'); + +module.exports = class OrderSerializer { + static serialize(order) { + if (order.type === 'ini') return new InitializationSerializer(order); + if (order.type === 'payment') return new PaymentSerializer(order); + if (order.type === 'status') return new StatusSerializer(order); + + throw Error('Incorect order type. Available types: ini, status, payment, statement'); + } +}; diff --git a/lib/orders/H004/PaymentSerializer.js b/lib/orders/H004/PaymentSerializer.js new file mode 100644 index 0000000..cc61487 --- /dev/null +++ b/lib/orders/H004/PaymentSerializer.js @@ -0,0 +1,131 @@ +'use strict'; + +const zlib = require('zlib'); +const crypto = require('crypto'); + +const js2xmlparser = require('js2xmlparser'); + +const consts = require('../../consts'); +const Crypto = require('../../crypto/Crypto'); + +const GenericSerializer = require('./GenericSerializer'); + +module.exports = class PaymentSerializer extends GenericSerializer { + constructor(order) { + super(order); + + this._transactionKey = order.transactionKey; + + this._xml = { + '@': this._rootAttributes, + header: { + '@': { authenticate: true }, + static: { + HostID: this._hostId, + Nonce: Crypto.nonce(), + Timestamp: Crypto.timestamp(), + PartnerID: this._partnerId, + UserID: this._userId, + Product: { + '@': { Language: 'en' }, + '#': consts.productString, + }, + OrderDetails: this._orderDetails, + BankPubKeyDigests: { + Authentication: { + '@': { Version: 'X002', Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256' }, + '#': Crypto.digestPublicKey(this._keys.bankX()), + }, + Encryption: { + '@': { Version: 'E002', Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256' }, + '#': Crypto.digestPublicKey(this._keys.bankE()), + }, + }, + SecurityMedium: '0000', + NumSegments: 1, + }, + mutable: { + TransactionPhase: 'Initialisation', + }, + }, + AuthSignature: GenericSerializer.authSignature(), + body: { + DataTransfer: { + DataEncryptionInfo: { + '@': { authenticate: true }, + EncryptionPubKeyDigest: { + '@': { Version: 'E002', Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256' }, + '#': Crypto.digestPublicKey(this._keys.bankE()), + }, + TransactionKey: Crypto.publicEncrypt(this._keys.bankE(), this._transactionKey).toString('base64'), + }, + SignatureData: { + '@': { authenticate: true }, + '#': this.encryptedOrderSignature(), + }, + }, + }, + }; + + if (order.hasTransactionId()) { + this._xml.header = { + '@': { authenticate: true }, + static: { + HostID: this._hostId, + TransactionID: this._transactionId, + }, + mutable: { + TransactionPhase: 'Transfer', + SegmentNumber: { + '@': { lastSegment: true }, + '#': 1, + }, + }, + }; + + this._xml.body = { + DataTransfer: { + OrderData: this.encryptedOrderData(), + }, + }; + } + } + + 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.digestWithHash(this._order.document.replace(/\n|\r/g, '')); + + return Crypto.sign(this._keys.a(), digested); + } + + encryptedOrderData() { + const dst = zlib.deflateSync(this._order.document.replace(/\n|\r/g, '')); + const cipher = crypto.createCipheriv('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); + + return Buffer.concat([cipher.update(Crypto.pad(dst)), cipher.final()]).toString('base64'); + } + + encryptedOrderSignature() { + const dst = zlib.deflateSync(this.orderSignature()); + const cipher = crypto.createCipheriv('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); + + return Buffer.concat([cipher.update(Crypto.pad(dst)), cipher.final()]).toString('base64'); + } +}; diff --git a/lib/orders/H004/StatusSerializer.js b/lib/orders/H004/StatusSerializer.js new file mode 100644 index 0000000..a1bc521 --- /dev/null +++ b/lib/orders/H004/StatusSerializer.js @@ -0,0 +1,67 @@ +'use strict'; + +const consts = require('../../consts'); +const Crypto = require('../../crypto/Crypto'); + +const GenericSerializer = require('./GenericSerializer'); + +module.exports = class StatusSerializer extends GenericSerializer { + constructor(order) { + super(order); + + this._xml = { + '@': this._rootAttributes, + header: { + '@': { authenticate: true }, + static: { + HostID: this._hostId, + Nonce: Crypto.nonce(), + Timestamp: Crypto.timestamp(), + PartnerID: this._partnerId, + UserID: this._userId, + Product: { + '@': { Language: 'en' }, + '#': consts.productString, + }, + OrderDetails: this._orderDetails, + BankPubKeyDigests: { + Authentication: { + '@': { Version: 'X002', Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256' }, + '#': Crypto.digestPublicKey(this._keys.bankX()), + }, + Encryption: { + '@': { Version: 'E002', Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256' }, + '#': Crypto.digestPublicKey(this._keys.bankE()), + }, + }, + SecurityMedium: '0000', + }, + mutable: { + TransactionPhase: 'Initialisation', + }, + }, + AuthSignature: GenericSerializer.authSignature(), + body: {}, + }; + + if (order.hasTransactionId()) { + this._xml.header = { + '@': { authenticate: true }, + static: { + HostID: this._hostId, + TransactionID: this._transactionId, + }, + mutable: { + TransactionPhase: 'Receipt', + }, + }; + + this._xml.body = { + TransferReceipt: { + '@': { authenticate: true }, + ReceiptCode: 0, + }, + }; + } + } +}; diff --git a/lib/orders/HAA.js b/lib/orders/HAA.js deleted file mode 100644 index 58ae7a6..0000000 --- a/lib/orders/HAA.js +++ /dev/null @@ -1,43 +0,0 @@ -'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 deleted file mode 100644 index 4b5770d..0000000 --- a/lib/orders/HAC.js +++ /dev/null @@ -1,54 +0,0 @@ -'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 deleted file mode 100644 index dfe9d26..0000000 --- a/lib/orders/HIA.js +++ /dev/null @@ -1,85 +0,0 @@ -'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() { // eslint-disable-line class-methods-use-this - 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 deleted file mode 100644 index f03d5d0..0000000 --- a/lib/orders/HKD.js +++ /dev/null @@ -1,43 +0,0 @@ -'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 deleted file mode 100644 index 207d2aa..0000000 --- a/lib/orders/HPB.js +++ /dev/null @@ -1,34 +0,0 @@ -'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() { // eslint-disable-line class-methods-use-this - return 'ebicsNoPubKeyDigestsRequest'; - } -}; diff --git a/lib/orders/HTD.js b/lib/orders/HTD.js deleted file mode 100644 index 071637c..0000000 --- a/lib/orders/HTD.js +++ /dev/null @@ -1,43 +0,0 @@ -'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 deleted file mode 100644 index 631f0eb..0000000 --- a/lib/orders/INI.js +++ /dev/null @@ -1,77 +0,0 @@ -'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() { // eslint-disable-line class-methods-use-this - 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); - } -};