diff --git a/cranelift/runtime-abi/runtime_functions.txt b/cranelift/runtime-abi/runtime_functions.txt index 85ab2b94..6d4f96df 100644 --- a/cranelift/runtime-abi/runtime_functions.txt +++ b/cranelift/runtime-abi/runtime_functions.txt @@ -152,6 +152,8 @@ crypto_verify_rsa_pkcs1_sha256|pith_crypto_verify_rsa_pkcs1_sha256|I64,I64,I64|I crypto_verify_rsa_pkcs1_sha384|pith_crypto_verify_rsa_pkcs1_sha384|I64,I64,I64|I64 crypto_verify_rsa_pss_sha256|pith_crypto_verify_rsa_pss_sha256|I64,I64,I64|I64 crypto_sign_rsa_pss_sha256_pkcs8|pith_crypto_sign_rsa_pss_sha256_pkcs8|I64,I64|I64 +crypto_blake2b|pith_crypto_blake2b|I64,I64,I64|I64 +crypto_argon2id|pith_crypto_argon2id|I64,I64,I64,I64,I64,I64|I64 pith_json_fill_struct|pith_json_fill_struct|I64,I64,I64|I64 pith_json_decode_missing_error|pith_json_decode_missing_error|I64,I64|I64 byte_buffer_new|pith_byte_buffer_new||I64 diff --git a/cranelift/runtime/src/argon2.rs b/cranelift/runtime/src/argon2.rs new file mode 100644 index 00000000..6c9f4147 --- /dev/null +++ b/cranelift/runtime/src/argon2.rs @@ -0,0 +1,594 @@ +//! Argon2 (RFC 9106) memory-hard password hashing. +//! +//! Hand-written on top of the BLAKE2b module for the same reason BLAKE2b is: +//! the runtime keeps its dependency set small, and `ring` offers neither +//! primitive. Only Argon2id is exposed to pith — it is the variant RFC 9106 +//! recommends for password hashing — but the core supports all three +//! variants so the official test vectors for each can pin down the indexing +//! logic, which is where a subtly wrong implementation would still produce +//! plausible-looking output. +//! +//! The memory matrix is filled single-threaded. The `lanes` parameter still +//! changes the output as the RFC requires; it just doesn't fan out across +//! threads. For the parameter sizes a password hash uses, correctness and +//! simplicity beat the modest wall-clock win. + +use crate::blake2b; + +/// Ceiling on the memory cost, in KiB (1 GiB). The memory parameter is an +/// allocation request from whatever config supplied it, so the runtime bounds +/// it and fails cleanly instead of letting a bad value take down the process. +pub const MAX_MEMORY_KIB: u32 = 1 << 20; + +/// Smallest salt accepted, per RFC 9106's recommendation for password hashing. +pub const MIN_SALT_LEN: usize = 8; + +/// Largest salt accepted. +pub const MAX_SALT_LEN: usize = 1024; + +/// Smallest tag (output) length, from RFC 9106. +pub const MIN_TAG_LEN: usize = 4; + +/// Largest tag (output) length accepted. +pub const MAX_TAG_LEN: usize = 1024; + +/// Ceiling on the pass count (RFC 9106 allows up to 2^32-1; nothing sane +/// needs more than this). +pub const MAX_PASSES: u32 = 1024; + +/// Ceiling on parallelism. +pub const MAX_LANES: u32 = 64; + +/// A memory block: 1024 bytes as 128 little-endian words. +const BLOCK_WORDS: usize = 128; +type Block = [u64; BLOCK_WORDS]; +const ZERO_BLOCK: Block = [0u64; BLOCK_WORDS]; + +/// Each lane is filled in four slices, with a synchronisation point between +/// slices (RFC 9106 section 3.4). +const SYNC_POINTS: usize = 4; + +const VERSION: u64 = 0x13; + +// argon2d and argon2i are never built outside of tests: they exist so the +// official vectors can pin the indexing logic on every addressing path, +// while only argon2id is exposed for real use +#[allow(dead_code)] +#[derive(Clone, Copy, PartialEq)] +enum Variant { + Argon2d = 0, + Argon2i = 1, + Argon2id = 2, +} + +/// Compute an Argon2id tag. `passes` is the time cost, `memory_kib` the +/// memory cost in KiB, `lanes` the parallelism degree. `secret` and `ad` are +/// the optional keyed-secret and associated-data inputs; pass empty slices +/// when unused. +#[allow(clippy::too_many_arguments)] +pub fn argon2id( + password: &[u8], + salt: &[u8], + secret: &[u8], + ad: &[u8], + passes: u32, + memory_kib: u32, + lanes: u32, + tag_len: usize, +) -> Result, &'static str> { + argon2( + Variant::Argon2id, + password, + salt, + secret, + ad, + passes, + memory_kib, + lanes, + tag_len, + ) +} + +#[allow(clippy::too_many_arguments)] +fn argon2( + variant: Variant, + password: &[u8], + salt: &[u8], + secret: &[u8], + ad: &[u8], + passes: u32, + memory_kib: u32, + lanes: u32, + tag_len: usize, +) -> Result, &'static str> { + if !(MIN_TAG_LEN..=MAX_TAG_LEN).contains(&tag_len) { + return Err("argon2 tag length out of range"); + } + if !(MIN_SALT_LEN..=MAX_SALT_LEN).contains(&salt.len()) { + return Err("argon2 salt length out of range"); + } + if passes == 0 || passes > MAX_PASSES { + return Err("argon2 pass count out of range"); + } + if lanes == 0 || lanes > MAX_LANES { + return Err("argon2 parallelism out of range"); + } + if memory_kib > MAX_MEMORY_KIB { + return Err("argon2 memory cost above the runtime cap"); + } + if memory_kib < 8 * lanes { + return Err("argon2 memory cost below 8 blocks per lane"); + } + + let lanes = lanes as usize; + // round the block count down to a multiple of 4 * lanes (RFC section 3.2: + // m' = 4 * p * floor(m / 4p)) + let segment_len = memory_kib as usize / (SYNC_POINTS * lanes); + let lane_len = segment_len * SYNC_POINTS; + let block_count = lane_len * lanes; + + // this is the caller-sized allocation; fail cleanly instead of aborting + // on out-of-memory + let mut memory: Vec = Vec::new(); + if memory.try_reserve_exact(block_count).is_err() { + return Err("argon2 memory allocation failed"); + } + memory.resize(block_count, ZERO_BLOCK); + + let h0 = initial_hash( + variant, password, salt, secret, ad, passes, memory_kib, lanes, tag_len, + )?; + + let mut instance = Instance { + memory, + lanes, + lane_len, + segment_len, + passes, + variant, + }; + instance.fill_first_blocks(&h0)?; + for pass in 0..passes as usize { + for slice in 0..SYNC_POINTS { + for lane in 0..lanes { + instance.fill_segment(pass, lane, slice); + } + } + } + let tag = instance.extract_tag(tag_len); + // best-effort scrubbing of password-derived state + instance.memory.fill(ZERO_BLOCK); + tag +} + +/// The initial 64-byte digest H_0 (RFC 9106 section 3.2). +#[allow(clippy::too_many_arguments)] +fn initial_hash( + variant: Variant, + password: &[u8], + salt: &[u8], + secret: &[u8], + ad: &[u8], + passes: u32, + memory_kib: u32, + lanes: usize, + tag_len: usize, +) -> Result<[u8; 64], &'static str> { + if password.len() > u32::MAX as usize + || secret.len() > u32::MAX as usize + || ad.len() > u32::MAX as usize + { + return Err("argon2 input too long"); + } + let mut input = Vec::with_capacity(40 + password.len() + salt.len() + secret.len() + ad.len()); + for fixed in [ + lanes as u32, + tag_len as u32, + memory_kib, + passes, + VERSION as u32, + variant as u32, + ] { + input.extend_from_slice(&fixed.to_le_bytes()); + } + for var in [password, salt, secret, ad] { + input.extend_from_slice(&(var.len() as u32).to_le_bytes()); + input.extend_from_slice(var); + } + let Some(digest) = blake2b::hash(64, b"", &input) else { + return Err("argon2 initial hash failed"); + }; + input.fill(0); + let mut h0 = [0u8; 64]; + h0.copy_from_slice(&digest); + Ok(h0) +} + +/// The variable-length hash H' (RFC 9106 section 3.3): plain BLAKE2b up to +/// 64 bytes, and a 32-byte chain of BLAKE2b digests beyond that. +fn h_prime(out: &mut [u8], input: &[u8]) -> Result<(), &'static str> { + let total = out.len(); + let mut prefixed = Vec::with_capacity(4 + input.len()); + prefixed.extend_from_slice(&(total as u32).to_le_bytes()); + prefixed.extend_from_slice(input); + if total <= 64 { + let Some(digest) = blake2b::hash(total, b"", &prefixed) else { + return Err("argon2 h' failed"); + }; + out.copy_from_slice(&digest); + return Ok(()); + } + let Some(mut v) = blake2b::hash(64, b"", &prefixed) else { + return Err("argon2 h' failed"); + }; + let mut pos = 0; + let mut remaining = total; + while remaining > 64 { + out[pos..pos + 32].copy_from_slice(&v[..32]); + pos += 32; + remaining -= 32; + let next_len = remaining.min(64); + let Some(next) = blake2b::hash(next_len, b"", &v) else { + return Err("argon2 h' failed"); + }; + v = next; + } + out[pos..].copy_from_slice(&v); + Ok(()) +} + +struct Instance { + memory: Vec, + lanes: usize, + lane_len: usize, + segment_len: usize, + passes: u32, + variant: Variant, +} + +impl Instance { + /// B[lane][0] and B[lane][1] seed each lane from H_0 (RFC section 3.2). + fn fill_first_blocks(&mut self, h0: &[u8; 64]) -> Result<(), &'static str> { + for lane in 0..self.lanes { + for col in 0..2usize { + let mut seed = [0u8; 72]; + seed[..64].copy_from_slice(h0); + seed[64..68].copy_from_slice(&(col as u32).to_le_bytes()); + seed[68..72].copy_from_slice(&(lane as u32).to_le_bytes()); + let mut block_bytes = [0u8; 1024]; + h_prime(&mut block_bytes, &seed)?; + self.memory[lane * self.lane_len + col] = block_from_le_bytes(&block_bytes); + } + } + Ok(()) + } + + /// Fill one segment of one lane (RFC section 3.4). Argon2id switches from + /// data-independent to data-dependent addressing after the first two + /// slices of the first pass. + fn fill_segment(&mut self, pass: usize, lane: usize, slice: usize) { + let data_independent = match self.variant { + Variant::Argon2d => false, + Variant::Argon2i => true, + Variant::Argon2id => pass == 0 && slice < 2, + }; + + // data-independent addressing draws J_1 || J_2 from a pseudo-random + // block regenerated every 128 references + let mut address_block = ZERO_BLOCK; + let mut input_block = ZERO_BLOCK; + if data_independent { + input_block[0] = pass as u64; + input_block[1] = lane as u64; + input_block[2] = slice as u64; + input_block[3] = self.memory.len() as u64; + input_block[4] = self.passes as u64; + input_block[5] = self.variant as u64; + // input_block[6] is the counter; next_addresses bumps it + } + + // the first two blocks of every lane are the seed blocks + let mut starting_index = 0; + if pass == 0 && slice == 0 { + starting_index = 2; + if data_independent { + next_addresses(&mut address_block, &mut input_block); + } + } + + for index in starting_index..self.segment_len { + let col = slice * self.segment_len + index; + let prev_col = if col == 0 { self.lane_len - 1 } else { col - 1 }; + let prev_idx = lane * self.lane_len + prev_col; + + let pseudo_rand = if data_independent { + if index % BLOCK_WORDS == 0 { + next_addresses(&mut address_block, &mut input_block); + } + address_block[index % BLOCK_WORDS] + } else { + self.memory[prev_idx][0] + }; + + let ref_lane = if pass == 0 && slice == 0 { + lane + } else { + ((pseudo_rand >> 32) as usize) % self.lanes + }; + let ref_index = self.index_alpha( + pass, + slice, + index, + pseudo_rand as u32, + ref_lane == lane, + ); + + let prev = self.memory[prev_idx]; + let reference = self.memory[ref_lane * self.lane_len + ref_index]; + let with_xor = pass > 0; + fill_block( + &prev, + &reference, + &mut self.memory[lane * self.lane_len + col], + with_xor, + ); + } + } + + /// Map J_1 to a reference block index (RFC section 3.4.1.2): compute the + /// size of the reachable window, then pick a position skewed towards the + /// window's recent end via the x^2 mapping. + fn index_alpha(&self, pass: usize, slice: usize, index: usize, j1: u32, same_lane: bool) -> usize { + let reference_area = if pass == 0 { + if slice == 0 { + // only this lane's earlier blocks in this segment (index is + // at least 2 here, so this never underflows) + index - 1 + } else if same_lane { + slice * self.segment_len + index - 1 + } else { + slice * self.segment_len - if index == 0 { 1 } else { 0 } + } + } else if same_lane { + self.lane_len - self.segment_len + index - 1 + } else { + self.lane_len - self.segment_len - if index == 0 { 1 } else { 0 } + }; + + let area = reference_area as u64; + let x = (j1 as u64 * j1 as u64) >> 32; + let relative = area - 1 - ((area * x) >> 32); + + // after the first pass the window starts just past the current slice + // and wraps around the lane + let start = if pass == 0 || slice == SYNC_POINTS - 1 { + 0 + } else { + (slice + 1) * self.segment_len + }; + (start + relative as usize) % self.lane_len + } + + /// XOR the last column together and hash it into the tag (RFC 3.2 step 7). + fn extract_tag(&self, tag_len: usize) -> Result, &'static str> { + let mut last = self.memory[self.lane_len - 1]; + for lane in 1..self.lanes { + let block = &self.memory[lane * self.lane_len + self.lane_len - 1]; + for (acc, word) in last.iter_mut().zip(block.iter()) { + *acc ^= word; + } + } + let mut block_bytes = [0u8; 1024]; + for (i, word) in last.iter().enumerate() { + block_bytes[i * 8..i * 8 + 8].copy_from_slice(&word.to_le_bytes()); + } + let mut tag = vec![0u8; tag_len]; + h_prime(&mut tag, &block_bytes)?; + Ok(tag) + } +} + +fn block_from_le_bytes(bytes: &[u8; 1024]) -> Block { + let mut block = ZERO_BLOCK; + for (i, word) in block.iter_mut().enumerate() { + let mut chunk = [0u8; 8]; + chunk.copy_from_slice(&bytes[i * 8..i * 8 + 8]); + *word = u64::from_le_bytes(chunk); + } + block +} + +/// Generate the next block of data-independent reference addresses: +/// addresses = G(zero, G(zero, input)) with the counter bumped first. +fn next_addresses(address: &mut Block, input: &mut Block) { + input[6] += 1; + let input_copy = *input; + fill_block(&ZERO_BLOCK, &input_copy, address, false); + let address_copy = *address; + fill_block(&ZERO_BLOCK, &address_copy, address, false); +} + +/// The compression function G (RFC section 3.5): XOR the inputs, run the +/// BLAKE2b-style permutation over rows then columns, and XOR the result back. +fn fill_block(prev: &Block, reference: &Block, next: &mut Block, with_xor: bool) { + let mut r = ZERO_BLOCK; + for (i, word) in r.iter_mut().enumerate() { + *word = prev[i] ^ reference[i]; + } + let mut z = r; + + // rows: 8 rows of 16 consecutive words + for row in 0..8 { + permute(&mut z, |i| row * 16 + i); + } + // columns: the block is an 8x8 grid of 16-byte cells; column `col` + // gathers the two words of each cell in that column + for col in 0..8 { + permute(&mut z, |i| (i / 2) * 16 + col * 2 + i % 2); + } + + for (i, word) in z.iter().enumerate() { + let value = r[i] ^ word; + if with_xor { + next[i] ^= value; + } else { + next[i] = value; + } + } +} + +/// The permutation P over 16 words selected by `idx`, mirroring a BLAKE2b +/// round with G_B replaced by the multiply-hardened G (RFC section 3.5). +fn permute(z: &mut Block, idx: impl Fn(usize) -> usize) { + let mut v = [0u64; 16]; + for (i, word) in v.iter_mut().enumerate() { + *word = z[idx(i)]; + } + for (a, b, c, d) in [ + (0, 4, 8, 12), + (1, 5, 9, 13), + (2, 6, 10, 14), + (3, 7, 11, 15), + (0, 5, 10, 15), + (1, 6, 11, 12), + (2, 7, 8, 13), + (3, 4, 9, 14), + ] { + quarter_round(&mut v, a, b, c, d); + } + for (i, word) in v.iter().enumerate() { + z[idx(i)] = *word; + } +} + +fn quarter_round(v: &mut [u64; 16], a: usize, b: usize, c: usize, d: usize) { + v[a] = mul_add(v[a], v[b]); + v[d] = (v[d] ^ v[a]).rotate_right(32); + v[c] = mul_add(v[c], v[d]); + v[b] = (v[b] ^ v[c]).rotate_right(24); + v[a] = mul_add(v[a], v[b]); + v[d] = (v[d] ^ v[a]).rotate_right(16); + v[c] = mul_add(v[c], v[d]); + v[b] = (v[b] ^ v[c]).rotate_right(63); +} + +/// a + b + 2 * lower32(a) * lower32(b), the multiplication that makes the +/// permutation compute-hard on 64-bit cores. +#[inline(always)] +fn mul_add(a: u64, b: u64) -> u64 { + let product = (a as u32 as u64).wrapping_mul(b as u32 as u64); + a.wrapping_add(b).wrapping_add(product.wrapping_add(product)) +} + +#[cfg(test)] +mod tests { + use super::{argon2, argon2id, Variant}; + + fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{:02x}", b)).collect() + } + + // the three test vectors from RFC 9106 section 5 share these inputs: + // m=32 KiB, t=3, p=4, 32-byte tag, and fixed password/salt/secret/ad + fn rfc_vector(variant: Variant) -> Result { + let password = [0x01u8; 32]; + let salt = [0x02u8; 16]; + let secret = [0x03u8; 8]; + let ad = [0x04u8; 12]; + argon2(variant, &password, &salt, &secret, &ad, 3, 32, 4, 32).map(|tag| hex(&tag)) + } + + #[test] + fn rfc_9106_argon2d_vector() { + assert_eq!( + rfc_vector(Variant::Argon2d), + Ok("512b391b6f1162975371d30919734294f868e3be3984f3c1a13a4db9fabe4acb".to_string()) + ); + } + + #[test] + fn rfc_9106_argon2i_vector() { + assert_eq!( + rfc_vector(Variant::Argon2i), + Ok("c814d9d1dc7f37aa13f0d77f2494bda1c8de6b016dd388d29952a4c4672b6ce8".to_string()) + ); + } + + #[test] + fn rfc_9106_argon2id_vector() { + assert_eq!( + rfc_vector(Variant::Argon2id), + Ok("0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659".to_string()) + ); + } + + #[test] + fn deterministic_and_salt_sensitive() { + let a = argon2id(b"password", b"somesalt", b"", b"", 1, 8, 1, 32); + let b = argon2id(b"password", b"somesalt", b"", b"", 1, 8, 1, 32); + let c = argon2id(b"password", b"othersalt", b"", b"", 1, 8, 1, 32); + assert!(a.is_ok()); + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn parameters_change_the_tag() { + let base = argon2id(b"password", b"somesalt", b"", b"", 2, 64, 1, 32); + assert_ne!(base, argon2id(b"password", b"somesalt", b"", b"", 3, 64, 1, 32)); + assert_ne!(base, argon2id(b"password", b"somesalt", b"", b"", 2, 128, 1, 32)); + assert_ne!(base, argon2id(b"password", b"somesalt", b"", b"", 2, 64, 2, 32)); + } + + #[test] + fn rejects_out_of_range_parameters() { + let salt = b"somesalt"; + // salt too short + assert!(argon2id(b"pw", b"short", b"", b"", 1, 8, 1, 32).is_err()); + // zero passes and zero lanes + assert!(argon2id(b"pw", salt, b"", b"", 0, 8, 1, 32).is_err()); + assert!(argon2id(b"pw", salt, b"", b"", 1, 8, 0, 32).is_err()); + // memory below 8 blocks per lane, and above the runtime cap + assert!(argon2id(b"pw", salt, b"", b"", 1, 7, 1, 32).is_err()); + assert!(argon2id(b"pw", salt, b"", b"", 1, 15, 2, 32).is_err()); + assert!(argon2id(b"pw", salt, b"", b"", 1, super::MAX_MEMORY_KIB + 1, 1, 32).is_err()); + // tag length outside 4..=1024 + assert!(argon2id(b"pw", salt, b"", b"", 1, 8, 1, 3).is_err()); + assert!(argon2id(b"pw", salt, b"", b"", 1, 8, 1, 1025).is_err()); + // the smallest legal configuration works + assert!(argon2id(b"pw", salt, b"", b"", 1, 8, 1, 4).is_ok()); + } + + #[test] + fn long_tags_use_the_hash_chain() { + // tags over 64 bytes exercise the iterative H' construction + let long = argon2id(b"password", b"somesalt", b"", b"", 1, 8, 1, 100); + assert!(matches!(&long, Ok(tag) if tag.len() == 100)); + let longer = argon2id(b"password", b"somesalt", b"", b"", 1, 8, 1, 128); + assert!(matches!(&longer, Ok(tag) if tag.len() == 128)); + // the shorter tag is not a prefix of the longer one + if let (Ok(long), Ok(longer)) = (long, longer) { + assert_ne!(long[..], longer[..100]); + } + } + + // not run by default: prints wall-clock timings for candidate password + // hashing defaults. run with: + // cargo test --release -p pith-runtime -- --ignored timing --nocapture + #[test] + #[ignore] + fn timing_for_candidate_defaults() { + let candidates = [ + ("owasp m=19 MiB t=2 p=1", 2u32, 19 * 1024, 1u32), + ("rfc second choice m=64 MiB t=3 p=4", 3, 64 * 1024, 4), + ("m=32 MiB t=3 p=1", 3, 32 * 1024, 1), + ]; + for (label, passes, memory_kib, lanes) in candidates { + let start = std::time::Instant::now(); + let result = argon2id(b"password", b"somesalt", b"", b"", passes, memory_kib, lanes, 32); + let elapsed = start.elapsed(); + assert!(result.is_ok()); + println!("{}: {:?}", label, elapsed); + } + } +} diff --git a/cranelift/runtime/src/blake2b.rs b/cranelift/runtime/src/blake2b.rs new file mode 100644 index 00000000..253ca47d --- /dev/null +++ b/cranelift/runtime/src/blake2b.rs @@ -0,0 +1,344 @@ +//! BLAKE2b (RFC 7693), keyed and unkeyed, with digests up to 64 bytes. +//! +//! Hand-written rather than pulled in as a crate: the runtime keeps its +//! dependency set small on purpose, and BLAKE2b is compact and fully +//! specified. Argon2 (see `argon2.rs`) builds on this module for both its +//! fixed-length hash H and its variable-length hash H'. + +/// Largest digest BLAKE2b can produce, in bytes. +pub const MAX_OUT_LEN: usize = 64; + +/// Largest key BLAKE2b accepts, in bytes. +pub const MAX_KEY_LEN: usize = 64; + +const BLOCK_LEN: usize = 128; + +/// Initialization vector from RFC 7693 section 2.6 (the same constants as +/// SHA-512's IV). +const IV: [u64; 8] = [ + 0x6a09e667f3bcc908, + 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, + 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, + 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, + 0x5be0cd19137e2179, +]; + +/// Message schedule from RFC 7693 section 2.7. BLAKE2b runs 12 rounds; rounds +/// 10 and 11 reuse the first two permutations. +const SIGMA: [[usize; 16]; 12] = [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], + [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], + [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8], + [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], + [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9], + [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], + [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10], + [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], + [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], +]; + +/// Incremental BLAKE2b state. Feed input with [`Blake2b::update`] and close +/// with [`Blake2b::finalize`]. +pub struct Blake2b { + h: [u64; 8], + /// Total bytes compressed so far (the RFC's 128-bit counter; a u128 keeps + /// the carry into the high word automatic). + t: u128, + buf: [u8; BLOCK_LEN], + buf_len: usize, + out_len: usize, +} + +impl Blake2b { + /// Start a hash producing `out_len` bytes, keyed when `key` is non-empty. + /// Returns `None` when `out_len` is 0 or over 64, or the key is over 64 + /// bytes. + pub fn new(out_len: usize, key: &[u8]) -> Option { + if out_len == 0 || out_len > MAX_OUT_LEN || key.len() > MAX_KEY_LEN { + return None; + } + let mut h = IV; + // parameter block word 0: digest length, key length, fanout 1, depth 1 + h[0] ^= 0x0101_0000 ^ ((key.len() as u64) << 8) ^ out_len as u64; + let mut state = Blake2b { + h, + t: 0, + buf: [0; BLOCK_LEN], + buf_len: 0, + out_len, + }; + // a key is hashed as a full padded first block before any input + if !key.is_empty() { + state.buf[..key.len()].copy_from_slice(key); + state.buf_len = BLOCK_LEN; + } + Some(state) + } + + /// Absorb `input` into the hash. + pub fn update(&mut self, mut input: &[u8]) { + while !input.is_empty() { + // only flush a full buffer once more input exists; the final + // block must stay buffered so finalize can flag it as last + if self.buf_len == BLOCK_LEN { + self.t += BLOCK_LEN as u128; + let block = self.buf; + self.compress(&block, false); + self.buf_len = 0; + } + let take = (BLOCK_LEN - self.buf_len).min(input.len()); + self.buf[self.buf_len..self.buf_len + take].copy_from_slice(&input[..take]); + self.buf_len += take; + input = &input[take..]; + } + } + + /// Finish the hash and return the digest. + pub fn finalize(mut self) -> Vec { + self.t += self.buf_len as u128; + self.buf[self.buf_len..].fill(0); + let block = self.buf; + self.compress(&block, true); + let mut out = vec![0u8; self.out_len]; + for (i, chunk) in out.chunks_mut(8).enumerate() { + chunk.copy_from_slice(&self.h[i].to_le_bytes()[..chunk.len()]); + } + out + } + + /// The compression function F from RFC 7693 section 3.2. + fn compress(&mut self, block: &[u8; BLOCK_LEN], last: bool) { + let mut m = [0u64; 16]; + for (i, word) in m.iter_mut().enumerate() { + let mut bytes = [0u8; 8]; + bytes.copy_from_slice(&block[i * 8..i * 8 + 8]); + *word = u64::from_le_bytes(bytes); + } + + let mut v = [0u64; 16]; + v[..8].copy_from_slice(&self.h); + v[8..].copy_from_slice(&IV); + v[12] ^= self.t as u64; + v[13] ^= (self.t >> 64) as u64; + if last { + v[14] = !v[14]; + } + + for sigma in &SIGMA { + // columns + mix(&mut v, 0, 4, 8, 12, m[sigma[0]], m[sigma[1]]); + mix(&mut v, 1, 5, 9, 13, m[sigma[2]], m[sigma[3]]); + mix(&mut v, 2, 6, 10, 14, m[sigma[4]], m[sigma[5]]); + mix(&mut v, 3, 7, 11, 15, m[sigma[6]], m[sigma[7]]); + // diagonals + mix(&mut v, 0, 5, 10, 15, m[sigma[8]], m[sigma[9]]); + mix(&mut v, 1, 6, 11, 12, m[sigma[10]], m[sigma[11]]); + mix(&mut v, 2, 7, 8, 13, m[sigma[12]], m[sigma[13]]); + mix(&mut v, 3, 4, 9, 14, m[sigma[14]], m[sigma[15]]); + } + + for i in 0..8 { + self.h[i] ^= v[i] ^ v[i + 8]; + } + } +} + +/// The mixing function G from RFC 7693 section 3.1. +#[inline(always)] +fn mix(v: &mut [u64; 16], a: usize, b: usize, c: usize, d: usize, x: u64, y: u64) { + v[a] = v[a].wrapping_add(v[b]).wrapping_add(x); + v[d] = (v[d] ^ v[a]).rotate_right(32); + v[c] = v[c].wrapping_add(v[d]); + v[b] = (v[b] ^ v[c]).rotate_right(24); + v[a] = v[a].wrapping_add(v[b]).wrapping_add(y); + v[d] = (v[d] ^ v[a]).rotate_right(16); + v[c] = v[c].wrapping_add(v[d]); + v[b] = (v[b] ^ v[c]).rotate_right(63); +} + +/// One-shot BLAKE2b: hash `data` (keyed when `key` is non-empty) into an +/// `out_len`-byte digest. Returns `None` on an out-of-range digest or key +/// length. +pub fn hash(out_len: usize, key: &[u8], data: &[u8]) -> Option> { + let mut state = Blake2b::new(out_len, key)?; + state.update(data); + Some(state.finalize()) +} + +#[cfg(test)] +mod tests { + use super::hash; + + fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{:02x}", b)).collect() + } + + fn hash_hex(out_len: usize, key: &[u8], data: &[u8]) -> Option { + hash(out_len, key, data).map(|digest| hex(&digest)) + } + + #[test] + fn rfc_7693_appendix_a_abc() { + assert_eq!( + hash_hex(64, b"", b"abc").as_deref(), + Some( + "ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d1\ + 7d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923" + ) + ); + } + + #[test] + fn unkeyed_empty_input() { + assert_eq!( + hash_hex(64, b"", b"").as_deref(), + Some( + "786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419\ + d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce" + ) + ); + } + + #[test] + fn truncated_digest_lengths() { + let fox: &[u8] = b"The quick brown fox jumps over the lazy dog"; + assert_eq!( + hash_hex(20, b"", fox).as_deref(), + Some("3c523ed102ab45a37d54f5610d5a983162fde84f") + ); + assert_eq!( + hash_hex(32, b"", fox).as_deref(), + Some("01718cec35cd3d796dd00020e0bfecb473ad23457d063b75eff29c0ffa2e58a9") + ); + assert_eq!( + hash_hex(48, b"", fox).as_deref(), + Some( + "b7c81b228b6bd912930e8f0b5387989691c1cee1e65aade4da3b86a3\ + c9f678fc8018f6ed9e2906720c8d2a3aeda9c03d" + ) + ); + } + + // the official blake2b-kat vectors hash the byte sequence 00 01 02 ... + // with the 64-byte key 00 01 02 ... 3f + fn kat_input(len: usize) -> Vec { + (0..len).map(|i| i as u8).collect() + } + + fn kat_key() -> Vec { + kat_input(64) + } + + #[test] + fn official_kat_keyed_empty() { + assert_eq!( + hash_hex(64, &kat_key(), b"").as_deref(), + Some( + "10ebb67700b1868efb4417987acf4690ae9d972fb7a590c2f02871799aaa4786\ + b5e996e8f0f4eb981fc214b005f42d2ff4233499391653df7aefcbc13fc51568" + ) + ); + } + + #[test] + fn official_kat_keyed_one_byte() { + assert_eq!( + hash_hex(64, &kat_key(), &kat_input(1)).as_deref(), + Some( + "961f6dd1e4dd30f63901690c512e78e4b45e4742ed197c3c5e45c549fd25f2e4\ + 187b0bc9fe30492b16b0d0bc4ef9b0f34c7003fac09a5ef1532e69430234cebd" + ) + ); + } + + #[test] + fn official_kat_keyed_block_boundaries() { + assert_eq!( + hash_hex(64, &kat_key(), &kat_input(64)).as_deref(), + Some( + "65676d800617972fbd87e4b9514e1c67402b7a331096d3bfac22f1abb95374ab\ + c942f16e9ab0ead33b87c91968a6e509e119ff07787b3ef483e1dcdccf6e3022" + ) + ); + assert_eq!( + hash_hex(64, &kat_key(), &kat_input(128)).as_deref(), + Some( + "72065ee4dd91c2d8509fa1fc28a37c7fc9fa7d5b3f8ad3d0d7a25626b57b1b44\ + 788d4caf806290425f9890a3a2a35a905ab4b37acfd0da6e4517b2525c9651e4" + ) + ); + assert_eq!( + hash_hex(64, &kat_key(), &kat_input(129)).as_deref(), + Some( + "64475dfe7600d7171bea0b394e27c9b00d8e74dd1e416a79473682ad3dfdbb70\ + 6631558055cfc8a40e07bd015a4540dcdea15883cbbf31412df1de1cd4152b91" + ) + ); + } + + #[test] + fn official_kat_keyed_255_bytes() { + assert_eq!( + hash_hex(64, &kat_key(), &kat_input(255)).as_deref(), + Some( + "142709d62e28fcccd0af97fad0f8465b971e82201dc51070faa0372aa43e9248\ + 4be1c1e73ba10906d5d1853db6a4106e0a7bf9800d373d6dee2d46d62ef2a461" + ) + ); + // keyed and truncated at the same time + assert_eq!( + hash_hex(32, &kat_key(), &kat_input(255)).as_deref(), + Some("fe7b76a61787c089141f9e10fca1e5092488d89c62ea793fb2c5b1f849b4f2cb") + ); + } + + #[test] + fn unkeyed_block_boundaries() { + assert_eq!( + hash_hex(64, b"", &kat_input(128)).as_deref(), + Some( + "2319e3789c47e2daa5fe807f61bec2a1a6537fa03f19ff32e87eecbfd64b7e0e\ + 8ccff439ac333b040f19b0c4ddd11a61e24ac1fe0f10a039806c5dcc0da3d115" + ) + ); + assert_eq!( + hash_hex(64, b"", &kat_input(129)).as_deref(), + Some( + "f59711d44a031d5f97a9413c065d1e614c417ede998590325f49bad2fd444d3e\ + 4418be19aec4e11449ac1a57207898bc57d76a1bcf3566292c20c683a5c4648f" + ) + ); + } + + #[test] + fn incremental_matches_one_shot() { + let data = kat_input(300); + let Some(whole) = hash(64, b"", &data) else { + assert!(false, "one-shot hash failed"); + return; + }; + let Some(mut state) = super::Blake2b::new(64, b"") else { + assert!(false, "state construction failed"); + return; + }; + for chunk in data.chunks(7) { + state.update(chunk); + } + assert_eq!(state.finalize(), whole); + } + + #[test] + fn rejects_out_of_range_lengths() { + assert!(hash(0, b"", b"data").is_none()); + assert!(hash(65, b"", b"data").is_none()); + assert!(hash(64, &[0u8; 65], b"data").is_none()); + assert!(hash(64, &[0u8; 64], b"data").is_some()); + assert!(hash(1, b"", b"").is_some()); + } +} diff --git a/cranelift/runtime/src/crypto.rs b/cranelift/runtime/src/crypto.rs index ab6b43fe..ce77b9ec 100644 --- a/cranelift/runtime/src/crypto.rs +++ b/cranelift/runtime/src/crypto.rs @@ -395,6 +395,67 @@ pub unsafe extern "C" fn pith_crypto_sign_rsa_pss_sha256_pkcs8(pkcs8: i64, messa sign_rsa_with(&signature::RSA_PSS_SHA256, pkcs8, message) } +/// BLAKE2b digest of `data`, keyed when `key` is non-empty. `out_len` selects +/// the digest length (1 to 64 bytes). Returns a bytes handle, or 0 on invalid +/// input. +#[no_mangle] +pub unsafe extern "C" fn pith_crypto_blake2b(data: i64, key: i64, out_len: i64) -> i64 { + let Some(data) = bytes_slice(data) else { + return 0; + }; + let Some(key) = bytes_slice(key) else { + return 0; + }; + if out_len < 1 || out_len > crate::blake2b::MAX_OUT_LEN as i64 { + return 0; + } + match crate::blake2b::hash(out_len as usize, key, data) { + Some(digest) => pith_bytes_from_vec(digest), + None => 0, + } +} + +/// Argon2id tag for `password` and `salt` with the given time cost (passes), +/// memory cost (KiB), and parallelism. Returns a bytes handle, or 0 when a +/// parameter is out of range (including the runtime's memory-cost cap). +#[no_mangle] +pub unsafe extern "C" fn pith_crypto_argon2id( + password: i64, + salt: i64, + passes: i64, + memory_kib: i64, + lanes: i64, + out_len: i64, +) -> i64 { + let Some(password) = bytes_slice(password) else { + return 0; + }; + let Some(salt) = bytes_slice(salt) else { + return 0; + }; + // the argon2 module re-validates ranges; these checks only make the + // i64 -> u32/usize conversions safe + if passes < 0 || memory_kib < 0 || lanes < 0 || out_len < 0 { + return 0; + } + if passes > u32::MAX as i64 || memory_kib > u32::MAX as i64 || lanes > u32::MAX as i64 { + return 0; + } + match crate::argon2::argon2id( + password, + salt, + b"", + b"", + passes as u32, + memory_kib as u32, + lanes as u32, + out_len as usize, + ) { + Ok(tag) => pith_bytes_from_vec(tag), + Err(_) => 0, + } +} + #[no_mangle] pub extern "C" fn pith_os_cert_roots_pem() -> *mut i8 { let mut candidates = Vec::new(); diff --git a/cranelift/runtime/src/lib.rs b/cranelift/runtime/src/lib.rs index cb6f267b..cef5cdc7 100644 --- a/cranelift/runtime/src/lib.rs +++ b/cranelift/runtime/src/lib.rs @@ -11,6 +11,8 @@ #![allow(clippy::missing_safety_doc)] +pub mod argon2; +pub mod blake2b; pub mod blocking; pub mod bytes; pub mod collections;