| // Copyright 2022 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| //! A demonstration of LDT's PRP behavior. |
| //! |
| //! For each trial, between 16 and 31 bytes of random data are generated and encrypted. One random |
| //! bit of the ciphertext is flipped, then the ciphertext is decrypted. The percentage of flipped |
| //! bits vs the original plaintext is recorded, and the first n bytes are compared. |
| //! |
| //! The output shows how many times a change to the first n bytes wasn't detected, as well as a |
| //! histogram of how many bits were flipped in the entire plaintext. |
| use clap::{self, Parser as _}; |
| use crypto_provider::aes::BLOCK_SIZE; |
| use crypto_provider::{CryptoProvider, CryptoRng}; |
| use crypto_provider_rustcrypto::RustCrypto; |
| use ldt::*; |
| use ldt_tbc::TweakableBlockCipher; |
| use rand::{distributions, Rng as _}; |
| |
| use rand_ext::*; |
| use xts_aes::{XtsAes128, XtsAes256}; |
| |
| fn main() { |
| let args = Args::parse(); |
| |
| run_trials(args) |
| } |
| |
| fn run_trials(args: Args) { |
| let mut rng = seeded_rng(); |
| let mut histo = (0..=100).map(|_| 0_u64).collect::<Vec<_>>(); |
| let mut undetected_changes = 0_u64; |
| let mut cp_rng = <RustCrypto as CryptoProvider>::CryptoRng::new(); |
| for _ in 0..args.trials { |
| let (percent, ok) = |
| if rng.gen() { |
| do_trial( |
| LdtEncryptCipher::<16, XtsAes128<RustCrypto>, Swap>::new( |
| &LdtKey::from_random::<RustCrypto>(&mut cp_rng), |
| ), |
| LdtDecryptCipher::<16, XtsAes128<RustCrypto>, Swap>::new( |
| &LdtKey::from_random::<RustCrypto>(&mut cp_rng), |
| ), |
| &mut rng, |
| DefaultPadder, |
| &args, |
| ) |
| } else { |
| do_trial( |
| LdtEncryptCipher::<16, XtsAes256<RustCrypto>, Swap>::new( |
| &LdtKey::from_random::<RustCrypto>(&mut cp_rng), |
| ), |
| LdtDecryptCipher::<16, XtsAes256<RustCrypto>, Swap>::new( |
| &LdtKey::from_random::<RustCrypto>(&mut cp_rng), |
| ), |
| &mut rng, |
| DefaultPadder, |
| &args, |
| ) |
| }; |
| |
| histo[percent] += 1; |
| if !ok { |
| undetected_changes += 1; |
| } |
| } |
| |
| let sum: u64 = histo.iter().sum(); |
| println!("Histogram of altered plaintext bits"); |
| for (pct, count) in histo.iter().enumerate() { |
| // primitive horizontal bar graph |
| println!( |
| "{:3}%: {:8} {}", |
| pct, |
| count, |
| (0..((*count as f64) / (sum as f64) * 500_f64).round() as u32) |
| .map(|_| "*") |
| .collect::<String>() |
| ); |
| } |
| |
| println!( |
| "{} of {} trials ({:.4}%) failed detect changes to the first {} decrypted bytes", |
| undetected_changes, |
| args.trials, |
| undetected_changes as f64 / (args.trials as f64) * 100_f64, |
| args.check_leading_bytes |
| ); |
| } |
| |
| fn do_trial<const B: usize, T: TweakableBlockCipher<B>, P: Padder<B, T>, M: Mix, R: rand::Rng>( |
| ldt_enc: LdtEncryptCipher<B, T, M>, |
| ldt_dec: LdtDecryptCipher<B, T, M>, |
| rng: &mut R, |
| padder: P, |
| args: &Args, |
| ) -> (usize, bool) { |
| let plaintext_len_range = distributions::Uniform::new_inclusive(BLOCK_SIZE, BLOCK_SIZE * 2 - 1); |
| let len = rng.sample(plaintext_len_range); |
| let plaintext = random_vec_rc(rng, len); |
| |
| let mut ciphertext = plaintext.clone(); |
| ldt_enc.encrypt(&mut ciphertext, &padder).unwrap(); |
| |
| // flip a random bit |
| ciphertext[rng.gen_range(0..len)] ^= 1 << rng.gen_range(0..8); |
| |
| ldt_dec.decrypt(&mut ciphertext, &padder).unwrap(); |
| assert_ne!(plaintext, ciphertext); |
| |
| let differing_bits: u32 = plaintext |
| .iter() |
| .zip(ciphertext.iter()) |
| .map(|(plain_byte, mangled_byte)| (plain_byte ^ mangled_byte).count_ones()) |
| .sum(); |
| |
| let percent = (differing_bits as f64) / (8_f64 * len as f64) * 100_f64; |
| let ok = plaintext[0..args.check_leading_bytes] != ciphertext[0..args.check_leading_bytes]; |
| |
| (percent.round() as usize, ok) |
| } |
| |
| #[derive(clap::Parser, Debug)] |
| struct Args { |
| /// How many trials to run |
| #[clap(long, default_value_t = 1_000_000)] |
| trials: u64, |
| /// How many leading bytes to confirm are unchanged |
| #[clap(long, default_value_t = 16)] |
| check_leading_bytes: usize, |
| } |