Skip to content

Commit

Permalink
fix(crypto): move FNV hashes from TypeScript to Rust/Wasm, fixing ite…
Browse files Browse the repository at this point in the history
…rator behaviour and performance
  • Loading branch information
jeremyBanks committed Mar 24, 2024
1 parent 1341b89 commit e153f43
Show file tree
Hide file tree
Showing 8 changed files with 2,898 additions and 2,484 deletions.
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

0 comments on commit e153f43

Please sign in to comment.