/** Content Security Policy Level 2, section 4.2 */ export type HashAlgorithm = "sha256" | "sha384" | "sha512"; /** Supported Hash Algorithms */ export const supportedHashAlgorithms = { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512", } as const satisfies Record<HashAlgorithm, HashAlgorithmIdentifier>; export type PrioritizedHashAlgorithm = "" | HashAlgorithm; /** [W3C Subresource Integrity getPrioritizedHashFunction(a, b)](https://www.w3.org/TR/SRI/#dfn-getprioritizedhashfunction-a-b) */ export type GetPrioritizedHashAlgorithm = ( a: HashAlgorithm, b: HashAlgorithm, ) => PrioritizedHashAlgorithm; /** [W3C Subresource Integrity getPrioritizedHashFunction(a, b)](https://www.w3.org/TR/SRI/#dfn-getprioritizedhashfunction-a-b) */ export function getPrioritizedHashAlgorithm( a: HashAlgorithm, b: HashAlgorithm, ): PrioritizedHashAlgorithm { 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; } export 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]*))?$/; export const SeparatorRegex = /[^\x21-\x7e]+/; /** Integrity Metadata Like */ export type IntegrityMetadataLike = { alg: PrioritizedHashAlgorithm; val: string; opt?: string[]; }; /** Integrity Metadata */ export class IntegrityMetadata implements IntegrityMetadataLike { alg: PrioritizedHashAlgorithm; val: string; opt: string[]; constructor(integrity: IntegrityMetadataLike | string | null | undefined) { 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("?") ?? [], }); } match(integrity: IntegrityMetadataLike | string | null | undefined): boolean { 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; } toString(): string { return IntegrityMetadata.stringify(this); } toJSON(): string { return this.toString(); } static stringify({ alg, val, opt = [] }: IntegrityMetadataLike): string { if (!alg) return ""; if (!val) return ""; if (!(alg in supportedHashAlgorithms)) return ""; return `${alg}-${[val, ...opt].join("?")}`; } } export async function createIntegrityMetadata( hashAlgorithm: HashAlgorithm, data: ArrayBuffer, opt: string[] = [], ): Promise<IntegrityMetadata> { const alg = hashAlgorithm.toLowerCase() as HashAlgorithm; 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); } /** Integrity Metadata Set Options */ export type IntegrityMetadataSetOptions = { getPrioritizedHashAlgorithm?: GetPrioritizedHashAlgorithm; }; /** Integrity Metadata Set */ export class IntegrityMetadataSet { /** [W3C Subresource Integrity 3.3.4 Get the strongest metadata from set.](https://www.w3.org/TR/SRI/#get-the-strongest-metadata-from-set) */ readonly strongest: Array<IntegrityMetadata> = []; #set: ReadonlyArray<IntegrityMetadata>; constructor( integrity: | ReadonlyArray<IntegrityMetadataLike | string | null | undefined> | IntegrityMetadataLike | string | null | undefined, { getPrioritizedHashAlgorithm: _getPrioritizedHashAlgorithm = getPrioritizedHashAlgorithm, }: IntegrityMetadataSetOptions = {}, ) { const set: ReadonlyArray< IntegrityMetadataLike | string | null | undefined > = [integrity] .flat() .flatMap( ( integrity: IntegrityMetadataLike | string | null | undefined, ): ReadonlyArray<IntegrityMetadataLike | string | null | undefined> => typeof integrity === "string" ? integrity.split(SeparatorRegex) : [integrity], ); this.#set = set .map((integrity) => new IntegrityMetadata(integrity)) .filter((integrityMetadata) => integrityMetadata.toString() !== ""); for (const integrityMetadata of this.#set) { const [strongest = new IntegrityMetadata("")] = this.strongest; const prioritizedHashAlgorithm = _getPrioritizedHashAlgorithm( strongest.alg as HashAlgorithm, integrityMetadata.alg as HashAlgorithm, ); switch (prioritizedHashAlgorithm) { case "": this.strongest.push(integrityMetadata); break; case integrityMetadata.alg: this.strongest = [integrityMetadata]; break; } } } *[Symbol.iterator](): Generator<IntegrityMetadata> { for (const integrityMetadata of this.#set) { yield integrityMetadata; } } get size(): number { return this.#set.length; } get strongestHashAlgorithms(): ReadonlyArray<HashAlgorithm> { const strongestHashAlgorithms = this.strongest .map(({ alg }) => alg as HashAlgorithm) .filter(Boolean); return [...new Set(strongestHashAlgorithms)]; } join(separator = " "): string { return this.#set.map(String).join(separator); } toString(): string { return this.join(); } toJSON(): string { return this.toString(); } } export async function createIntegrityMetadataSet( hashAlgorithms: HashAlgorithm[], data: ArrayBuffer, options: IntegrityMetadataSetOptions = { getPrioritizedHashAlgorithm, }, ): Promise<IntegrityMetadataSet> { const integrityMetadata = await Promise.all( hashAlgorithms.map((alg) => createIntegrityMetadata(alg, data)), ); return new IntegrityMetadataSet(integrityMetadata.join(" "), options); }