1 // Copyright 2022 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 //! A demonstration of LDT's PRP behavior.
16 //!
17 //! For each trial, between 16 and 31 bytes of random data are generated and encrypted. One random
18 //! bit of the ciphertext is flipped, then the ciphertext is decrypted. The percentage of flipped
19 //! bits vs the original plaintext is recorded, and the first n bytes are compared.
20 //!
21 //! The output shows how many times a change to the first n bytes wasn't detected, as well as a
22 //! histogram of how many bits were flipped in the entire plaintext.
23 
24 #![allow(clippy::unwrap_used, clippy::indexing_slicing)]
25 
26 use clap::Parser as _;
27 use crypto_provider::aes::BLOCK_SIZE;
28 use crypto_provider::{CryptoProvider, CryptoRng};
29 use crypto_provider_rustcrypto::RustCrypto;
30 use ldt::*;
31 use ldt_tbc::TweakableBlockCipher;
32 use rand::{distributions, Rng as _};
33 
34 use rand_ext::*;
35 use xts_aes::{XtsAes128, XtsAes256};
36 
main()37 fn main() {
38     let args = Args::parse();
39 
40     run_trials(args)
41 }
42 
run_trials(args: Args)43 fn run_trials(args: Args) {
44     let mut rng = seeded_rng();
45     let mut histo = (0..=100).map(|_| 0_u64).collect::<Vec<_>>();
46     let mut undetected_changes = 0_u64;
47     let mut cp_rng = <RustCrypto as CryptoProvider>::CryptoRng::new();
48     for _ in 0..args.trials {
49         let (percent, ok) =
50             if rng.gen() {
51                 do_trial(
52                     LdtEncryptCipher::<16, XtsAes128<RustCrypto>, Swap>::new(
53                         &LdtKey::from_random::<RustCrypto>(&mut cp_rng),
54                     ),
55                     LdtDecryptCipher::<16, XtsAes128<RustCrypto>, Swap>::new(
56                         &LdtKey::from_random::<RustCrypto>(&mut cp_rng),
57                     ),
58                     &mut rng,
59                     DefaultPadder,
60                     &args,
61                 )
62             } else {
63                 do_trial(
64                     LdtEncryptCipher::<16, XtsAes256<RustCrypto>, Swap>::new(
65                         &LdtKey::from_random::<RustCrypto>(&mut cp_rng),
66                     ),
67                     LdtDecryptCipher::<16, XtsAes256<RustCrypto>, Swap>::new(
68                         &LdtKey::from_random::<RustCrypto>(&mut cp_rng),
69                     ),
70                     &mut rng,
71                     DefaultPadder,
72                     &args,
73                 )
74             };
75 
76         histo[percent] += 1;
77         if !ok {
78             undetected_changes += 1;
79         }
80     }
81 
82     let sum: u64 = histo.iter().sum();
83     println!("Histogram of altered plaintext bits");
84     for (pct, count) in histo.iter().enumerate() {
85         // primitive horizontal bar graph
86         println!(
87             "{:3}%: {:8} {}",
88             pct,
89             count,
90             (0..((*count as f64) / (sum as f64) * 500_f64).round() as u32)
91                 .map(|_| "*")
92                 .collect::<String>()
93         );
94     }
95 
96     println!(
97         "{} of {} trials ({:.4}%) failed detect changes to the first {} decrypted bytes",
98         undetected_changes,
99         args.trials,
100         undetected_changes as f64 / (args.trials as f64) * 100_f64,
101         args.check_leading_bytes
102     );
103 }
104 
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)105 fn do_trial<const B: usize, T: TweakableBlockCipher<B>, P: Padder<B, T>, M: Mix, R: rand::Rng>(
106     ldt_enc: LdtEncryptCipher<B, T, M>,
107     ldt_dec: LdtDecryptCipher<B, T, M>,
108     rng: &mut R,
109     padder: P,
110     args: &Args,
111 ) -> (usize, bool) {
112     let plaintext_len_range = distributions::Uniform::new_inclusive(BLOCK_SIZE, BLOCK_SIZE * 2 - 1);
113     let len = rng.sample(plaintext_len_range);
114     let plaintext = random_vec_rc(rng, len);
115 
116     let mut ciphertext = plaintext.clone();
117     ldt_enc.encrypt(&mut ciphertext, &padder).unwrap();
118 
119     // flip a random bit
120     ciphertext[rng.gen_range(0..len)] ^= 1 << rng.gen_range(0..8);
121 
122     ldt_dec.decrypt(&mut ciphertext, &padder).unwrap();
123     assert_ne!(plaintext, ciphertext);
124 
125     let differing_bits: u32 = plaintext
126         .iter()
127         .zip(ciphertext.iter())
128         .map(|(plain_byte, mangled_byte)| (plain_byte ^ mangled_byte).count_ones())
129         .sum();
130 
131     let percent = (differing_bits as f64) / (8_f64 * len as f64) * 100_f64;
132     let ok = plaintext[0..args.check_leading_bytes] != ciphertext[0..args.check_leading_bytes];
133 
134     (percent.round() as usize, ok)
135 }
136 
137 #[derive(clap::Parser, Debug)]
138 struct Args {
139     /// How many trials to run
140     #[clap(long, default_value_t = 1_000_000)]
141     trials: u64,
142     /// How many leading bytes to confirm are unchanged
143     #[clap(long, default_value_t = 16)]
144     check_leading_bytes: usize,
145 }
146