Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions src/ff1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ 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.
Expand Down Expand Up @@ -232,24 +235,35 @@ fn generate_s<'a, CIPH: BlockEncrypt>(
.take(d)
}

/// A struct for performing FF1 encryption and decryption operations
/// using the default 10 Feistel rounds
Comment on lines +249 to +250

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// A struct for performing FF1 encryption and decryption operations
/// using the default 10 Feistel rounds
/// A struct for performing FF1 encryption and decryption operations.
///
/// This implements FF1 as specified in [NIST SP 800-38G revision 1], with 10 Feistel
/// rounds and a minimum domain size of $\mathsf{radix}^\mathsf{minlen} \geq 1,000,000$.
///
/// [NIST Special Publication 800-38G]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38Gr1-draft.pdf

pub type FF1<CIPH> = FF1fr<10, CIPH>;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub type FF1<CIPH> = FF1fr<10, CIPH>;
pub type FF1<CIPH> = FF1Core<10, CIPH>;


/// A struct for performing hardened FF1 encryption and decryption operations
/// using 18 Feistel rounds
pub type FF1h<CIPH> = FF1fr<18, CIPH>;
Comment on lines +253 to +261

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically no longer FF1, because the number of rounds is part of the FF1 specification. So I do not want to describe it as "hardened FF1", not least because any future research breakthroughs could lead to further weakening such that 18 rounds also becomes insufficient.

NIST faced a similar issue with FF3, the original version of which was broken in a way that couldn't be fixed by changing bounds. They named the updated version FF3-1, suggesting this is likely how they would also name subsequent updates. If an FF1-1 were published, I'd name it FF1_1 (underscores are allowed in struct names under Rust naming conventions when separating integers). In the interest of not colliding with this, I propose that we name this increased-round modification :

Suggested change
/// A struct for performing hardened FF1 encryption and decryption operations
/// using 18 Feistel rounds
pub type FF1h<CIPH> = FF1fr<18, CIPH>;
/// Non-standard FPE algorithm that modifies FF1 to use 18 Feistel rounds.
///
/// Other than the modification to the number of Feistel rounds, this follows the FF1
/// algorithm as specified in [NIST SP 800-38G revision 1], with a minimum domain size of
/// $\mathsf{radix}^\mathsf{minlen} \geq 1,000,000$.
///
/// [NIST Special Publication 800-38G]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38Gr1-draft.pdf
pub type FF1R18<CIPH> = FF1Core<18, CIPH>;


/// A struct for performing FF1 encryption and decryption operations.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// A struct for performing FF1 encryption and decryption operations.
/// A struct for performing FF1 encryption and decryption operations

