diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 43b0a84c..1ba9ebec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,6 +53,11 @@ jobs: - name: format run: cargo fmt -- --check + # Clippy denies the panic lints in library code; it has none for + # `assert!` / `debug_assert!`, so this covers them. + - name: no runtime asserts + run: python3 scripts/no-runtime-asserts.py + # Broken or private intra-doc links fail here rather than on docs.rs. - name: docs env: diff --git a/docs/plans/2026-06-12-ore-v2-architecture.md b/docs/plans/2026-06-12-ore-v2-architecture.md index 07146ca4..096d039b 100644 --- a/docs/plans/2026-06-12-ore-v2-architecture.md +++ b/docs/plans/2026-06-12-ore-v2-architecture.md @@ -610,7 +610,9 @@ PR 2's trait change, which should be called out in the changelog). > with variable-length `Var*` types and string encryption. Lexicographic cross-length > comparison, equality-across-nonces, and >14-block plaintexts are tested; perf > `docs/benchmarks/2026-06-15-chained-results.md` (~0.69 µs/block, below fixed-N's ~0.81). -> Not yet in: `u128`/`Decimal` via the const-N + accumulator path. Wire vectors are +> `u128`/`i128` landed 2026-10 as `OreEncrypt` impls on the chained scheme with +> block-count-typed outputs (`ChainedCipherText<22>`), not a separate const-N scheme +> (issue #105). `Decimal` can follow the same way via `encrypt_fixed`. Wire vectors are > pinned (`tests/compat_chained_vectors.rs`). ## Testing strategy diff --git a/packages/orderable-bytes/src/decimal.rs b/packages/orderable-bytes/src/decimal.rs index d523256a..36d1465e 100644 --- a/packages/orderable-bytes/src/decimal.rs +++ b/packages/orderable-bytes/src/decimal.rs @@ -138,13 +138,8 @@ impl_fixed_orderable_bytes! { // `[-29, 28]` covers both branches without leaking the zero/non-zero // distinction in debug builds either. let leading_exp = digits as i32 - 1 + trailing - scale; - debug_assert!( - (-29..=28).contains(&leading_exp), - "leading_exp {} out of bounds — mantissa or scale corrupted", - leading_exp, - ); + // `leading_exp + EXP_BIAS` is in `35..=92`, so it fits the 7-bit field. let biased_exp = (leading_exp + EXP_BIAS) as u8; - debug_assert!(biased_exp <= EXP_MASK, "biased_exp overflowed 7 bits"); // Pad the significand out to 29 decimal digits so same-exponent compares // across different significand lengths are byte-wise correct. @@ -174,11 +169,9 @@ impl_fixed_orderable_bytes! { let stepped = padded_mantissa.wrapping_mul(10); padded_mantissa = (padded_mantissa & !mask) | (stepped & mask); } + // `padded_mantissa ≤ 10^29 < 2^97` (above), so the bytes dropped here + // are zero. let mant_be = padded_mantissa.to_be_bytes(); - debug_assert!( - mant_be[..16 - MANTISSA_BYTES].iter().all(|&b| b == 0), - "padded mantissa overflowed 104 bits", - ); let mant_field = &mant_be[16 - MANTISSA_BYTES..]; // Sign-class handling is folded into a single branchless mask so the diff --git a/packages/orderable-bytes/src/lib.rs b/packages/orderable-bytes/src/lib.rs index d6bb3995..0d459dcd 100644 --- a/packages/orderable-bytes/src/lib.rs +++ b/packages/orderable-bytes/src/lib.rs @@ -1,4 +1,19 @@ #![deny(missing_docs)] +// Library code does not panic: it returns an error or carries the invariant +// in a type. Tests may. Clippy has no lint for `assert!`; +// `scripts/no-runtime-asserts.py` covers that in CI. +#![cfg_attr( + not(test), + deny( + clippy::panic, + clippy::unwrap_used, + clippy::expect_used, + clippy::unreachable, + clippy::todo, + clippy::unimplemented, + clippy::panic_in_result_fn + ) +)] //! Canonical, order-preserving byte encodings for plaintext types. //! //! Each supported type implements [`OrderableBytes`], which maps a diff --git a/packages/ore-rs/CHANGELOG.md b/packages/ore-rs/CHANGELOG.md index 5900168c..f8ac46a9 100644 --- a/packages/ore-rs/CHANGELOG.md +++ b/packages/ore-rs/CHANGELOG.md @@ -13,7 +13,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 bytes, `DEFAULT_MAX_STR_BYTES`). A longer string returns `OreError::TooLong`. Ciphertext size and encryption time grow with the padded length and the string is caller-supplied, so a ceiling bounds what one input can cost; it also bounds the number of length buckets - a column can show. `init_with_padding` is replaced by `init_with`. + the ciphertexts can show. `init_with_padding` is replaced by `init_with`. +- `u128` and `i128` can be encrypted, on the chained scheme (`scheme::chained`): `OreEncrypt` + impls producing `ChainedCipherText<22>` / `ChainedLeft<22>`, whose typed parsers and + comparators accept only that block count, plus `encrypt_fixed` / `encrypt_left_fixed` for + any `FixedOrderableBytes` type and `VarLeft::from_slice`. Wire vectors are pinned in + `tests/compat_chained_vectors.rs` (#105). +- `scheme::bit2::MAX_BLOCKS` (15): the most bytes the legacy scheme takes. + +### Changed + +- `OreEncrypt` no longer requires `T: OreCipher`, so the chained scheme (which is not an + `OreCipher`) can implement it. Existing impls and callers are unaffected. +- Every `OreEncrypt` impl macro now emits a round-trip and ordering test for the type it + implements, so a type can't be declared supported without being exercised (#105). +- Library code no longer panics through `assert!`, `debug_assert!`, `unwrap` or `expect`. + Length contracts between internal buffers are carried by array types, so a mismatch is a + compile error, and clippy's panic lints are denied outside tests (CI also rejects runtime + assertions, which clippy cannot lint). **Breaking:** `RightBlock32::set_bit` takes the bit + as a `u8`, which names exactly its 256 bits. + +### Removed + +- **Breaking:** `OreEncrypt` for `u128` and `i128`. Every call panicked (see + Fixed), and code that called `1u128.encrypt(&legacy_ore)` no longer compiles. Encrypt + 128-bit integers with the chained scheme (`scheme::chained`) instead (#105). + +### Fixed + +- `OreAes128` panicked with an index out of bounds on any 16-byte plaintext, which is what + the `u128`/`i128` impls on that scheme had always done (#105). Those impls are removed (see + Removed): the legacy scheme cannot take 16 bytes, and the impl macro now rejects such a type + at compile time. Encrypting a `PlainText` with `N > 15` directly returns + `OreError::TooManyBlocks` instead of panicking. +- `OreAes128Bit6` panicked on a `PlainText` with `N > 14` (`MAX_BLOCKS`) through + `OreCipher::encrypt` / `encrypt_left`; it now returns `OreError::TooManyBlocks`, as the legacy + scheme does. ## [0.9.0-rc.1] - 2026-10-12 diff --git a/packages/ore-rs/README.md b/packages/ore-rs/README.md index 11626c06..6ef1aeda 100644 --- a/packages/ore-rs/README.md +++ b/packages/ore-rs/README.md @@ -21,7 +21,7 @@ Reference documentation is on [docs.rs/ore-rs](https://docs.rs/ore-rs). ## Supported plaintext types -`OreEncrypt` is implemented for `bool`, all integer widths (`u8`–`u128`, `i8`–`i128`), `char`, `f32`, and `f64`. `u128` and `i128` cannot be encrypted yet: `OreAes128` takes at most 15 bytes (its left-tag input puts the block index at byte `N` of one AES block), so encrypting a 16-byte value panics, and `OreAes128Bit6` covers types up to 64 bits. Two optional features extend this via the sibling [`orderable-bytes`](https://crates.io/crates/orderable-bytes) crate: +`OreEncrypt` is implemented for `bool`, integers up to 64 bits (`u8`–`u64`, `i8`–`i64`), `char`, `f32`, and `f64` on both `OreAes128` and `OreAes128Bit6`. `u128` and `i128` are implemented on the chained scheme only (`OreAes128Bit6Chained`, `scheme::chained`): at 16 bytes they exceed both the 15-byte limit of `OreAes128` and the 14-block packed prefix of `OreAes128Bit6`, and the chained scheme's CMAC accumulator has no length cap. Their ciphertexts are `ChainedCipherText<22>` / `ChainedLeft<22>`, which pin the block count: the typed parsers and comparators refuse a ciphertext of another length. That is a length guard, not a type guard; a 16-byte string or an `i128` is 22 blocks too, and under one key equal encodings (`0u128`, `i128::MIN`) encrypt identically, so use one key per kind of value. Two optional features extend this via the sibling [`orderable-bytes`](https://crates.io/crates/orderable-bytes) crate: - `chrono` — ORE support for `chrono::NaiveDate` and `chrono::DateTime` - `decimal` — ORE support for `rust_decimal::Decimal` @@ -83,7 +83,7 @@ Beyond the order and the first differing block of a compared pair, an `OreAes128 * the number of leading zero bytes of an integer, which is its order of magnitude, as the same value in every ciphertext under a key; * other runs of zero bytes, such as the trailing zero mantissa bytes of a round float. -The wire format of `OreAes128` is frozen, so this cannot change for existing data. For new data, use `OreAes128Bit6` (`scheme::bit2_w6`, ORE v2), which binds the block index and the block count into its seeds. It covers types up to 64 bits; `Decimal` stays on `OreAes128` for now. `u128` and `i128` are supported by neither scheme (see [Supported plaintext types](#supported-plaintext-types)). See "Known leakage of the legacy scheme" in [the v2 plan](https://github.com/cipherstash/ore.rs/blob/main/docs/plans/2026-06-12-ore-v2-architecture.md). +The wire format of `OreAes128` is frozen, so this cannot change for existing data. For new data, use `OreAes128Bit6` (`scheme::bit2_w6`, ORE v2), which binds the block index and the block count into its seeds. It covers types up to 64 bits; `Decimal` stays on `OreAes128` for now. `u128` and `i128` use the chained scheme (see [Supported plaintext types](#supported-plaintext-types)). See "Known leakage of the legacy scheme" in [the v2 plan](https://github.com/cipherstash/ore.rs/blob/main/docs/plans/2026-06-12-ore-v2-architecture.md). To report a security issue, see [SECURITY.md](https://github.com/cipherstash/ore.rs/blob/main/SECURITY.md) or email security@cipherstash.com. diff --git a/packages/ore-rs/src/ciphertext.rs b/packages/ore-rs/src/ciphertext.rs index 19f53c66..bc5fc3a2 100644 --- a/packages/ore-rs/src/ciphertext.rs +++ b/packages/ore-rs/src/ciphertext.rs @@ -25,11 +25,10 @@ pub struct WireHeader { pub(crate) const WIRE_HEADER_LEN: usize = 4; impl WireHeader { - pub(crate) fn write(&self, num_blocks: usize, out: &mut Vec) { - debug_assert!(num_blocks <= u16::MAX as usize); + pub(crate) fn write(&self, num_blocks: u16, out: &mut Vec) { out.push(self.version); out.push(self.scheme_id); - out.extend_from_slice(&(num_blocks as u16).to_be_bytes()); + out.extend_from_slice(&num_blocks.to_be_bytes()); } /// Validate `data`'s header against `self` and an expected block count, @@ -47,6 +46,24 @@ impl WireHeader { } } +/// A const block count `N` as the header's `u16`. An `N` the header cannot +/// hold is a compile error, so the cast below never truncates. +struct WireCount; + +impl WireCount { + const FITS: () = assert!( + N <= u16::MAX as usize, + "WireCount: too many blocks for the wire header" + ); + + #[inline] + #[allow(clippy::let_unit_value)] + fn get() -> u16 { + let () = Self::FITS; + N as u16 + } +} + /// `(version, scheme_id, block_count)` as parsed from a wire header. pub(crate) type ParsedHeader = (u8, u8, usize); @@ -191,7 +208,7 @@ impl OreOutput for Left { fn to_bytes(&self) -> Vec { let mut vec = Vec::with_capacity(Self::size()); if let Some(header) = S::WIRE_HEADER { - header.write(N, &mut vec); + header.write(WireCount::::get(), &mut vec); } self.write_body(&mut vec); vec @@ -250,7 +267,7 @@ impl OreOutput for Right { fn to_bytes(&self) -> Vec { let mut vec = Vec::with_capacity(Self::size()); if let Some(header) = S::WIRE_HEADER { - header.write(N, &mut vec); + header.write(WireCount::::get(), &mut vec); } self.write_body(&mut vec); vec @@ -276,7 +293,7 @@ impl OreOutput for CipherText { fn to_bytes(&self) -> Vec { let mut vec = Vec::with_capacity(Self::size()); if let Some(header) = S::WIRE_HEADER { - header.write(N, &mut vec); + header.write(WireCount::::get(), &mut vec); } self.left.write_body(&mut vec); self.right.write_body(&mut vec); diff --git a/packages/ore-rs/src/encrypt.rs b/packages/ore-rs/src/encrypt.rs index 3354a033..e58d4860 100644 --- a/packages/ore-rs/src/encrypt.rs +++ b/packages/ore-rs/src/encrypt.rs @@ -1,12 +1,13 @@ use crate::ciphertext::*; -use crate::scheme::bit2::OreAes128; +use crate::scheme::bit2::{OreAes128, MAX_BLOCKS}; use crate::PlainText; use crate::{OreCipher, OreError}; use orderable_bytes::{FixedOrderableBytes, OrderableBytes}; use rand::{Rng, SeedableRng}; /// Type-directed entry point for encrypting plaintext values with a given -/// [`OreCipher`]. +/// cipher: an [`OreCipher`], or the chained scheme +/// [`crate::scheme::chained::OreAes128Bit6Chained`]. /// /// Each implementation knows how to canonicalise its target type into the /// fixed-size byte plaintext expected by the cipher. For primitives and @@ -16,7 +17,7 @@ use rand::{Rng, SeedableRng}; /// comparison. The associated output types pin the resulting ciphertext /// shape, with `LeftOutput` the query-only half and `FullOutput` the full /// ciphertext suitable for storage. -pub trait OreEncrypt { +pub trait OreEncrypt { /// Output type produced by [`encrypt_left`](Self::encrypt_left). type LeftOutput: OreOutput; /// Output type produced by [`encrypt`](Self::encrypt). @@ -45,8 +46,6 @@ const U32_LEN: usize = ::ENCODED_LEN; const I32_LEN: usize = ::ENCODED_LEN; const U64_LEN: usize = ::ENCODED_LEN; const I64_LEN: usize = ::ENCODED_LEN; -const U128_LEN: usize = ::ENCODED_LEN; -const I128_LEN: usize = ::ENCODED_LEN; const CHAR_LEN: usize = ::ENCODED_LEN; const F32_LEN: usize = ::ENCODED_LEN; const F64_LEN: usize = ::ENCODED_LEN; @@ -58,8 +57,17 @@ const F64_LEN: usize = ::ENCODED_LEN; // were previously blanket impls over `T: OreCipher`; scheme-specific // impls keep coherence simple now that block count != byte count for // some schemes. +// +// Each invocation also emits a test that the type round-trips, orders `$lo` +// below `$hi`, and compares equal across nonces, so a type cannot be listed +// as supported without being exercised (issue #105). macro_rules! impl_ore_encrypt_via_orderable_bytes { - ($type:ty, $len_const:ident) => { + ($type:ty, $len_const:ident, $tests:ident, $lo:expr, $hi:expr) => { + // The legacy left-tag input puts the block index at byte `N` of one + // AES block, so the scheme takes at most 15 bytes. `u128`/`i128` + // (16 bytes) must not get these impls: see `scheme::chained`. + const _: () = assert!($len_const <= MAX_BLOCKS); + impl OreEncrypt> for $type { type LeftOutput = Left, $len_const>; type FullOutput = CipherText, $len_const>; @@ -72,23 +80,65 @@ macro_rules! impl_ore_encrypt_via_orderable_bytes { cipher.encrypt(&self.to_orderable_bytes()) } } + + #[cfg(test)] + mod $tests { + use super::*; + use crate::scheme::bit2::OreAes128ChaCha20; + use std::cmp::Ordering; + + #[test] + fn round_trip_order_and_equality() { + let ore: OreAes128ChaCha20 = OreCipher::init(&[0x11; 16], &[0x22; 16]).unwrap(); + let (lo, hi): ($type, $type) = ($lo, $hi); + assert!(lo < hi); + + let lo_ct = lo.encrypt(&ore).unwrap(); + let hi_ct = hi.encrypt(&ore).unwrap(); + let lo_left = lo.encrypt_left(&ore).unwrap(); + let lo_ct2 = lo.encrypt(&ore).unwrap(); + + let lo_bytes = lo_ct.to_bytes(); + let hi_bytes = hi_ct.to_bytes(); + assert_eq!( + CipherText::::from_slice(&lo_bytes) + .unwrap() + .to_bytes(), + lo_bytes + ); + assert_eq!( + Left::::from_slice(&lo_left.to_bytes()) + .unwrap() + .to_bytes(), + lo_left.to_bytes() + ); + + let cmp = OreAes128ChaCha20::compare_raw_slices; + assert_eq!(cmp(&lo_bytes, &hi_bytes), Some(Ordering::Less)); + assert_eq!(cmp(&hi_bytes, &lo_bytes), Some(Ordering::Greater)); + assert_ne!(lo_bytes, lo_ct2.to_bytes()); + assert_eq!(cmp(&lo_bytes, &lo_ct2.to_bytes()), Some(Ordering::Equal)); + // The typed comparator, where the scheme has one. + assert_eq!(lo_ct.cmp(&hi_ct), Ordering::Less); + } + } }; } -impl_ore_encrypt_via_orderable_bytes!(bool, BOOL_LEN); -impl_ore_encrypt_via_orderable_bytes!(u8, U8_LEN); -impl_ore_encrypt_via_orderable_bytes!(i8, I8_LEN); -impl_ore_encrypt_via_orderable_bytes!(u16, U16_LEN); -impl_ore_encrypt_via_orderable_bytes!(i16, I16_LEN); -impl_ore_encrypt_via_orderable_bytes!(u32, U32_LEN); -impl_ore_encrypt_via_orderable_bytes!(i32, I32_LEN); -impl_ore_encrypt_via_orderable_bytes!(u64, U64_LEN); -impl_ore_encrypt_via_orderable_bytes!(i64, I64_LEN); -impl_ore_encrypt_via_orderable_bytes!(u128, U128_LEN); -impl_ore_encrypt_via_orderable_bytes!(i128, I128_LEN); -impl_ore_encrypt_via_orderable_bytes!(char, CHAR_LEN); -impl_ore_encrypt_via_orderable_bytes!(f32, F32_LEN); -impl_ore_encrypt_via_orderable_bytes!(f64, F64_LEN); +impl_ore_encrypt_via_orderable_bytes!(bool, BOOL_LEN, bool_tests, false, true); +impl_ore_encrypt_via_orderable_bytes!(u8, U8_LEN, u8_tests, 0u8, u8::MAX); +impl_ore_encrypt_via_orderable_bytes!(i8, I8_LEN, i8_tests, i8::MIN, i8::MAX); +impl_ore_encrypt_via_orderable_bytes!(u16, U16_LEN, u16_tests, 0u16, u16::MAX); +impl_ore_encrypt_via_orderable_bytes!(i16, I16_LEN, i16_tests, i16::MIN, i16::MAX); +impl_ore_encrypt_via_orderable_bytes!(u32, U32_LEN, u32_tests, 0u32, u32::MAX); +impl_ore_encrypt_via_orderable_bytes!(i32, I32_LEN, i32_tests, i32::MIN, i32::MAX); +impl_ore_encrypt_via_orderable_bytes!(u64, U64_LEN, u64_tests, 0u64, u64::MAX); +impl_ore_encrypt_via_orderable_bytes!(i64, I64_LEN, i64_tests, i64::MIN, i64::MAX); +// `u128`/`i128` (16 bytes) exceed this scheme's 15-byte limit and are +// provided by `scheme::chained` instead (issue #105). +impl_ore_encrypt_via_orderable_bytes!(char, CHAR_LEN, char_tests, '\0', char::MAX); +impl_ore_encrypt_via_orderable_bytes!(f32, F32_LEN, f32_tests, f32::MIN, f32::MAX); +impl_ore_encrypt_via_orderable_bytes!(f64, F64_LEN, f64_tests, f64::MIN, f64::MAX); impl OreEncrypt> for PlainText { type LeftOutput = Left, N>; diff --git a/packages/ore-rs/src/lib.rs b/packages/ore-rs/src/lib.rs index 28567270..bb560c5c 100644 --- a/packages/ore-rs/src/lib.rs +++ b/packages/ore-rs/src/lib.rs @@ -18,8 +18,10 @@ //! ## Example: Encrypt a number with ORE. //! //! To encrypt a number you need to initialize an [`OreCipher`] as well as `use` the [`OreEncrypt`] trait, -//! which comes with implementations for `bool`, all integer widths (`u8`–`u128`, `i8`–`i128`), `char`, +//! which comes with implementations for `bool`, integers up to 64 bits (`u8`–`u64`, `i8`–`i64`), `char`, //! `f32` and `f64` (plus `chrono` and `rust_decimal` types behind the `chrono`/`decimal` features). +//! `u128` and `i128` are too wide for the fixed-width schemes and are encrypted with the +//! chained scheme instead: see [`scheme::chained`]. //! //! To initialize the Cipher, you must decide on the scheme you want to use. There is only one ORE //! Scheme right now so that's easy but in the future more schemes will become available. @@ -134,6 +136,21 @@ //! ``` #![deny(missing_docs)] +// Library code does not panic: it returns an error or carries the invariant +// in a type. Tests may. Clippy has no lint for `assert!`; +// `scripts/no-runtime-asserts.py` covers that in CI. +#![cfg_attr( + not(test), + deny( + clippy::panic, + clippy::unwrap_used, + clippy::expect_used, + clippy::unreachable, + clippy::todo, + clippy::unimplemented, + clippy::panic_in_result_fn + ) +)] #[cfg(feature = "chrono")] mod chrono; @@ -158,14 +175,15 @@ pub mod ct_bench { /// the builder the schemes use (the target's oblivious builder). pub fn lemire_fy_prp_from_stream(stream: &[u8; 512]) -> u8 { LemireFyPrp::<64>::from_stream(stream) - .expect("512 bytes is a full stream") .permute(Symbol::from_low_bits(0)) .get() } - fn from_stream_with(stream: &[u8; 512], build: fn(&[u8], &mut [u8; 64], &mut [u8; 64])) -> u8 { + fn from_stream_with( + stream: &[u8; 512], + build: fn(&[u8; 512], &mut [u8; 64], &mut [u8; 64]), + ) -> u8 { LemireFyPrp::<64>::from_stream_with(stream, build) - .expect("512 bytes is a full stream") .permute(Symbol::from_low_bits(0)) .get() } diff --git a/packages/ore-rs/src/primitives.rs b/packages/ore-rs/src/primitives.rs index b45ffeb7..9ec6ba44 100644 --- a/packages/ore-rs/src/primitives.rs +++ b/packages/ore-rs/src/primitives.rs @@ -20,11 +20,26 @@ pub trait Prf { pub trait Hash { fn new(key: &HashKey) -> Self; - fn hash(&self, data: &[u8]) -> u8; + fn hash(&self, data: &[u8; 16]) -> u8; /// Hash every block in `input` (in place, trashing it) and pack the /// 1-bit outputs LSB-first into `out`: bit `j` of `out` is the hash of - /// `input[j]`. `out.len() * 8` must equal `input.len()`. - fn hash_all_into(&self, input: &mut [AesBlock], out: &mut [u8]); + /// `input[j]`. `D` must be `8 * B`, checked at compile time + /// ([`Packed`]). + fn hash_all_into( + &self, + input: &mut [AesBlock; D], + out: &mut [u8; B], + ); +} + +/// `D` one-bit values packed LSB-first into `B` bytes. [`Self::OK`] is a +/// compile-time check that they fit exactly; a function taking a +/// `[_; D]` and a `[u8; B]` evaluates it, so a mismatched pair does not +/// compile. +pub(crate) struct Packed; + +impl Packed { + pub(crate) const OK: () = assert!(D == 8 * B, "Packed: D values need exactly D / 8 bytes"); } #[derive(Debug, Error)] @@ -86,13 +101,15 @@ pub trait Prp: Sized { #[allow(dead_code)] fn invert(&self, data: S) -> S; + /// The indicator mask: one bit per domain value, `[u8; DOMAIN / 8]`. + type Mask; + /// XOR the indicator mask for `data` into `out`: bit `j` of the mask is /// `1` iff `invert(j) > data`. Bit order matches - /// `RightBlock32::set_bit` (LSB-first within each byte). `out.len() * 8` - /// must equal the permutation domain. + /// `RightBlock32::set_bit` (LSB-first within each byte). /// /// This is the bulk form of the per-`j` `invert`-and-compare loop the /// right-ciphertext encoder needs; implementations walk their inverse /// table linearly instead of doing `DOMAIN` indexed lookups. - fn indicator_mask_xor(&self, data: S, out: &mut [u8]); + fn indicator_mask_xor(&self, data: S, out: &mut Self::Mask); } diff --git a/packages/ore-rs/src/primitives/hash.rs b/packages/ore-rs/src/primitives/hash.rs index 898c6765..089dd8a3 100644 --- a/packages/ore-rs/src/primitives/hash.rs +++ b/packages/ore-rs/src/primitives/hash.rs @@ -15,41 +15,22 @@ impl Hash for Aes128Z2Hash { Self { cipher } } - fn hash(&self, data: &[u8]) -> u8 { - /* - * Slice size is not known at compile time so we assert here - * We could do this with compile checks but this would require an additional - * copy (and doesn't entirely avoid runtime checks anyway) - * See https://stackoverflow.com/questions/38168956/take-slice-of-certain-length-known-at-compile-time - */ - assert_eq!(data.len(), 16); - // Can we clone into GenericArray directly? Are we doing an extra copy? - let mut output = [0u8; 16]; - output.clone_from_slice(data); - let block = GenericArray::from_mut_slice(&mut output); - self.cipher.encrypt_block(block); - output[0] & 1u8 + fn hash(&self, data: &[u8; 16]) -> u8 { + let mut block = AesBlock::from(*data); + self.cipher.encrypt_block(&mut block); + block[0] & 1u8 } - fn hash_all_into(&self, data: &mut [AesBlock], out: &mut [u8]) { - // A real assert: the scalar pack below zips `out` with `data`, so a - // mismatch would silently truncate the mask in a release build. - assert_eq!( - out.len() * 8, - data.len(), - "hash_all_into: out.len() * 8 must equal data.len()" - ); + fn hash_all_into( + &self, + data: &mut [AesBlock; D], + out: &mut [u8; B], + ) { self.cipher.encrypt_blocks(data); // Pack the Z2 (1-bit) outputs LSB-first, eight blocks per byte — - // the same bit order as `RightBlock32::set_bit`. The 256-block case - // (Bit8's per-block RO output) has a vectorised gather; other sizes - // use the scalar pack. - if data.len() == 256 { - crate::primitives::simd::lsb_mask_256(data, out); - } else { - crate::primitives::simd::scalar::lsb_mask(data, out); - } + // the same bit order as `RightBlock32::set_bit`. + crate::primitives::simd::lsb_mask(data, out); } } @@ -106,17 +87,13 @@ pub(crate) fn gf128_double_u128(x: u128) -> u128 { (x << 1) ^ ((x >> 127) * 0x87) } -/// In-place GF(2^128) doubling `b ← 2·b`; `b` must be exactly 16 bytes -/// (big-endian field element). Used by the scalar comparator path; the bulk +/// In-place GF(2^128) doubling `b ← 2·b` on a big-endian field element. +/// Used by the scalar comparator path; the bulk /// encrypt path folds the doubling and nonce XOR into one `u128` pass (see /// `hash_all_into`). #[inline] -fn gf128_double(b: &mut [u8]) { - debug_assert_eq!(b.len(), 16); - let mut arr = [0u8; 16]; - arr.copy_from_slice(b); - let doubled = gf128_double_u128(u128::from_be_bytes(arr)); - b.copy_from_slice(&doubled.to_be_bytes()); +fn gf128_double(b: &mut [u8; 16]) { + *b = gf128_double_u128(u128::from_be_bytes(*b)).to_be_bytes(); } impl Hash for FixedPiZ2Hash { @@ -124,54 +101,41 @@ impl Hash for FixedPiZ2Hash { Self { nonce: *nonce } } - fn hash(&self, data: &[u8]) -> u8 { - assert_eq!(data.len(), 16); + fn hash(&self, data: &[u8; 16]) -> u8 { // BHKR σ-MMO: m = σ(x) ⊕ r; return lsb(π(m) ⊕ m). - let mut block = [0u8; 16]; - block.copy_from_slice(data); + let mut block = *data; gf128_double(&mut block); // σ(x) = 2x for (slot, &r) in block.iter_mut().zip(self.nonce.iter()) { *slot ^= r; // m = σ(x) ⊕ r } let m_lsb = block[0] & 1u8; - let block = GenericArray::from_mut_slice(&mut block); - pi().encrypt_block(block); + let mut block = AesBlock::from(block); + pi().encrypt_block(&mut block); (block[0] & 1u8) ^ m_lsb } - fn hash_all_into(&self, data: &mut [AesBlock], out: &mut [u8]) { - debug_assert_eq!(out.len() * 8, data.len()); - + fn hash_all_into( + &self, + data: &mut [AesBlock; D], + out: &mut [u8; B], + ) { // BHKR σ-MMO: m = σ(x) ⊕ r, then out = lsb(m) ^ lsb(π(m)), with // σ(x) = 2x in GF(2^128). Form m in place first so the feedforward // captures lsb(m) rather than lsb(x). Doubling and nonce XOR are fused // into one u128 pass per block — this is the 704-evals/u64 hot loop. - let mut nonce_arr = [0u8; 16]; - nonce_arr.copy_from_slice(self.nonce.as_slice()); - let nonce = u128::from_be_bytes(nonce_arr); + let nonce = u128::from_be_bytes(self.nonce.into()); for block in data.iter_mut() { - let mut arr = [0u8; 16]; - arr.copy_from_slice(block.as_slice()); - let m = gf128_double_u128(u128::from_be_bytes(arr)) ^ nonce; - block.copy_from_slice(&m.to_be_bytes()); + let m = gf128_double_u128(u128::from_be_bytes((*block).into())) ^ nonce; + *block = m.to_be_bytes().into(); } // feedforward lsb(m) - if data.len() == 256 { - crate::primitives::simd::lsb_mask_256(data, out); - } else { - crate::primitives::simd::scalar::lsb_mask(data, out); - } + crate::primitives::simd::lsb_mask(data, out); pi().encrypt_blocks(data); // π(m) - let mut pi_mask = [0u8; 32]; - let pi_mask = &mut pi_mask[..out.len()]; - if data.len() == 256 { - crate::primitives::simd::lsb_mask_256(data, pi_mask); - } else { - crate::primitives::simd::scalar::lsb_mask(data, pi_mask); - } + let mut pi_mask = [0u8; B]; + crate::primitives::simd::lsb_mask(data, &mut pi_mask); for (slot, &m) in out.iter_mut().zip(pi_mask.iter()) { *slot ^= m; } @@ -205,24 +169,6 @@ mod tests { assert_eq!(0u8, hash.hash(&input)); } - #[test] - #[should_panic(expected = "assertion `left == right` failed")] - fn hash_test_input_too_small() { - let hash = init_hash(); - let input: [u8; 8] = hex!("00010203 04050607"); - - assert_eq!(0u8, hash.hash(&input)); - } - - #[test] - #[should_panic(expected = "assertion `left == right` failed")] - fn hash_test_input_too_large() { - let hash = init_hash(); - let input: [u8; 24] = hex!("00010203 04050607 ffffffff bbbbbbbb cccccccc abababab"); - - hash.hash(&input); - } - // The comparator uses the scalar `hash`; encryption uses the bulk // `hash_all_into`. For the BHKR σ-MMO they must agree bit-for-bit, over // both the scalar (n=64, the Bit6 domain) and SIMD (n=256) `lsb_mask` @@ -232,25 +178,25 @@ mod tests { let nonce: [u8; 16] = hex!("0f0e0d0c 0b0a0908 07060504 03020100"); let h: FixedPiZ2Hash = Hash::new(GenericArray::from_slice(&nonce)); - for &n in &[64usize, 256usize] { - let mut blocks: Vec = (0..n) - .map(|i| { - let mut b = [0u8; 16]; - for (j, slot) in b.iter_mut().enumerate() { - *slot = (i.wrapping_mul(31).wrapping_add(j)) as u8; - } - *GenericArray::from_slice(&b) - }) - .collect(); - - let mut expected = vec![0u8; n / 8]; + check::<64, 8>(&h); + check::<256, 32>(&h); + + fn check(h: &FixedPiZ2Hash) { + let mut blocks = [AesBlock::default(); D]; + for (i, block) in blocks.iter_mut().enumerate() { + for (j, slot) in block.iter_mut().enumerate() { + *slot = (i.wrapping_mul(31).wrapping_add(j)) as u8; + } + } + + let mut expected = [0u8; B]; for (i, b) in blocks.iter().enumerate() { - expected[i / 8] |= h.hash(b.as_slice()) << (i % 8); + expected[i / 8] |= h.hash(&(*b).into()) << (i % 8); } - let mut out = vec![0u8; n / 8]; + let mut out = [0u8; B]; h.hash_all_into(&mut blocks, &mut out); - assert_eq!(out, expected, "scalar vs bulk mismatch for n={}", n); + assert_eq!(out, expected, "scalar vs bulk mismatch for n={}", D); } } diff --git a/packages/ore-rs/src/primitives/prp.rs b/packages/ore-rs/src/primitives/prp.rs index ed269d6e..3117a747 100644 --- a/packages/ore-rs/src/primitives/prp.rs +++ b/packages/ore-rs/src/primitives/prp.rs @@ -100,9 +100,9 @@ macro_rules! impl_knuth_shuffle_prp { oblivious_lookup(&self.permutation, input) } - fn indicator_mask_xor(&self, data: Symbol<$domain>, out: &mut [u8]) { - debug_assert_eq!(out.len() * 8, $domain); + type Mask = [u8; $domain / 8]; + fn indicator_mask_xor(&self, data: Symbol<$domain>, out: &mut Self::Mask) { // `invert(j)` is `self.permutation[j]` (see `invert` // above), so the mask is one pass over the table: a // bytewise `> data` compare packed to bits — vectorised @@ -187,14 +187,14 @@ macro_rules! impl_lemire_fy_prp { impl LemireFyPrp<$domain> { /// Build the permutation directly from a precomputed draw stream - /// (shape (ii)): `stream` must be at least `($domain - 1) * 8` - /// bytes, consumed as `$domain - 1` little-endian u64 draws and - /// Lemire-reduced. The chained scheme feeds the CMAC accumulator's + /// (shape (ii)): `$domain - 1` little-endian u64 draws, each + /// Lemire-reduced (see [`oblivious::STREAM_BYTES`] for the + /// stream's length). The chained scheme feeds the CMAC accumulator's /// `PRP_STREAM` branch here, avoiding a per-block AES key schedule; /// [`Prp::new`] feeds it an AES-CTR keystream. Either way the /// tables come from the oblivious builder ([`oblivious::build`]), /// so no address depends on the stream. - pub(crate) fn from_stream(stream: &[u8]) -> PrpResult { + pub(crate) fn from_stream(stream: &[u8; oblivious::STREAM_BYTES]) -> Self { // Fisher–Yates with Lemire-reduced wide draws: draw `d` // (8 bytes) drives step `$domain - 1 - d`. Fixed trip count, // branch-free index reduction, and no secret-dependent @@ -210,19 +210,19 @@ macro_rules! impl_lemire_fy_prp { /// so they time this same function. `build` is a generic /// parameter, not a function pointer, so the scheme path is /// monomorphised and the builder still inlines. - pub(crate) fn from_stream_with(stream: &[u8], build: F) -> PrpResult + pub(crate) fn from_stream_with( + stream: &[u8; oblivious::STREAM_BYTES], + build: F, + ) -> Self where - F: FnOnce(&[u8], &mut [u8; $domain], &mut [u8; $domain]), + F: FnOnce(&[u8; oblivious::STREAM_BYTES], &mut [u8; $domain], &mut [u8; $domain]), { - if stream.len() < ($domain - 1) * 8 { - return Err(PrpError); - } let mut perm = Self { permutation: [0u8; $domain], inverse: [0u8; $domain], }; build(stream, &mut perm.permutation, &mut perm.inverse); - Ok(perm) + perm } } @@ -244,12 +244,14 @@ macro_rules! impl_lemire_fy_prp { } cipher.encrypt_blocks(&mut blocks); - let mut stream = [0u8; $stream_blocks * 16]; + // A whole stream by type: `$stream_blocks` blocks must be + // `oblivious::STREAM_BYTES`, or this does not compile. + let mut stream: [u8; oblivious::STREAM_BYTES] = [0u8; $stream_blocks * 16]; for (i, b) in blocks.iter().enumerate() { stream[i * 16..(i + 1) * 16].copy_from_slice(b); } - let perm = Self::from_stream(&stream)?; + let perm = Self::from_stream(&stream); // The keystream determined the permutation — wipe it. stream.zeroize(); @@ -268,8 +270,9 @@ macro_rules! impl_lemire_fy_prp { oblivious_lookup(&self.permutation, input) } - fn indicator_mask_xor(&self, data: Symbol<$domain>, out: &mut [u8]) { - debug_assert_eq!(out.len() * 8, $domain); + type Mask = [u8; $domain / 8]; + + fn indicator_mask_xor(&self, data: Symbol<$domain>, out: &mut Self::Mask) { $gt_mask(&self.permutation, data.get(), out); } } @@ -377,22 +380,6 @@ mod tests { assert!((0..64u8).any(|v| a.permute(sym64(v)) != c.permute(sym64(v)))); } - /// `from_stream` rejects every stream shorter than 63 * 8 bytes and - /// accepts the first full length. Exhaustive over the 504 short lengths; - /// this was a Kani harness, which took 335 s to prove the same thing. - #[test] - fn fy_from_stream_rejects_every_short_stream() { - let buf = [0u8; 63 * 8]; - for len in 0..buf.len() { - assert!( - LemireFyPrp::<64>::from_stream(&buf[..len]).is_err(), - "len {}", - len - ); - } - assert!(LemireFyPrp::<64>::from_stream(&buf).is_ok()); - } - #[test] fn fy_rejects_short_key() { assert!( as Prp>>::new(&[0u8; 8]).is_err()); @@ -472,7 +459,7 @@ mod kani_proofs { #[kani::proof] #[kani::unwind(64)] fn lemire_draw_in_range() { - let stream: [u8; 63 * 8] = kani::any(); + let stream: [u8; oblivious::STREAM_BYTES] = kani::any(); for i in 1..64 { let d = 63 - i; let mut draw = [0u8; 8]; diff --git a/packages/ore-rs/src/primitives/prp/oblivious.rs b/packages/ore-rs/src/primitives/prp/oblivious.rs index fdfc744f..25cbda1c 100644 --- a/packages/ore-rs/src/primitives/prp/oblivious.rs +++ b/packages/ore-rs/src/primitives/prp/oblivious.rs @@ -52,14 +52,18 @@ use zeroize::Zeroize; /// The domain the oblivious builders support. pub(crate) const DOMAIN: usize = 64; -/// Bytes of stream the builders consume: one 8-byte draw per step. -pub(crate) const STREAM_BYTES: usize = (DOMAIN - 1) * 8; +/// Bytes in a draw stream: one 8-byte draw per step for the `DOMAIN - 1` +/// steps, plus 8 bytes the builders never read, which make the stream a +/// whole number of AES blocks (32), as [`super::LemireFyPrp`] produces it. +/// Every builder takes exactly this array, so a short stream is a type +/// error, not a runtime check. +pub(crate) const STREAM_BYTES: usize = DOMAIN * 8; /// The Lemire-reduced draw for Fisher–Yates step `i` (`1..64`): draw /// `63 - i`, as a little-endian u64, mapped to `0..=i` by multiply-high. /// Identical to the indexed builder's reduction. #[inline(always)] -pub(crate) fn lemire_draw(stream: &[u8], i: usize) -> u8 { +pub(crate) fn lemire_draw(stream: &[u8; STREAM_BYTES], i: usize) -> u8 { let d = DOMAIN - 1 - i; let mut draw = [0u8; 8]; draw.copy_from_slice(&stream[d * 8..d * 8 + 8]); @@ -72,18 +76,19 @@ pub(crate) fn lemire_draw(stream: &[u8], i: usize) -> u8 { j } -/// Build `perm` and `inverse` from `stream` (at least [`STREAM_BYTES`] -/// bytes) with a data-independent memory access pattern, using the fastest +/// Build `perm` and `inverse` from `stream` with a data-independent memory access pattern, using the fastest /// oblivious builder for the target. #[inline] -pub(crate) fn build(stream: &[u8], perm: &mut [u8; DOMAIN], inverse: &mut [u8; DOMAIN]) { - assert!(stream.len() >= STREAM_BYTES); - +pub(crate) fn build( + stream: &[u8; STREAM_BYTES], + perm: &mut [u8; DOMAIN], + inverse: &mut [u8; DOMAIN], +) { // Kani cannot model the NEON and SSSE3 intrinsics, so under `cargo kani` // every target dispatches to the SWAR builder, which the harnesses prove // equal to the reference builder. #[cfg(all(target_arch = "aarch64", not(kani)))] - // SAFETY: NEON is baseline on aarch64; `stream` length asserted above. + // SAFETY: NEON is baseline on aarch64; `stream` is a full stream by type. unsafe { neon::build(stream, perm, inverse) } @@ -92,7 +97,7 @@ pub(crate) fn build(stream: &[u8], perm: &mut [u8; DOMAIN], inverse: &mut [u8; D { #[cfg(all(target_arch = "x86_64", not(kani)))] if is_x86_feature_detected!("ssse3") { - // SAFETY: SSSE3 presence just checked; `stream` length asserted. + // SAFETY: SSSE3 presence just checked; `stream` is a full stream by type. unsafe { ssse3::build(stream, perm, inverse) }; return; } @@ -111,8 +116,11 @@ pub(crate) fn build(stream: &[u8], perm: &mut [u8; DOMAIN], inverse: &mut [u8; D pub(crate) mod reference { use super::*; - pub(crate) fn build(stream: &[u8], perm: &mut [u8; DOMAIN], inverse: &mut [u8; DOMAIN]) { - assert!(stream.len() >= STREAM_BYTES); + pub(crate) fn build( + stream: &[u8; STREAM_BYTES], + perm: &mut [u8; DOMAIN], + inverse: &mut [u8; DOMAIN], + ) { for (k, p) in perm.iter_mut().enumerate() { *p = k as u8; } @@ -136,8 +144,11 @@ pub(crate) mod scalar { use super::*; use subtle_ng::{ConditionallySelectable, ConstantTimeEq}; - pub(crate) fn build(stream: &[u8], perm: &mut [u8; DOMAIN], inverse: &mut [u8; DOMAIN]) { - assert!(stream.len() >= STREAM_BYTES); + pub(crate) fn build( + stream: &[u8; STREAM_BYTES], + perm: &mut [u8; DOMAIN], + inverse: &mut [u8; DOMAIN], + ) { for (k, (p, v)) in perm.iter_mut().zip(inverse.iter_mut()).enumerate() { *p = k as u8; *v = k as u8; @@ -218,8 +229,11 @@ pub(crate) mod swar { } } - pub(crate) fn build(stream: &[u8], perm: &mut [u8; DOMAIN], inverse: &mut [u8; DOMAIN]) { - assert!(stream.len() >= STREAM_BYTES); + pub(crate) fn build( + stream: &[u8; STREAM_BYTES], + perm: &mut [u8; DOMAIN], + inverse: &mut [u8; DOMAIN], + ) { let mut index = [0u64; 8]; for (w, word) in index.iter_mut().enumerate() { let mut b = [0u8; 8]; @@ -259,7 +273,7 @@ pub(crate) mod swar { #[cfg(all(target_arch = "aarch64", not(kani)))] mod neon { - use super::{lemire_draw, DOMAIN}; + use super::{lemire_draw, DOMAIN, STREAM_BYTES}; use core::arch::aarch64::*; /// `0, 1, …, 63`: the lane index of each table entry. @@ -298,7 +312,7 @@ mod neon { /// dependency through `perm`. It covers all four chunks. #[inline(always)] unsafe fn steps( - stream: &[u8], + stream: &[u8; STREAM_BYTES], p: &mut [uint8x16_t; 4], v: &mut [uint8x16_t; 4], index: &[uint8x16_t; 4], @@ -330,15 +344,19 @@ mod neon { /// # Safety /// - /// Requires NEON (baseline on aarch64) and `stream.len() >= - /// STREAM_BYTES` (asserted by the caller). The only memory operations + /// Requires NEON (baseline on aarch64). `stream` is a full stream by + /// type. The only memory operations /// are the load of the constant `INDEX`, the bounds-checked stream reads /// in `lemire_draw`, and the two 64-byte stores into `perm` and /// `inverse`, which are exclusive references to 64-byte arrays. Both /// tables live in vector registers in between: every `p[k]` / `v[k]` /// index is a compile-time constant after inlining. #[target_feature(enable = "neon")] - pub(super) unsafe fn build(stream: &[u8], perm: &mut [u8; DOMAIN], inverse: &mut [u8; DOMAIN]) { + pub(super) unsafe fn build( + stream: &[u8; STREAM_BYTES], + perm: &mut [u8; DOMAIN], + inverse: &mut [u8; DOMAIN], + ) { let ix = vld1q_u8_x4(INDEX.as_ptr()); let index = [ix.0, ix.1, ix.2, ix.3]; let mut p = index; @@ -356,7 +374,7 @@ mod neon { #[cfg(all(target_arch = "x86_64", not(kani)))] mod ssse3 { - use super::{lemire_draw, DOMAIN}; + use super::{lemire_draw, DOMAIN, STREAM_BYTES}; use core::arch::x86_64::*; /// `select(m, x, y)`: `x` where the mask lane is all ones, else `y`. @@ -391,7 +409,7 @@ mod ssse3 { /// builder's `steps` (public chunk bounds, value-side inverse). #[inline(always)] unsafe fn steps( - stream: &[u8], + stream: &[u8; STREAM_BYTES], p: &mut [__m128i; 4], v: &mut [__m128i; 4], index: &[__m128i; 4], @@ -416,11 +434,15 @@ mod ssse3 { /// # Safety /// - /// Requires SSSE3 (checked by the caller) and `stream.len() >= - /// STREAM_BYTES` (asserted by the caller). Unaligned stores cover + /// Requires SSSE3 (checked by the caller). `stream` is a full stream by + /// type. Unaligned stores cover /// exactly the 64-byte `perm` and `inverse` arrays. #[target_feature(enable = "ssse3")] - pub(super) unsafe fn build(stream: &[u8], perm: &mut [u8; DOMAIN], inverse: &mut [u8; DOMAIN]) { + pub(super) unsafe fn build( + stream: &[u8; STREAM_BYTES], + perm: &mut [u8; DOMAIN], + inverse: &mut [u8; DOMAIN], + ) { let mut index = [_mm_setzero_si128(); 4]; for (c, chunk) in index.iter_mut().enumerate() { *chunk = _mm_add_epi8( @@ -449,7 +471,10 @@ mod tests { type Tables = ([u8; DOMAIN], [u8; DOMAIN]); - fn run(builder: fn(&[u8], &mut [u8; DOMAIN], &mut [u8; DOMAIN]), stream: &[u8]) -> Tables { + fn run( + builder: fn(&[u8; STREAM_BYTES], &mut [u8; DOMAIN], &mut [u8; DOMAIN]), + stream: &[u8; STREAM_BYTES], + ) -> Tables { let mut perm = [0u8; DOMAIN]; let mut inverse = [0u8; DOMAIN]; builder(stream, &mut perm, &mut inverse); @@ -544,11 +569,4 @@ mod tests { assert_eq!(usize::from(inverse[usize::from(*p)]), k); } } - - /// One byte short of a full stream is refused. - #[test] - #[should_panic] - fn build_rejects_a_short_stream() { - run(build, &[0u8; STREAM_BYTES - 1]); - } } diff --git a/packages/ore-rs/src/primitives/simd.rs b/packages/ore-rs/src/primitives/simd.rs index 8e9ee046..fd83d4f6 100644 --- a/packages/ore-rs/src/primitives/simd.rs +++ b/packages/ore-rs/src/primitives/simd.rs @@ -30,19 +30,15 @@ //! between scalar and vector paths is pinned by tests below; equivalence //! against the per-bit reference is pinned in `prp.rs`. -use super::AesBlock; +use super::{AesBlock, Packed}; +use core::convert::TryFrom; /// XOR the indicator mask for `x` over `table` into `out`: /// `out[j/8] ^= ((table[j] > x) as u8) << (j%8)`. #[inline] -pub(crate) fn gt_mask_xor_256(table: &[u8; 256], x: u8, out: &mut [u8]) { - // A real assert, so every backend sees the same contract in release: the - // scalar path would otherwise silently truncate, and the vector paths - // would panic only on a short slice. - assert_eq!(out.len(), 32); - +pub(crate) fn gt_mask_xor_256(table: &[u8; 256], x: u8, out: &mut [u8; 32]) { #[cfg(target_arch = "aarch64")] - // SAFETY: NEON is baseline on aarch64; `out` length asserted above. + // SAFETY: NEON is baseline on aarch64; the lengths are in the types. unsafe { neon::gt_mask_xor_256(table, x, out) } @@ -53,7 +49,7 @@ pub(crate) fn gt_mask_xor_256(table: &[u8; 256], x: u8, out: &mut [u8]) { { #[cfg(target_arch = "x86_64")] if is_x86_feature_detected!("avx2") { - // SAFETY: AVX2 presence just checked; `out` length asserted above. + // SAFETY: AVX2 presence just checked; the lengths are in the types. unsafe { avx2::gt_mask_xor_256(table, x, out) }; return; } @@ -66,13 +62,9 @@ pub(crate) fn gt_mask_xor_256(table: &[u8; 256], x: u8, out: &mut [u8]) { /// AVX2 gains little at this size (two 32-lane compares vs the scalar /// loop's ~9 ns); only NEON is dispatched. #[inline] -pub(crate) fn gt_mask_xor_64(table: &[u8; 64], x: u8, out: &mut [u8]) { - // A real assert, as for `gt_mask_xor_256`: the same contract on every - // backend in release. - assert_eq!(out.len(), 8); - +pub(crate) fn gt_mask_xor_64(table: &[u8; 64], x: u8, out: &mut [u8; 8]) { #[cfg(target_arch = "aarch64")] - // SAFETY: NEON is baseline on aarch64; `out` length asserted above. + // SAFETY: NEON is baseline on aarch64; the lengths are in the types. unsafe { neon::gt_mask_xor_64(table, x, out) } @@ -81,19 +73,29 @@ pub(crate) fn gt_mask_xor_64(table: &[u8; 64], x: u8, out: &mut [u8]) { scalar::gt_mask_xor(table, x, out); } -/// Pack the LSB of byte 0 of each of 256 AES blocks into `out`: -/// `out[j/8] |= (blocks[j][0] & 1) << (j%8)` over zeroed positions -/// (callers pass `out` slots they own; bits are assigned, not accumulated). +/// Pack the LSB of byte 0 of each block into `out`: bit `j` of `out` is +/// `blocks[j][0] & 1` (bits are assigned, not accumulated). The 256-block +/// case (Bit8's per-block RO output) has a vectorised gather; other sizes +/// use the scalar pack. `D` must be `8 * B`, checked at compile time. #[inline] -pub(crate) fn lsb_mask_256(blocks: &[AesBlock], out: &mut [u8]) { - // Real asserts (not debug_assert): the NEON path gathers `blocks` via raw - // pointers assuming exactly 256 blocks, so a shorter slice would read out - // of bounds (UB) in a release build without this check. - assert_eq!(blocks.len(), 256); - assert_eq!(out.len(), 32); +pub(crate) fn lsb_mask(blocks: &[AesBlock; D], out: &mut [u8; B]) { + // Decided by `D` and `B` alone, so the untaken arm is compiled out. + if let (Ok(blocks), Ok(out)) = ( + <&[AesBlock; 256]>::try_from(blocks.as_slice()), + <&mut [u8; 32]>::try_from(out.as_mut_slice()), + ) { + lsb_mask_256(blocks, out); + return; + } + scalar::lsb_mask(blocks, out); +} +/// [`lsb_mask`] for 256 blocks. +#[inline] +pub(crate) fn lsb_mask_256(blocks: &[AesBlock; 256], out: &mut [u8; 32]) { #[cfg(target_arch = "aarch64")] - // SAFETY: NEON is baseline on aarch64; lengths asserted above. + // SAFETY: NEON is baseline on aarch64; the NEON path gathers `blocks` + // through raw pointers, which the array type keeps to 256 blocks. unsafe { neon::lsb_mask_256(blocks, out) } @@ -103,10 +105,9 @@ pub(crate) fn lsb_mask_256(blocks: &[AesBlock], out: &mut [u8]) { } pub(crate) mod scalar { - use super::AesBlock; + use super::{AesBlock, Packed}; - /// Length-generic scalar indicator pack: `table.len()` must be a - /// multiple of 8 and equal to `out.len() * 8`. + /// Scalar indicator pack: `D` must be `8 * B`, checked at compile time. /// /// Used by the non-aarch64 dispatchers and as the test oracle; on /// aarch64 the lib always reaches NEON, so outside `cfg(test)` it is @@ -114,7 +115,13 @@ pub(crate) mod scalar { /// from being dead code without an `allow`. (`lsb_mask` below stays /// always compiled: `hash.rs` calls it directly for non-256 inputs.) #[cfg(any(not(target_arch = "aarch64"), test))] - pub(crate) fn gt_mask_xor(table: &[u8], x: u8, out: &mut [u8]) { + #[allow(clippy::let_unit_value)] + pub(crate) fn gt_mask_xor( + table: &[u8; D], + x: u8, + out: &mut [u8; B], + ) { + let () = Packed::::OK; for (slot, chunk) in out.iter_mut().zip(table.chunks_exact(8)) { let mut byte = 0u8; for (bit, &p) in chunk.iter().enumerate() { @@ -124,14 +131,13 @@ pub(crate) mod scalar { } } - /// Length-generic scalar LSB pack: `blocks.len()` must be a multiple - /// of 8 and equal to `out.len() * 8`. - pub(crate) fn lsb_mask(blocks: &[AesBlock], out: &mut [u8]) { - assert_eq!( - out.len() * 8, - blocks.len(), - "lsb_mask: out.len() * 8 must equal blocks.len()" - ); + /// Scalar LSB pack: `D` must be `8 * B`, checked at compile time. + #[allow(clippy::let_unit_value)] + pub(crate) fn lsb_mask( + blocks: &[AesBlock; D], + out: &mut [u8; B], + ) { + let () = Packed::::OK; for (slot, chunk) in out.iter_mut().zip(blocks.chunks_exact(8)) { let mut byte = 0u8; for (bit, block) in chunk.iter().enumerate() { @@ -160,7 +166,7 @@ mod neon { } #[target_feature(enable = "neon")] - pub(super) unsafe fn gt_mask_xor_256(table: &[u8; 256], x: u8, out: &mut [u8]) { + pub(super) unsafe fn gt_mask_xor_256(table: &[u8; 256], x: u8, out: &mut [u8; 32]) { let xv = vdupq_n_u8(x); for i in 0..16 { let t = vld1q_u8(table.as_ptr().add(i * 16)); @@ -171,7 +177,7 @@ mod neon { } #[target_feature(enable = "neon")] - pub(super) unsafe fn gt_mask_xor_64(table: &[u8; 64], x: u8, out: &mut [u8]) { + pub(super) unsafe fn gt_mask_xor_64(table: &[u8; 64], x: u8, out: &mut [u8; 8]) { let xv = vdupq_n_u8(x); for i in 0..4 { let t = vld1q_u8(table.as_ptr().add(i * 16)); @@ -210,7 +216,7 @@ mod neon { } #[target_feature(enable = "neon")] - pub(super) unsafe fn lsb_mask_256(blocks: &[AesBlock], out: &mut [u8]) { + pub(super) unsafe fn lsb_mask_256(blocks: &[AesBlock; 256], out: &mut [u8; 32]) { let p = blocks.as_ptr() as *const u8; let one = vdupq_n_u8(1); for i in 0..16 { @@ -229,7 +235,7 @@ mod avx2 { /// AVX2 has only *signed* byte compares; XOR-ing 0x80 into both sides /// maps unsigned `>` onto signed `>`. #[target_feature(enable = "avx2")] - pub(super) unsafe fn gt_mask_xor_256(table: &[u8; 256], x: u8, out: &mut [u8]) { + pub(super) unsafe fn gt_mask_xor_256(table: &[u8; 256], x: u8, out: &mut [u8; 32]) { let bias = _mm256_set1_epi8(i8::MIN); let xv = _mm256_xor_si256(_mm256_set1_epi8(x as i8), bias); for i in 0..8 { @@ -293,21 +299,6 @@ mod tests { } } - #[test] - #[should_panic(expected = "lsb_mask: out.len() * 8 must equal blocks.len()")] - fn scalar_lsb_mask_rejects_a_short_output() { - let blocks = [AesBlock::default(); 64]; - let mut out = [0u8; 7]; - scalar::lsb_mask(&blocks, &mut out); - } - - #[test] - #[should_panic] - fn gt_mask_xor_256_rejects_a_short_output() { - let mut out = [0u8; 31]; - gt_mask_xor_256(&[0u8; 256], 0, &mut out); - } - #[test] fn lsb_mask_dispatched_matches_scalar() { let mut rng = ChaCha20Rng::seed_from_u64(11); diff --git a/packages/ore-rs/src/scheme/bit2.rs b/packages/ore-rs/src/scheme/bit2.rs index 42e0abd4..967ef344 100644 --- a/packages/ore-rs/src/scheme/bit2.rs +++ b/packages/ore-rs/src/scheme/bit2.rs @@ -99,14 +99,21 @@ pub(crate) fn encode_right_block( prp: &W::Prp, x: W::Symbol, hasher: &H, - ro_blocks: &mut [AesBlock], + ro_blocks: &mut W::RoKeyBuf, ) { - debug_assert_eq!(ro_blocks.len(), W::DOMAIN); - let out = block.as_mut_bytes(); - hasher.hash_all_into(ro_blocks, out); + let out = block.as_mut_bits(); + W::hash_into(hasher, ro_blocks, out); prp.indicator_mask_xor(x, out); } +/// Maximum number of blocks (= plaintext bytes): the left-tag input packs +/// `prefix ‖ xt ‖ index@N` into one 16-byte AES block, so `N ≤ 15`. A +/// 16-byte plaintext (`u128`/`i128`) used to index past the block and panic +/// (issue #105); `encrypt`/`encrypt_left` now return +/// [`OreError::TooManyBlocks`], and the `OreEncrypt` impls refuse such types +/// at compile time. +pub const MAX_BLOCKS: usize = 15; + impl OreAes128 { /// Fill `left` with the PRF₁ tag inputs and permuted values for `x`, /// using the given PRP seeds. After this returns, `left.f[n]` holds the @@ -150,6 +157,9 @@ impl OreCipher for OreAes128 { } fn encrypt_left(&self, x: &PlainText) -> EncryptLeftResult { + if N > MAX_BLOCKS { + return Err(OreError::TooManyBlocks); + } let mut output = Left::::init(); let seeds = derive_prp_seeds(&self.prf2, x); @@ -160,6 +170,9 @@ impl OreCipher for OreAes128 { } fn encrypt(&self, x: &PlainText) -> EncryptResult { + if N > MAX_BLOCKS { + return Err(OreError::TooManyBlocks); + } let mut left = Left::::init(); let mut right = Right::::init(); @@ -376,6 +389,23 @@ mod tests { type Ore = OreAes128ChaCha20; + /// A 16-byte plaintext does not fit the left-tag input (issue #105): an + /// error, not an out-of-bounds panic. 15 bytes is the most the scheme takes. + #[test] + fn sixteen_bytes_is_an_error_not_a_panic() { + let ore = init_ore(); + assert!(matches!( + ore.encrypt(&[0u8; 16]), + Err(OreError::TooManyBlocks) + )); + assert!(matches!( + ore.encrypt_left(&[0u8; 16]), + Err(OreError::TooManyBlocks) + )); + assert!(ore.encrypt(&[0u8; 15]).is_ok()); + assert!(ore.encrypt_left(&[0u8; 15]).is_ok()); + } + fn init_ore() -> Ore { let mut k1: [u8; 16] = Default::default(); let mut k2: [u8; 16] = Default::default(); diff --git a/packages/ore-rs/src/scheme/bit2/block_types.rs b/packages/ore-rs/src/scheme/bit2/block_types.rs index 4de40211..cc00a16a 100644 --- a/packages/ore-rs/src/scheme/bit2/block_types.rs +++ b/packages/ore-rs/src/scheme/bit2/block_types.rs @@ -17,14 +17,12 @@ pub struct RightBlock32 { } impl RightBlock32 { - /// Set bit `bit` (in `0..256`) to `value` (which must be `0` or `1`), - /// overwriting whatever the bit held. Panics in debug if `bit >= 256`. + /// Set bit `bit` to the low bit of `value`, overwriting whatever the + /// bit held. A `u8` names every one of the 256 bits and no other. #[inline] - pub fn set_bit(&mut self, bit: usize, value: u8) { - debug_assert!(bit < 256); - let byte_index = bit / 8; + pub fn set_bit(&mut self, bit: u8, value: u8) { let shift = bit % 8; - let b = &mut self.data[byte_index]; + let b = &mut self.data[usize::from(bit / 8)]; *b = (*b & !(1 << shift)) | ((value & 1) << shift); } @@ -37,15 +35,14 @@ impl RightBlock32 { /// Mutable form of [`Self::bytes`]. #[inline] - pub(crate) fn bytes_mut(&mut self) -> &mut [u8] { + pub(crate) fn bytes_mut(&mut self) -> &mut [u8; 32] { &mut self.data } - /// Read bit `bit` (in `0..256`); returns `0` or `1`. Panics in debug if - /// `bit >= 256`. + /// Read bit `bit` (in `0..256`); returns `0` or `1`. A `bit` past the + /// block reads as `0`: the oblivious select finds no byte. #[inline] pub fn get_bit(&self, bit: usize) -> u8 { - debug_assert!(bit < 256); // `bit` is the secret permuted symbol; read the byte obliviously so the // access address does not depend on it. See `width::ct_select_byte`. let byte = crate::scheme::width::ct_select_byte(&self.data, bit / 8); diff --git a/packages/ore-rs/src/scheme/bit2_w6.rs b/packages/ore-rs/src/scheme/bit2_w6.rs index e24308fb..4f9c80cb 100644 --- a/packages/ore-rs/src/scheme/bit2_w6.rs +++ b/packages/ore-rs/src/scheme/bit2_w6.rs @@ -12,8 +12,9 @@ //! the block count in byte 15 must fit one AES block; seeds also use byte //! 14 for the block index), which covers all //! primitives up to 64 bits. `Decimal` stays on the legacy scheme until the -//! chained-prefix construction lands (plan §5). `u128`/`i128` fit neither: -//! the legacy scheme takes at most 15 bytes and panics on 16. +//! chained-prefix construction lands (plan §5). `u128`/`i128` (22 blocks) +//! fit neither this scheme nor the legacy one (15 bytes at most); they are +//! encrypted with [`crate::scheme::chained`], which has no length cap. //! //! **Status: wire format NOT yet frozen.** The Z2 hash is the fixed-π MMO //! construction proposed in plan §6 (option 3), pending crypto review; @@ -151,7 +152,9 @@ impl OreAes128Bit6 { &self, x: &Blocks6>, ) -> EncryptLeftResult { - assert!(N <= MAX_BLOCKS); + if N > MAX_BLOCKS { + return Err(OreError::TooManyBlocks); + } let mut output = Left::::init(); let seeds = self.derive_prp_seeds(x.inner()); @@ -176,7 +179,9 @@ impl OreAes128Bit6 { &self, x: &Blocks6>, ) -> EncryptResult { - assert!(N <= MAX_BLOCKS); + if N > MAX_BLOCKS { + return Err(OreError::TooManyBlocks); + } let mut left = Left::::init(); let mut right = Right::::init(); @@ -259,7 +264,7 @@ impl OreCipher for OreAes128Bit6 { /// Encrypt `x`, whose entries are **6-bit block values** (`< 64`, as /// the crate-private `scheme::decompose` produces); `N` is the block count - /// (≤ [`MAX_BLOCKS`]). The [`crate::OreEncrypt`] impls handle the + /// (≤ [`MAX_BLOCKS`]; more returns [`OreError::TooManyBlocks`]). The [`crate::OreEncrypt`] impls handle the /// byte→block decomposition for primitive types and skip the check /// below. A value `>= 64` returns [`OreError::PrpError`]. fn encrypt_left(&self, x: &PlainText) -> EncryptLeftResult { @@ -424,8 +429,11 @@ mod encrypt_impls { use orderable_bytes::{FixedOrderableBytes, OrderableBytes}; use rand::{Rng, SeedableRng}; + // Each invocation also emits a test that the type round-trips, orders + // `$lo` below `$hi`, and compares equal across nonces, so a type cannot + // be listed as supported without being exercised (issue #105). macro_rules! impl_ore_encrypt_bit6 { - ($type:ty, $blocks_const:ident) => { + ($type:ty, $blocks_const:ident, $tests:ident, $lo:expr, $hi:expr) => { /// Block count for this type at 6-bit width. const $blocks_const: usize = num_blocks_6bit(<$type as FixedOrderableBytes>::ENCODED_LEN); @@ -458,21 +466,68 @@ mod encrypt_impls { }) } } + + #[cfg(test)] + mod $tests { + use super::*; + use crate::scheme::bit2_w6::OreAes128Bit6ChaCha20; + use crate::{OreCipher, OreOutput}; + use std::cmp::Ordering; + + #[test] + fn round_trip_order_and_equality() { + let ore: OreAes128Bit6ChaCha20 = + OreCipher::init(&[0x11; 16], &[0x22; 16]).unwrap(); + let (lo, hi): ($type, $type) = ($lo, $hi); + assert!(lo < hi); + + let lo_ct = lo.encrypt(&ore).unwrap(); + let hi_ct = hi.encrypt(&ore).unwrap(); + let lo_left = lo.encrypt_left(&ore).unwrap(); + let lo_ct2 = lo.encrypt(&ore).unwrap(); + + let lo_bytes = lo_ct.to_bytes(); + let hi_bytes = hi_ct.to_bytes(); + assert_eq!( + CipherText::::from_slice(&lo_bytes) + .unwrap() + .to_bytes(), + lo_bytes + ); + assert_eq!( + Left::::from_slice( + &lo_left.to_bytes() + ) + .unwrap() + .to_bytes(), + lo_left.to_bytes() + ); + + let cmp = OreAes128Bit6ChaCha20::compare_raw_slices; + assert_eq!(cmp(&lo_bytes, &hi_bytes), Some(Ordering::Less)); + assert_eq!(cmp(&hi_bytes, &lo_bytes), Some(Ordering::Greater)); + assert_ne!(lo_bytes, lo_ct2.to_bytes()); + assert_eq!(cmp(&lo_bytes, &lo_ct2.to_bytes()), Some(Ordering::Equal)); + assert_eq!(lo_ct.cmp(&hi_ct), Ordering::Less); + } + } }; } - impl_ore_encrypt_bit6!(bool, BOOL_BLOCKS); - impl_ore_encrypt_bit6!(u8, U8_BLOCKS); - impl_ore_encrypt_bit6!(i8, I8_BLOCKS); - impl_ore_encrypt_bit6!(u16, U16_BLOCKS); - impl_ore_encrypt_bit6!(i16, I16_BLOCKS); - impl_ore_encrypt_bit6!(u32, U32_BLOCKS); - impl_ore_encrypt_bit6!(i32, I32_BLOCKS); - impl_ore_encrypt_bit6!(u64, U64_BLOCKS); - impl_ore_encrypt_bit6!(i64, I64_BLOCKS); - impl_ore_encrypt_bit6!(char, CHAR_BLOCKS); - impl_ore_encrypt_bit6!(f32, F32_BLOCKS); - impl_ore_encrypt_bit6!(f64, F64_BLOCKS); + impl_ore_encrypt_bit6!(bool, BOOL_BLOCKS, bool_tests, false, true); + impl_ore_encrypt_bit6!(u8, U8_BLOCKS, u8_tests, 0u8, u8::MAX); + impl_ore_encrypt_bit6!(i8, I8_BLOCKS, i8_tests, i8::MIN, i8::MAX); + impl_ore_encrypt_bit6!(u16, U16_BLOCKS, u16_tests, 0u16, u16::MAX); + impl_ore_encrypt_bit6!(i16, I16_BLOCKS, i16_tests, i16::MIN, i16::MAX); + impl_ore_encrypt_bit6!(u32, U32_BLOCKS, u32_tests, 0u32, u32::MAX); + impl_ore_encrypt_bit6!(i32, I32_BLOCKS, i32_tests, i32::MIN, i32::MAX); + impl_ore_encrypt_bit6!(u64, U64_BLOCKS, u64_tests, 0u64, u64::MAX); + impl_ore_encrypt_bit6!(i64, I64_BLOCKS, i64_tests, i64::MIN, i64::MAX); + // `u128`/`i128` (22 blocks) exceed `MAX_BLOCKS` and are provided by + // `scheme::chained` instead (issue #105). + impl_ore_encrypt_bit6!(char, CHAR_BLOCKS, char_tests, '\0', char::MAX); + impl_ore_encrypt_bit6!(f32, F32_BLOCKS, f32_tests, f32::MIN, f32::MAX); + impl_ore_encrypt_bit6!(f64, F64_BLOCKS, f64_tests, f64::MIN, f64::MAX); } #[cfg(test)] @@ -496,6 +551,23 @@ mod tests { OreCipher::init(&k1, &k2).unwrap() } + /// A block count past `MAX_BLOCKS` through the public `OreCipher` path + /// is an error, as on the legacy scheme, not a panic (review of #113). + #[test] + fn too_many_blocks_is_an_error_not_a_panic() { + let ore = init_ore(); + assert!(matches!( + ore.encrypt(&[0u8; MAX_BLOCKS + 1]), + Err(OreError::TooManyBlocks) + )); + assert!(matches!( + ore.encrypt_left(&[0u8; MAX_BLOCKS + 1]), + Err(OreError::TooManyBlocks) + )); + assert!(ore.encrypt(&[0u8; MAX_BLOCKS]).is_ok()); + assert!(ore.encrypt_left(&[0u8; MAX_BLOCKS]).is_ok()); + } + /// Cross-scheme regression (review of #82): under keys shared with the /// legacy scheme, a legacy `N = 15` left ciphertext must never publish /// one of Bit6's PRF₁ tags. Bit6's `f[0]` for an 11-block value has PRF₁ diff --git a/packages/ore-rs/src/scheme/bit2_w6/block_types.rs b/packages/ore-rs/src/scheme/bit2_w6/block_types.rs index 7ae81dbb..1c8519f8 100644 --- a/packages/ore-rs/src/scheme/bit2_w6/block_types.rs +++ b/packages/ore-rs/src/scheme/bit2_w6/block_types.rs @@ -14,10 +14,10 @@ pub struct RightBlock8 { } impl RightBlock8 { - /// Read bit `bit` (in `0..64`); returns `0` or `1`. + /// Read bit `bit` (in `0..64`); returns `0` or `1`. A `bit` past the + /// block reads as `0`: the oblivious select finds no byte. #[inline] pub fn get_bit(&self, bit: usize) -> u8 { - debug_assert!(bit < 64); // `bit` is the secret permuted symbol; read the byte obliviously so the // access address does not depend on it. See `width::ct_select_byte`. let byte = crate::scheme::width::ct_select_byte(&self.data, bit / 8); @@ -33,10 +33,12 @@ impl RightBlock8 { } impl RightBitVec for RightBlock8 { + type Bits = [u8; 8]; + fn get_bit(&self, bit: usize) -> u8 { RightBlock8::get_bit(self, bit) } - fn as_mut_bytes(&mut self) -> &mut [u8] { + fn as_mut_bits(&mut self) -> &mut [u8; 8] { &mut self.data } } diff --git a/packages/ore-rs/src/scheme/chained.rs b/packages/ore-rs/src/scheme/chained.rs index 6243cbdc..4abb91d8 100644 --- a/packages/ore-rs/src/scheme/chained.rs +++ b/packages/ore-rs/src/scheme/chained.rs @@ -50,7 +50,8 @@ //! The header also carries the padding policy, and the comparators return //! `None` for two ciphertexts made under different policies: an unpadded //! `"abc"` and a padded `"abc"` have different blocks, and would compare -//! unequal. A column's stored values and its queries must use one policy. +//! unequal. Stored values and the queries that compare with them must use +//! one policy. //! //! A cipher also has a longest string it encrypts //! ([`Options::with_max_bytes`](crate::scheme::chained::Options::with_max_bytes), @@ -68,6 +69,35 @@ //! zero symbols, since a trailing zero symbol is a real value there, so they //! are not offered as an API (#110). //! +//! **Fixed-length types.** `u128` and `i128` (22 blocks) are too wide for +//! the packed prefix of `OreAes128Bit6` and for the legacy scheme, so their +//! [`crate::OreEncrypt`] impls live here. The outputs, [`crate::scheme::chained::ChainedCipherText`] and +//! [`crate::scheme::chained::ChainedLeft`], carry the block count in the type: their parsers and +//! comparators take only that count, so a `u128` ciphertext is never put +//! into the shorter-sorts-first order with a ciphertext of another length. +//! The count is a length guard, not a type guard: a 16-byte string and an +//! `i128` are 22 blocks too, and under one key an equal encoding gives an +//! equal ciphertext (`0u128`, `i128::MIN`, sixteen NUL bytes). Use one key +//! per kind of value. A fixed-length value is not padded (every value of the type +//! has the same length, so there is nothing to hide) and is not subject to +//! the cipher's string ceiling; its ciphertext is tagged +//! [`Padding::None`](crate::scheme::chained::Padding::None), and the typed +//! parsers accept no other tag. +//! +//! ```rust +//! use ore_rs::scheme::chained::{ChainedCipherText, OreAes128Bit6ChainedChaCha20, U128_BLOCKS}; +//! use ore_rs::{OreEncrypt, OreOutput}; +//! use std::cmp::Ordering; +//! +//! let ore = OreAes128Bit6ChainedChaCha20::init(&[0u8; 16]).unwrap(); +//! let a = 1u128.encrypt(&ore).unwrap().to_bytes(); +//! let b = u128::MAX.encrypt(&ore).unwrap().to_bytes(); +//! assert_eq!( +//! ChainedCipherText::::compare_raw_slices(&a, &b), +//! Some(Ordering::Less) +//! ); +//! ``` +//! //! **Stored rows compare offline.** A full ciphertext carries its //! deterministic left half (`xt`, `f`), so anyone holding two stored full //! ciphertexts can run `compare_raw_slices` with no @@ -77,7 +107,7 @@ use std::cell::RefCell; use std::cmp::Ordering; -use std::convert::TryFrom; +use std::convert::{TryFrom, TryInto}; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; @@ -87,14 +117,16 @@ use zeroize::{Zeroize, Zeroizing}; use aes::cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit}; use aes::Aes128Enc; -use crate::ciphertext::{parse_header, ParseError, WireHeader, WIRE_HEADER_LEN}; +use orderable_bytes::FixedOrderableBytes; + +use crate::ciphertext::{parse_header, OreOutput, ParseError, WireHeader, WIRE_HEADER_LEN}; use crate::primitives::cmac::{final_block, prefix_block, Branch, CmacAccumulator, WIDTH_BIT6}; use crate::primitives::hash::FixedPiZ2Hash; use crate::primitives::prp::LemireFyPrp; use crate::primitives::{stack, AesBlock, Hash, HashKey, Prp}; use crate::scheme::decompose::{num_blocks_6bit, Blocks6}; use crate::scheme::width::FirstDiff; -use crate::OreError; +use crate::{OreEncrypt, OreError}; const VERSION: u8 = 0x02; const SCHEME_ID: u8 = 0x03; @@ -155,16 +187,16 @@ fn right_at(s: &[u8], count: usize, i: usize) -> &[u8] { /// ciphertext reveals a length bucket rather than the exact length (see the /// module docs). /// -/// Lengths are in UTF-8 bytes. The policy belongs to a column: its stored -/// values and its queries must use the same one, and changing it means -/// re-encrypting the column. The comparators refuse to compare ciphertexts +/// Lengths are in UTF-8 bytes. The policy belongs with the key: stored +/// values and the queries that compare with them must use the same one, +/// and changing it means re-encrypting the stored values. The comparators refuse to compare ciphertexts /// made under different policies. /// /// Padding hides the exact length in the ciphertext only: the time the /// encryptor takes still depends on the exact length of the string. /// /// More policies may follow (a growth ratio between buckets, boundaries -/// fitted to a column), so the enum is non-exhaustive. +/// fitted to the data), so the enum is non-exhaustive. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum Padding { @@ -283,8 +315,9 @@ impl Padding { pub const DEFAULT_MAX_STR_BYTES: usize = 1024; /// Per-cipher settings for the string encryptors, passed to -/// [`OreAes128Bit6Chained::init_with`]. They belong to a column, like the -/// key: its stored values and its queries use the same ones. +/// [`OreAes128Bit6Chained::init_with`]. Like the key, they belong to the +/// values that are compared together: stored values and the queries that +/// compare with them use the same ones. /// /// ```rust /// use ore_rs::scheme::chained::{OreAes128Bit6ChainedChaCha20, Options, Padding}; @@ -325,8 +358,8 @@ impl Options { /// The longest string, in UTF-8 bytes, the cipher encrypts; a longer /// one returns [`OreError::TooLong`]. Bounds what one input can cost in - /// ciphertext size and encryption time, and how many length buckets a - /// column can show. Under [`Padding::Fixed`] the fixed length is also a + /// ciphertext size and encryption time, and how many length buckets the + /// ciphertexts can show. Under [`Padding::Fixed`] the fixed length is also a /// ceiling, and the lower of the two applies. `init_with` refuses a /// ceiling of 0 or one that pads past [`MAX_STR_BYTES`]. pub fn with_max_bytes(self, max_bytes: usize) -> Self { @@ -406,6 +439,31 @@ impl VarLeft { pub fn padding(&self) -> Padding { self.padding } + + /// Parse the bytes produced by [`Self::to_bytes`]. + pub fn from_slice(data: &[u8]) -> Result { + let (count, padding) = parse_chained_header(data).ok_or(ParseError)?; + if data.len() != left_len(count) { + return Err(ParseError); + } + if !symbols_in_domain(data, count) { + return Err(ParseError); + } + Ok(parse_left_view(data, count, padding)) + } +} + +/// Read `xt` and `f` out of a validated left view (a `VarLeft` or the +/// prefix of a full ciphertext) of `count` blocks, tagged `padding`. +fn parse_left_view(data: &[u8], count: usize, padding: Padding) -> VarLeft { + let xt = data[HEADER_LEN..HEADER_LEN + count].to_vec(); + let mut f = Vec::with_capacity(count); + for n in 0..count { + let mut fb = [0u8; F_LEN]; + fb.copy_from_slice(f_at(data, count, n)); + f.push(fb); + } + VarLeft { xt, f, padding } } impl VarCipherText { @@ -441,13 +499,7 @@ impl VarCipherText { if !symbols_in_domain(data, count) { return Err(ParseError); } - let xt = data[HEADER_LEN..HEADER_LEN + count].to_vec(); - let mut f = Vec::with_capacity(count); - for n in 0..count { - let mut fb = [0u8; F_LEN]; - fb.copy_from_slice(f_at(data, count, n)); - f.push(fb); - } + let left = parse_left_view(data, count, padding); let mut nonce = [0u8; NONCE_LEN]; nonce.copy_from_slice(nonce_at(data, count)); let mut blocks = Vec::with_capacity(count); @@ -457,15 +509,20 @@ impl VarCipherText { blocks.push(rb); } Ok(VarCipherText { - left: VarLeft { xt, f, padding }, + left, right: VarRight { nonce, blocks }, }) } } /// Write the wire header: version, scheme id, block count, padding policy. +/// +/// Every ciphertext has at most `u16::MAX` blocks: encryption refuses more +/// ([`OreError::TooManyBlocks`]) and parsing reads the count from a `u16`. +/// Were a longer one made, the saturated count would not match its body, so +/// every parser would refuse the bytes. fn write_header(count: usize, padding: Padding, out: &mut Vec) { - WIRE.write(count, out); + WIRE.write(u16::try_from(count).unwrap_or(u16::MAX), out); out.extend_from_slice(&padding.to_wire()); } @@ -542,7 +599,7 @@ impl OreAes128Bit6Chained { } /// Derive the per-block PRP from the accumulator's `PRP_STREAM` branch. - fn prp_at(&self, acc: &CmacAccumulator, n: u16) -> Result, OreError> { + fn prp_at(&self, acc: &CmacAccumulator, n: u16) -> LemireFyPrp { let mut stream = [0u8; STREAM_BLOCKS * 16]; for (c, chunk) in stream.chunks_mut(16).enumerate() { chunk.copy_from_slice(&acc.finalize(&final_block( @@ -552,9 +609,9 @@ impl OreAes128Bit6Chained { WIDTH_BIT6, ))); } - let prp = LemireFyPrp::::from_stream(&stream)?; + let prp = LemireFyPrp::::from_stream(&stream); stream.zeroize(); - Ok(prp) + prp } /// Full Left+Right ciphertext for raw symbols `x` (one 6-bit symbol per @@ -600,7 +657,7 @@ impl OreAes128Bit6Chained { for n in 0..count { let sym = x.symbol(n); let n16 = n as u16; - let prp = self.prp_at(&acc, n16)?; + let prp = self.prp_at(&acc, n16); let permuted = prp.permute(sym).get(); xt.push(permuted); @@ -669,7 +726,7 @@ impl OreAes128Bit6Chained { for n in 0..count { let sym = x.symbol(n); let n16 = n as u16; - let prp = self.prp_at(&acc, n16)?; + let prp = self.prp_at(&acc, n16); let permuted = prp.permute(sym).get(); xt.push(permuted); f.push(acc.finalize(&final_block( @@ -711,6 +768,52 @@ impl OreAes128Bit6Chained { }) } + /// Encrypt a fixed-length value (`u128`, `i128`, `Decimal`, …) through + /// its order-preserving [`FixedOrderableBytes`] encoding. This is how + /// types too wide for the packed prefix of `OreAes128Bit6` (more than + /// 14 blocks) get a 6-bit-block ciphertext: the accumulator has no + /// length cap. + /// + /// Every value of one type encodes to the same number of bytes, so the + /// lexicographic comparators order these ciphertexts as the plaintexts. + /// The encoding is not padded (there is no length to hide) and the + /// cipher's [`Options`] do not apply: the ciphertext is tagged + /// [`Padding::None`], and the string ceiling is not checked. + /// The output is an untyped [`VarCipherText`]; the [`OreEncrypt`] impls + /// wrap it in [`ChainedCipherText`], which pins the block count so the + /// typed parsers and comparators refuse a ciphertext of another length + /// or a padded one. That is a length guard, not a type guard: + /// ciphertexts of two types with equal-length encodings (`u128` and + /// `i128`, or an unpadded 16-byte string) compare, and equal encodings + /// under one key give equal ciphertexts. Use one key per kind of value. + /// + /// A type whose encoding is longer than [`MAX_STR_BYTES`] returns + /// [`OreError::TooManyBlocks`] before the encoding is made (`[u8; N]` + /// is a `FixedOrderableBytes` for any `N`). The encoding is borrowed + /// where the type allows it, so a byte array is never copied onto the + /// stack. + pub fn encrypt_fixed(&self, x: &T) -> Result { + check_fixed_len::()?; + // The borrowed encoding: a byte array is not copied, so a large one + // leaves no plaintext past the scrub's reach. A type that does + // build its encoding (an integer's 16 bytes) builds it in this + // frame, so it is made inside the scrub. + stack::scrubbed(|| { + let bytes = x.to_orderable_bytes(); + self.encrypt_blocks(&bytes_to_blocks(bytes.as_ref())?, Padding::None) + }) + } + + /// Left-only ciphertext of a fixed-length value; see + /// [`Self::encrypt_fixed`]. + pub fn encrypt_left_fixed(&self, x: &T) -> Result { + check_fixed_len::()?; + stack::scrubbed(|| { + let bytes = x.to_orderable_bytes(); + self.encrypt_left_blocks(&bytes_to_blocks(bytes.as_ref())?, Padding::None) + }) + } + /// Compare two serialised full ciphertexts (lexicographic; shorter prefix /// sorts first). `None` if either is not a well-formed ciphertext of this /// scheme, or if they were padded under different policies. @@ -804,6 +907,232 @@ fn symbols_in_domain(view: &[u8], count: usize) -> bool { (0..count).all(|i| usize::from(xt_at(view, i)) < DOMAIN) } +/// The block count of a serialised view whose header is this scheme's and +/// whose padding tag is [`Padding::None`], the only tag the fixed-length +/// path writes; `None` for any other header. +fn unpadded_count(view: &[u8]) -> Option { + let (count, padding) = parse_chained_header(view)?; + (padding == Padding::None).then_some(count) +} + +// --------------------------------------------------------------------------- +// Fixed-length plaintexts: block-count-typed outputs and `OreEncrypt` impls. +// --------------------------------------------------------------------------- + +/// Left half of a chained ciphertext for a fixed-length plaintext of exactly +/// `BLOCKS` 6-bit blocks (a `u128` or `i128` is [`U128_BLOCKS`] = 22). The +/// bytes are those of an unpadded [`VarLeft`] of that length — same wire +/// format, same scheme id, tagged [`Padding::None`] — so the untyped +/// comparators accept them. The type pins the block count: +/// [`OreOutput::from_slice`] and the typed comparators on +/// [`ChainedCipherText`] reject any other count, and any padded ciphertext, +/// which keeps a fixed-length type's ciphertexts out of the lexicographic +/// (shorter-first) ordering the scheme applies across lengths. +#[derive(Clone, Debug)] +pub struct ChainedLeft(VarLeft); + +/// Full chained ciphertext for a fixed-length plaintext of exactly `BLOCKS` +/// 6-bit blocks; see [`ChainedLeft`]. +#[derive(Clone, Debug)] +pub struct ChainedCipherText(VarCipherText); + +impl ChainedLeft { + /// The untyped ciphertext. + pub fn as_var(&self) -> &VarLeft { + &self.0 + } + + /// The untyped ciphertext. + pub fn into_var(self) -> VarLeft { + self.0 + } +} + +impl ChainedCipherText { + /// The untyped ciphertext. + pub fn as_var(&self) -> &VarCipherText { + &self.0 + } + + /// The untyped ciphertext. + pub fn into_var(self) -> VarCipherText { + self.0 + } + + /// Compare two serialised full ciphertexts of exactly `BLOCKS` blocks, + /// both unpadded. `None` if either is malformed, padded, or has another + /// block count, where [`OreAes128Bit6Chained::compare_raw_slices`] + /// would order them lexicographically. An unpadded 16-byte string is + /// also 22 blocks: the count is a guard against mixing types, not a + /// proof of the plaintext type. + pub fn compare_raw_slices(a: &[u8], b: &[u8]) -> Option { + if unpadded_count(a)? != BLOCKS || unpadded_count(b)? != BLOCKS { + return None; + } + OreAes128Bit6ChainedChaCha20::compare_raw_slices(a, b) + } + + /// Compare a serialised [`ChainedLeft`] against a serialised full + /// ciphertext, both of exactly `BLOCKS` blocks; see + /// [`Self::compare_raw_slices`] and + /// [`OreAes128Bit6Chained::compare_left_to_full`]. + pub fn compare_left_to_full(left: &[u8], full: &[u8]) -> Option { + if unpadded_count(left)? != BLOCKS || unpadded_count(full)? != BLOCKS { + return None; + } + OreAes128Bit6ChainedChaCha20::compare_left_to_full(left, full) + } +} + +impl From> for VarLeft { + fn from(left: ChainedLeft) -> Self { + left.0 + } +} + +impl From> for VarCipherText { + fn from(ct: ChainedCipherText) -> Self { + ct.0 + } +} + +/// Succeeds only for an unpadded ciphertext of exactly `BLOCKS` blocks. +impl TryFrom for ChainedLeft { + type Error = ParseError; + fn try_from(left: VarLeft) -> Result { + if left.xt.len() != BLOCKS || left.padding != Padding::None { + return Err(ParseError); + } + Ok(Self(left)) + } +} + +/// Succeeds only for an unpadded ciphertext of exactly `BLOCKS` blocks. +impl TryFrom for ChainedCipherText { + type Error = ParseError; + fn try_from(ct: VarCipherText) -> Result { + if ct.left.xt.len() != BLOCKS || ct.left.padding != Padding::None { + return Err(ParseError); + } + Ok(Self(ct)) + } +} + +impl OreOutput for ChainedLeft { + fn size() -> usize { + left_len(BLOCKS) + } + + fn to_bytes(&self) -> Vec { + self.0.to_bytes() + } + + fn from_slice(data: &[u8]) -> Result { + VarLeft::from_slice(data)?.try_into() + } +} + +impl OreOutput for ChainedCipherText { + fn size() -> usize { + total_len(BLOCKS) + } + + fn to_bytes(&self) -> Vec { + self.0.to_bytes() + } + + fn from_slice(data: &[u8]) -> Result { + VarCipherText::from_slice(data)?.try_into() + } +} + +/// `OreEncrypt` for a fixed-length type on the chained scheme. Each +/// invocation also emits a test that the type round-trips, orders `$lo` +/// below `$hi`, and compares equal across nonces, so a type cannot be +/// listed as supported without being exercised (issue #105). +macro_rules! impl_ore_encrypt_chained { + ($type:ty, $blocks_const:ident, $tests:ident, $lo:expr, $hi:expr) => { + /// Block count of this type on the chained scheme (6-bit blocks). + pub const $blocks_const: usize = + num_blocks_6bit(<$type as FixedOrderableBytes>::ENCODED_LEN); + + impl OreEncrypt> for $type { + type LeftOutput = ChainedLeft<$blocks_const>; + type FullOutput = ChainedCipherText<$blocks_const>; + + fn encrypt_left( + &self, + cipher: &OreAes128Bit6Chained, + ) -> Result { + cipher.encrypt_left_fixed(self).map(ChainedLeft) + } + + fn encrypt( + &self, + cipher: &OreAes128Bit6Chained, + ) -> Result { + cipher.encrypt_fixed(self).map(ChainedCipherText) + } + } + + #[cfg(test)] + mod $tests { + use super::*; + + #[test] + fn round_trip_order_and_equality() { + let ore = OreAes128Bit6ChainedChaCha20::init(&[0x11; 16]).unwrap(); + let (lo, hi): ($type, $type) = ($lo, $hi); + assert!(lo < hi); + + let lo_ct = lo.encrypt(&ore).unwrap(); + let hi_ct = hi.encrypt(&ore).unwrap(); + let lo_left = lo.encrypt_left(&ore).unwrap(); + let lo_ct2 = lo.encrypt(&ore).unwrap(); + + let lo_bytes = lo_ct.to_bytes(); + let hi_bytes = hi_ct.to_bytes(); + assert_eq!(lo_bytes.len(), ChainedCipherText::<$blocks_const>::size()); + assert_eq!( + lo_left.to_bytes().len(), + ChainedLeft::<$blocks_const>::size() + ); + assert_eq!( + ChainedCipherText::<$blocks_const>::from_slice(&lo_bytes) + .unwrap() + .to_bytes(), + lo_bytes + ); + assert_eq!( + ChainedLeft::<$blocks_const>::from_slice(&lo_left.to_bytes()) + .unwrap() + .to_bytes(), + lo_left.to_bytes() + ); + + let cmp = ChainedCipherText::<$blocks_const>::compare_raw_slices; + assert_eq!(cmp(&lo_bytes, &hi_bytes), Some(Ordering::Less)); + assert_eq!(cmp(&hi_bytes, &lo_bytes), Some(Ordering::Greater)); + assert_ne!(lo_bytes, lo_ct2.to_bytes()); + assert_eq!(cmp(&lo_bytes, &lo_ct2.to_bytes()), Some(Ordering::Equal)); + assert_eq!( + ChainedCipherText::<$blocks_const>::compare_left_to_full( + &lo_left.to_bytes(), + &hi_bytes + ), + Some(Ordering::Less) + ); + } + } + }; +} + +// `u128`/`i128` are 22 blocks, past the 14-block packed prefix of +// `OreAes128Bit6` and the 15-byte limit of the legacy scheme; the chained +// scheme is the only one that takes them. +impl_ore_encrypt_chained!(u128, U128_BLOCKS, u128_tests, 0u128, u128::MAX); +impl_ore_encrypt_chained!(i128, I128_BLOCKS, i128_tests, i128::MIN, i128::MAX); + /// Longest string, in UTF-8 bytes, whose block count fits the wire format's /// `u16`: `⌈8n/6⌉ ≤ 65_535` gives `n ≤ 49_151`. pub const MAX_STR_BYTES: usize = u16::MAX as usize * 6 / 8; @@ -811,6 +1140,16 @@ pub const MAX_STR_BYTES: usize = u16::MAX as usize * 6 / 8; const _: () = assert!(num_blocks_6bit(MAX_STR_BYTES) <= u16::MAX as usize); const _: () = assert!(num_blocks_6bit(MAX_STR_BYTES + 1) > u16::MAX as usize); +/// Refuse a fixed-length type whose encoding would not fit the wire format, +/// before [`OreAes128Bit6Chained::encrypt_fixed`] copies the encoding onto +/// the stack. Decided by the type, so it never depends on the value. +fn check_fixed_len() -> Result<(), OreError> { + if T::ENCODED_LEN > MAX_STR_BYTES { + return Err(OreError::TooManyBlocks); + } + Ok(()) +} + /// Check raw symbols for the tests' `encrypt_var` and `encrypt_left_var`, /// refusing more blocks than the wire format can number *before* scanning /// them. @@ -845,6 +1184,20 @@ fn str_to_blocks(s: &str, options: &Options) -> Result )) } +/// Decompose `bytes` into 6-bit blocks, unpadded, refusing input too long +/// for the wire format *before* allocating or decomposing it. The fixed- +/// length path: the decomposition is the whole plaintext (it inverts), so +/// the buffer is wiped when dropped. +fn bytes_to_blocks(bytes: &[u8]) -> Result>>, OreError> { + if bytes.len() > MAX_STR_BYTES { + return Err(OreError::TooManyBlocks); + } + Ok(Blocks6::decompose( + bytes, + Zeroizing::new(vec![0u8; num_blocks_6bit(bytes.len())]), + )) +} + /// Refuse a string that ends in NUL: padded, it would encrypt to the same /// blocks as the string without it. One branch on the last byte, which /// reveals only whether the encryption is refused. @@ -1538,7 +1891,7 @@ mod tests { /// A ciphertext whose tag names a policy that cannot produce its block /// count was made by no cipher, and is refused: rewriting an unpadded - /// ciphertext's tag does not smuggle it into a padded column. + /// ciphertext's tag does not smuggle it in among padded ciphertexts. #[test] fn parsers_reject_counts_the_policy_cannot_produce() { type C = OreAes128Bit6ChainedChaCha20; @@ -1607,6 +1960,142 @@ mod tests { assert_eq!(cmp(&c, long_a, long_a), Ordering::Equal); } + /// A typed (fixed-length) ciphertext is a 22-block `Var*` on the wire, + /// so the untyped comparators order it against anything; the typed + /// parsers and comparators take only the pinned block count. + #[test] + fn fixed_length_outputs_pin_the_block_count() { + let c = ore(); + let n = 7u128.encrypt(&c).unwrap(); + let n_left = 7u128.encrypt_left(&c).unwrap(); + // Strings are unpadded here: a padded one carries another policy + // tag, which every comparator refuses before looking at the count. + let unpadded = ore_with(Padding::None); + // 5 bytes = 7 blocks. + let s = unpadded.encrypt_str("alice").unwrap(); + // 16 bytes = 22 blocks; and the same string padded (to 16 bytes). + let s16 = unpadded.encrypt_str("0123456789abcdef").unwrap(); + let s16_padded = c.encrypt_str("0123456789abcdef").unwrap(); + assert_eq!(s16_padded.left.xt.len(), U128_BLOCKS); + assert_eq!(n.as_var().left.xt.len(), U128_BLOCKS); + assert_eq!(U128_BLOCKS, 22); + assert_eq!(I128_BLOCKS, 22); + + // Same bytes as the untyped artefact of the same value. + assert_eq!( + VarCipherText::from_slice(&n.to_bytes()).unwrap().to_bytes(), + n.to_bytes() + ); + assert_eq!( + VarLeft::from_slice(&n_left.to_bytes()).unwrap().to_bytes(), + n_left.to_bytes() + ); + + // Untyped: lexicographic across lengths. Typed: refused. + assert!( + OreAes128Bit6ChainedChaCha20::compare_raw_slices(&n.to_bytes(), &s.to_bytes()) + .is_some() + ); + let cmp = ChainedCipherText::::compare_raw_slices; + assert_eq!(cmp(&n.to_bytes(), &s.to_bytes()), None); + assert_eq!(cmp(&s.to_bytes(), &n.to_bytes()), None); + assert_eq!( + ChainedCipherText::::compare_left_to_full( + &n_left.to_bytes(), + &s.to_bytes() + ), + None + ); + assert_eq!( + ChainedCipherText::::compare_left_to_full( + &unpadded.encrypt_left_str("alice").unwrap().to_bytes(), + &n.to_bytes() + ), + None + ); + // The count is the guard, not the type: an unpadded 16-byte string + // has 22 blocks too and passes. A padded one of 22 blocks does not. + assert!(cmp(&n.to_bytes(), &s16.to_bytes()).is_some()); + assert_eq!(cmp(&n.to_bytes(), &s16_padded.to_bytes()), None); + assert!(ChainedCipherText::::from_slice(&s16_padded.to_bytes()).is_err()); + assert!(ChainedCipherText::::try_from(s16_padded.clone()).is_err()); + assert!(ChainedCipherText::::from_slice(&s16.to_bytes()).is_ok()); + + // Typed parsers and conversions reject other counts. + assert!(ChainedCipherText::::from_slice(&s.to_bytes()).is_err()); + assert!(ChainedLeft::::from_slice( + &unpadded.encrypt_left_str("alice").unwrap().to_bytes() + ) + .is_err()); + assert!(ChainedCipherText::::try_from(s.clone()).is_err()); + assert!(ChainedCipherText::::try_from(n.clone().into_var()).is_ok()); + assert!(ChainedCipherText::<23>::from_slice(&n.to_bytes()).is_err()); + assert!(ChainedLeft::<23>::from_slice(&n_left.to_bytes()).is_err()); + } + + /// `encrypt_fixed` is what the `OreEncrypt` impls call: same bytes for + /// the left half, and the decomposition of the orderable encoding. + #[test] + fn encrypt_fixed_matches_the_typed_path() { + let c = ore(); + let x = 0x0123_4567_89ab_cdef_u128 << 64 | 0xfedc_ba98_7654_3210; + assert_eq!( + c.encrypt_left_fixed(&x).unwrap().to_bytes(), + x.encrypt_left(&c).unwrap().to_bytes() + ); + let blocks = bytes_to_blocks(&x.to_be_bytes()).unwrap(); + assert_eq!( + c.encrypt_left_var(blocks.as_bytes()).unwrap().to_bytes(), + x.encrypt_left(&c).unwrap().to_bytes() + ); + // Untyped output of the fixed path parses as the typed output. + let ct = c.encrypt_fixed(&x).unwrap(); + assert!(ChainedCipherText::::from_slice(&ct.to_bytes()).is_ok()); + } + + /// `[u8; N]` is a fixed-length type for any `N`: one past the wire + /// limit is refused by the type's length, before its encoding is + /// copied, and the largest that fits is accepted. + #[test] + fn fixed_types_past_the_wire_limit_are_refused_by_length() { + let c = ore(); + let too_wide = Box::new([0u8; MAX_STR_BYTES + 1]); + assert!(matches!( + c.encrypt_fixed(&*too_wide), + Err(OreError::TooManyBlocks) + )); + assert!(matches!( + c.encrypt_left_fixed(&*too_wide), + Err(OreError::TooManyBlocks) + )); + assert!(matches!(check_fixed_len::<[u8; MAX_STR_BYTES]>(), Ok(()))); + assert!(matches!( + check_fixed_len::<[u8; MAX_STR_BYTES + 1]>(), + Err(OreError::TooManyBlocks) + )); + } + + #[test] + fn signed_128_orders_across_zero() { + let c = ore(); + let vals = [i128::MIN, -1, 0, 1, i128::MAX]; + let cts: Vec> = vals + .iter() + .map(|v| v.encrypt(&c).unwrap().to_bytes()) + .collect(); + for (i, a) in cts.iter().enumerate() { + for (j, b) in cts.iter().enumerate() { + assert_eq!( + ChainedCipherText::::compare_raw_slices(a, b), + Some(vals[i].cmp(&vals[j])), + "{} vs {}", + vals[i], + vals[j] + ); + } + } + } + #[test] fn cross_scheme_bytes_rejected() { let c = ore(); @@ -1832,6 +2321,24 @@ mod tests { true } + /// Typed `u128` ciphertexts order as the integers, on both the full + /// and the left-query path. + fn prop_u128_order(a: u128, b: u128) -> bool { + let c = ore(); + let (ca, cb) = (a.encrypt(&c).unwrap().to_bytes(), b.encrypt(&c).unwrap().to_bytes()); + let la = a.encrypt_left(&c).unwrap().to_bytes(); + ChainedCipherText::::compare_raw_slices(&ca, &cb) == Some(a.cmp(&b)) + && ChainedCipherText::::compare_left_to_full(&la, &cb) == Some(a.cmp(&b)) + } + + fn prop_i128_order(a: i128, b: i128) -> bool { + let c = ore(); + let (ca, cb) = (a.encrypt(&c).unwrap().to_bytes(), b.encrypt(&c).unwrap().to_bytes()); + let la = a.encrypt_left(&c).unwrap().to_bytes(); + ChainedCipherText::::compare_raw_slices(&ca, &cb) == Some(a.cmp(&b)) + && ChainedCipherText::::compare_left_to_full(&la, &cb) == Some(a.cmp(&b)) + } + /// Two encryptions of one plaintext (fresh nonces each) compare Equal, /// and never produce identical bytes (the nonce streams differ). fn prop_equality_across_nonces(x: Vec) -> bool { @@ -1877,7 +2384,7 @@ mod tests { let mut acc = CmacAccumulator::new(&ore.k_acc); let mut needles = Vec::new(); for (n, &sym) in blocks.as_bytes().iter().enumerate() { - let prp = ore.prp_at(&acc, n as u16).unwrap(); + let prp = ore.prp_at(&acc, n as u16); let symbols = (0..64).map(crate::primitives::Symbol::<64>::from_low_bits); needles.push(symbols.clone().map(|i| prp.permute(i).get()).collect()); needles.push(symbols.map(|i| prp.invert(i).get()).collect()); @@ -1929,6 +2436,58 @@ mod tests { ); } + /// A byte array as large as the wire format allows, on the heap: the + /// fixed-length encryptors must borrow it. A copy on the stack would be + /// larger than the optimised scrub and leave plaintext in dead stack. + fn assert_no_fixed_plaintext(encrypt: fn(&OreAes128Bit6ChainedChaCha20, &[u8; MAX_STR_BYTES])) { + use crate::primitives::stack::{residue, SCRUB_BYTES}; + fn plaintext() -> Box<[u8; MAX_STR_BYTES]> { + let v: Vec = (0..MAX_STR_BYTES).map(|i| (i * 37 + 11) as u8).collect(); + v.into_boxed_slice().try_into().unwrap() + } + // Windows from the start, middle and end of the array; a copy + // that outgrew the scrub leaves at least its deepest part behind. + let p = plaintext(); + let needles: Vec> = [0, MAX_STR_BYTES / 2, MAX_STR_BYTES - 64] + .iter() + .map(|&at| p[at..at + 64].to_vec()) + .collect(); + + // Positive control: the owned encoding, made under the scrub, + // survives it when it is larger than the scrub (optimised builds). + if MAX_STR_BYTES > SCRUB_BYTES { + let dead = residue::after(|| { + let p = plaintext(); + stack::scrubbed(|| { + core::hint::black_box(p.to_fixed_orderable_bytes()); + }); + }); + assert!(needles.iter().any(|n| residue::contains(&dead, n))); + } + + let dead = residue::after(move || encrypt(&ore(), &plaintext())); + for needle in &needles { + assert!( + !residue::contains(&dead, needle), + "plaintext left in dead stack" + ); + } + } + + #[test] + fn encrypt_fixed_leaves_no_large_array_on_the_stack() { + assert_no_fixed_plaintext(|ore, x| { + core::hint::black_box(ore.encrypt_fixed(x).unwrap()); + }); + } + + #[test] + fn encrypt_left_fixed_leaves_no_large_array_on_the_stack() { + assert_no_fixed_plaintext(|ore, x| { + core::hint::black_box(ore.encrypt_left_fixed(x).unwrap()); + }); + } + #[test] fn encrypt_left_str_leaves_no_prp_tables_on_the_stack() { assert_no_block_tables( diff --git a/packages/ore-rs/src/scheme/decompose.rs b/packages/ore-rs/src/scheme/decompose.rs index 38d2719a..a26b0c07 100644 --- a/packages/ore-rs/src/scheme/decompose.rs +++ b/packages/ore-rs/src/scheme/decompose.rs @@ -23,14 +23,14 @@ pub(crate) const fn num_blocks_6bit(n: usize) -> usize { } /// Decompose `bytes` MSB-first into 6-bit block values, writing them to -/// `out`. `out.len()` must be exactly `num_blocks_6bit(bytes.len())`. +/// `out`, which is normally `num_blocks_6bit(bytes.len())` long. /// /// Block `i` holds plaintext bits `[6i, 6i + 6)` (bit 0 = MSB of byte 0), /// so lexicographic order over block values equals lexicographic order over -/// the input bytes. +/// the input bytes. Bits past the end of `bytes` read as zero, so a longer +/// `out` holds the decomposition of `bytes` followed by NULs, and a shorter +/// one a prefix of it. pub(crate) fn decompose_6bit(bytes: &[u8], out: &mut [u8]) { - debug_assert_eq!(out.len(), num_blocks_6bit(bytes.len())); - for (i, slot) in out.iter_mut().enumerate() { let bit = 6 * i; let byte = bit / 8; @@ -38,7 +38,7 @@ pub(crate) fn decompose_6bit(bytes: &[u8], out: &mut [u8]) { // Take 6 bits starting at `offset` within `bytes[byte]`, spilling // into the next byte (or zero padding past the end). - let hi = bytes[byte] as u16; + let hi = *bytes.get(byte).unwrap_or(&0) as u16; let lo = *bytes.get(byte + 1).unwrap_or(&0) as u16; let window = (hi << 8) | lo; @@ -103,8 +103,8 @@ impl> Blocks6 { } impl + AsMut<[u8]>> Blocks6 { - /// Decompose `bytes` into `out` (see [`decompose_6bit`]). - /// `out.as_ref().len()` must be `num_blocks_6bit(bytes.len())`. + /// Decompose `bytes` into `out` (see [`decompose_6bit`]), which is + /// normally `num_blocks_6bit(bytes.len())` long. pub(crate) fn decompose(bytes: &[u8], mut out: B) -> Self { decompose_6bit(bytes, out.as_mut()); Self(out) @@ -114,11 +114,10 @@ impl + AsMut<[u8]>> Blocks6 { /// `num_blocks_6bit(bytes.len())` blocks of `out`, and zeros after. NUL /// bytes add only zero bits, so this is the decomposition of `bytes` /// padded with NULs to any length with `out.as_ref().len()` blocks, - /// which must be at least `num_blocks_6bit(bytes.len())`. + /// which should be at least `num_blocks_6bit(bytes.len())` (a shorter + /// `out` holds a prefix; see [`decompose_6bit`]). pub(crate) fn decompose_padded(bytes: &[u8], mut out: B) -> Self { - let (head, tail) = out.as_mut().split_at_mut(num_blocks_6bit(bytes.len())); - decompose_6bit(bytes, head); - tail.fill(0); + decompose_6bit(bytes, out.as_mut()); Self(out) } } diff --git a/packages/ore-rs/src/scheme/width.rs b/packages/ore-rs/src/scheme/width.rs index 3cff3cbf..e6d5089b 100644 --- a/packages/ore-rs/src/scheme/width.rs +++ b/packages/ore-rs/src/scheme/width.rs @@ -100,7 +100,6 @@ pub(crate) fn ct_select_byte(block: &[u8], idx: usize) -> u8 { #[inline] pub(crate) fn ct_assign_bytes(dst: &mut [u8], src: &[u8], choice: subtle_ng::Choice) { use subtle_ng::ConditionallySelectable; - debug_assert_eq!(dst.len(), src.len()); for (d, s) in dst.iter_mut().zip(src) { d.conditional_assign(s, choice); } @@ -170,7 +169,8 @@ impl FirstDiff { /// `xt` either. #[inline] pub(crate) fn resolve(&self, hasher: &H) -> u8 { - debug_assert!(usize::from(self.xt) < R * 8); + // `xt` is a parsed, in-domain symbol. Were it not, the oblivious + // select below would read no byte and give 0, not go out of bounds. let byte = ct_select_byte(&self.right, usize::from(self.xt / 8)); ct_bit(byte, self.xt % 8) ^ hasher.hash(&self.f) } @@ -183,16 +183,20 @@ pub trait RightBitVec { /// scheme; the legacy comparator calls the inherent method.) #[allow(dead_code)] fn get_bit(&self, bit: usize) -> u8; + /// The raw bitvector, `[u8; DOMAIN / 8]`. + type Bits; /// The raw bitvector bytes, LSB-first within each byte (the same bit /// order as `RightBlock32::set_bit`), for bulk mask construction. - fn as_mut_bytes(&mut self) -> &mut [u8]; + fn as_mut_bits(&mut self) -> &mut Self::Bits; } impl RightBitVec for RightBlock32 { + type Bits = [u8; 32]; + fn get_bit(&self, bit: usize) -> u8 { RightBlock32::get_bit(self, bit) } - fn as_mut_bytes(&mut self) -> &mut [u8] { + fn as_mut_bits(&mut self) -> &mut [u8; 32] { self.bytes_mut() } } @@ -209,10 +213,20 @@ pub trait BlockWidth: sealed::Sealed + 'static { type RightBlock: CipherTextBlock + RightBitVec; /// A block value: a [`Symbol`] of the `DOMAIN`-element domain. type Symbol: Copy; - /// PRP over the block domain. - type Prp: Prp; + /// PRP over the block domain. Its indicator mask is a right block's + /// bitvector. + type Prp: Prp::Bits>; /// Buffer holding `DOMAIN` random-oracle keys. type RoKeyBuf: AesBlockBuf; + + /// Hash the `DOMAIN` random-oracle keys in `ro` (in place, trashing + /// them) into `out`, one bit per key: [`Hash::hash_all_into`] at this + /// width's sizes, which the types fix. + fn hash_into( + hasher: &H, + ro: &mut Self::RoKeyBuf, + out: &mut ::Bits, + ); } /// The 8-bit block width used by the legacy [`crate::scheme::bit2`] scheme: @@ -226,6 +240,10 @@ impl BlockWidth for Bit8 { type Symbol = Symbol<256>; type Prp = crate::primitives::prp::KnuthShufflePRP; type RoKeyBuf = [AesBlock; 256]; + + fn hash_into(hasher: &H, ro: &mut [AesBlock; 256], out: &mut [u8; 32]) { + hasher.hash_all_into(ro, out); + } } /// The 6-bit block width used by [`crate::scheme::bit2_w6`]: six plaintext @@ -243,6 +261,10 @@ impl BlockWidth for Bit6 { type Prp = crate::primitives::prp::LemireFyPrp<64>; type Symbol = Symbol<64>; type RoKeyBuf = [AesBlock; 64]; + + fn hash_into(hasher: &H, ro: &mut [AesBlock; 64], out: &mut [u8; 8]) { + hasher.hash_all_into(ro, out); + } } #[cfg(kani)] diff --git a/packages/ore-rs/tests/compat_chained_vectors.rs b/packages/ore-rs/tests/compat_chained_vectors.rs index a4b333c4..e37963c3 100644 --- a/packages/ore-rs/tests/compat_chained_vectors.rs +++ b/packages/ore-rs/tests/compat_chained_vectors.rs @@ -24,8 +24,10 @@ //! ``` use ore_rs::scheme::chained::{ - Options, OreAes128Bit6Chained, OreAes128Bit6ChainedChaCha20, Padding, VarCipherText, + ChainedCipherText, ChainedLeft, Options, OreAes128Bit6Chained, OreAes128Bit6ChainedChaCha20, + Padding, VarCipherText, I128_BLOCKS, U128_BLOCKS, }; +use ore_rs::{OreEncrypt, OreOutput}; use rand::{RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng; use std::cmp::Ordering; @@ -117,6 +119,11 @@ const STRINGS: &[&str] = &[ "bob", ]; +/// Fixed-length integers pinned through the `OreEncrypt` impls (22 blocks +/// each): the domain edges, zero, and a value with every byte distinct. +const U128S: &[u128] = &[0, 1, 0x0123_4567_89ab_cdef_fedc_ba98_7654_3210, u128::MAX]; +const I128S: &[i128] = &[i128::MIN, -1, 0, 1, i128::MAX]; + #[path = "compat_chained_vectors/vectors.rs"] mod vectors; use vectors::*; @@ -183,6 +190,127 @@ fn full_str_vectors_other_policies() { } } +#[test] +fn left_u128_vectors() { + let ore = cipher_left(); + for (x, expected) in LEFT_U128 { + let left = x.encrypt_left(&ore).unwrap(); + assert_eq!( + hex::encode(left.to_bytes()), + *expected, + "left mismatch for {x}" + ); + assert_eq!(left.to_bytes().len(), ChainedLeft::::size()); + } + for (x, expected) in LEFT_I128 { + let left = x.encrypt_left(&ore).unwrap(); + assert_eq!( + hex::encode(left.to_bytes()), + *expected, + "left mismatch for {x}" + ); + assert_eq!(left.to_bytes().len(), ChainedLeft::::size()); + } +} + +#[test] +fn full_u128_vectors() { + for (x, expected) in FULL_U128 { + let ct = x.encrypt(&cipher_a()).unwrap(); + assert_eq!( + hex::encode(ct.to_bytes()), + *expected, + "full mismatch for {x}" + ); + assert_eq!( + ct.to_bytes().len(), + ChainedCipherText::::size() + ); + } + for (x, expected) in FULL_I128 { + let ct = x.encrypt(&cipher_a()).unwrap(); + assert_eq!( + hex::encode(ct.to_bytes()), + *expected, + "full mismatch for {x}" + ); + assert_eq!( + ct.to_bytes().len(), + ChainedCipherText::::size() + ); + } +} + +/// Pinned integer ciphertexts order as the integers under the typed +/// comparators, and parse back through the typed parsers. +#[test] +fn compare_u128_vectors_matches_integer_order() { + for (xa, ha) in FULL_U128 { + for (xb, hb) in FULL_U128 { + assert_eq!( + ChainedCipherText::::compare_raw_slices(&pinned(ha), &pinned(hb)), + Some(xa.cmp(xb)), + "u128 {xa} vs {xb}" + ); + } + for (xl, hl) in LEFT_U128 { + assert_eq!( + ChainedCipherText::::compare_left_to_full(&pinned(hl), &pinned(ha)), + Some(xl.cmp(xa)), + "u128 left {xl} vs {xa}" + ); + } + let ct = ChainedCipherText::::from_slice(&pinned(ha)).unwrap(); + assert_eq!(ct.to_bytes(), pinned(ha)); + } + for (xa, ha) in FULL_I128 { + for (xb, hb) in FULL_I128 { + assert_eq!( + ChainedCipherText::::compare_raw_slices(&pinned(ha), &pinned(hb)), + Some(xa.cmp(xb)), + "i128 {xa} vs {xb}" + ); + } + for (xl, hl) in LEFT_I128 { + assert_eq!( + ChainedCipherText::::compare_left_to_full(&pinned(hl), &pinned(ha)), + Some(xl.cmp(xa)), + "i128 left {xl} vs {xa}" + ); + } + } +} + +/// The typed comparator refuses a string ciphertext of another block count; +/// the untyped one orders an unpadded one lexicographically. A padded +/// string carries another policy tag, which both refuse. +#[test] +fn typed_comparator_rejects_other_block_counts() { + let n = pinned(FULL_U128[0].1); + let (alice, unpadded) = FULL_STR_UNPADDED + .iter() + .find(|(s, _)| *s == "alice") + .map(|(s, h)| (*s, pinned(h))) + .unwrap(); + assert_eq!(alice, "alice"); + assert_eq!( + ChainedCipherText::::compare_raw_slices(&n, &unpadded), + None + ); + assert!(OreAes128Bit6ChainedChaCha20::compare_raw_slices(&n, &unpadded).is_some()); + assert!(ChainedCipherText::::from_slice(&unpadded).is_err()); + + let padded = pinned(full_alice()); + assert_eq!( + ChainedCipherText::::compare_raw_slices(&n, &padded), + None + ); + assert_eq!( + OreAes128Bit6ChainedChaCha20::compare_raw_slices(&n, &padded), + None + ); +} + #[test] fn full_alice_alternate_nonce() { let ct = cipher_b().encrypt_str("alice").unwrap(); @@ -487,4 +615,36 @@ fn generate() { } println!("];"); } + println!(); + + println!("pub const LEFT_U128: &[(u128, &str)] = &["); + for x in U128S { + let left = x.encrypt_left(&ore).unwrap(); + println!(" ({x}, \"{}\"),", hex::encode(left.to_bytes())); + } + println!("];"); + println!(); + + println!("pub const LEFT_I128: &[(i128, &str)] = &["); + for x in I128S { + let left = x.encrypt_left(&ore).unwrap(); + println!(" ({x}, \"{}\"),", hex::encode(left.to_bytes())); + } + println!("];"); + println!(); + + println!("pub const FULL_U128: &[(u128, &str)] = &["); + for x in U128S { + let ct = x.encrypt(&cipher_a()).unwrap(); + println!(" ({x}, \"{}\"),", hex::encode(ct.to_bytes())); + } + println!("];"); + println!(); + + println!("pub const FULL_I128: &[(i128, &str)] = &["); + for x in I128S { + let ct = x.encrypt(&cipher_a()).unwrap(); + println!(" ({x}, \"{}\"),", hex::encode(ct.to_bytes())); + } + println!("];"); } diff --git a/packages/ore-rs/tests/compat_chained_vectors/vectors.rs b/packages/ore-rs/tests/compat_chained_vectors/vectors.rs index 33b80380..9fefc471 100644 --- a/packages/ore-rs/tests/compat_chained_vectors/vectors.rs +++ b/packages/ore-rs/tests/compat_chained_vectors/vectors.rs @@ -69,3 +69,33 @@ pub const FULL_STR_FIXED: &[(&str, &str)] = &[ ("alicf", "020300360200283933281b2b370f1d160003093a28212410393f3c1e2610380d3f06122909090e34362a062f2f221a391f20250a153b32070f291b2321367ae8e098c5628373545e3027b3940019889a97dee6bd3b45d3e9df587803f858651a8d94901d58e061adcbe4a2873f98ce0340bbca2f8c50b6037d68324d54934c1d5eeccca87af1a0ddfabb106717331ae99d5a41b641fe7ea107efd8108e5a463f1ba05316b6c7337631775afaf6fdcbb35f5a17dbe24a8b24e10ac1010e1806002a3d79d789cc4e15656e17bff81e82e676c22f2880eb508d603cb248f7f5b9f1afb088ec7b87dc8574ec0064742c5cc22ecdd321903f5590f1f9dec1c9d1ab73068df42316ad1dbcf6874e41c20880f149c147ce1ae372b7192966683e9185b889a9bd358a91e65f9fdcaeff64b6b6a8c6684e02a8ac3e5089bfb60b4b8626829411da26cb65e224121ea4f38d9156286a4a8b45815ac80ad0310512a1e73c50dab2ec083c32e200040a2ef9b993672af7fb0a10b203eb748e11afe8b01b694f246017e6fe1b7e388bddf3ebe2140fdfba3a5c3cc7c96c2559306c131210aa4a36c4a2b7a0b878c92c1f4224d511d74bc0668526ba125c74c3d8bcc3973e8ae157a45e03d17f964de09b6415905a7ed62bb42e7a60d61de7a2676ddba3ae1fd9aef7c525fd641f0503f8e032d036b508a3c244339c7d88534929526724c4ee659fb0869753aac25ca0a9a0aa14279a5d9f783ad9f813074c8495769b114b053b1dd3260e432393df19cac5afc56916b12b5304e0d6521a361ae035d14f438ded634be98aebac4cb877183f649aa743a87f8d2976284f0d7e1f448e14a82e5ef6ac2bbd66984225c44708f513f45ed566bc7274d237fe728631895c29c9ecaa5f063ea28b08a5c0be2313ff850651ba7666e460176e4a926ac2f23efdf6bb5d71601c3066cdc45271224402aaa4ac461e126a5d136d2f49d6cc77204b2bc95157e590c05d237b48f734c9a5285a410d629324a9ea90295f6cb330a9fe9d4379791e1737e8e73f27fd403a9eb09e04a0a697429209f8e5bd007ada044a6d22a035d19d5fba524e8517c7180a7acb3f5d77bb0d79e38e59fa08df16119eef624824f7deae81dfc8d8ffefe885e9fe5ca052b591967c016587ea4bb0a74288c05062bdbe4c8d910225d9122366aa3e129e835c86f79c98849dae9e03d40f3a3aabcc5bb197fe4d5a8c94368967f4f35034d3a926c216fa5671d3bbfda32f5893a3dfc2863d4575475f0b8a92040e73f1fcb7625fb4d53b7e78761d2302df6598191f46e5830216445436978803697ad909aecea3e41349e02b95ae7093e40add632becdd226d0b708753fab7a69a596e20b624d50e3853b8aa1c1297938c88fe70c4aa4a0a06ec6a2f37440f036e3d0f0e8220787a27e3df58a189ec88cfca3977d74b73aeb564bfed34c2e0c2814622d87e5d55ce2b38716476e65c0661c9b47a2710cb3be0a43062969982e8c68dcdbee03e76d94043b18b4a8777bf99916426da532da5733c18e143a8664e29eef67d8f655956eaf449090cda1c1dfdf14bc197a1a6feac478377014d692e04748bf979c59f4c62fc29d55912ed766eb4b0f69e135f838d6c3cac70f5c8204f90d039e2df60e017f2e5227863b52fd120466e8dc1e4a8ffef685c9a903ca9bb9b73f21023b0596f1073848ac9e285899b853878fcb9c931bd9ec86f635edab6536a5e24209b48508a1a71a90538077170b96d3126cadcb060a129cfe4f013b01ceef97a24a0cc4d3e5c2ce10c8c937b216d51c9846c859a38e91161f5600b908fba3c7010e6d5d8ac9b8cfb1a99862c8c8c7ed2a6ccf4742e303b37151ff9ee7fd65247bc4ad178f2e9ed9be6338552dc02bc32cd6f8c753adc4ff9dabccad04925acb426dcfdd43a8d589ed5fea80d1b"), ("bob", "0203003602002839161537281619211425122f023a3310161b3e173e083c05123506231d393600010a220823320e1f2a241c3a04143c31011213040619367ae8e098c5628373545e3027b394003da2a56461c75d557b49be80ffe925eccb16677fb78c701aec7e9917fcf1ec517ba5f2c16fec4488f1ee9a6d431e2ee65392355338c729e9aef437e524816912235b213478b67f75563f3029d145fbb696dde8f75b8c51bfba6263ad658ca48d4f4cf05a0fdb61eed7a31d992b8413197b4c80909829f0d18cec051ca72af5ca5cf0b6dca340c81cf0e521c04bf556665a96d4fd38686a081f91b7d0020f9a9aad2002f9c680b5c323fa4d3715a7fa8b8f3fb48057c64c5297b72dca1c33ddf0cac2f1b5bab88d904096767217426cd28fbf1b052ff2c3dc7fabe26b6ae36f4d802cc7170908d982574a04cc9f985e5430870b0ccdf5f9e749b806284ab14a7149b89b4789cb01ec5c5c9fec5cba1bfef6ab78126069b782a6813fa464220fe794574f5e1cd720b11f9f96d179ad5ad3d7b64981cf1fd6835354508f03403f6043b2c4a9853d41c8d303d751224f928ef1801c087d7dbba093684920d737492145f340af43c5aa9dda81da0acda49ada07a51114f3f93978cd9b81c13c5c55b65de9d5623482674cd9593e89e454f0949e38e482c116d342dc792dfff932e3381c490f0d06c06e22617cdf9f7ceb796c7991f9bb7fd7d9e0474febbd4e7fde0b6b86831ab486d81873aca9a7d002d1a68174889fb3d847dc583f37260c31e52243ade5ed4f0a4c32dde62fd78a0f5ff0e7d4f2b57625b82f04a5e5d3adb1f0fe872ddda85c34f37d13d1070243d752560c58a7f7ce8795ea5fb6fb6b9d3dde2e858345ae7486428114f3763839c635996258e5595ddca00be97ae347f867b458b39177378763d62048a5b30215508105d608a87adc975ec9c5189c221b4e14944a27b734706db5c6a46bfb655cdb8c4db62799192bdca0b601f6fca11bb101cb0fff7d56e2b0195655c8916187f8b43ad4cef2893e7b983178217fd8978e677222fca083d61b44db033cf3b448780a7803d50f14c678dd247d677e73a95d6a6238c884375bb75cf6de0ecf73e4b5645dfa4e2f118c9e1f0cea643d5dcd8bc894b28d4ab556eb18fb9ce2eb49111fd8e2be2989f5b0da9b4d044afc8758e23b7515cfa4386587cef6c43b00be1b05a253e5df75484892b6255c6d3e20728b49a79f399932e3b1c1cc6a4d253685258eb42a504201b93fcda5b8599b88043bdda6c1c9c6caa17e57925f925543f4f82a7e98191f46e5830216445436978803697ad909aecea3e41349e42bd7ddb591651a358d939875e2204f27ae45ae6d051c061b130a391ad5b1db3c3890dd2408c9b48401f6550698ac14640cb0acf6b9991f5e985d8c785e3937af2b08c3155845e42d232480d90ff884ffb749813cd5ca95c27317bca9d2013bf753669e68ce87551aae698df5eec378fc482691846481620e1fcdc1a1925541e1549716c17a45b7b86fda6951d8d031f9f3387369775d74f1263fb627f2a2a3cdf305228f584a0255b602c736727b62b79756f3f1b77092884dabd44aa4babf4ec9a807ddd635d504a2f9ac7b484c8de8d5f01dc59f004c87086f33b298aea24bd5135e3cb521ea8fa468e1daf0e62fe2e9b0c6a4413c7afe37c5e827d9a34ce0a858a87aa1c0d6d63d84b09639af96a0df51929cdc119d93508161938e0fec08a30e603e9817162e6a5a97b14e56af23e2cc4176f67dc6b75c062026a45311eb7d3058a37ca8bf676bbc9537f6b2d44056543b77068c78133d1891a597a4be85b14f38f92a04168abc1ab88f17de8c0d16fc1adacb923a68e6d6a39a631270c2644457ba2b07276be17144af45a1eac73c6903a750e1ba5ddac2d1dce783763fbc9b256d3ab310"), ]; + +pub const LEFT_U128: &[(u128, &str)] = &[ + (0, "020300160000001d3d170022252220062e182e1c39373a1b153e0410065d826fb3dfbb8090bbcde5ed24acacf0fbc06a3f7f2751d6db18b546cc98c3d0aeaaf841bf3cb2fba85d7b1d1b9d4157d3ffa2a4eb52058bf81e171b324b052c576f57bda58f63f43dda07e9a2a2638c4d5600fe23e1db08227d32b1328e8463aca6e93a4c0d5481cd5e68a39eb917f1517fc3d8df957fc13fa29e15db6822a7cf179133bf7055c301cfdc3adfc65bf4473b18c877aa37a474f4152c95e9aaf1a1ee1a7d20836c27f65070e3fae7b2102d73eaf9bb4a6fba39a353266082419f9f9b12d3286a06e71b596aad71cf7733cf20f779d28a3326093d8ba86567d38dd6c776c38126c319860eb749c07e5ae7014b2dd3c6014bfd87619070f1ba368867e7f6aff17d6936f08d41cfbc7f9297666eee56d7e5a99be4a57785e01943d18d3319e36bc425946471c8d8e8a544fde353aa5a875bebd0752b398e413480b55bee6bfdb5975960f5c236889fdef4fea52d96d3623339f50a37d87ee1c032c7"), + (1, "020300160000001d3d170022252220062e182e1c39373a1b153e0410145d826fb3dfbb8090bbcde5ed24acacf0fbc06a3f7f2751d6db18b546cc98c3d0aeaaf841bf3cb2fba85d7b1d1b9d4157d3ffa2a4eb52058bf81e171b324b052c576f57bda58f63f43dda07e9a2a2638c4d5600fe23e1db08227d32b1328e8463aca6e93a4c0d5481cd5e68a39eb917f1517fc3d8df957fc13fa29e15db6822a7cf179133bf7055c301cfdc3adfc65bf4473b18c877aa37a474f4152c95e9aaf1a1ee1a7d20836c27f65070e3fae7b2102d73eaf9bb4a6fba39a353266082419f9f9b12d3286a06e71b596aad71cf7733cf20f779d28a3326093d8ba86567d38dd6c776c38126c319860eb749c07e5ae7014b2dd3c6014bfd87619070f1ba368867e7f6aff17d6936f08d41cfbc7f9297666eee56d7e5a99be4a57785e01943d18d3319e36bc425946471c8d8e8a544fde353aa5a875bebd0752b398e413480b55bee6bfdb5975960f5c236889fdef4fe11fad85af517f3cd8969d53f11d6123d"), + (1512366075204170947332355369683137040, "020300160000001d090d20183b2e3e343a13143f2e2f050f032b063d235d826fb3dfbb8090bbcde5ed24acacf033cb63848c1924d21d62b4b4ba876321468321cbc63ba6d5efd0b2aa07dd8d22bd07c39b601817b88d35c07bfef5bb0def33e3635ddb322dc2abe3c8139df8983b7d202307e223c83dd20fc97de402dcd83207bf3128d085d365955ccb6025020ed684d07e6178988244c546ffe4898ef29758ae4c493390124d7ec88d96753a37e95bf98d41c9fbba972e6ad56f44734d8f13c405003ca48fa94d94b03c1ed7cd8c09aff62e1512bf57698a51909f1e4742fc434bfe3e300b63a2b7632299667dcd2fe8f1178c0d3c01d7880f8727162ddc7ae70f63e7153eaec6c38f153482fbcfed35454e2220dfd2832fa8c9b45e6aea49bcc57b0d2181409cbb630c9a82d53ee803ef1bb3b6d400cf83458214b744ffedd4cd3b249f44c989f67a8f1fdcaf0c0f0d729f57b6e8ccd1b2439317208bd659c1fde343bbb8c8763bd5d9e88750b04dd187a2f0b5c1206323d234536e"), + (340282366920938463463374607431768211455, "020300160000001c012537140012141c25342e373a0a2e10090d302f1bfc9c0ae91c66774477c4a9846a8f65aa7fb660a84a5e7f8872d29acbc1557e78279399c19d0b74d9460f6f5620b4eb57ce4a0b8d36ed95c277071ca46b389c8e5682e02633bcd397cfbe45d1dcc7d3c3c4ebe08e3a21605fdd12869e152ae90f286f71b03daa32b35f79930a45fc0971741d2c815362a51cfdea0a2d6198e902e3620b9b3a0a00220073eb4a204417dd314d2fc13a260eb4afb685fce8822bb8568871da673a29aba2b8be25604d98cb72bc44ae9f1129070837ef62f971692f7f4e6c460aa533d95498ddcd5953af3b470b785f15d711572e9658386f5024e8f5e234f33374e030ce055673193f308d07d2ac7992d735f617460368c2cc74afa65f070f3e1ef92a640bda8140273a7914f18d0911709ab8fcc5544a10002e46444b551a0dc2f633be0513f6945a6913df84624165ff15ce7f3211c4c46ef0961b4b05e1af6d21695c776cf62d8ac2e5a72072c7d94562999daa115609ecdc59"), +]; + +pub const LEFT_I128: &[(i128, &str)] = &[ + (-170141183460469231731687303715884105728, "020300160000001d3d170022252220062e182e1c39373a1b153e0410065d826fb3dfbb8090bbcde5ed24acacf0fbc06a3f7f2751d6db18b546cc98c3d0aeaaf841bf3cb2fba85d7b1d1b9d4157d3ffa2a4eb52058bf81e171b324b052c576f57bda58f63f43dda07e9a2a2638c4d5600fe23e1db08227d32b1328e8463aca6e93a4c0d5481cd5e68a39eb917f1517fc3d8df957fc13fa29e15db6822a7cf179133bf7055c301cfdc3adfc65bf4473b18c877aa37a474f4152c95e9aaf1a1ee1a7d20836c27f65070e3fae7b2102d73eaf9bb4a6fba39a353266082419f9f9b12d3286a06e71b596aad71cf7733cf20f779d28a3326093d8ba86567d38dd6c776c38126c319860eb749c07e5ae7014b2dd3c6014bfd87619070f1ba368867e7f6aff17d6936f08d41cfbc7f9297666eee56d7e5a99be4a57785e01943d18d3319e36bc425946471c8d8e8a544fde353aa5a875bebd0752b398e413480b55bee6bfdb5975960f5c236889fdef4fea52d96d3623339f50a37d87ee1c032c7"), + (-1, "020300160000002e1d000f0728193803273b2d3a14312f2b1d0c2c393e8d45d702c3baa9c11320c59cd64fb0170fc7a3ce698c5c5934bc587e835d6f85a0b52cf2a9ddc0e65872d3b9cd9b2938e0b6311eb0951d18bb160569e6e503a5b647c51918f2a0bf0821256b542fbf1a3119490b9e26e50d6cdead3f3584961c95a0fc83ef6359515dc0ec3664b258d5426ed1d68e4e32812415174cd77c01feda8937b26861246650f221f22354d011b160bc61684388670c68cda90d04fd7118b682bed95ee03679db942fcd4c3a6a76c6ec7882a97d41790050e7e7a3e97d5c0d01fedca787be884577ba52a20fb71fbe8128f3b0762424264512837ff37c13947be67871e76d47ae575580adc5e7d3a5a53db5fae68b524857aad5bbf68f272480b24cecc7d9e52bafb692810d7219e95fb9c0edd2ea378f9ed71d65c975aedb9bb46362eaf166ebf4f43c3a32001cfaaeba007e83995a1439762b5b1db03c9b4421843711098eb44d58985955a72a5eed9d458ebfee0ad29fcf127eadbe"), + (0, "0203001600000038183a122336323d2630320e202c0536232825281f01da70c42b151372073e985ba41fe3e6730d53f1ca868eea4bacfe6e3df5fdfa7d67083f40ff96506ad8c90ed73e54c84b6783875efb5f1fd08256f50dbad14a0205a48c31d0772f6e888d8848f56fac843975810b92085a17dd02ecc7a5bc1dd5fe8841b5925d2145731ff1271f3ead53dd05f81f121a15c00a300aa5f15b8fccd371e36cb04b6482014b5586819016296e6aac921b387f2a56da40c4833085bac88f76bfed67327d73fbdc7d98513ca8fd90c8b0d681f61a668521b7c9a7fa13781d4d664e0e1dff52d103b7f3ebe12c168ff0400c5fcf22f57c02aa831a02bbc6a99cb2f2529b7394a410935bfb388a7a0c9aa7b45e1f416b023cacc24a5b0372b08746d94557c2168ae483c27d7f334acc8eb15c7fc0884851af37dac14fd6fd903c93c5b464c9193704288ebf07dcd7333708bd94491fc5797729f48ffee13050fc27f48c7330054d352e913b4c7c76f17cbf56881f4525c35965cc62da59"), + (1, "0203001600000038183a122336323d2630320e202c0536232825281f22da70c42b151372073e985ba41fe3e6730d53f1ca868eea4bacfe6e3df5fdfa7d67083f40ff96506ad8c90ed73e54c84b6783875efb5f1fd08256f50dbad14a0205a48c31d0772f6e888d8848f56fac843975810b92085a17dd02ecc7a5bc1dd5fe8841b5925d2145731ff1271f3ead53dd05f81f121a15c00a300aa5f15b8fccd371e36cb04b6482014b5586819016296e6aac921b387f2a56da40c4833085bac88f76bfed67327d73fbdc7d98513ca8fd90c8b0d681f61a668521b7c9a7fa13781d4d664e0e1dff52d103b7f3ebe12c168ff0400c5fcf22f57c02aa831a02bbc6a99cb2f2529b7394a410935bfb388a7a0c9aa7b45e1f416b023cacc24a5b0372b08746d94557c2168ae483c27d7f334acc8eb15c7fc0884851af37dac14fd6fd903c93c5b464c9193704288ebf07dcd7333708bd94491fc5797729f48ffee13050fc27f48c7330054d352e913b4c7c76aa6d4f5c784620f006640e134e76ad"), + (170141183460469231731687303715884105727, "020300160000001c012537140012141c25342e373a0a2e10090d302f1bfc9c0ae91c66774477c4a9846a8f65aa7fb660a84a5e7f8872d29acbc1557e78279399c19d0b74d9460f6f5620b4eb57ce4a0b8d36ed95c277071ca46b389c8e5682e02633bcd397cfbe45d1dcc7d3c3c4ebe08e3a21605fdd12869e152ae90f286f71b03daa32b35f79930a45fc0971741d2c815362a51cfdea0a2d6198e902e3620b9b3a0a00220073eb4a204417dd314d2fc13a260eb4afb685fce8822bb8568871da673a29aba2b8be25604d98cb72bc44ae9f1129070837ef62f971692f7f4e6c460aa533d95498ddcd5953af3b470b785f15d711572e9658386f5024e8f5e234f33374e030ce055673193f308d07d2ac7992d735f617460368c2cc74afa65f070f3e1ef92a640bda8140273a7914f18d0911709ab8fcc5544a10002e46444b551a0dc2f633be0513f6945a6913df84624165ff15ce7f3211c4c46ef0961b4b05e1af6d21695c776cf62d8ac2e5a72072c7d94562999daa115609ecdc59"), +]; + +pub const FULL_U128: &[(u128, &str)] = &[ + (0, "020300160000001d3d170022252220062e182e1c39373a1b153e0410065d826fb3dfbb8090bbcde5ed24acacf0fbc06a3f7f2751d6db18b546cc98c3d0aeaaf841bf3cb2fba85d7b1d1b9d4157d3ffa2a4eb52058bf81e171b324b052c576f57bda58f63f43dda07e9a2a2638c4d5600fe23e1db08227d32b1328e8463aca6e93a4c0d5481cd5e68a39eb917f1517fc3d8df957fc13fa29e15db6822a7cf179133bf7055c301cfdc3adfc65bf4473b18c877aa37a474f4152c95e9aaf1a1ee1a7d20836c27f65070e3fae7b2102d73eaf9bb4a6fba39a353266082419f9f9b12d3286a06e71b596aad71cf7733cf20f779d28a3326093d8ba86567d38dd6c776c38126c319860eb749c07e5ae7014b2dd3c6014bfd87619070f1ba368867e7f6aff17d6936f08d41cfbc7f9297666eee56d7e5a99be4a57785e01943d18d3319e36bc425946471c8d8e8a544fde353aa5a875bebd0752b398e413480b55bee6bfdb5975960f5c236889fdef4fea52d96d3623339f50a37d87ee1c032c798191f46e5830216445436978803697a848f024abe74230b12db314a9699cd84d8ea9b4af271d79c82d9db07a87b3081c9a78eeb955a4a50a938e7bf92bbab93f61dd99079b86c7bd81e350f5141776e185307a33b8589f5bc0ac63cf9dc5195d30933b341948da34df9c3893753b72c8e1b407b9d5906f5337e878c708699c1fc1c70cd65f7f28fff1f879a07940768535c6619f795b5eb7454917c2e14f95b4b5513ff6689c07ce1ab5730ef9b261a2b4a787d29890e590a1b42bf275e19a7"), + (1, "020300160000001d3d170022252220062e182e1c39373a1b153e0410145d826fb3dfbb8090bbcde5ed24acacf0fbc06a3f7f2751d6db18b546cc98c3d0aeaaf841bf3cb2fba85d7b1d1b9d4157d3ffa2a4eb52058bf81e171b324b052c576f57bda58f63f43dda07e9a2a2638c4d5600fe23e1db08227d32b1328e8463aca6e93a4c0d5481cd5e68a39eb917f1517fc3d8df957fc13fa29e15db6822a7cf179133bf7055c301cfdc3adfc65bf4473b18c877aa37a474f4152c95e9aaf1a1ee1a7d20836c27f65070e3fae7b2102d73eaf9bb4a6fba39a353266082419f9f9b12d3286a06e71b596aad71cf7733cf20f779d28a3326093d8ba86567d38dd6c776c38126c319860eb749c07e5ae7014b2dd3c6014bfd87619070f1ba368867e7f6aff17d6936f08d41cfbc7f9297666eee56d7e5a99be4a57785e01943d18d3319e36bc425946471c8d8e8a544fde353aa5a875bebd0752b398e413480b55bee6bfdb5975960f5c236889fdef4fe11fad85af517f3cd8969d53f11d6123d98191f46e5830216445436978803697a848f024abe74230b12db314a9699cd84d8ea9b4af271d79c82d9db07a87b3081c9a78eeb955a4a50a938e7bf92bbab93f61dd99079b86c7bd81e350f5141776e185307a33b8589f5bc0ac63cf9dc5195d30933b341948da34df9c3893753b72c8e1b407b9d5906f5337e878c708699c1fc1c70cd65f7f28fff1f879a07940768535c6619f795b5eb7454917c2e14f95b4b5513ff6689c07ce1ab5730ef9b261a2b4a787d29890e590a19dcd7b61e19a0"), + (1512366075204170947332355369683137040, "020300160000001d090d20183b2e3e343a13143f2e2f050f032b063d235d826fb3dfbb8090bbcde5ed24acacf033cb63848c1924d21d62b4b4ba876321468321cbc63ba6d5efd0b2aa07dd8d22bd07c39b601817b88d35c07bfef5bb0def33e3635ddb322dc2abe3c8139df8983b7d202307e223c83dd20fc97de402dcd83207bf3128d085d365955ccb6025020ed684d07e6178988244c546ffe4898ef29758ae4c493390124d7ec88d96753a37e95bf98d41c9fbba972e6ad56f44734d8f13c405003ca48fa94d94b03c1ed7cd8c09aff62e1512bf57698a51909f1e4742fc434bfe3e300b63a2b7632299667dcd2fe8f1178c0d3c01d7880f8727162ddc7ae70f63e7153eaec6c38f153482fbcfed35454e2220dfd2832fa8c9b45e6aea49bcc57b0d2181409cbb630c9a82d53ee803ef1bb3b6d400cf83458214b744ffedd4cd3b249f44c989f67a8f1fdcaf0c0f0d729f57b6e8ccd1b2439317208bd659c1fde343bbb8c8763bd5d9e88750b04dd187a2f0b5c1206323d234536e98191f46e5830216445436978803697a848f024abe74230b800931e2bfdbe18402ae082c300b4e5ae6f1449a863109859c9c7b968f15cf17b6214ddd607af67a3d786e69744b783947559ef818c9363b0572b68e5c241954a6e088793fa1e1b830117ae5c8911c70de44876eebad39deb38537125d0ae38aecc63d5d25863458acad22772885c65a875cf5a5e6d24c6e027a308945d56469fcee2aec1f66176f9abb14a1a20d858642b60d80fd1edd9bbd3d14f98ebcc2dcf2eeeb2045265200"), + (340282366920938463463374607431768211455, "020300160000001c012537140012141c25342e373a0a2e10090d302f1bfc9c0ae91c66774477c4a9846a8f65aa7fb660a84a5e7f8872d29acbc1557e78279399c19d0b74d9460f6f5620b4eb57ce4a0b8d36ed95c277071ca46b389c8e5682e02633bcd397cfbe45d1dcc7d3c3c4ebe08e3a21605fdd12869e152ae90f286f71b03daa32b35f79930a45fc0971741d2c815362a51cfdea0a2d6198e902e3620b9b3a0a00220073eb4a204417dd314d2fc13a260eb4afb685fce8822bb8568871da673a29aba2b8be25604d98cb72bc44ae9f1129070837ef62f971692f7f4e6c460aa533d95498ddcd5953af3b470b785f15d711572e9658386f5024e8f5e234f33374e030ce055673193f308d07d2ac7992d735f617460368c2cc74afa65f070f3e1ef92a640bda8140273a7914f18d0911709ab8fcc5544a10002e46444b551a0dc2f633be0513f6945a6913df84624165ff15ce7f3211c4c46ef0961b4b05e1af6d21695c776cf62d8ac2e5a72072c7d94562999daa115609ecdc5998191f46e5830216445436978803697a7b70fd95418bdcf4cfb11b544705455dbb6844800bcb1b9ccd3819c53fef6a4189e58af413f65514cd3650ea26949a9f2b907c24223f8b72256a7a75f143feb8cca225340fd23c93ada449d4bd701f7a612dce51421688804b19c8ad847421f9bb2bc59919ca1709b6bdca22268c3e4d9f49b10e0806bee4916b724430c34b514a031e9f1e04a3971273d3bb13e902f156ddba6deff95d82ad5e5993771d6d65bcaf06a9237ae21dcbd2d1e665bef033"), +]; + +pub const FULL_I128: &[(i128, &str)] = &[ + (-170141183460469231731687303715884105728, "020300160000001d3d170022252220062e182e1c39373a1b153e0410065d826fb3dfbb8090bbcde5ed24acacf0fbc06a3f7f2751d6db18b546cc98c3d0aeaaf841bf3cb2fba85d7b1d1b9d4157d3ffa2a4eb52058bf81e171b324b052c576f57bda58f63f43dda07e9a2a2638c4d5600fe23e1db08227d32b1328e8463aca6e93a4c0d5481cd5e68a39eb917f1517fc3d8df957fc13fa29e15db6822a7cf179133bf7055c301cfdc3adfc65bf4473b18c877aa37a474f4152c95e9aaf1a1ee1a7d20836c27f65070e3fae7b2102d73eaf9bb4a6fba39a353266082419f9f9b12d3286a06e71b596aad71cf7733cf20f779d28a3326093d8ba86567d38dd6c776c38126c319860eb749c07e5ae7014b2dd3c6014bfd87619070f1ba368867e7f6aff17d6936f08d41cfbc7f9297666eee56d7e5a99be4a57785e01943d18d3319e36bc425946471c8d8e8a544fde353aa5a875bebd0752b398e413480b55bee6bfdb5975960f5c236889fdef4fea52d96d3623339f50a37d87ee1c032c798191f46e5830216445436978803697a848f024abe74230b12db314a9699cd84d8ea9b4af271d79c82d9db07a87b3081c9a78eeb955a4a50a938e7bf92bbab93f61dd99079b86c7bd81e350f5141776e185307a33b8589f5bc0ac63cf9dc5195d30933b341948da34df9c3893753b72c8e1b407b9d5906f5337e878c708699c1fc1c70cd65f7f28fff1f879a07940768535c6619f795b5eb7454917c2e14f95b4b5513ff6689c07ce1ab5730ef9b261a2b4a787d29890e590a1b42bf275e19a7"), + (-1, "020300160000002e1d000f0728193803273b2d3a14312f2b1d0c2c393e8d45d702c3baa9c11320c59cd64fb0170fc7a3ce698c5c5934bc587e835d6f85a0b52cf2a9ddc0e65872d3b9cd9b2938e0b6311eb0951d18bb160569e6e503a5b647c51918f2a0bf0821256b542fbf1a3119490b9e26e50d6cdead3f3584961c95a0fc83ef6359515dc0ec3664b258d5426ed1d68e4e32812415174cd77c01feda8937b26861246650f221f22354d011b160bc61684388670c68cda90d04fd7118b682bed95ee03679db942fcd4c3a6a76c6ec7882a97d41790050e7e7a3e97d5c0d01fedca787be884577ba52a20fb71fbe8128f3b0762424264512837ff37c13947be67871e76d47ae575580adc5e7d3a5a53db5fae68b524857aad5bbf68f272480b24cecc7d9e52bafb692810d7219e95fb9c0edd2ea378f9ed71d65c975aedb9bb46362eaf166ebf4f43c3a32001cfaaeba007e83995a1439762b5b1db03c9b4421843711098eb44d58985955a72a5eed9d458ebfee0ad29fcf127eadbe98191f46e5830216445436978803697adb49afce01a413493ecccc99282b69cec2417b13755c88ea7d8dc5762a900d689a4d01d5c790493f85d25d89db887d928af09111bd02119ad5aa967907f09f723ac5c4a07ded1d0bb3c576b20f7983e82b7a00af4b1f3e2c98b92074bc16e5c5d6b83a0d5f132ae0dfc88b4ee54c17e8996fb9e8cb29a1ad4c9a03bf06bfa4df862d47775debcb5080c10908fde917cc9504f8813e617784dda38d7bd7b90524c3bfaa7654532bccbf2af91da8419c12"), + (0, "0203001600000038183a122336323d2630320e202c0536232825281f01da70c42b151372073e985ba41fe3e6730d53f1ca868eea4bacfe6e3df5fdfa7d67083f40ff96506ad8c90ed73e54c84b6783875efb5f1fd08256f50dbad14a0205a48c31d0772f6e888d8848f56fac843975810b92085a17dd02ecc7a5bc1dd5fe8841b5925d2145731ff1271f3ead53dd05f81f121a15c00a300aa5f15b8fccd371e36cb04b6482014b5586819016296e6aac921b387f2a56da40c4833085bac88f76bfed67327d73fbdc7d98513ca8fd90c8b0d681f61a668521b7c9a7fa13781d4d664e0e1dff52d103b7f3ebe12c168ff0400c5fcf22f57c02aa831a02bbc6a99cb2f2529b7394a410935bfb388a7a0c9aa7b45e1f416b023cacc24a5b0372b08746d94557c2168ae483c27d7f334acc8eb15c7fc0884851af37dac14fd6fd903c93c5b464c9193704288ebf07dcd7333708bd94491fc5797729f48ffee13050fc27f48c7330054d352e913b4c7c76f17cbf56881f4525c35965cc62da5998191f46e5830216445436978803697adb49afce01a41348d82e9915dc320cb46258438a78fde4099ea912092910bb935baf4e0f8fa5d1b45ad7de790c61b068e3f814ebd57a82fed4867ed1c53fab0d5d18508c7cf716a82cacfda317bce31532672351f2e07b6de8a6bbcb79cf05289a16950be9ac3f3733b81d75f3cb1db68ca497568488d5df938ae1e53f528a8b37697ea9f9508f69dbe4ae3cf7560a4a598d0e39da59a610d7aa0073e97ceb9f7bc6eff11faa8092c6c1601ccab2964c"), + (1, "0203001600000038183a122336323d2630320e202c0536232825281f22da70c42b151372073e985ba41fe3e6730d53f1ca868eea4bacfe6e3df5fdfa7d67083f40ff96506ad8c90ed73e54c84b6783875efb5f1fd08256f50dbad14a0205a48c31d0772f6e888d8848f56fac843975810b92085a17dd02ecc7a5bc1dd5fe8841b5925d2145731ff1271f3ead53dd05f81f121a15c00a300aa5f15b8fccd371e36cb04b6482014b5586819016296e6aac921b387f2a56da40c4833085bac88f76bfed67327d73fbdc7d98513ca8fd90c8b0d681f61a668521b7c9a7fa13781d4d664e0e1dff52d103b7f3ebe12c168ff0400c5fcf22f57c02aa831a02bbc6a99cb2f2529b7394a410935bfb388a7a0c9aa7b45e1f416b023cacc24a5b0372b08746d94557c2168ae483c27d7f334acc8eb15c7fc0884851af37dac14fd6fd903c93c5b464c9193704288ebf07dcd7333708bd94491fc5797729f48ffee13050fc27f48c7330054d352e913b4c7c76aa6d4f5c784620f006640e134e76ad98191f46e5830216445436978803697adb49afce01a41348d82e9915dc320cb46258438a78fde4099ea912092910bb935baf4e0f8fa5d1b45ad7de790c61b068e3f814ebd57a82fed4867ed1c53fab0d5d18508c7cf716a82cacfda317bce31532672351f2e07b6de8a6bbcb79cf05289a16950be9ac3f3733b81d75f3cb1db68ca497568488d5df938ae1e53f528a8b37697ea9f9508f69dbe4ae3cf7560a4a598d0e39da59a610d7aa0073e97ceb9f7bc6eff11faa809282cd401c9cb22058"), + (170141183460469231731687303715884105727, "020300160000001c012537140012141c25342e373a0a2e10090d302f1bfc9c0ae91c66774477c4a9846a8f65aa7fb660a84a5e7f8872d29acbc1557e78279399c19d0b74d9460f6f5620b4eb57ce4a0b8d36ed95c277071ca46b389c8e5682e02633bcd397cfbe45d1dcc7d3c3c4ebe08e3a21605fdd12869e152ae90f286f71b03daa32b35f79930a45fc0971741d2c815362a51cfdea0a2d6198e902e3620b9b3a0a00220073eb4a204417dd314d2fc13a260eb4afb685fce8822bb8568871da673a29aba2b8be25604d98cb72bc44ae9f1129070837ef62f971692f7f4e6c460aa533d95498ddcd5953af3b470b785f15d711572e9658386f5024e8f5e234f33374e030ce055673193f308d07d2ac7992d735f617460368c2cc74afa65f070f3e1ef92a640bda8140273a7914f18d0911709ab8fcc5544a10002e46444b551a0dc2f633be0513f6945a6913df84624165ff15ce7f3211c4c46ef0961b4b05e1af6d21695c776cf62d8ac2e5a72072c7d94562999daa115609ecdc5998191f46e5830216445436978803697a7b70fd95418bdcf4cfb11b544705455dbb6844800bcb1b9ccd3819c53fef6a4189e58af413f65514cd3650ea26949a9f2b907c24223f8b72256a7a75f143feb8cca225340fd23c93ada449d4bd701f7a612dce51421688804b19c8ad847421f9bb2bc59919ca1709b6bdca22268c3e4d9f49b10e0806bee4916b724430c34b514a031e9f1e04a3971273d3bb13e902f156ddba6deff95d82ad5e5993771d6d65bcaf06a9237ae21dcbd2d1e665bef033"), +]; diff --git a/scripts/no-runtime-asserts.py b/scripts/no-runtime-asserts.py new file mode 100755 index 00000000..725440dd --- /dev/null +++ b/scripts/no-runtime-asserts.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Fail if library code can panic through an assertion macro. + +The crates deny clippy's panic lints (`panic`, `unwrap_used`, `expect_used`, +`unreachable`, `todo`, `unimplemented`, `panic_in_result_fn`) outside tests, +but clippy has no lint for `assert!` or `debug_assert!` in general. This +check covers those: library code returns an error or carries the invariant +in a type instead. + +Exempt: +- items under `#[cfg(test)]`, `#[cfg(kani)]`, `#[test]` or `#[kani::proof]`; +- compile-time assertions, `const NAME: () = assert!(...)`, which cannot + panic at run time. + +Usage: scripts/no-runtime-asserts.py [SRC_DIR ...] +(default: the library sources of every workspace crate). +""" + +import pathlib +import re +import sys + +DEFAULT_DIRS = ["packages/ore-rs/src", "packages/orderable-bytes/src"] + +EXEMPT_ATTR = re.compile(r"#\[\s*(cfg\(\s*(test|kani)\s*\)|test|kani::proof)\s*\]") +ASSERT = re.compile(r"(? str: + """Replace comments and string/char literal contents with spaces, + keeping newlines, so that line numbers and brace positions survive.""" + out = [] + i, n = 0, len(src) + while i < n: + c = src[i] + if src.startswith("//", i): + j = src.find("\n", i) + j = n if j == -1 else j + out.append(" " * (j - i)) + i = j + elif src.startswith("/*", i): + depth, j = 1, i + 2 + while j < n and depth: + if src.startswith("/*", j): + depth, j = depth + 1, j + 2 + elif src.startswith("*/", j): + depth, j = depth - 1, j + 2 + else: + j += 1 + out.append(re.sub(r"[^\n]", " ", src[i:j])) + i = j + elif c == '"' or (c == "b" and src.startswith('b"', i)): + j = i + (2 if c == "b" else 1) + while j < n and src[j] != '"': + j += 2 if src[j] == "\\" else 1 + j += 1 + out.append('"' + re.sub(r"[^\n]", " ", src[i + 1 : j - 1]) + '"') + i = j + else: + m = re.match(r"b?'(\\.|[^\\'\n])'", src[i:]) + if m: + out.append(" " * len(m.group(0))) + i += len(m.group(0)) + else: + out.append(c) + i += 1 + return "".join(out) + + +def exempt_spans(src: str): + """(start, end) offsets of every exempt item: from its attribute to the + end of its body (matching brace) or to its `;`.""" + spans = [] + for m in EXEMPT_ATTR.finditer(src): + j = m.end() + while j < len(src) and src[j] not in "{;": + j += 1 + if j < len(src) and src[j] == "{": + depth = 0 + while j < len(src): + if src[j] == "{": + depth += 1 + elif src[j] == "}": + depth -= 1 + if depth == 0: + break + j += 1 + spans.append((m.start(), j + 1)) + return spans + + +def check(path: pathlib.Path): + src = blank_comments_and_literals(path.read_text()) + spans = exempt_spans(src) + for m in ASSERT.finditer(src): + if any(a <= m.start() < b for a, b in spans): + continue + line_start = src.rfind("\n", 0, m.start()) + 1 + line_end = src.find("\n", m.start()) + line = src[line_start : line_end if line_end != -1 else len(src)] + if CONST_ASSERT.search(line): + continue + lineno = src.count("\n", 0, m.start()) + 1 + yield f"{path}:{lineno}: {line.strip()}" + + +def main(argv): + dirs = argv[1:] or DEFAULT_DIRS + found = [hit for d in dirs for f in sorted(pathlib.Path(d).rglob("*.rs")) for hit in check(f)] + for hit in found: + print(hit) + if found: + print( + f"\n{len(found)} runtime assertion(s) in library code. Return an error or " + "carry the invariant in a type; a compile-time check is " + "`const NAME: () = assert!(...)`.", + file=sys.stderr, + ) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv))