UNPKG

js-nextgen-psd2

Version:

Library to communicate with European Banks using NextGenPSD2 standard and PSD2 XS2A

239 lines (203 loc) 10.2 kB
const https = require('https') const crypto = require('crypto') const axios = require('axios') const urlencoded = require('form-urlencoded') class PSD2Client { constructor(signingCertificate, signingKey, options) { const opts = typeof(options) === 'object' && options != null ? options : { } let sslAgent = undefined try { let signingCertificateBuffer = null if (Buffer.isBuffer(signingCertificate)) { signingCertificateBuffer = signingCertificate } if (typeof(signingCertificate) === 'string') { signingCertificateBuffer = Buffer.from(signingCertificate, 'hex') } if (signingCertificateBuffer === null) { throw new Error(`Unable to read signing certificate buffer.`) } let signingKeyBuffer = null if (Buffer.isBuffer(signingKey)) { signingKeyBuffer = signingKey } if (typeof(signingKey) === 'string') { signingKeyBuffer = Buffer.from(signingKey, 'hex') } if (signingKeyBuffer === null) { throw new Error(`Unable to read signing key buffer.`) } const signingX509 = new crypto.X509Certificate(signingCertificateBuffer) const signingPriv = crypto.createPrivateKey(signingKeyBuffer) if (signingX509.checkPrivateKey(signingPriv) === true) { this._identifier = `SN=${signingX509.serialNumber},CA=${signingX509.issuer.split('\n').join(',')}` this._certificate = signingX509.raw.toString('base64') this._key = signingPriv } else { throw new Error(`Signing key doesn't match signing certificate.`) } if (opts.sslCertificate && opts.sslKey) { let sslCertificateBuffer = null if (Buffer.isBuffer(opts.sslCertificate)) { sslCertificateBuffer = opts.sslCertificate } if (typeof(opts.sslCertificate) === 'string') { sslCertificateBuffer = Buffer.from(opts.sslCertificate, 'hex') } if (sslCertificateBuffer === null) { throw new Error(`Unable to read authentication certificate buffer.`) } let sslKeyBuffer = null if (Buffer.isBuffer(opts.sslKey)) { sslKeyBuffer = opts.sslKey } if (typeof(opts.sslKey) === 'string') { sslKeyBuffer = Buffer.from(opts.sslKey, 'hex') } if (sslKeyBuffer === null) { throw new Error(`Unable to read authentication key buffer.`) } const sslX509 = new crypto.X509Certificate(sslCertificateBuffer) const sslPriv = crypto.createPrivateKey(sslKeyBuffer) if (sslX509.checkPrivateKey(sslPriv) === true) { sslAgent = new https.Agent({ cert: sslCertificateBuffer, key: sslKeyBuffer }) } else { throw new Error(`SSL key doesn't match SSL certificate.`) } } else { if (opts.sslCertificate) { throw new Error('SSL key not provided!') } } } catch (error) { let message = `Message: 'unknown'` try { message = typeof(error.message) === 'string' ? `Message: '${error.message}'` : `Message: 'unknown'` } catch { } throw new Error(`Unable to verify certificates. ${ message }`) } try { let axiosOpts = { timeout: typeof(opts.timeout) === 'number' ? opts.timeout : 16000, validateStatus: () => true } if (sslAgent) { axiosOpts['httpsAgent'] = sslAgent } this._axios = axios.create(axiosOpts) } catch (error) { let message = `Message: 'unknown'` try { message = typeof(error.message) === 'string' ? `Message: '${error.message}'` : `Message: 'unknown'` } catch { } throw new Error(`Unable to create axios remote client. ${ message }`) } } _signedRequest(headers, body) { const FORBIDDEN_HEADERS = [ 'digest', 'x-request-id', 'tpp-signature-certificate', 'signature' ] const SIGNED_HEADERS = [ 'digest', 'x-request-id', 'psu-id', 'psu-corporate-id', 'tpp-redirect-uri' ] let httpHeaders = typeof(headers) === 'object' ? headers : { } let httpBody = Buffer.isBuffer(body) ? body : null let headersResult = { } let bodyResult = Buffer.isBuffer(httpBody) ? httpBody : null // We have to sanitize the headers parameter. We // should only accept haders with value type string // or number. And remove headers used for signature. let headersKeys = Object.keys(httpHeaders) .filter(key => typeof(key) === 'string') .filter(key => typeof(httpHeaders[key]) === 'string' || typeof(httpHeaders[key]) === 'number' || typeof(httpHeaders[key]) === 'boolean') .filter(key => FORBIDDEN_HEADERS.includes(key.toLowerCase()) === false) headersKeys.forEach(key => headersResult[key] = httpHeaders[key]) // We should add a random generated request // identifier. let reqid = crypto.randomUUID() headersKeys.push('X-Request-Id') headersResult['X-Request-Id'] = reqid // Compute body hash. We are using SHA-256, but // we should implement SHA-512 algorithm too. // TODO: Implement SHA-512 hashing method. let bodyHashCrypto = crypto.createHash('sha256') bodyHashCrypto.update(httpBody ? httpBody : Buffer.from('', 'hex')) let bodyHash = bodyHashCrypto.digest('base64') headersKeys.push('Digest') headersResult['Digest'] = `SHA-256=${bodyHash}` // Compute request signature. We will sign the // request with RSA-SHA256. Currently no more algorithms // are supported by EU. let headersToSign = headersKeys.filter(key => SIGNED_HEADERS.includes(key.toLowerCase())) let headersSignatureString = headersToSign.map(key => `${key.toLowerCase()}: ${headersResult[key]}`) .sort() .join('\n') let headersSignatureCrypto = crypto.createSign('RSA-SHA256') headersSignatureCrypto.update(headersSignatureString) let headersSignature = headersSignatureCrypto.sign(this._key, 'base64') headersResult['TPP-Signature-Certificate'] = this._certificate headersResult['Signature'] = `keyId="${this._identifier}",algorithm="sha-256",headers="${headersToSign.sort().join(' ').toLowerCase()}",signature="${headersSignature}"` return { request: reqid, headers: headersResult, body: bodyResult } } async send(method, path, headers, body, encoding) { const AVAILABLE_METHODS = [ 'get', 'post', 'put', 'delete' ] if (typeof(method) !== 'string') throw new Error(`Unable to send request. Message: 'invalid method type'`) if (AVAILABLE_METHODS.includes(method.toLowerCase()) === false) throw new Error(`Unable to send request. Message: 'invalid method'`) if (typeof(path) !== 'string') throw new Error(`Unable to send request. Message: 'invalid path'`) let sanitizedHeaders = typeof(headers) === 'object' ? headers : { } let sanitizedBody = null if (body) { try { if (Buffer.isBuffer(body)) { sanitizedHeaders['Content-Type'] = typeof(sanitizedHeaders['Content-Type']) === 'string' ? sanitizedHeaders['Content-Type'] : 'application/octet-stream' sanitizedBody = body } else { if (encoding === 'json' || encoding === undefined) { let serializedBody = JSON.stringify(body) let bufferedBody = Buffer.from(serializedBody, 'utf-8') sanitizedHeaders['Content-Type'] = 'application/json' sanitizedBody = bufferedBody } else if (encoding === 'urlencoded') { let serializedBody = urlencoded(body) let bufferedBody = Buffer.from(serializedBody, 'utf-8') sanitizedHeaders['Content-Type'] = 'x-www-form-urlencoded' sanitizedBody = bufferedBody } else { throw new Error(`Invalid provided encoding (${encoding}).`) } } } catch (error) { let message = `Message: 'unknown'` try { message = typeof(error.message) === 'string' ? `Message: '${error.message}'` : `Message: 'unknown'` } catch { } throw new Error(`Unable to serialize request. ${ message }`) } } try { let signedRequest = this._signedRequest(sanitizedHeaders, sanitizedBody) let result = await this._axios.request({ method: method, url: path, headers: signedRequest.headers, data: signedRequest.body }) return { request: signedRequest.request, status: result.status, headers: result.headers, body: result.data } } catch (error) { let message = `Message: 'unknown'` try { message = typeof(error.message) === 'string' ? `Message: '${error.message}'` : `Message: 'unknown'` } catch { } throw new Error(`Unable to send request. ${ message }`) } } } module.exports = PSD2Client