diff --git a/Cargo.toml b/Cargo.toml index b166338..943af4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ curve25519-dalek = { package = "curve25519-dalek", version="3.2", features=["se sha3 = "0.9.1" serde = {version="1.0.123", features=["derive"]} bit-vec = {version="0.6.3"} -brute-force = {version="0.1.1", features=["curve25519"], optional=true} rayon = {version="1.5.0", optional=true} [dev-dependencies] @@ -35,6 +34,6 @@ name = "entangled" harness = false [features] -entangled = ["brute-force"] +entangled = [] bulk_verify = ["rayon"] simd = ["curve25519-dalek/simd_backend"] \ No newline at end of file diff --git a/benches/entangled.rs b/benches/entangled.rs index 5e35b53..babd011 100644 --- a/benches/entangled.rs +++ b/benches/entangled.rs @@ -3,7 +3,7 @@ use fuzzytags::{RootSecret, TaggingKey}; use rand::rngs::OsRng; use std::time::Duration; -fn benchmark_entangled(c: &mut Criterion) { +fn benchmark_entangled5(c: &mut Criterion) { let mut group = c.benchmark_group("entangling"); group.measurement_time(Duration::new(10, 0)); group.sample_size(10); @@ -11,12 +11,25 @@ fn benchmark_entangled(c: &mut Criterion) { for p in [24].iter() { let secret_key_1 = RootSecret::<24>::generate(&mut rng); let secret_key_2 = RootSecret::<24>::generate(&mut rng); + let secret_key_3 = RootSecret::<24>::generate(&mut rng); + let secret_key_4 = RootSecret::<24>::generate(&mut rng); + let secret_key_5 = RootSecret::<24>::generate(&mut rng); + let public_key_1 = secret_key_1.tagging_key(); let public_key_2 = secret_key_2.tagging_key(); + let public_key_3 = secret_key_3.tagging_key(); + let public_key_4 = secret_key_4.tagging_key(); + let public_key_5 = secret_key_5.tagging_key(); group.bench_with_input(BenchmarkId::from_parameter(p), p, |b, _gamma| { b.iter(|| { TaggingKey::generate_entangled_tag( - vec![public_key_1.clone(), public_key_2.clone()], + vec![ + public_key_1.clone(), + public_key_2.clone(), + public_key_3.clone(), + public_key_4.clone(), + public_key_5.clone(), + ], &mut rng, *p, ) @@ -25,5 +38,5 @@ fn benchmark_entangled(c: &mut Criterion) { } } -criterion_group!(benches, benchmark_entangled); +criterion_group!(benches, benchmark_entangled5); criterion_main!(benches); diff --git a/benches/fuzzy_tags_benches.rs b/benches/fuzzy_tags_benches.rs index 80f1c21..5a8a163 100644 --- a/benches/fuzzy_tags_benches.rs +++ b/benches/fuzzy_tags_benches.rs @@ -1,5 +1,5 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; -use fuzzytags::{RootSecret}; +use fuzzytags::RootSecret; use rand::rngs::OsRng; use std::time::Duration; diff --git a/src/lib.rs b/src/lib.rs index 9d962a9..1f794be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,11 +15,7 @@ use std::fmt; use std::fmt::{Display, Formatter}; use std::ops::{Mul, Sub}; -#[cfg(feature = "entangled")] -use brute_force::adaptors; -#[cfg(feature = "entangled")] -use brute_force::brute_force; - +use rand::Rng; use rand_core::{CryptoRng, RngCore}; #[cfg(feature = "bulk_verify")] use rayon::iter::IndexedParallelIterator; @@ -27,6 +23,7 @@ use rayon::iter::IndexedParallelIterator; use rayon::iter::IntoParallelRefIterator; #[cfg(feature = "bulk_verify")] use rayon::iter::ParallelIterator; +use std::borrow::BorrowMut; #[cfg(feature = "bulk_verify")] use std::sync::mpsc::channel; @@ -41,6 +38,7 @@ use std::sync::mpsc::channel; pub struct Tag { u: RistrettoPoint, y: Scalar, + z: Vec, ciphertexts: BitVec, } @@ -70,7 +68,7 @@ impl<'de, const GAMMA: u8> Deserialize<'de> for Tag<{ GAMMA }> { type Value = Tag<{ GAMMA }>; fn expecting(&self, formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - formatter.write_str("64 bytes + GAMMA+bits of data") + formatter.write_str("64 bytes + GAMMA Bytes + GAMMA + bits of data") } fn visit_seq(self, mut seq: A) -> Result, A::Error> @@ -90,12 +88,13 @@ impl<'de, const GAMMA: u8> Deserialize<'de> for Tag<{ GAMMA }> { _ => break, } } + Tag::::decompress(&bytes).ok_or(serde::de::Error::custom("invalid fuzzytag")) } } // support up to GAMMA = 64 - deserializer.deserialize_tuple(72, FuzzyTagVisitor::) + deserializer.deserialize_tuple((72 + GAMMA) as usize, FuzzyTagVisitor::) } } @@ -122,6 +121,7 @@ impl Tag<{ GAMMA }> { let mut bytes = vec![]; bytes.extend_from_slice(self.u.compress().as_bytes()); bytes.extend_from_slice(self.y.as_bytes()); + bytes.extend_from_slice(self.z.as_slice()); bytes.extend_from_slice(self.ciphertexts.to_bytes().as_slice()); bytes } @@ -145,9 +145,10 @@ impl Tag<{ GAMMA }> { /// assert_eq!(tag, decompressed_tag); /// ``` pub fn decompress(bytes: &[u8]) -> Option> { - if bytes.len() > 64 { + if bytes.len() > (64 + GAMMA as usize) { let (u_bytes, rest) = bytes.split_at(32); - let (y_bytes, ciphertext) = rest.split_at(32); + let (y_bytes, rest) = rest.split_at(32); + let (z_bytes, ciphertext) = rest.split_at({ GAMMA } as usize); // if the ciphertext is too short, then this is an invalid tag let min_bytes = GAMMA / 8; @@ -160,13 +161,20 @@ impl Tag<{ GAMMA }> { Ok(fixed_size) => fixed_size, _ => return None, }; + let mut ciphertexts = BitVec::from_bytes(ciphertext); ciphertexts.truncate(GAMMA as usize); return match ( CompressedRistretto::from_slice(u_bytes).decompress(), Scalar::from_canonical_bytes(y_bytes_fixed), + z_bytes.to_vec(), ) { - (Some(u), Some(y)) => Some(Tag { u, y, ciphertexts }), + (Some(u), Some(y), z) => Some(Tag { + u, + y, + z: z, + ciphertexts, + }), _ => None, }; } @@ -178,9 +186,10 @@ impl Display for Tag<{ GAMMA }> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!( f, - "{} {} {}", + "{} {} {} {}", hex::encode(self.u.compress().as_bytes()), hex::encode(self.y.as_bytes()), + hex::encode(self.z.as_slice()), hex::encode(self.ciphertexts.to_bytes()) ) } @@ -257,6 +266,25 @@ impl RootSecret<{ GAMMA }> { TaggingKey:: { 0: tagging_key } } + /// derive the tagging key for this secret + /// Example: + /// ``` + /// use fuzzytags::RootSecret; + /// use rand::rngs::OsRng; + /// let mut rng = OsRng; + /// let secret = RootSecret::<24>::generate(&mut rng); + /// let anti_key = secret.anti_key(); + /// ``` + pub fn anti_key(&self) -> TaggingKey<{ GAMMA }> { + let g = RISTRETTO_BASEPOINT_POINT; + let mut tagging_key = vec![]; + for sk_i in self.secret.iter() { + let pk_i = g.mul(sk_i.invert()); + tagging_key.push(pk_i); + } + TaggingKey:: { 0: tagging_key } + } + /// precompute the first part of h fn pre_h(u: RistrettoPoint, w: RistrettoPoint) -> PrecomputeH { let mut hash = sha3::Sha3_256::new(); @@ -266,6 +294,11 @@ impl RootSecret<{ GAMMA }> { return PrecomputeH(hash); } + fn nonce_h(mut hash: PrecomputeH, nonce: u8) -> PrecomputeH { + hash.0.update(vec![nonce]); + hash + } + /// compute the rest of h from a precomputed hash fn post_h(mut hash: PrecomputeH, h: RistrettoPoint) -> u8 { hash.0.update(h.compress().as_bytes()); @@ -353,7 +386,9 @@ impl DetectionKey<{ GAMMA }> { return false; } - let m = RootSecret::::g(tag.u, &tag.ciphertexts); + let mut bitvec = BitVec::from_bytes(&*tag.z); + bitvec.append(tag.ciphertexts.clone().borrow_mut()); + let m = RootSecret::::g(tag.u, &bitvec); let g = RISTRETTO_BASEPOINT_POINT; // Re-derive w = g^z from the public tag. @@ -370,9 +405,10 @@ impl DetectionKey<{ GAMMA }> { // for each secret part... let mut result = 0; - for (x_i, c_i) in self.0.iter().zip(&tag.ciphertexts) { + for (nonce_i, (x_i, c_i)) in self.0.iter().zip(&tag.ciphertexts).enumerate() { // re-derive the key from the tag - let k_i = RootSecret::::post_h(pre_h.clone(), tag.u.mul(x_i)); + let nonce_h = RootSecret::::nonce_h(pre_h.clone(), tag.z[nonce_i]); + let k_i = RootSecret::::post_h(nonce_h, tag.u.mul(x_i)); // calculate the "original" plaintext let b_i = k_i ^ (c_i as u8); @@ -418,7 +454,10 @@ impl DetectionKey<{ GAMMA }> { return vec![]; } - let m = RootSecret::::g(tag.u, &tag.ciphertexts); + let mut bitvec = BitVec::from_bytes(&*tag.z); + bitvec.append(tag.ciphertexts.clone().borrow_mut()); + let m = RootSecret::::g(tag.u, &bitvec); + let g = RISTRETTO_BASEPOINT_POINT; // Re-derive w = g^z from the public tag. @@ -440,9 +479,10 @@ impl DetectionKey<{ GAMMA }> { .enumerate() .for_each_with(tx.clone(), |tx, (index, detection_key)| { let mut result = 0; - for (x_i, c_i) in detection_key.0.iter().zip(&tag.ciphertexts) { + for (nonce_i, (x_i, c_i)) in detection_key.0.iter().zip(&tag.ciphertexts).enumerate() { // re-derive the key from the tag - let k_i = RootSecret::::post_h(pre_h.clone(), tag.u.mul(x_i)); + let nonce_h = RootSecret::::nonce_h(pre_h.clone(), tag.z[nonce_i]); + let k_i = RootSecret::::post_h(nonce_h, tag.u.mul(x_i)); // calculate the "original" plaintext let b_i = k_i ^ (c_i as u8); @@ -516,8 +556,14 @@ impl TaggingKey<{ GAMMA }> { // construct the ciphertext portion of the tag let mut ciphertexts = BitVec::with_capacity(GAMMA.into()); - for h_i in self.0.iter() { - let k_i = RootSecret::::post_h(pre_h.clone(), h_i.mul(r)); + let mut nonces = vec![]; + for _i in 0..GAMMA { + nonces.push(rng.gen()) + } + + for (nonce_i, h_i) in self.0.iter().enumerate() { + let nonce_h = RootSecret::::nonce_h(pre_h.clone(), nonces[nonce_i]); + let k_i = RootSecret::::post_h(nonce_h, h_i.mul(r)); // encrypt a plaintext of all 1's let c_i = k_i ^ 0x01; ciphertexts.push(c_i == 0x01); @@ -538,10 +584,17 @@ impl TaggingKey<{ GAMMA }> { // used to derive the key. // finally calculate a `y` = 1/r * (z-m) which will be used to re-derive `w` - let m = RootSecret::::g(u, &ciphertexts); + let mut bitvec = BitVec::from_bytes(&*nonces); + bitvec.append(ciphertexts.clone().borrow_mut()); + let m = RootSecret::::g(u, &bitvec); let y = r.invert().mul(z.sub(m)); - return Tag { u, y, ciphertexts }; + return Tag { + u, + y, + z: nonces, + ciphertexts, + }; } #[cfg(feature = "entangled")] @@ -578,23 +631,41 @@ impl TaggingKey<{ GAMMA }> { tagging_key_precomputes.push(precompute); } - let config = brute_force::Config::default(); + // generate some random points... - let f = |z: &Scalar| { + loop { + let z = Scalar::random(rng); let w = g.mul(z); let pre_h = RootSecret::::pre_h(u, w); let mut key = vec![]; + + let mut nonces = vec![]; + for (i, precompute) in tagging_key_precomputes[0].iter().enumerate() { - let k_i = RootSecret::::post_h(pre_h.clone(), *precompute); - if i < length { - for precompute in tagging_key_precomputes.iter().skip(1) { - let n_k_i = RootSecret::::post_h(pre_h.clone(), precompute[i]); - if k_i != n_k_i { - return None; + for _nonce_attmepts in 0..=255 { + let nonce: u8 = rng.gen(); + let mut success = true; + let nonce_h = RootSecret::::nonce_h(pre_h.clone(), nonce); + let k_i = RootSecret::::post_h(nonce_h.clone(), *precompute); + + if i < length { + for precompute in tagging_key_precomputes.iter().skip(1) { + let n_k_i = RootSecret::::post_h(nonce_h.clone(), precompute[i]); + if k_i != n_k_i { + success = false; + break; + } } } - key.push(k_i) + + if success { + nonces.push(nonce); + key.push(k_i); + break; + } } + + continue; } // generate the tag @@ -606,11 +677,17 @@ impl TaggingKey<{ GAMMA }> { } // This is the same as generate_tag, kept separate to avoid over-decomposition - let m = RootSecret::::g(u, &ciphertexts); + let mut bitvec = BitVec::from_bytes(&*nonces); + bitvec.append(ciphertexts.clone().borrow_mut()); + let m = RootSecret::::g(u, &bitvec); let y = r.invert().mul(z.sub(m)); - return Some(Tag { u, y, ciphertexts }); - }; - brute_force(config, adaptors::auto_advance(f)) + return Tag { + u, + y, + z: nonces, + ciphertexts, + }; + } } } @@ -661,7 +738,7 @@ mod tests { // Test some bincode... let bincode_tag = bincode::serialize(&tag); - // println!("Serialized: {:?}", bincode_tag); + // println!("Serialized: {:?}", bincode_tag); let deserialized_tag: Tag<24> = bincode::deserialize(&bincode_tag.unwrap()).unwrap(); //println!("Deserialized: {}", deserialized_tag); //assert_eq!(tag.compress(), deserialized_tag.compress()); @@ -697,7 +774,7 @@ mod tests { // it takes ~2 minutes on a standard desktop to find a length=24 match for 2 parties, so for testing let's keep things light let len = 16; let entangled_tag = TaggingKey::generate_entangled_tag(tagging_keys, &mut rng, len); - println!("{}", entangled_tag); + println!("entangled tag: {}", entangled_tag); for secret in secrets.iter() { let detection_key = secret.extract_detection_key(len); assert!(detection_key.test_tag(&entangled_tag)); @@ -710,26 +787,26 @@ mod tests { fn test_check_multiple() { use crate::TaggingKey; let mut rng = OsRng; - let secrets: Vec> = (0..2) + let secrets: Vec> = (0..5) .map(|_x| RootSecret::<24>::generate(&mut rng)) .collect(); let tagging_keys: Vec> = secrets.iter().map(|x| x.tagging_key()).collect(); // it takes ~2 minutes on a standard desktop to find a length=24 match for 2 parties, so for testing let's keep things light - let entangled_tag = TaggingKey::generate_entangled_tag(tagging_keys, &mut rng, 16); + let entangled_tag = TaggingKey::generate_entangled_tag(tagging_keys, &mut rng, 24); let detection_keys = secrets .iter() .map(|x| x.extract_detection_key(16)) .collect(); let results = DetectionKey::test_tag_bulk(&detection_keys, &entangled_tag); - assert_eq!(results.len(), 2); + assert_eq!(results.len(), 5); } #[test] fn correctness() { let number_of_messages = 100; let mut rng = OsRng; - let secret = RootSecret::<16>::generate(&mut rng); + let secret = RootSecret::<24>::generate(&mut rng); for i in 0..number_of_messages { let tag = secret.tagging_key().generate_tag(&mut rng); println!("{}: {}", i, tag); @@ -741,6 +818,7 @@ mod tests { let tag = Tag { u: RistrettoPoint::default(), y: Scalar::default(), + z: vec![], ciphertexts: BitVec::from_elem(24, false), }; tag @@ -750,6 +828,7 @@ mod tests { let mut tag = Tag { u: RistrettoPoint::default(), y: Scalar::default(), + z: vec![], ciphertexts: BitVec::from_elem(24, false), }; tag.ciphertexts.set_all(); @@ -798,21 +877,37 @@ mod tests { #[test] fn false_positives() { let mut rng = OsRng; - let number_of_messages = 1000; - let secret = RootSecret::<24>::generate(&mut rng); + let number_of_reference_checks = 100; + let number_of_messages = 100; + let limit = 1; let mut false_positives = 0; - for _i in 0..number_of_messages { - let secret2 = RootSecret::<24>::generate(&mut rng); - let tag = secret2.tagging_key().generate_tag(&mut rng); - assert!(secret2.extract_detection_key(3).test_tag(&tag)); - if secret.extract_detection_key(3).test_tag(&tag) == true { - false_positives += 1; + let total_attempts = number_of_reference_checks * number_of_messages; + + let secret = RootSecret::<24>::generate(&mut rng); + for _i in 0..number_of_reference_checks { + let secret = RootSecret::<24>::generate(&mut rng); + for _i in 0..number_of_messages { + let secret2 = RootSecret::<24>::generate(&mut rng); + let tag = secret2.tagging_key().generate_tag(&mut rng); + assert!(secret2.extract_detection_key(limit).test_tag(&tag)); + if secret.extract_detection_key(limit).test_tag(&tag) == true { + false_positives += 1; + } } } println!( "Expected False Positive Rate: {}\nActual False Positive Rate: {}", - secret.extract_detection_key(3).false_positive_probability(), - (false_positives as f64 / number_of_messages as f64) + secret + .extract_detection_key(limit) + .false_positive_probability(), + (false_positives as f64 / (total_attempts) as f64) + ); + assert!( + secret + .extract_detection_key(limit) + .false_positive_probability() + - (false_positives as f64 / (total_attempts) as f64) + < 0.01 ); } }