diff --git a/.github/workflows/hybrid-array.yml b/.github/workflows/hybrid-array.yml index ad71bac..948f000 100644 --- a/.github/workflows/hybrid-array.yml +++ b/.github/workflows/hybrid-array.yml @@ -40,6 +40,7 @@ jobs: - run: cargo build --no-default-features --target ${{ matrix.target }} --features bytemuck - run: cargo build --no-default-features --target ${{ matrix.target }} --features extra-sizes - run: cargo build --no-default-features --target ${{ matrix.target }} --features serde + - run: cargo build --no-default-features --target ${{ matrix.target }} --features subtle - run: cargo build --no-default-features --target ${{ matrix.target }} --features zeroize - run: cargo build --no-default-features --target ${{ matrix.target }} --all-features diff --git a/Cargo.lock b/Cargo.lock index 7654188..e053c38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,6 +35,7 @@ dependencies = [ "bincode", "bytemuck", "serde", + "subtle", "typenum", "zeroize", ] @@ -77,6 +78,12 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.100" diff --git a/Cargo.toml b/Cargo.toml index 1840c5b..c52a988 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ typenum = { version = "1.17", features = ["const-generics"] } # optional dependencies bytemuck = { version = "1", optional = true, default-features = false } serde = { version = "1", optional = true, default-features = false } +subtle = { version = "2", optional = true, default-features = false } zeroize = { version = "1.8", optional = true, default-features = false } [dev-dependencies] diff --git a/src/lib.rs b/src/lib.rs index 3d657ae..eb4cf22 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,6 +133,9 @@ use typenum::{Diff, Sum}; #[cfg(feature = "bytemuck")] use bytemuck::{Pod, Zeroable}; +#[cfg(feature = "subtle")] +use subtle::{Choice, ConditionallySelectable, ConstantTimeEq}; + #[cfg(feature = "zeroize")] use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -854,6 +857,41 @@ where { } +#[cfg(feature = "subtle")] +impl ConditionallySelectable for Array +where + Self: Copy, + T: ConditionallySelectable, + U: ArraySize, +{ + #[inline] + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + let mut output = *a; + output.conditional_assign(b, choice); + output + } + + fn conditional_assign(&mut self, other: &Self, choice: Choice) { + for (a_i, b_i) in self.iter_mut().zip(other) { + a_i.conditional_assign(b_i, choice) + } + } +} + +#[cfg(feature = "subtle")] +impl ConstantTimeEq for Array +where + T: ConstantTimeEq, + U: ArraySize, +{ + #[inline] + fn ct_eq(&self, other: &Self) -> Choice { + self.iter() + .zip(other.iter()) + .fold(Choice::from(1), |acc, (a, b)| acc & a.ct_eq(b)) + } +} + #[cfg(feature = "zeroize")] impl Zeroize for Array where diff --git a/tests/subtle.rs b/tests/subtle.rs new file mode 100644 index 0000000..ef87023 --- /dev/null +++ b/tests/subtle.rs @@ -0,0 +1,29 @@ +//! Tests for `subtle` crate integration. + +#![cfg(feature = "subtle")] + +use hybrid_array::{Array, typenum::U3}; +use subtle::{Choice, ConditionallySelectable, ConstantTimeEq}; + +#[test] +fn constant_time_eq() { + let a: Array = Array([0, 0, 0]); + let b: Array = Array([1, 2, 3]); + + assert!(bool::from(a.ct_eq(&a))); + assert!(!bool::from(a.ct_ne(&a))); + assert!(!bool::from(a.ct_eq(&b))); + assert!(bool::from(a.ct_ne(&b))); +} + +#[test] +fn conditional_select() { + let a: Array = Array([0, 0, 0]); + let b: Array = Array([1, 2, 3]); + + let c = Array::conditional_select(&a, &b, Choice::from(0)); + assert_eq!(a, c); + + let d = Array::conditional_select(&a, &b, Choice::from(1)); + assert_eq!(b, d); +}