xml-crypto
Version:
Xml digital signature and encryption library for Node.js
256 lines • 9.73 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateDigestValue = exports.findAncestorNs = exports.derToPem = exports.pemToDer = exports.normalizePem = exports.BASE64_REGEX = exports.EXTRACT_X509_CERTS = exports.PEM_FORMAT_REGEX = exports.encodeSpecialCharactersInText = exports.encodeSpecialCharactersInAttribute = exports.findChilds = exports.findChildren = exports.findAttr = exports.isArrayHasLength = void 0;
const xpath = require("xpath");
const isDomNode = require("@xmldom/is-dom-node");
function isArrayHasLength(array) {
return Array.isArray(array) && array.length > 0;
}
exports.isArrayHasLength = isArrayHasLength;
function attrEqualsExplicitly(attr, localName, namespace) {
return attr.localName === localName && (attr.namespaceURI === namespace || namespace == null);
}
function attrEqualsImplicitly(attr, localName, namespace, node) {
return (attr.localName === localName &&
((!attr.namespaceURI && node?.namespaceURI === namespace) || namespace == null));
}
function findAttr(element, localName, namespace) {
for (let i = 0; i < element.attributes.length; i++) {
const attr = element.attributes[i];
if (attrEqualsExplicitly(attr, localName, namespace) ||
attrEqualsImplicitly(attr, localName, namespace, element)) {
return attr;
}
}
return null;
}
exports.findAttr = findAttr;
function findChildren(node, localName, namespace) {
const element = node.documentElement ?? node;
const res = [];
for (let i = 0; i < element.childNodes.length; i++) {
const child = element.childNodes[i];
if (isDomNode.isElementNode(child) &&
child.localName === localName &&
(child.namespaceURI === namespace || namespace == null)) {
res.push(child);
}
}
return res;
}
exports.findChildren = findChildren;
/** @deprecated */
function findChilds(node, localName, namespace) {
return findChildren(node, localName, namespace);
}
exports.findChilds = findChilds;
const xml_special_to_encoded_attribute = {
"&": "&",
"<": "<",
'"': """,
"\r": "
",
"\n": "
",
"\t": "	",
};
const xml_special_to_encoded_text = {
"&": "&",
"<": "<",
">": ">",
"\r": "
",
};
function encodeSpecialCharactersInAttribute(attributeValue) {
return attributeValue.replace(/([&<"\r\n\t])/g, function (str, item) {
/** Special character normalization.
* @see:
* - https://www.w3.org/TR/xml-c14n#ProcessingModel (Attribute Nodes)
* - https://www.w3.org/TR/xml-c14n#Example-Chars
*/
return xml_special_to_encoded_attribute[item];
});
}
exports.encodeSpecialCharactersInAttribute = encodeSpecialCharactersInAttribute;
function encodeSpecialCharactersInText(text) {
return text.replace(/([&<>\r])/g, function (str, item) {
/** Special character normalization.
* @see:
* - https://www.w3.org/TR/xml-c14n#ProcessingModel (Text Nodes)
* - https://www.w3.org/TR/xml-c14n#Example-Chars
*/
return xml_special_to_encoded_text[item];
});
}
exports.encodeSpecialCharactersInText = encodeSpecialCharactersInText;
/**
* PEM format has wide range of usages, but this library
* is enforcing RFC7468 which focuses on PKIX, PKCS and CMS.
*
* https://www.rfc-editor.org/rfc/rfc7468
*
* PEM_FORMAT_REGEX is validating given PEM file against RFC7468 'stricttextualmsg' definition.
*
* With few exceptions;
* - 'posteb' MAY have 'eol', but it is not mandatory.
* - 'preeb' and 'posteb' lines are limited to 64 characters, but
* should not cause any issues in context of PKIX, PKCS and CMS.
*/
exports.PEM_FORMAT_REGEX = new RegExp("^-----BEGIN [A-Z\x20]{1,48}-----([^-]*)-----END [A-Z\x20]{1,48}-----$", "s");
exports.EXTRACT_X509_CERTS = new RegExp("-----BEGIN CERTIFICATE-----[^-]*-----END CERTIFICATE-----", "g");
exports.BASE64_REGEX = new RegExp("^(?:[A-Za-z0-9\\+\\/]{4}\\n{0,1})*(?:[A-Za-z0-9\\+\\/]{2}==|[A-Za-z0-9\\+\\/]{3}=)?$", "s");
/**
* -----BEGIN [LABEL]-----
* base64([DATA])
* -----END [LABEL]-----
*
* Above is shown what PEM file looks like. As can be seen, base64 data
* can be in single line or multiple lines.
*
* This function normalizes PEM presentation to;
* - contain PEM header and footer as they are given
* - normalize line endings to '\n'
* - normalize line length to maximum of 64 characters
* - ensure that 'preeb' has line ending '\n'
*
* With a couple of notes:
* - 'eol' is normalized to '\n'
*
* @param pem The PEM string to normalize to RFC7468 'stricttextualmsg' definition
*/
function normalizePem(pem) {
return `${(pem
.trim()
.replace(/(\r\n|\r)/g, "\n")
.match(/.{1,64}/g) ?? []).join("\n")}\n`;
}
exports.normalizePem = normalizePem;
/**
* @param pem The PEM-encoded base64 certificate to strip headers from
*/
function pemToDer(pem) {
if (!exports.PEM_FORMAT_REGEX.test(pem.trim())) {
throw new Error("Invalid PEM format.");
}
return Buffer.from(pem
.replace(/(\r\n|\r)/g, "")
.replace(/-----BEGIN [A-Z\x20]{1,48}-----\n?/, "")
.replace(/-----END [A-Z\x20]{1,48}-----\n?/, ""), "base64");
}
exports.pemToDer = pemToDer;
/**
* @param der The DER-encoded base64 certificate to add PEM headers too
* @param pemLabel The label of the header and footer to add
*/
function derToPem(der, pemLabel) {
const base64Der = Buffer.isBuffer(der)
? der.toString("base64").trim()
: der.replace(/(\r\n|\r)/g, "").trim();
if (exports.PEM_FORMAT_REGEX.test(base64Der)) {
return normalizePem(base64Der);
}
if (exports.BASE64_REGEX.test(base64Der.replace(/ /g, ""))) {
if (pemLabel == null) {
throw new Error("PEM label is required when DER is given.");
}
const pem = `-----BEGIN ${pemLabel}-----\n${base64Der.replace(/ /g, "")}\n-----END ${pemLabel}-----`;
return normalizePem(pem);
}
throw new Error("Unknown DER format.");
}
exports.derToPem = derToPem;
function collectAncestorNamespaces(node, nsArray = []) {
if (!isDomNode.isElementNode(node.parentNode)) {
return nsArray;
}
const parent = node.parentNode;
if (!parent) {
return nsArray;
}
if (parent.attributes && parent.attributes.length > 0) {
for (let i = 0; i < parent.attributes.length; i++) {
const attr = parent.attributes[i];
if (attr && attr.nodeName && attr.nodeName.search(/^xmlns:?/) !== -1) {
nsArray.push({
prefix: attr.nodeName.replace(/^xmlns:?/, ""),
namespaceURI: attr.nodeValue || "",
});
}
}
}
return collectAncestorNamespaces(parent, nsArray);
}
function findNSPrefix(subset) {
const subsetAttributes = subset.attributes;
for (let k = 0; k < subsetAttributes.length; k++) {
const nodeName = subsetAttributes[k].nodeName;
if (nodeName.search(/^xmlns:?/) !== -1) {
return nodeName.replace(/^xmlns:?/, "");
}
}
return subset.prefix || "";
}
function isElementSubset(docSubset) {
return docSubset.every((node) => isDomNode.isElementNode(node));
}
/**
* Extract ancestor namespaces in order to import it to root of document subset
* which is being canonicalized for non-exclusive c14n.
*
* @param doc - Usually a product from `new xmldom.DOMParser().parseFromString()`
* @param docSubsetXpath - xpath query to get document subset being canonicalized
* @param namespaceResolver - xpath namespace resolver
* @returns i.e. [{prefix: "saml", namespaceURI: "urn:oasis:names:tc:SAML:2.0:assertion"}]
*/
function findAncestorNs(doc, docSubsetXpath, namespaceResolver) {
if (docSubsetXpath == null) {
return [];
}
const docSubset = xpath.selectWithResolver(docSubsetXpath, doc, namespaceResolver);
if (!isArrayHasLength(docSubset)) {
return [];
}
if (!isElementSubset(docSubset)) {
throw new Error("Document subset must be list of elements");
}
// Remove duplicate on ancestor namespace
const ancestorNs = collectAncestorNamespaces(docSubset[0]);
const ancestorNsWithoutDuplicate = [];
for (let i = 0; i < ancestorNs.length; i++) {
let notOnTheList = true;
for (const v in ancestorNsWithoutDuplicate) {
if (ancestorNsWithoutDuplicate[v].prefix === ancestorNs[i].prefix) {
notOnTheList = false;
break;
}
}
if (notOnTheList) {
ancestorNsWithoutDuplicate.push(ancestorNs[i]);
}
}
// Remove namespaces which are already declared in the subset with the same prefix
const returningNs = [];
const subsetNsPrefix = findNSPrefix(docSubset[0]);
for (const ancestorNs of ancestorNsWithoutDuplicate) {
if (ancestorNs.prefix !== subsetNsPrefix) {
returningNs.push(ancestorNs);
}
}
return returningNs;
}
exports.findAncestorNs = findAncestorNs;
function validateDigestValue(digest, expectedDigest) {
const buffer = Buffer.from(digest, "base64");
const expectedBuffer = Buffer.from(expectedDigest, "base64");
if (typeof buffer.equals === "function") {
return buffer.equals(expectedBuffer);
}
if (buffer.length !== expectedBuffer.length) {
return false;
}
for (let i = 0; i < buffer.length; i++) {
if (buffer[i] !== expectedBuffer[i]) {
return false;
}
}
return true;
}
exports.validateDigestValue = validateDigestValue;
//# sourceMappingURL=utils.js.map
;