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
JavaScript
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 };