code optimization

This commit is contained in:
Vladislav Hristov
2018-06-15 09:33:41 +03:00
parent d5d80ee1b8
commit 187636019c
26 changed files with 776 additions and 669 deletions

114
lib/orders/H004/response.js Normal file
View File

@@ -0,0 +1,114 @@
'use strict';
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 = (xml, keys) => ({
keys,
doc: new DOMParser().parseFromString(xml, '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;
},
isLastSegment() {
const select = xpath.useNamespaces({ xmlns: 'urn:org:ebics:H004' });
const node = select("//xmlns:header/xmlns:mutable/*[@lastSegment='true']", this.doc);
return !!node.length;
},
orderData() {
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');
return zlib.inflateSync(data).toString();
},
transactionKey() {
const keyNodeText = this.doc.getElementsByTagNameNS('urn:org:ebics:H004', 'TransactionKey')[0].textContent;
return Crypto.privateDecrypt(this.keys.e(), Buffer.from(keyNodeText, 'base64'));
},
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 : '';
},
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 };
}
return bankKeys;
},
toXML() {
return new XMLSerializer().serializeToString(this.doc);
},
});

View File

@@ -0,0 +1,19 @@
'use strict';
const orders = require('../orders');
const iniSerializer = require('./serializers/ini');
const downloadSerializer = require('./serializers/download');
const uploadSerializer = require('./serializers/upload');
module.exports = {
use(order) {
const { version, orderType } = order;
if (orders.version(version).isIni(orderType)) return iniSerializer.use(order);
if (orders.version(version).isDownload(orderType)) return downloadSerializer.use(order);
if (orders.version(version).isUpload(orderType)) return uploadSerializer.use(order);
throw Error('Error from orders/orders.js: Wrong order version/type.');
},
};

View File

@@ -0,0 +1,62 @@
'use strict';
const js2xmlparser = require('js2xmlparser');
const Crypto = require('../../../crypto/Crypto');
const genericSerializer = require('./generic');
module.exports = {
use(orderBuilder) {
const {
ebicsData, orderDetails, keys, productString, transactionId,
} = orderBuilder;
const {
rootName, xmlOptions, xmlSchema, receipt, transfer,
} = genericSerializer(orderBuilder);
this.rootName = rootName;
this.xmlOptions = xmlOptions;
this.xmlSchema = xmlSchema;
this.receipt = receipt;
this.transfer = transfer;
if (transactionId) return this.receipt();
this.xmlSchema.header = {
'@': { authenticate: true },
static: {
HostID: ebicsData.hostId,
Nonce: Crypto.nonce(),
Timestamp: Crypto.timestamp(),
PartnerID: ebicsData.partnerId,
UserID: ebicsData.userId,
Product: {
'@': { Language: 'en' },
'#': productString,
},
OrderDetails: orderDetails,
BankPubKeyDigests: {
Authentication: {
'@': { Version: 'X002', Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256' },
'#': Crypto.digestPublicKey(keys.bankX()),
},
Encryption: {
'@': { Version: 'E002', Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256' },
'#': Crypto.digestPublicKey(keys.bankE()),
},
},
SecurityMedium: '0000',
},
mutable: {
TransactionPhase: 'Initialisation',
},
};
return this;
},
toXML() {
return js2xmlparser.parse(this.rootName, this.xmlSchema, this.xmlOptions);
},
};

View File

@@ -0,0 +1,133 @@
'use strict';
const rootName = 'ebicsRequest';
const rootAttributes = {
'xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
xmlns: 'urn:org:ebics:H004',
Version: 'H004',
Revision: '1',
};
const header = {};
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': {},
});
const body = {};
const xmlOptions = {
declaration: {
include: true,
encoding: 'utf-8',
},
format: {
doubleQuotes: true,
indent: '',
newline: '',
pretty: true,
},
};
module.exports = (orderBuilder) => {
const { ebicsData, transactionId } = orderBuilder;
return {
rootName,
xmlOptions,
xmlSchema: {
'@': rootAttributes,
header,
AuthSignature: authSignature,
body,
},
receipt() {
this.xmlSchema = {
'@': rootAttributes,
header: {
'@': { authenticate: true },
static: {
HostID: ebicsData.hostId,
TransactionID: transactionId,
},
mutable: {
TransactionPhase: 'Receipt',
},
},
AuthSignature: authSignature,
body: {
TransferReceipt: {
'@': { authenticate: true },
ReceiptCode: 0,
},
},
};
return this;
},
transfer(encryptedOrderData) {
this.xmlSchema = {
'@': rootAttributes,
header: {
'@': { authenticate: true },
static: {
HostID: ebicsData.hostId,
TransactionID: transactionId,
},
mutable: {
TransactionPhase: 'Transfer',
SegmentNumber: {
'@': { lastSegment: true },
'#': 1,
},
},
},
AuthSignature: authSignature,
body: {
DataTransfer: {
OrderData: encryptedOrderData,
},
},
};
return this;
},
};
};

