Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(crypto): validate that digest length must be an integer between 0 and isize::MAX #3799

Merged
merged 6 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 34 additions & 6 deletions crypto/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ const stdCrypto: StdCrypto = ((x) => x)({
data: BufferSource | AsyncIterable<BufferSource> | Iterable<BufferSource>,
): Promise<ArrayBuffer> {
const { name, length } = normalizeAlgorithm(algorithm);

assertValidDigestLength(length);

iuioiua marked this conversation as resolved.
Show resolved Hide resolved
const bytes = bufferSourceBytes(data);

if (FNVAlgorithms.includes(name)) {
Expand Down Expand Up @@ -280,28 +283,30 @@ const stdCrypto: StdCrypto = ((x) => x)({
algorithm: DigestAlgorithm,
data: BufferSource | Iterable<BufferSource>,
): ArrayBuffer {
algorithm = normalizeAlgorithm(algorithm);
const { name, length } = normalizeAlgorithm(algorithm);

assertValidDigestLength(length);

const bytes = bufferSourceBytes(data);

if (FNVAlgorithms.includes(algorithm.name)) {
return fnv(algorithm.name, bytes);
if (FNVAlgorithms.includes(name)) {
return fnv(name, bytes);
}

const wasmCrypto = instantiateWasm();
if (bytes) {
return wasmCrypto.digest(algorithm.name, bytes, algorithm.length)
return wasmCrypto.digest(name, bytes, length)
.buffer;
} else if ((data as Iterable<BufferSource>)[Symbol.iterator]) {
const context = new wasmCrypto.DigestContext(algorithm.name);
const context = new wasmCrypto.DigestContext(name);
for (const chunk of data as Iterable<BufferSource>) {
const chunkBytes = bufferSourceBytes(chunk);
if (!chunkBytes) {
throw new TypeError("data contained chunk of the wrong type");
}
context.update(chunkBytes);
}
return context.digestAndDrop(algorithm.length).buffer;
return context.digestAndDrop(length).buffer;
} else {
throw new TypeError(
"data must be a BufferSource or Iterable<BufferSource>",
Expand All @@ -328,6 +333,29 @@ const webCryptoDigestAlgorithms = [
export type FNVAlgorithms = "FNV32" | "FNV32A" | "FNV64" | "FNV64A";
export type DigestAlgorithmName = WasmDigestAlgorithm | FNVAlgorithms;

/*
* The largest digest length the current WASM implementation can support. This
* is the value of `isize::MAX` on 32-bit platforms like WASM, which is the
* maximum allowed capacity of a Rust `Vec<u8>`.
*/
const MAX_DIGEST_LENGTH = 0x7FFF_FFFF;

/**
* Asserts that a number is a valid length for a digest, which must be an
* integer that fits in a Rust `Vec<u8>`, or be undefined.
*/
function assertValidDigestLength(value?: number) {
if (
value !== undefined &&
(value < 0 || value > MAX_DIGEST_LENGTH ||
!Number.isInteger(value))
) {
throw new RangeError(
`length must be an integer between 0 and ${MAX_DIGEST_LENGTH}, inclusive`,
);
}
}

export type DigestAlgorithmObject = {
name: DigestAlgorithmName;
length?: number;
Expand Down
37 changes: 29 additions & 8 deletions crypto/crypto_test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
import { assert, assertEquals, assertInstanceOf } from "../assert/mod.ts";
import { assert, assertEquals, assertInstanceOf, fail } from "../assert/mod.ts";
import { crypto as stdCrypto } from "./mod.ts";
import { repeat } from "../bytes/repeat.ts";
import { dirname, fromFileUrl } from "../path/mod.ts";
Expand Down Expand Up @@ -1363,7 +1363,11 @@ for (const algorithm of digestAlgorithms) {
const bytePieces = pieces.map((piece) =>
typeof piece === "string" ? new TextEncoder().encode(piece) : piece
) as Array<BufferSource>;
try {

// Expected value will either be a hex string, if the case is expected
// to return successfully, or an error class/constructor function, if
// the case is expected to throw.
if (typeof expected === "string") {
const actual = toHexString(
await stdCrypto.subtle.digest({
...options,
Expand All @@ -1379,15 +1383,32 @@ for (const algorithm of digestAlgorithms) {
JSON.stringify(options)
}) returned unexpected value\n actual: ${actual}\nexpected: ${expected}`,
);
} catch (error) {
if (expected instanceof Function) {
assert(
error instanceof expected,
`got a different error than expected: ${error}`,
} else if (typeof expected === "function") {
let error;
try {
await stdCrypto.subtle.digest({
...options,
name: algorithm,
}, bytePieces);
} catch (caughtError) {
error = caughtError;
}
if (error !== undefined) {
assertInstanceOf(
error,
expected,
);
} else {
throw error;
fail(
`${algorithm} of ${caption}${
i > 0 ? ` (but not until variation [${i}]!)` : ""
} with options ${
JSON.stringify(options)
}) expected an exception of type ${expected.name}, but none was thrown.`,
);
}
} else {
throw new TypeError("expected value has an unexpected type");
iuioiua marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand Down