All files / kernel-utils/src subresource-integrity.ts

100% Statements 31/31
100% Branches 10/10
100% Functions 5/5
100% Lines 30/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110                  167x             167x                                 167x 39x                               167x     36x 36x 36x     48x 48x     48x 4x   44x 66x   44x 3x       41x 33x 33x   41x 39x     36x 36x   167x                   167x       19x 19x           19x 20x   167x  
import { bytesToBase64 } from '@metamask/utils';
 
/**
 * The hash algorithms subresource integrity is defined over, weakest first:
 * metadata naming several of them is checked against the strongest, and this
 * order is what "strongest" means.
 *
 * @see https://w3c.github.io/webappsec-subresource-integrity/#valid-sri-hash-algorithm-token-set
 */
const SRI_ALGORITHMS = harden([
  { token: 'sha256', digest: 'SHA-256' },
  { token: 'sha384', digest: 'SHA-384' },
  { token: 'sha512', digest: 'SHA-512' },
] as const);
 
/** The whitespace integrity metadata separates its digests with. */
const ASCII_WHITESPACE = /[\t\n\f\r ]+/u;
 
export type IntegrityCheck = {
  /** The `crypto.subtle` name of the algorithm to digest with. */
  algorithm: (typeof SRI_ALGORITHMS)[number]['digest'];
  /** The digests, any one of which the bytes may match. */
  values: readonly string[];
};
 
/**
 * A digest is written base64 or base64url, padded or unpadded, and all four
 * spellings name the same bytes. Normalized rather than decoded, so that a
 * value which is not base64 at all fails to match instead of failing to parse.
 *
 * @param value - A digest as written.
 * @returns The same digest, comparable to any other spelling of itself.
 */
const normalizeDigest = (value: string): string =>
  value.replace(/[=]+$/u, '').replace(/-/gu, '+').replace(/_/gu, '/');
 
/**
 * Read integrity metadata — the `integrity` member of a `fetch` init — as the
 * single check a body has to pass.
 *
 * A token naming an algorithm that is not one of SRI's is dropped rather than
 * refused, because metadata may name several and only the strongest of those
 * understood is checked. Metadata naming nothing understood therefore comes
 * back as `undefined`, which is the caller's to answer for: `fetch` ignores
 * such metadata and hands back the body unchecked.
 *
 * @param metadata - The caller's integrity metadata; `''` for none.
 * @returns The strongest algorithm named and the digests written for it, or
 * `undefined` if the metadata names no digest that can be checked.
 */
export const parseIntegrityMetadata = (
  metadata: string,
): IntegrityCheck | undefined => {
  let strongest = -1;
  let values: string[] = [];
  for (const token of metadata.split(ASCII_WHITESPACE)) {
    // Whatever follows a `?` is an option, which SRI defines no use for and
    // reads past.
    const [expression] = token.split('?') as [string];
    const separator = expression.indexOf('-');
    // A token carrying no `-` names no digest, so there is nothing to check it
    // against — as there is not for the empty token an empty metadata splits to.
    if (separator < 0) {
      continue;
    }
    const index = SRI_ALGORITHMS.findIndex(
      ({ token: name }) => name === expression.slice(0, separator),
    );
    if (index < 0) {
      continue;
    }
    // A stronger algorithm discards what the weaker ones asked for; a second
    // digest for the algorithm already in hand is another way to satisfy it.
    if (index > strongest) {
      strongest = index;
      values = [];
    }
    if (index === strongest) {
      values.push(expression.slice(separator + 1));
    }
  }
  const algorithm = SRI_ALGORITHMS[strongest]?.digest;
  return algorithm === undefined ? undefined : harden({ algorithm, values });
};
harden(parseIntegrityMetadata);
 
/**
 * Digest bytes and compare — the check `fetch` runs itself when it is given
 * both a body and an `integrity`.
 *
 * @param bytes - The body to check.
 * @param check - What it has to match, from {@link parseIntegrityMetadata}.
 * @returns Whether the bytes match any digest in `check`.
 */
export const bytesMatchIntegrity = async (
  bytes: Uint8Array,
  check: IntegrityCheck,
): Promise<boolean> => {
  const { algorithm, values } = check;
  const digest = new Uint8Array(
    // The `crypto` global is what a browser realm has too, and the only place a
    // digest can come from there.
    // eslint-disable-next-line n/no-unsupported-features/node-builtins
    await globalThis.crypto.subtle.digest(algorithm, bytes),
  );
  const actual = normalizeDigest(bytesToBase64(digest));
  return values.some((value) => normalizeDigest(value) === actual);
};
harden(bytesMatchIntegrity);