View File

@@ -0,0 +1,144 @@
'use strict';
const zlib = require('zlib');
const js2xmlparser = require('js2xmlparser');
const Crypto = require('../../../crypto/Crypto');
const genericSerializer = require('./generic');
const keySignature = (ebicsData, key, xmlOptions) => {
const xmlOrderData = {
'@': {
'xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
xmlns: 'http://www.ebics.org/S001',
},
SignaturePubKeyInfo: {
PubKeyValue: {
'ds:RSAKeyValue': {
'ds:Modulus': key.n().toString('base64'),
'ds:Exponent': key.e().toString('base64'),
},
TimeStamp: Crypto.timestamp(),
},
SignatureVersion: 'A006',
},
PartnerID: ebicsData.partnerId,
UserID: ebicsData.userId,
};
return js2xmlparser.parse('SignaturePubKeyOrderData', xmlOrderData, xmlOptions);
};
const orderData = (ebicsData, keys, xmlOptions) => {
const xmlOrderData = {
'@': {
'xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
xmlns: 'urn:org:ebics:H004',
},
AuthenticationPubKeyInfo: {
PubKeyValue: {
'ds:RSAKeyValue': {
'ds:Modulus': keys.x().n().toString('base64'),
'ds:Exponent': keys.x().e().toString('base64'),
},
},
AuthenticationVersion: 'X002',
},
EncryptionPubKeyInfo: {
PubKeyValue: {
'ds:RSAKeyValue': {
'ds:Modulus': keys.e().n().toString('base64'),
'ds:Exponent': keys.e().e().toString('base64'),
},
},
EncryptionVersion: 'E002',
},
PartnerID: ebicsData.partnerId,
UserID: ebicsData.userId,
};
return js2xmlparser.parse('HIARequestOrderData', xmlOrderData, xmlOptions);
};
const commonHeader = (ebicsData, orderDetails, productString) => ({
'@': { authenticate: true },
static: {
HostID: ebicsData.hostId,
Nonce: Crypto.nonce(),
Timestamp: Crypto.timestamp(),
PartnerID: ebicsData.partnerId,
UserID: ebicsData.userId,
Product: {
'@': { Language: 'en' },
'#': productString,
},
OrderDetails: orderDetails,
SecurityMedium: '0000',
},
mutable: {},
});
const process = {
INI: {
rootName: 'ebicsUnsecuredRequest',
header: (ebicsData, orderDetails, productString) => {
const ch = commonHeader(ebicsData, orderDetails, productString);
delete ch.static.Nonce;
delete ch.static.Timestamp;
return ch;
},
body: (ebicsData, keys, xmlOptions) => ({
DataTransfer: {
OrderData: Buffer.from(zlib.deflateSync(keySignature(ebicsData, keys.a(), xmlOptions))).toString('base64'),
},
}),
},
HIA: {
rootName: 'ebicsUnsecuredRequest',
header: (ebicsData, orderDetails, productString) => {
const ch = commonHeader(ebicsData, orderDetails, productString);
delete ch.static.Nonce;
delete ch.static.Timestamp;
return ch;
},
body: (ebicsData, keys, xmlOptions) => ({
DataTransfer: {
OrderData: Buffer.from(zlib.deflateSync(orderData(ebicsData, keys, xmlOptions))).toString('base64'),
},
}),
},
HPB: {
rootName: 'ebicsNoPubKeyDigestsRequest',
header: (ebicsData, orderDetails, productString) => commonHeader(ebicsData, orderDetails, productString),
body: () => ({}),
},
};
module.exports = {
use(orderBuilder) {
const { xmlOptions, xmlSchema } = genericSerializer(orderBuilder);
const {
ebicsData, orderDetails, keys, productString,
} = orderBuilder;
const orderType = orderDetails.OrderType.toUpperCase();
this.rootName = process[orderType].rootName;
this.xmlOptions = xmlOptions;
this.xmlSchema = xmlSchema;
this.xmlSchema.header = process[orderType].header(ebicsData, orderDetails, productString);
this.xmlSchema.body = process[orderType].body(ebicsData, keys, this.xmlOptions);
if (orderType !== 'HPB' && Object.prototype.hasOwnProperty.call(this.xmlSchema, 'AuthSignature'))
delete this.xmlSchema.AuthSignature;
return this;
},
toXML() {
return js2xmlparser.parse(this.rootName, this.xmlSchema, this.xmlOptions);
},
};

View File

@@ -0,0 +1,88 @@
'use strict';
const zlib = require('zlib');
const crypto = require('crypto');
const js2xmlparser = require('js2xmlparser');
const Crypto = require('../../../crypto/Crypto');
const downloadSerializer = require('./download');
const signatureValue = (document, key) => {
const digested = Crypto.digestWithHash(document.replace(/\n|\r/g, ''));
return Crypto.sign(key, digested);
};
const orderSignature = (ebicsData, document, key, xmlOptions) => {
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: signatureValue(document, key),
PartnerID: ebicsData.partnerId,
UserID: ebicsData.userId,
},
};
return js2xmlparser.parse('UserSignatureData', xmlObj, xmlOptions);
};
const encryptedOrderSignature = (ebicsData, document, transactionKey, key, xmlOptions) => {
const dst = zlib.deflateSync(orderSignature(ebicsData, document, key, xmlOptions));
const cipher = crypto.createCipheriv('aes-128-cbc', 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');
};
const encryptedOrderData = (document, transactionKey) => {
const dst = zlib.deflateSync(document.replace(/\n|\r/g, ''));
const cipher = crypto.createCipheriv('aes-128-cbc', 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');
};
module.exports = {
use(orderBuilder) {
const {
ebicsData, keys, transactionId, transactionKey, document,
} = orderBuilder;
const {
rootName, xmlOptions, xmlSchema, transfer,
} = downloadSerializer.use(orderBuilder);
this.rootName = rootName;
this.xmlOptions = xmlOptions;
this.xmlSchema = xmlSchema;
this.transfer = transfer;
if (transactionId) return this.transfer(encryptedOrderData(document, transactionKey));
this.xmlSchema.header.static.NumSegments = 1;
this.xmlSchema.body = {
DataTransfer: {
DataEncryptionInfo: {
'@': { authenticate: true },
EncryptionPubKeyDigest: {
'@': { Version: 'E002', Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256' },
'#': Crypto.digestPublicKey(keys.bankE()),
},
TransactionKey: Crypto.publicEncrypt(keys.bankE(), transactionKey).toString('base64'),
},
SignatureData: {
'@': { authenticate: true },
'#': encryptedOrderSignature(ebicsData, document, transactionKey, keys.a(), this.xmlOptions),
},
},
};
return this;
},
toXML() {
return js2xmlparser.parse(this.rootName, this.xmlSchema, this.xmlOptions);
},
};

50
lib/orders/H004/signer.js Normal file
View File

@@ -0,0 +1,50 @@
'use strict';
// const crypto = require('crypto');
const Crypto = require('../../crypto/Crypto');
const { DOMParser, XMLSerializer } = require('xmldom');
const xpath = require('xpath');
const C14n = require('xml-crypto/lib/c14n-canonicalization').C14nCanonicalization;
const digest = (doc) => {
// get the xml node, where the digested value is supposed to be
const nodeDigestValue = doc.getElementsByTagName('ds:DigestValue')[0];
// canonicalize the node that has authenticate='true' attribute
const contentToDigest = xpath.select("//*[@authenticate='true']", doc)
.map(x => 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.digestWithHash(fixedContent).toString('base64').trim();
return doc;
};
const sign = (doc, key) => {
const nodeSignatureValue = doc.getElementsByTagName('ds:SignatureValue')[0];
if (nodeSignatureValue) {
const select = xpath.useNamespaces({ ds: 'http://www.w3.org/2000/09/xmldsig#' });
const contentToSign = (new C14n().process(select('//ds:SignedInfo', 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 = Crypto.privateSign(key, contentToSign); // this.keys.x().key.sign(contentToSign, 'base64');
}
return doc;
};
const toXML = doc => new XMLSerializer().serializeToString(doc);
module.exports = {
sign(data, keys) {
const keyX = keys.x();
const doc = new DOMParser().parseFromString(data, 'text/xml');
return toXML(sign(digest(doc), keyX));
},
};

35
lib/orders/orders.js Normal file
View File

@@ -0,0 +1,35 @@
'use strict';
const orders = {
H004: {
ini: ['INI', 'HIA', 'HPB'],
download: ['HAA', 'HTD', 'XTD', 'HPD', 'HKD', 'PTK', 'HAC', 'STA', 'VMK', 'C52', 'C53', 'C54', 'Z01'],
upload: ['AZV', 'CD1', 'CDB', 'CDD', 'CDS', 'CCT', 'CCS', 'XE3'],
},
};
module.exports = {
version(v) {
this.orders = orders[v.toUpperCase()];
return this;
},
isIni(orderType) {
const { ini } = this.orders;
return ini.includes(orderType.toUpperCase());
},
isDownload(orderType) {
const { download } = this.orders;
return download.includes(orderType.toUpperCase());
},
isUpload(orderType) {
const { upload } = this.orders;
return upload.includes(orderType.toUpperCase());
},
};