mirror of
https://github.com/kou029w/websri.git
synced 2025-03-30 02:15:17 +00:00
442 lines
14 KiB
TypeScript
442 lines
14 KiB
TypeScript
/**
|
|
* Represents the available hash algorithms used for Subresource Integrity.
|
|
* @see {@link https://www.w3.org/TR/CSP2/#hash_algo}
|
|
*/
|
|
export type HashAlgorithm = "sha256" | "sha384" | "sha512";
|
|
|
|
/**
|
|
* A constant object defining the supported hash algorithms and their corresponding string
|
|
* representations for cryptographic operations. These algorithms are referenced by name when
|
|
* working with hashing functions in Web Crypto APIs.
|
|
*/
|
|
export const supportedHashAlgorithms = {
|
|
/** SHA-256 hash algorithm */
|
|
sha256: "SHA-256",
|
|
/** SHA-384 hash algorithm */
|
|
sha384: "SHA-384",
|
|
/** SHA-512 hash algorithm */
|
|
sha512: "SHA-512",
|
|
} as const satisfies Record<HashAlgorithm, HashAlgorithmIdentifier>;
|
|
|
|
/**
|
|
* A union type representing either an empty string or a valid hash algorithm.
|
|
* The empty string is used when no hash algorithm is selected or is considered equal.
|
|
*/
|
|
export type PrioritizedHashAlgorithm = "" | HashAlgorithm;
|
|
|
|
/**
|
|
* Function to prioritize two hash algorithms, returning the stronger or an empty string if both
|
|
* are unsupported or equal.
|
|
* @see {@link https://www.w3.org/TR/SRI/#dfn-getprioritizedhashfunction-a-b}
|
|
* @param a The first hash algorithm to compare.
|
|
* @param b The second hash algorithm to compare.
|
|
* @returns The hash algorithm or an empty string if both algorithms are unsupported or equal.
|
|
*/
|
|
export type GetPrioritizedHashAlgorithm = (
|
|
a: HashAlgorithm,
|
|
b: HashAlgorithm,
|
|
) => PrioritizedHashAlgorithm;
|
|
|
|
/**
|
|
* Function to prioritize two hash algorithms, returning the stronger or an empty string if both
|
|
* are unsupported or equal.
|
|
* @see {@link https://www.w3.org/TR/SRI/#dfn-getprioritizedhashfunction-a-b}
|
|
* @param a The first hash algorithm to compare.
|
|
* @param b The second hash algorithm to compare.
|
|
* @returns The hash algorithm or an empty string if both algorithms are unsupported or equal.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Regular expression for matching integrity metadata format.
|
|
*/
|
|
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]*))?$/;
|
|
|
|
/**
|
|
* Regular expression for separating integrity metadata.
|
|
*/
|
|
export const SeparatorRegex = /[^\x21-\x7e]+/;
|
|
|
|
/**
|
|
* Represents the structure of integrity metadata used for validating resources with Subresource
|
|
* Integrity.
|
|
*/
|
|
export type IntegrityMetadataLike = {
|
|
/** Hash algorithm */
|
|
alg: PrioritizedHashAlgorithm;
|
|
/** The base64-encoded hash value of the resource */
|
|
val: string;
|
|
/** Optional additional attributes */
|
|
opt?: string[];
|
|
};
|
|
|
|
/**
|
|
* Class representing integrity metadata, consisting of a hash algorithm and hash value.
|
|
*/
|
|
export class IntegrityMetadata implements IntegrityMetadataLike {
|
|
/** Hash algorithm */
|
|
alg: PrioritizedHashAlgorithm;
|
|
/** The base64-encoded hash value of the resource */
|
|
val: string;
|
|
/** Optional additional attributes */
|
|
opt: string[];
|
|
|
|
/**
|
|
* 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: 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("?") ?? [],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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: 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;
|
|
}
|
|
|
|
/**
|
|
* Converts the integrity metadata into a string representation.
|
|
* @returns The string representation of the integrity metadata.
|
|
*/
|
|
toString(): string {
|
|
return IntegrityMetadata.stringify(this);
|
|
}
|
|
|
|
/**
|
|
* Converts the integrity metadata into a JSON string.
|
|
* @returns The JSON string representation of the integrity metadata.
|
|
*/
|
|
toJSON(): string {
|
|
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 = [] }: IntegrityMetadataLike): string {
|
|
if (!alg) return "";
|
|
if (!val) return "";
|
|
if (!(alg in supportedHashAlgorithms)) return "";
|
|
return `${alg}-${[val, ...opt].join("?")}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Asynchronously creates an `IntegrityMetadata` object from a hash algorithm and data.
|
|
* @param hashAlgorithm The hash algorithm to use (e.g., `sha256`).
|
|
* @param data The data to hash (in `ArrayBuffer` format).
|
|
* @param opt Optional additional attributes.
|
|
* @returns A promise that resolves to an `IntegrityMetadata` object.
|
|
* @example
|
|
* ```js
|
|
* const res = new Response("Hello, world!");
|
|
* const data = await res.arrayBuffer();
|
|
* const integrityMetadata = await createIntegrityMetadata("sha256", data);
|
|
* ```
|
|
*/
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Options for configuring an `IntegrityMetadataSet`.
|
|
*/
|
|
export type IntegrityMetadataSetOptions = {
|
|
/** A custom function to determine the prioritized hash algorithm. */
|
|
getPrioritizedHashAlgorithm?: GetPrioritizedHashAlgorithm;
|
|
};
|
|
|
|
/**
|
|
* Class representing a set of integrity metadata, used for managing multiple hash algorithms and
|
|
* their associated metadata.
|
|
*/
|
|
export class IntegrityMetadataSet {
|
|
#set: ReadonlyArray<IntegrityMetadata>;
|
|
#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:
|
|
| ReadonlyArray<IntegrityMetadataLike | string | null | undefined>
|
|
| IntegrityMetadataLike
|
|
| string
|
|
| null
|
|
| undefined,
|
|
{
|
|
getPrioritizedHashAlgorithm:
|
|
_getPrioritizedHashAlgorithm = getPrioritizedHashAlgorithm,
|
|
}: IntegrityMetadataSetOptions = {},
|
|
) {
|
|
this.#set = [integrity]
|
|
.flat()
|
|
.flatMap(
|
|
(
|
|
integrity: IntegrityMetadataLike | string | null | undefined,
|
|
): ReadonlyArray<IntegrityMetadataLike | string | null | undefined> => {
|
|
if (typeof integrity === "string") {
|
|
return integrity.split(SeparatorRegex);
|
|
}
|
|
|
|
return [integrity];
|
|
},
|
|
)
|
|
.map((integrity) => new IntegrityMetadata(integrity))
|
|
.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](): Generator<IntegrityMetadata> {
|
|
for (const integrityMetadata of this.#set) {
|
|
yield new IntegrityMetadata(integrityMetadata);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The number of integrity metadata entries in the set.
|
|
*/
|
|
get size(): number {
|
|
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(): IntegrityMetadataSet {
|
|
let strongest = new IntegrityMetadataSet([]);
|
|
|
|
for (const integrityMetadata of this.#set) {
|
|
const [{ alg } = new IntegrityMetadata("")] = strongest;
|
|
|
|
const prioritizedHashAlgorithm = this.#getPrioritizedHashAlgorithm(
|
|
alg as HashAlgorithm,
|
|
integrityMetadata.alg as HashAlgorithm,
|
|
);
|
|
|
|
switch (prioritizedHashAlgorithm) {
|
|
case "":
|
|
strongest = new IntegrityMetadataSet([
|
|
...strongest,
|
|
integrityMetadata,
|
|
]);
|
|
break;
|
|
case integrityMetadata.alg:
|
|
strongest = new IntegrityMetadataSet(integrityMetadata);
|
|
break;
|
|
case alg:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return strongest;
|
|
}
|
|
|
|
/**
|
|
* Returns an array of the strongest supported hash algorithms in the set.
|
|
*/
|
|
get strongestHashAlgorithms(): ReadonlyArray<HashAlgorithm> {
|
|
const strongestHashAlgorithms = [...this.strongest]
|
|
.map(({ alg }) => alg as HashAlgorithm)
|
|
.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: IntegrityMetadataLike | string | null | undefined): boolean {
|
|
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 = " "): string {
|
|
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(): string {
|
|
return this.join();
|
|
}
|
|
|
|
/**
|
|
* Converts the set of integrity metadata to a JSON string.
|
|
* @returns The JSON string representation of the set.
|
|
*/
|
|
toJSON(): string {
|
|
return this.toString();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Asynchronously creates an `IntegrityMetadataSet` from a set of hash algorithms and data.
|
|
* @param hashAlgorithms A single hash algorithm or an array of supported hash algorithms.
|
|
* @param data The data to hash (in `ArrayBuffer` format).
|
|
* @param options Optional configuration options for the metadata set.
|
|
* @returns A promise that resolves to an `IntegrityMetadataSet` object.
|
|
* @example
|
|
* ```js
|
|
* const res = new Response("Hello, world!");
|
|
* const data = await res.arrayBuffer();
|
|
* const set = await createIntegrityMetadataSet(["sha256", "sha384", "sha512"], data);
|
|
* ```
|
|
*/
|
|
export async function createIntegrityMetadataSet(
|
|
hashAlgorithms: ReadonlyArray<HashAlgorithm> | HashAlgorithm,
|
|
data: ArrayBuffer,
|
|
options: IntegrityMetadataSetOptions = {
|
|
getPrioritizedHashAlgorithm,
|
|
},
|
|
): Promise<IntegrityMetadataSet> {
|
|
const set = await Promise.all(
|
|
[hashAlgorithms].flat().map((alg) => createIntegrityMetadata(alg, data)),
|
|
);
|
|
|
|
return new IntegrityMetadataSet(set, options);
|
|
}
|