libas2
Version:
Implementation of the AS2 protocol as presented in RFC 4130 and related RFCs
155 lines (154 loc) • 7.06 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AS2Crypto = void 0;
const Constants_1 = require("../Constants");
const AS2MimeNode_1 = require("../AS2MimeNode");
const Helpers_1 = require("../Helpers");
const MimeNode = require("nodemailer/lib/mime-node");
const AS2Parser_1 = require("../AS2Parser");
const crypto_1 = require("crypto");
const AS2SignedData_1 = require("./AS2SignedData");
const AS2EnvelopedData_1 = require("./AS2EnvelopedData");
const { CRLF, ENCRYPTION_FILENAME, SIGNATURE_FILENAME, ERROR } = Constants_1.AS2Constants;
/** List of supported signing algorithms.
* @typedef {'sha-1'|'sha-256'|'sha-384'|'sha-512'} AS2Signing
*/
/** List of supported encryption algorithms.
* @typedef {'des-EDE3-CBC'|'aes128-CBC'|'aes192-CBC'|'aes256-CBC'|'aes128-GCM'|'aes192-GCM'|'aes256-GCM'} AS2Encryption
*/
/** Options for encrypting payloads.
* @typedef {object} EncryptionOptions
* @property {string|Buffer} cert
* @property {AS2Encryption} encryption
*/
/** Options for decrypting payloads.
* @typedef {object} DecryptionOptions
* @property {string|Buffer} cert
* @property {string|Buffer} key
*/
/** Options for decrypting payloads.
* @typedef {object} SigningOptions
* @property {string|Buffer} cert
* @property {string|Buffer} key
* @property {AS2Signing} algorithm
*/
/** Options for decrypting payloads.
* @typedef {object} VerificationOptions
* @property {string|Buffer} cert
*/
/** Class for cryptography methods supported by AS2. */
class AS2Crypto {
static async buildNode(node) {
return node.parsed ? await node.build() : await MimeNode.prototype.build.bind(node)();
}
/** A fix for signing with Nodemailer to produce verifiable SMIME;
* the library joins multipart boundaries without the part's trailing CRLF,
* where OpenSSL and other SMIME clients keep each part's last CRLF.
* @private
*/
static removeTrailingCrLf(buffer) {
const trailingBytes = buffer.slice(buffer.length - 2, buffer.length);
return trailingBytes.toString('utf8') === CRLF ? buffer.slice(0, buffer.length - 2) : buffer;
}
/** Crux to generate UUID-like random strings
* @returns {string} A UUID-like random string.
*/
static generateUniqueId() {
const byteLengths = [4, 2, 2, 2, 6];
return byteLengths.map(byteLength => crypto_1.randomBytes(byteLength).toString('hex')).join('-');
}
/** Method to decrypt an AS2MimeNode from a PKCS7 encrypted AS2MimeNode.
* @param {AS2MimeNode} node - The AS2MimeNode to decrypt.
* @param {DecryptionOptions} options - Options to decrypt the MIME message.
* @returns {Promise<AS2MimeNode>} The decrypted MIME as an AS2MimeNode.
*/
static async decrypt(node, options) {
const data = Buffer.isBuffer(node.content) ? node.content : Buffer.from(node.content, 'base64');
const envelopedData = new AS2EnvelopedData_1.AS2EnvelopedData(data, true);
const buffer = await envelopedData.decrypt(options.cert, options.key);
const revivedNode = await AS2Parser_1.AS2Parser.parse(buffer);
return revivedNode;
}
/** Method to envelope an AS2MimeNode in an encrypted AS2MimeNode.
* @param {AS2MimeNode} node - The AS2MimeNode to encrypt.
* @param {EncryptionOptions} options - Options to encrypt the MIME message.
* @returns {Promise<AS2MimeNode>} The encrypted MIME as an AS2MimeNode.
*/
static async encrypt(node, options) {
options = Helpers_1.getEncryptionOptions(options);
const rootNode = new AS2MimeNode_1.AS2MimeNode({
filename: ENCRYPTION_FILENAME,
contentType: 'application/pkcs7-mime; smime-type=enveloped-data'
});
Helpers_1.canonicalTransform(node);
const buffer = await AS2Crypto.buildNode(node);
const envelopedData = new AS2EnvelopedData_1.AS2EnvelopedData(buffer);
const derBuffer = await envelopedData.encrypt(options.cert, options.encryption);
rootNode.setContent(derBuffer);
return rootNode;
}
static async verify(node, options, getDigest) {
const contentPart = await AS2Crypto.buildNode(node.childNodes[0]);
const contentPartNoCrLf = AS2Crypto.removeTrailingCrLf(contentPart);
const signaturePart = Buffer.isBuffer(node.childNodes[1].content)
? node.childNodes[1].content
: Buffer.from(node.childNodes[1].content, 'base64');
const signedData = new AS2SignedData_1.AS2SignedData(contentPart, signaturePart);
// Deal with Nodemailer trailing CRLF bug by trying with and without CRLF
if (await signedData.verify(options.cert)) {
return getDigest ? signedData.getMessageDigest() : true;
}
const signedDataNoCrLf = new AS2SignedData_1.AS2SignedData(contentPartNoCrLf, signaturePart);
const result = await signedDataNoCrLf.verify(options.cert);
return getDigest && result ? signedDataNoCrLf.getMessageDigest() : result;
}
/** Method to sign data against a certificate and key pair.
* @param {AS2MimeNode} node - The AS2MimeNode to sign.
* @param {EncryptionOptions} options - Options to sign the MIME message.
* @returns {Promise<AS2MimeNode>} The signed MIME as a multipart AS2MimeNode.
*/
static async sign(node, options) {
const rootNode = new AS2MimeNode_1.AS2MimeNode({
contentType: `multipart/signed; protocol="application/pkcs7-signature"; micalg=${options.algorithm};`,
encrypt: node._encrypt
});
const contentNode = rootNode.appendChild(node);
const contentHeaders = contentNode._headers;
for (let i = 0, len = contentHeaders.length; i < len; i++) {
const header = contentHeaders[i];
if (header.key.toLowerCase() === 'content-type')
continue;
rootNode.setHeader(header.key, header.value);
contentHeaders.splice(i, 1);
i--;
len--;
}
Helpers_1.canonicalTransform(contentNode);
const canonical = AS2Crypto.removeTrailingCrLf(await AS2Crypto.buildNode(contentNode));
const signedData = new AS2SignedData_1.AS2SignedData(canonical);
const derBuffer = await signedData.sign({
cert: options.cert,
key: options.key,
algorithm: options.algorithm
});
rootNode.appendChild(new AS2MimeNode_1.AS2MimeNode({
filename: SIGNATURE_FILENAME,
contentType: 'application/pkcs7-signature',
content: derBuffer
}));
return rootNode;
}
/** Not yet implemented; do not use.
* @throws ERROR.NOT_IMPLEMENTED
*/
static async compress(node, options) {
throw new Error(ERROR.NOT_IMPLEMENTED);
}
/** Not yet implemented; do not use.
* @throws ERROR.NOT_IMPLEMENTED
*/
static async decompress(node, options) {
throw new Error(ERROR.NOT_IMPLEMENTED);
}
}
exports.AS2Crypto = AS2Crypto;