UNPKG

websri

Version:

A universal Subresource Integrity (SRI) utility for Node.js, browsers, Cloudflare Workers, Deno, Bun, and other web-compatible runtimes.

276 lines (274 loc) 8.91 kB
const supportedHashAlgorithms = { /** SHA-256 hash algorithm */ sha256: "SHA-256", /** SHA-384 hash algorithm */ sha384: "SHA-384", /** SHA-512 hash algorithm */ sha512: "SHA-512" }; function getPrioritizedHashAlgorithm(a, b) { if (a === b) return ""; if (!(a in supportedHashAlgorithms)) { return b in supportedHashAlgorithms ? b : ""; } if (!(b in supportedHashAlgorithms)) { return a in supportedHashAlgorithms ? a : ""; } return a < b ? b : a; } const IntegrityMetadataRegex = /^(?<alg>sha256|sha384|sha512)-(?<val>(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)(?:[?](?<opt>[\x21-\x7e]*))?$/; const SeparatorRegex = /[^\x21-\x7e]+/; class IntegrityMetadata { /** Hash algorithm */ alg; /** The base64-encoded hash value of the resource */ val; /** Optional additional attributes */ opt; /** * Creates an instance of `IntegrityMetadata` from a given object or string. * @param integrity The integrity metadata input, which can be a string or object. * @example * ```js * new IntegrityMetadata("sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=") * ``` * * or * * ```js * new IntegrityMetadata({ * alg: "sha256", * val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=", * }) * ``` */ constructor(integrity) { const integrityString = typeof integrity === "object" && integrity !== null ? IntegrityMetadata.stringify(integrity) : String(integrity ?? "").trim(); const { alg = "", val = "", opt } = IntegrityMetadataRegex.exec(integrityString)?.groups ?? {}; Object.assign(this, { alg, val, opt: opt?.split("?") ?? [] }); } /** * Compares the current integrity metadata with another object or string. * @param integrity The integrity metadata to compare with. * @returns `true` if the integrity metadata matches, `false` otherwise. * @example * ```js * integrityMetadata.match("sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=") * ``` * * or * * ```js * integrityMetadata.match({ * alg: "sha256", * val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=", * }) * ``` */ match(integrity) { const { alg, val } = new IntegrityMetadata(integrity); if (!alg) return false; if (!val) return false; if (!(alg in supportedHashAlgorithms)) return false; return alg === this.alg && val === this.val; } /** * Converts the integrity metadata into a string representation. * @returns The string representation of the integrity metadata. */ toString() { return IntegrityMetadata.stringify(this); } /** * Converts the integrity metadata into a JSON string. * @returns The JSON string representation of the integrity metadata. */ toJSON() { return this.toString(); } /** * Static method to stringify an integrity metadata object. * @param integrity The integrity metadata object to stringify. * @returns The stringified integrity metadata. * @example * ```js * IntegrityMetadata.stringify({ * alg: "sha256", * val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=", * }) // "sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=" * ``` */ static stringify({ alg, val, opt = [] }) { if (!alg) return ""; if (!val) return ""; if (!(alg in supportedHashAlgorithms)) return ""; return `${alg}-${[val, ...opt].join("?")}`; } } async function createIntegrityMetadata(hashAlgorithm, data, opt = []) { const alg = hashAlgorithm.toLowerCase(); if (!(alg in supportedHashAlgorithms)) { return new IntegrityMetadata(""); } const hashAlgorithmIdentifier = supportedHashAlgorithms[alg]; const arrayBuffer = await crypto.subtle.digest(hashAlgorithmIdentifier, data); const val = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))); const integrity = IntegrityMetadata.stringify({ alg, val, opt }); return new IntegrityMetadata(integrity); } class IntegrityMetadataSet { #set; #getPrioritizedHashAlgorithm = getPrioritizedHashAlgorithm; /** * Create an instance of `IntegrityMetadataSet` from integrity metadata or an array of integrity * metadata. * @param integrity The integrity metadata or an array of integrity metadata. * @param options Optional configuration options for hash algorithm prioritization. * @example * ```js * new IntegrityMetadataSet([ * "sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=", * "sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r", * "sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==", * ]) * ``` * * or * * ```js * new IntegrityMetadataSet(` * sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM= * sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r * sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ== * `) * ``` */ constructor(integrity, { getPrioritizedHashAlgorithm: _getPrioritizedHashAlgorithm = getPrioritizedHashAlgorithm } = {}) { this.#set = [integrity].flat().flatMap( (integrity2) => { if (typeof integrity2 === "string") { return integrity2.split(SeparatorRegex); } return [integrity2]; } ).map((integrity2) => new IntegrityMetadata(integrity2)).filter((integrityMetadata) => integrityMetadata.toString() !== ""); this.#getPrioritizedHashAlgorithm = _getPrioritizedHashAlgorithm; } /** * Enables iteration over the set of integrity metadata. * @returns A generator that yields each IntegrityMetadata object. * @example * ```js * [...integrityMetadataSet] * ``` */ *[Symbol.iterator]() { for (const integrityMetadata of this.#set) { yield new IntegrityMetadata(integrityMetadata); } } /** * The number of integrity metadata entries in the set. */ get size() { return this.#set.length; } /** * The strongest (most secure) integrity metadata from the set. * @see {@link https://www.w3.org/TR/SRI/#get-the-strongest-metadata-from-set} */ get strongest() { let strongest = new IntegrityMetadataSet([]); for (const integrityMetadata of this.#set) { const [{ alg } = new IntegrityMetadata("")] = strongest; const prioritizedHashAlgorithm = this.#getPrioritizedHashAlgorithm( alg, integrityMetadata.alg ); switch (prioritizedHashAlgorithm) { case "": strongest = new IntegrityMetadataSet([ ...strongest, integrityMetadata ]); break; case integrityMetadata.alg: strongest = new IntegrityMetadataSet(integrityMetadata); break; } } return strongest; } /** * Returns an array of the strongest supported hash algorithms in the set. */ get strongestHashAlgorithms() { const strongestHashAlgorithms = [...this.strongest].map(({ alg }) => alg).filter(Boolean); return [...new Set(strongestHashAlgorithms)]; } /** * Checks if a given integrity metadata object or string matches any in the set. * @param integrity The integrity metadata to match. * @returns `true` if the integrity metadata matches, `false` otherwise. * @example * ```js * integrityMetadataSet.match("sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=") * ``` * * or * * ```js * integrityMetadataSet.match({ * alg: "sha256", * val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=", * }) * ``` */ match(integrity) { return this.#set.some( (integrityMetadata) => integrityMetadata.match(integrity) ); } /** * Joins the integrity metadata in the set into a single string, separated by the specified * separator. * @param separator The separator to use (default is a space). * @returns The joined string representation of the set. */ join(separator = " ") { return this.#set.map(String).join(separator); } /** * Converts the set of integrity metadata to a string representation. * @returns The string representation of the set. */ toString() { return this.join(); } /** * Converts the set of integrity metadata to a JSON string. * @returns The JSON string representation of the set. */ toJSON() { return this.toString(); } } async function createIntegrityMetadataSet(hashAlgorithms, data, options = { getPrioritizedHashAlgorithm }) { const set = await Promise.all( [hashAlgorithms].flat().map((alg) => createIntegrityMetadata(alg, data)) ); return new IntegrityMetadataSet(set, options); } export { IntegrityMetadata, IntegrityMetadataRegex, IntegrityMetadataSet, SeparatorRegex, createIntegrityMetadata, createIntegrityMetadataSet, getPrioritizedHashAlgorithm, supportedHashAlgorithms };