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): move FNV hashes from TypeScript to Rust/Wasm and implement iteration functionality #4515

Merged
merged 11 commits into from
Mar 26, 2024
Merged
109 changes: 56 additions & 53 deletions crypto/_benches/bench.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,70 @@
#!/usr/bin/env -S deno run
#!/usr/bin/env -S deno bench
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assert, assertEquals } from "../../assert/mod.ts";
import { crypto as stdCrypto, DIGEST_ALGORITHM_NAMES } from "../mod.ts";

import { crypto as stdCrypto } from "../mod.ts";
import { crypto as oldCrypto } from "https://deno.land/std@0.220.1/crypto/mod.ts";

const webCrypto = globalThis.crypto;

// Wasm is limited to 32-bit operations, which SHA-256 is optimized for, while
// SHA-512 is optimized for 64-bit operations and may be slower.
for (const algorithm of ["SHA-256", "SHA-512"] as const) {
for (
const length of [
64,
262_144,
4_194_304,
67_108_864,
524_291_328,
] as const
) {
// Create a test input buffer and do some operations to hopefully ensure
// it's fully initialized and not optimized away.
const buffer = new Uint8Array(length);
for (let i = 0; i < length; i++) {
buffer[i] = (i + (i % 13) + (i % 31)) % 255;
}
let sum = 0;
for (const byte of buffer) {
sum += byte;
}
assert(sum > 0);
const BENCHMARKED_DIGEST_ALGORITHM_NAMES = [
"SHA-256",
"SHA-512",
"BLAKE3",
"FNV32A",
"FNV64A",
] satisfies (typeof DIGEST_ALGORITHM_NAMES[number])[];

for (
const implementation of [
"runtime WebCrypto (target)",
"std/crypto Wasm (you are here)",
] as const
) {
let lastDigest: ArrayBuffer | undefined;
const WEB_CRYPTO_DIGEST_ALGORITHM_NAMES = [
"SHA-1",
"SHA-256",
"SHA-384",
"SHA-512",
] satisfies (typeof DIGEST_ALGORITHM_NAMES[number])[];

Deno.bench({
name: `${algorithm.padEnd(12)} ${
length
.toString()
.padStart(12)
}B ${implementation}`,
async fn() {
let digest;
if (implementation === "std/crypto Wasm (you are here)") {
digest = stdCrypto.subtle.digestSync(algorithm, buffer);
} else if (implementation === "runtime WebCrypto (target)") {
digest = await webCrypto.subtle.digest(algorithm, buffer);
} else {
throw new Error(`Unknown implementation ${implementation}`);
}
for (
const [length, humanLength] of [
[64, "64 B"],
[262_144, "256 KiB"],
[4_194_304, "4 MiB"],
[67_108_864, "64 MiB"],
[524_291_328, "512 MiB"],
] as const
) {
const buffer = new Uint8Array(length);
for (let i = 0; i < length; i++) {
buffer[i] = (i + (i % 13) + (i % 31)) % 256;
}

assert(digest.byteLength > 0);
for (const name of ["FNV32A", "FNV64A"] as const) {
Deno.bench({
group: `${humanLength} in ${name}`,
name: `TypeScript (from v0.220.1) with ${humanLength} in ${name}`,
async fn() {
await oldCrypto.subtle.digest(name, buffer);
},
});
}

if (lastDigest) {
assertEquals(lastDigest, digest);
}
lastDigest = digest;
for (const name of BENCHMARKED_DIGEST_ALGORITHM_NAMES) {
if (
(WEB_CRYPTO_DIGEST_ALGORITHM_NAMES as readonly string[]).includes(name)
) {
Deno.bench({
group: `${humanLength} in ${name}`,
name: `Runtime WebCrypto with ${humanLength} in ${name}`,
async fn() {
await webCrypto.subtle.digest(name, buffer);
},
});
}

Deno.bench({
group: `${humanLength} in ${name}`,
name: `Rust/Wasm with ${humanLength} in ${name}`,
baseline: true,
async fn() {
await stdCrypto.subtle.digest(name, [buffer]);
},
});
}
}
4,529 changes: 2,279 additions & 2,250 deletions crypto/_wasm/lib/deno_std_wasm_crypto.generated.mjs

Large diffs are not rendered by default.

25 changes: 15 additions & 10 deletions crypto/_wasm/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@ export {
} from "./lib/deno_std_wasm_crypto.generated.mjs";

/**
* All cryptographic hash/digest algorithms supported by std/crypto/_wasm.
* All cryptographic hash/digest algorithms supported by std/crypto.
*
* For algorithms that are supported by WebCrypto, the name here must match the
* one used by WebCrypto. Otherwise we should prefer the formatting used in the
* official specification. All names are uppercase to facilitate case-insensitive
* comparisons required by the WebCrypto spec.
* For algorithms that are supported by WebCrypto, the name here will match the
* one used by WebCrypto. Otherwise we prefer the formatting used in the
* algorithm's official specification. All names are uppercase to facilitate
* case-insensitive comparisons required by the WebCrypto spec.
*/
export const digestAlgorithms = [
export const DIGEST_ALGORITHM_NAMES = [
"BLAKE2B",
"BLAKE2B-128",
"BLAKE2B-160",
"BLAKE2B-224",
"BLAKE2B-256",
"BLAKE2B-384",
"BLAKE2B",
"BLAKE2S",
"BLAKE3",
"KECCAK-224",
Expand All @@ -37,11 +37,16 @@ export const digestAlgorithms = [
"SHA-224",
"SHA-256",
"SHA-512",
// insecure (collidable and length-extendable):
// insecure (length-extendable and collidable):
"MD4",
"MD5",
"SHA-1",
// insecure (non-cryptographic)
"FNV32",
"FNV32A",
"FNV64",
"FNV64A",
] as const;

/** An algorithm name supported by std/crypto/_wasm. */
export type DigestAlgorithm = typeof digestAlgorithms[number];
/** An algorithm name supported by std/crypto. */
export type DigestAlgorithmName = typeof DIGEST_ALGORITHM_NAMES[number];
20 changes: 20 additions & 0 deletions crypto/_wasm/src/digest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ pub enum Context {
Shake128(Box<sha3::Shake128>),
Shake256(Box<sha3::Shake256>),
Tiger(Box<tiger::Tiger>),
Fnv32(Box<crate::fnv::Fnv32>),
Fnv32A(Box<crate::fnv::Fnv32A>),
Fnv64(Box<crate::fnv::Fnv64>),
Fnv64A(Box<crate::fnv::Fnv64A>),
}

use Context::*;
Expand Down Expand Up @@ -65,6 +69,10 @@ impl Context {
"SHAKE128" => Shake128(Default::default()),
"SHAKE256" => Shake256(Default::default()),
"TIGER" => Tiger(Default::default()),
"FNV32" => Fnv32(Box::new(crate::fnv::Fnv32::new())),
"FNV32A" => Fnv32A(Box::new(crate::fnv::Fnv32A::new())),
"FNV64" => Fnv64(Box::new(crate::fnv::Fnv64::new())),
"FNV64A" => Fnv64A(Box::new(crate::fnv::Fnv64A::new())),
_ => return Err("unsupported algorithm"),
})
}
Expand Down Expand Up @@ -99,6 +107,10 @@ impl Context {
Sha384(context) => context.output_size(),
Sha512(context) => context.output_size(),
Tiger(context) => context.output_size(),
Fnv32(_) => 4,
Fnv32A(_) => 4,
Fnv64(_) => 8,
Fnv64A(_) => 8,

// https://doi.org/10.6028/NIST.FIPS.202's Table 4 indicates that in order
// to reach the target security strength for these algorithms, the output
Expand Down Expand Up @@ -144,6 +156,10 @@ impl Context {
Tiger(context) => Digest::update(&mut **context, data),
Shake128(context) => (&mut **context).update(data),
Shake256(context) => (&mut **context).update(data),
Fnv32(context) => (&mut **context).update(data),
Fnv32A(context) => (&mut **context).update(data),
Fnv64(context) => (&mut **context).update(data),
Fnv64A(context) => (&mut **context).update(data),
};
}

Expand Down Expand Up @@ -189,6 +205,10 @@ impl Context {
Tiger(context) => context.finalize(),
Shake128(context) => context.finalize_boxed(length),
Shake256(context) => context.finalize_boxed(length),
Fnv32(context) => context.digest(),
Fnv32A(context) => context.digest(),
Fnv64(context) => context.digest(),
Fnv64A(context) => context.digest(),
})
}
}
90 changes: 90 additions & 0 deletions crypto/_wasm/src/fnv.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
const BASIS_32: u32 = 2166136261;
const PRIME_32: u32 = 16777619;

const BASIS_64: u64 = 14695981039346656037;
const PRIME_64: u64 = 1099511628211;

pub struct Fnv32 {
state: u32,
}

impl Fnv32 {
pub fn new() -> Fnv32 {
Fnv32 { state: BASIS_32 }
}

pub fn update(&mut self, bytes: impl AsRef<[u8]>) {
for byte in bytes.as_ref() {
self.state = self.state.wrapping_mul(PRIME_32);
self.state ^= u32::from(*byte);
}
}

pub fn digest(&self) -> Box<[u8]> {
Box::new(self.state.to_be_bytes())
}
}

pub struct Fnv32A {
state: u32,
}

impl Fnv32A {
pub fn new() -> Fnv32A {
Fnv32A { state: BASIS_32 }
}

pub fn update(&mut self, bytes: impl AsRef<[u8]>) {
for byte in bytes.as_ref() {
self.state ^= u32::from(*byte);
self.state = self.state.wrapping_mul(PRIME_32);
}
}

pub fn digest(&self) -> Box<[u8]> {
Box::new(self.state.to_be_bytes())
}
}

pub struct Fnv64 {
state: u64,
}

impl Fnv64 {
pub fn new() -> Fnv64 {
Fnv64 { state: BASIS_64 }
}

pub fn update(&mut self, bytes: impl AsRef<[u8]>) {
for byte in bytes.as_ref() {
self.state = self.state.wrapping_mul(PRIME_64);
self.state ^= u64::from(*byte);
}
}

pub fn digest(&self) -> Box<[u8]> {
Box::new(self.state.to_be_bytes())
}
}

pub struct Fnv64A {
state: u64,
}

impl Fnv64A {
pub fn new() -> Fnv64A {
Fnv64A { state: BASIS_64 }
}

pub fn update(&mut self, bytes: impl AsRef<[u8]>) {
for byte in bytes.as_ref() {
self.state ^= u64::from(*byte);
self.state = self.state.wrapping_mul(PRIME_64);
}
}

pub fn digest(&self) -> Box<[u8]> {
Box::new(self.state.to_be_bytes())
}
}
1 change: 1 addition & 0 deletions crypto/_wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use js_sys::Uint8Array;
use wasm_bindgen::prelude::*;

mod digest;
mod fnv;

/// Returns the digest of the given `data` using the given hash `algorithm`.
///
Expand Down
Loading