pub struct FF1<CIPH: BlockCipher> {
/// with an adjustable number of Feistel rounds
pub struct FF1fr<const FEISTEL_ROUNDS: u8, CIPH: BlockCipher> {

@str4d str4d Mar 2, 2023

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per #25 (comment) I do not want to expose the ability to configure an arbitrary number of rounds, as this includes round numbers that are insecure. If/when Rust gains full const generics, I would consider reopening this, because then we could enforce e.g. FEISTEL_ROUNDS >= 10. But for now, this should not be part of the public API:

Suggested change
/// with an adjustable number of Feistel rounds
pub struct FF1fr<const FEISTEL_ROUNDS: u8, CIPH: BlockCipher> {
/// with an adjustable number of Feistel rounds.
struct FF1Core<const FEISTEL_ROUNDS: u8, CIPH: BlockCipher> {

@daira daira Apr 3, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not do this?

/// Non-standard (in general) FPE algorithm based on FF1 but with an adjustable number
/// of Feistel rounds.
///
/// The number of Feistel rounds will be `10 + EXTRA_ROUNDS_ABOVE_10`. Apart from that
/// modification, this follows the FF1 algorithm as specified in [NIST SP 800-38G revision 1],
/// with a minimum domain size of $\mathsf{radix}^\mathsf{minlen} \geq 1,000,000$.
///
/// [NIST Special Publication 800-38G]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38Gr1-draft.pdf
pub struct FF1WithExtraRounds<const EXTRA_ROUNDS_ABOVE_10: u8, CIPH: BlockCipher> { ... }

/// A struct for performing FF1 encryption and decryption operations.
///
/// This implements FF1 as specified in [NIST SP 800-38G revision 1], with 10 Feistel
/// rounds and a minimum domain size of $\mathsf{radix}^\mathsf{minlen} \geq 1,000,000$.
///
/// [NIST Special Publication 800-38G]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38Gr1-draft.pdf
pub type FF1<CIPH> = FF1WithExtraRounds<0, CIPH>;

/// Non-standard FPE algorithm that modifies FF1 to use 18 Feistel rounds.
///
/// Other than the modification to the number of Feistel rounds, this follows the FF1
/// algorithm as specified in [NIST SP 800-38G revision 1], with a minimum domain size of
/// $\mathsf{radix}^\mathsf{minlen} \geq 1,000,000$.
///
/// [NIST Special Publication 800-38G]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38Gr1-draft.pdf
pub type FF1R18<CIPH> = FF1WithExtraRounds<8, CIPH>;

(poor man's range typing!)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's no way to raise the floor of this bound later if NIST devices to replace FF1 with a higher-round version. So for now I'd prefer to only expose explicit round versions.

ciph: CIPH,
radix: Radix,
}

impl<CIPH: BlockCipher + KeyInit> FF1<CIPH> {
impl<const FEISTEL_ROUNDS: u8, CIPH: BlockCipher + KeyInit> FF1fr<FEISTEL_ROUNDS, CIPH> {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
impl<const FEISTEL_ROUNDS: u8, CIPH: BlockCipher + KeyInit> FF1fr<FEISTEL_ROUNDS, CIPH> {
impl<const FEISTEL_ROUNDS: u8, CIPH: BlockCipher + KeyInit> FF1Core<FEISTEL_ROUNDS, CIPH> {

/// 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));
let radix = Radix::from_u32(radix)?;
Ok(FF1 { ciph, radix })
Ok(FF1fr { ciph, radix })

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Ok(FF1fr { ciph, radix })
Ok(Self { ciph, radix })

}
}

impl<CIPH: BlockCipher + BlockEncrypt + Clone> FF1<CIPH> {
impl<const FEISTEL_ROUNDS: u8, CIPH: BlockCipher + BlockEncrypt + Clone>
FF1fr<FEISTEL_ROUNDS, CIPH>

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
FF1fr<FEISTEL_ROUNDS, CIPH>
FF1Core<FEISTEL_ROUNDS, CIPH>

{
/// Encrypts the given numeral string.
///
/// Returns an error if the numeral string is not in the required radix.
Expand Down Expand Up @@ -294,7 +308,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());
Expand Down Expand Up @@ -372,8 +386,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());
Expand Down
19 changes: 19 additions & 0 deletions src/ff1/ff1_18.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use aes::Aes256;

use crate::ff1::{BinaryNumeralString, FF1h};

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
use crate::ff1::{BinaryNumeralString, FF1h};
use crate::ff1::{BinaryNumeralString, FF1R18};


#[test]
fn test_doc_example_18_rounds() {
let key = [0; 32];
let radix = 2;
let pt = [0xab, 0xcd, 0xef];

let ff = FF1h::<Aes256>::new(&key, radix).unwrap();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let ff = FF1h::<Aes256>::new(&key, radix).unwrap();
let ff = FF1R18::<Aes256>::new(&key, radix).unwrap();

let ct = ff
.encrypt(&[], &BinaryNumeralString::from_bytes_le(&pt))
.unwrap();
assert_eq!(ct.to_bytes_le(), [0x5a, 0x6c, 0x20]);

let p2 = ff.decrypt(&[], &ct).unwrap();
assert_eq!(p2.to_bytes_le(), pt);
}