-
Notifications
You must be signed in to change notification settings - Fork 10
Add the ability to increase the number of Feistel Rounds #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
628cec3
569f403
6ebc151
3596de5
66f50b3
df0a1e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,18 +1,22 @@ | ||||||
| //! A Rust implementation of the FF1 algorithm, specified in | ||||||
| //! [NIST Special Publication 800-38G](http://dx.doi.org/10.6028/NIST.SP.800-38G). | ||||||
| //! [NIST Special Publication 800-38G](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38G.pdf). | ||||||
| //! | ||||||
| //! This crate implements **FF1 only**. FF3 (also defined in NIST SP 800-38G) is | ||||||
| //! not provided. | ||||||
|
|
||||||
| use core::cmp; | ||||||
|
|
||||||
| use cipher::{ | ||||||
| generic_array::GenericArray, Block, BlockCipher, BlockEncrypt, BlockEncryptMut, InnerIvInit, | ||||||
| KeyInit, | ||||||
| KeyInit, Unsigned, | ||||||
| }; | ||||||
| use zeroize::Zeroize; | ||||||
|
|
||||||
| #[cfg(test)] | ||||||
| use static_assertions::const_assert; | ||||||
|
|
||||||
| mod error; | ||||||
| pub use error::{InvalidRadix, NumeralStringError}; | ||||||
| pub use error::{FF1NewError, InvalidRadix, NumeralStringError}; | ||||||
|
|
||||||
| #[cfg(feature = "alloc")] | ||||||
| mod alloc; | ||||||
|
|
@@ -22,14 +26,17 @@ pub use self::alloc::{BinaryNumeralString, FlexibleNumeralString}; | |||||
| #[cfg(test)] | ||||||
| mod test_vectors; | ||||||
|
|
||||||
| #[cfg(test)] | ||||||
| mod ff1_18; | ||||||
|
|
||||||
| /// The minimum allowed numeral string length for any radix. | ||||||
| const MIN_NS_LEN: u32 = 2; | ||||||
| /// The maximum allowed numeral string length for any radix. | ||||||
| const MAX_NS_LEN: usize = u32::MAX as usize; | ||||||
|
|
||||||
| /// The minimum allowed value of radix^minlen. | ||||||
| /// | ||||||
| /// Defined in [NIST SP 800-38G Revision 1](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38Gr1-draft.pdf). | ||||||
| /// Defined in [NIST SP 800-38G](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38G.pdf). | ||||||
| #[cfg(test)] | ||||||
| const MIN_NS_DOMAIN_SIZE: u32 = 1_000_000; | ||||||
|
|
||||||
|
|
@@ -121,8 +128,7 @@ impl Radix { | |||||
|
|
||||||
| fn to_u32(&self) -> u32 { | ||||||
| match *self { | ||||||
| Radix::Any { radix, .. } => radix, | ||||||
| Radix::PowerTwo { radix, .. } => radix, | ||||||
| Radix::Any { radix, .. } | Radix::PowerTwo { radix, .. } => radix, | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
@@ -181,6 +187,14 @@ struct Prf<CIPH: BlockCipher + BlockEncrypt> { | |||||
| offset: usize, | ||||||
| } | ||||||
|
|
||||||
| impl<CIPH: BlockCipher + BlockEncrypt> Drop for Prf<CIPH> { | ||||||
| fn drop(&mut self) { | ||||||
| // Zero the CBC output block to remove any key-derived bytes. | ||||||
| // Note: `Block<CIPH> = GenericArray<u8, _>` implements Zeroize because u8: Zeroize. | ||||||
| self.buf[0].zeroize(); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| impl<CIPH: BlockCipher + BlockEncrypt + Clone> Prf<CIPH> { | ||||||
| fn new(ciph: &CIPH) -> Self { | ||||||
| let ciph = ciph.clone(); | ||||||
|
|
@@ -232,24 +246,65 @@ fn generate_s<'a, CIPH: BlockEncrypt>( | |||||
| .take(d) | ||||||
| } | ||||||
|
|
||||||
| /// A struct for performing FF1 encryption and decryption operations. | ||||||
| pub struct FF1<CIPH: BlockCipher> { | ||||||
| /// A struct for performing FF1 encryption and decryption operations | ||||||
| /// using the default 10 Feistel rounds | ||||||
| pub type FF1<CIPH> = FF1fr<10, CIPH>; | ||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| /// A struct for performing hardened FF1 encryption and decryption operations | ||||||
| /// using 18 Feistel rounds. | ||||||
| /// | ||||||
| /// # ⚠ Non-standard | ||||||
| /// NIST SP 800-38G standardises exactly **10 rounds** for FF1. This 18-round | ||||||
| /// variant provides a higher security margin but is **not NIST-compliant** and | ||||||
| /// will fail CAVP certification. It is **not interoperable** with conforming | ||||||
| /// implementations. | ||||||
| pub type FF1h<CIPH> = FF1fr<18, CIPH>; | ||||||
|
|
||||||
| /// A struct for performing FF1 encryption and decryption operations | ||||||
| /// with an adjustable number of Feistel rounds. | ||||||
| /// | ||||||
| /// # Key material | ||||||
| /// This struct holds the expanded cipher key schedule. When `FF1fr` is dropped | ||||||
| /// the key schedule is zeroed if `CIPH` implements [`ZeroizeOnDrop`] — all | ||||||
| /// `aes 0.8` types (`Aes128`, `Aes192`, `Aes256`) do so by default. | ||||||
| /// The internal CBC output buffer is always zeroed on drop. | ||||||
| /// | ||||||
| /// [`ZeroizeOnDrop`]: zeroize::ZeroizeOnDrop | ||||||
| pub struct FF1fr<const FEISTEL_ROUNDS: u8, CIPH: BlockCipher> { | ||||||
| ciph: CIPH, | ||||||
| radix: Radix, | ||||||
| } | ||||||
|
|
||||||
| impl<CIPH: BlockCipher + KeyInit> FF1<CIPH> { | ||||||
| impl<const FEISTEL_ROUNDS: u8, CIPH: BlockCipher + KeyInit> FF1fr<FEISTEL_ROUNDS, CIPH> { | ||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| /// Creates a new FF1 object for the given key and radix. | ||||||
| /// | ||||||
| /// Returns an error if the given radix is not in [2..2^16]. | ||||||
| pub fn new(key: &[u8], radix: u32) -> Result<Self, InvalidRadix> { | ||||||
| let ciph = CIPH::new(GenericArray::from_slice(key)); | ||||||
| /// Returns an error if: | ||||||
| /// - the radix is not in `[2..2^16]`, or | ||||||
| /// - the key length does not match the cipher's requirement. | ||||||
| /// | ||||||
| /// # Panics | ||||||
| /// Panics at construction time if `FEISTEL_ROUNDS < 8` or if the cipher's | ||||||
| /// block size is not 128 bits (16 bytes), as required by NIST SP 800-38G §4.3. | ||||||
| pub fn new(key: &[u8], radix: u32) -> Result<Self, FF1NewError> { | ||||||
| assert!( | ||||||
| FEISTEL_ROUNDS >= 8, | ||||||
| "FF1fr requires at least 8 Feistel rounds; got FEISTEL_ROUNDS = {}", | ||||||
| FEISTEL_ROUNDS | ||||||
| ); | ||||||
| assert_eq!( | ||||||
| CIPH::BlockSize::USIZE, | ||||||
| 16, | ||||||
| "FF1 requires a 128-bit (16-byte) block cipher (NIST SP 800-38G §4.3)" | ||||||
| ); | ||||||
| let ciph = CIPH::new_from_slice(key).map_err(|_| FF1NewError::InvalidKeyLength)?; | ||||||
| let radix = Radix::from_u32(radix)?; | ||||||
| Ok(FF1 { ciph, radix }) | ||||||
| Ok(FF1fr { ciph, radix }) | ||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| } | ||||||
| } | ||||||
|
|
||||||
| impl<CIPH: BlockCipher + BlockEncrypt + Clone> FF1<CIPH> { | ||||||
| impl<const FEISTEL_ROUNDS: u8, CIPH: BlockCipher + BlockEncrypt + Clone> | ||||||
| FF1fr<FEISTEL_ROUNDS, CIPH> | ||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| { | ||||||
| /// Encrypts the given numeral string. | ||||||
| /// | ||||||
| /// Returns an error if the numeral string is not in the required radix. | ||||||
|
|
@@ -267,6 +322,11 @@ impl<CIPH: BlockCipher + BlockEncrypt + Clone> FF1<CIPH> { | |||||
| let n = x.numeral_count(); | ||||||
| let t = tweak.len(); | ||||||
|
|
||||||
| // Enforce NIST SP 800-38G §5.1: t must fit in 4 bytes in the P-block. | ||||||
| if t > u32::MAX as usize { | ||||||
| return Err(NumeralStringError::TweakTooLong); | ||||||
| } | ||||||
|
|
||||||
| // 1. Let u = floor(n / 2); v = n - u | ||||||
| let u = n / 2; | ||||||
| let v = n - u; | ||||||
|
|
@@ -280,8 +340,16 @@ impl<CIPH: BlockCipher + BlockEncrypt + Clone> FF1<CIPH> { | |||||
| // 4. Let d = 4 * ceil(b / 4) + 4. | ||||||
| let d = 4 * ((b + 3) / 4) + 4; | ||||||
|
|
||||||
| // 5. Let P = [1, 2, 1] || [radix] || [10] || [u mod 256] || [n] || [t]. | ||||||
| let mut p = [1, 2, 1, 0, 0, 0, 10, u as u8, 0, 0, 0, 0, 0, 0, 0, 0]; | ||||||
| // 5. Build the 16-byte P-block (NIST SP 800-38G §6.2, Step 5): | ||||||
| // P = [1]₁ VERS: 1 (FF1) | ||||||
| // || [2]₁ ALGO: 2 (AES-CBCMAC) | ||||||
| // || [1]₁ constant | ||||||
| // || [radix]₃ radix in 3 big-endian bytes | ||||||
| // || [10]₁ constant | ||||||
| // || [u mod 256]₁ | ||||||
| // || [n]₄ numeral string length | ||||||
| // || [t]₄ tweak length | ||||||
| let mut p = [1, 2, 1, 0, 0, 0, 10, (u % 256) as u8, 0, 0, 0, 0, 0, 0, 0, 0]; | ||||||
| p[3..6].copy_from_slice(&self.radix.to_u32().to_be_bytes()[1..]); | ||||||
| p[8..12].copy_from_slice(&(n as u32).to_be_bytes()); | ||||||
| p[12..16].copy_from_slice(&(t as u32).to_be_bytes()); | ||||||
|
|
@@ -294,7 +362,7 @@ impl<CIPH: BlockCipher + BlockEncrypt + Clone> FF1<CIPH> { | |||||
| for _ in 0..((((-(t as i32) - (b as i32) - 1) % 16) + 16) % 16) { | ||||||
| prf.update(&[0]); | ||||||
| } | ||||||
| for i in 0..10 { | ||||||
| for i in 0..FEISTEL_ROUNDS { | ||||||
| let mut prf = prf.clone(); | ||||||
| prf.update(&[i]); | ||||||
| prf.update(x_b.num_radix(self.radix.to_u32()).to_bytes(b).as_ref()); | ||||||
|
|
@@ -345,6 +413,11 @@ impl<CIPH: BlockCipher + BlockEncrypt + Clone> FF1<CIPH> { | |||||
| let n = x.numeral_count(); | ||||||
| let t = tweak.len(); | ||||||
|
|
||||||
| // Enforce NIST SP 800-38G §5.1: t must fit in 4 bytes in the P-block. | ||||||
| if t > u32::MAX as usize { | ||||||
| return Err(NumeralStringError::TweakTooLong); | ||||||
| } | ||||||
|
|
||||||
| // 1. Let u = floor(n / 2); v = n - u | ||||||
| let u = n / 2; | ||||||
| let v = n - u; | ||||||
|
|
@@ -358,8 +431,16 @@ impl<CIPH: BlockCipher + BlockEncrypt + Clone> FF1<CIPH> { | |||||
| // 4. Let d = 4 * ceil(b / 4) + 4. | ||||||
| let d = 4 * ((b + 3) / 4) + 4; | ||||||
|
|
||||||
| // 5. Let P = [1, 2, 1] || [radix] || [10] || [u mod 256] || [n] || [t]. | ||||||
| let mut p = [1, 2, 1, 0, 0, 0, 10, u as u8, 0, 0, 0, 0, 0, 0, 0, 0]; | ||||||
| // 5. Build the 16-byte P-block (NIST SP 800-38G §6.2, Step 5): | ||||||
| // P = [1]₁ VERS: 1 (FF1) | ||||||
| // || [2]₁ ALGO: 2 (AES-CBCMAC) | ||||||
| // || [1]₁ constant | ||||||
| // || [radix]₃ radix in 3 big-endian bytes | ||||||
| // || [10]₁ constant | ||||||
| // || [u mod 256]₁ | ||||||
| // || [n]₄ numeral string length | ||||||
| // || [t]₄ tweak length | ||||||
| let mut p = [1, 2, 1, 0, 0, 0, 10, (u % 256) as u8, 0, 0, 0, 0, 0, 0, 0, 0]; | ||||||
| p[3..6].copy_from_slice(&self.radix.to_u32().to_be_bytes()[1..]); | ||||||
| p[8..12].copy_from_slice(&(n as u32).to_be_bytes()); | ||||||
| p[12..16].copy_from_slice(&(t as u32).to_be_bytes()); | ||||||
|
|
@@ -372,8 +453,8 @@ impl<CIPH: BlockCipher + BlockEncrypt + Clone> FF1<CIPH> { | |||||
| for _ in 0..((((-(t as i32) - (b as i32) - 1) % 16) + 16) % 16) { | ||||||
| prf.update(&[0]); | ||||||
| } | ||||||
| for i in 0..10 { | ||||||
| let i = 9 - i; | ||||||
| for i in 0..FEISTEL_ROUNDS { | ||||||
| let i = FEISTEL_ROUNDS - 1 - i; | ||||||
| let mut prf = prf.clone(); | ||||||
| prf.update(&[i]); | ||||||
| prf.update(x_a.num_radix(self.radix.to_u32()).to_bytes(b).as_ref()); | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.