use std::collections::HashMap;
use std::env;
const NEGL_PROB: f64 = 7.888609052210118e-31;
#[derive(Debug, Clone)]
struct Parameters {
blocksize: usize,
sboxes: usize,
data_complexity: usize,
keysize: usize,
identity_bits: usize,
verbosity: Verbosity,
with_multiplicity: bool,
}
#[derive(Debug, Clone, PartialEq)]
enum Verbosity {
Quiet,
Normal,
Verbose,
}
impl Parameters {
fn new(blocksize: usize, sboxes: usize, data_complexity: usize, keysize: usize) -> Self {
Parameters {
blocksize,
sboxes,
data_complexity,
keysize,
identity_bits: blocksize - 3 * sboxes,
verbosity: Verbosity::Normal,
with_multiplicity: false,
}
}
fn print(&self) {
println!("Block size: {}", self.blocksize);
println!("# of Sboxes: {}", self.sboxes);
println!("Data complexity: {}", self.data_complexity);
println!("Key size: {}", self.keysize);
}
}
struct Memorizer<T> {
cache: HashMap<String, T>,
}
impl<T: Clone> Memorizer<T> {
fn new() -> Self {
Memorizer {
cache: HashMap::new(),
}
}
fn get_or_compute<F>(&mut self, key: String, compute: F) -> T
where
F: FnOnce() -> T,
{
if let Some(value) = self.cache.get(&key) {
value.clone()
} else {
let value = compute();
self.cache.insert(key, value.clone());
value
}
}
}
fn main() {
let params = parse_program_arguments();
check_parameter_validity(¶ms);
if params.verbosity == Verbosity::Verbose {
println!("{}", "-".repeat(46));
println!("LowMC rounds determination");
println!("{}", "-".repeat(46));
params.print();
println!("{}", "-".repeat(46));
}
let mut cache = Memorizer::new();
if params.verbosity == Verbosity::Verbose {
println!("Calculating statistical rounds");
}
let statistical_rounds = determine_statistical_rounds(¶ms, &mut cache);
if params.verbosity == Verbosity::Verbose {
println!("Calculating boomerang rounds");
}
let boomerang_rounds = determine_boomerang_rounds(¶ms, &mut cache);
if params.verbosity == Verbosity::Verbose {
println!("Calculating derivative rounds");
}
let derivative_rounds = determine_derivative_rounds(¶ms);
if params.verbosity == Verbosity::Verbose {
println!("Calculating interpolation rounds");
}
let interpolation_rounds = determine_interpolation_rounds(¶ms);
if params.verbosity == Verbosity::Verbose {
println!("Calculating round-key guessing rounds");
}
let keyguess_state_rounds = determine_keyguess_state_rounds(¶ms);
let keyguess_bit_rounds = determine_keyguess_bit_rounds(¶ms, 1);
if params.verbosity == Verbosity::Verbose {
println!("Calculating polytopic attack rounds");
}
let polytopic_rounds = determine_polytopic_attack_rounds(¶ms);
let distinguishers = vec![
(
"Statistical with state guessing",
statistical_rounds + keyguess_state_rounds,
),
("Boomerang attack", boomerang_rounds),
(
"Derivative + bit guessing",
derivative_rounds + keyguess_bit_rounds,
),
(
"Derivative + interpolation",
derivative_rounds + interpolation_rounds,
),
("Impossible polytopic attack", polytopic_rounds),
];
print_rounds(¶ms, &distinguishers);
}
fn determine_statistical_rounds(params: &Parameters, cache: &mut Memorizer<u128>) -> usize {
let mut upper_bound = 1;
loop {
if no_good_trail_after_round(params, upper_bound, cache) {
break;
}
upper_bound *= 2;
}
let mut lower_excl_bound = upper_bound / 2;
while lower_excl_bound + 1 < upper_bound {
let rounds = lower_excl_bound + (upper_bound - lower_excl_bound) / 2;
if no_good_trail_after_round(params, rounds, cache) {
upper_bound = rounds;
} else {
lower_excl_bound = rounds;
}
}
upper_bound
}
fn determine_boomerang_rounds(params: &Parameters, cache: &mut Memorizer<u128>) -> usize {
let mut upper_bound = 2;
loop {
if no_good_boomerang_after_round(params, upper_bound, cache) {
break;
}
upper_bound *= 2;
}
let mut lower_excl_bound = upper_bound / 2;
while lower_excl_bound + 1 < upper_bound {
let rounds = lower_excl_bound + (upper_bound - lower_excl_bound) / 2;
if no_good_boomerang_after_round(params, rounds, cache) {
upper_bound = rounds;
} else {
lower_excl_bound = rounds;
}
}
upper_bound
}
fn determine_derivative_rounds(params: &Parameters) -> usize {
let degree_rounds = determine_degree_rounds(params);
let influence_rounds = determine_influence_rounds(params);
degree_rounds + influence_rounds
}
fn determine_polytopic_attack_rounds(params: &Parameters) -> usize {
let mut attacked_rounds = Vec::new();
for ddiff_size in 1..=(2 * params.keysize / params.blocksize + 1) {
if (ddiff_size + 1).ilog2() as usize > params.data_complexity {
continue; }
let mut rounds = determine_free_rounds(params, (ddiff_size + 1).ilog2() as usize);
rounds += polytopic_listing_rounds(params, ddiff_size);
rounds += polytopic_listing_rounds(params, ddiff_size)
+ determine_free_rounds(
params,
params.blocksize - params.data_complexity / ddiff_size,
);
attacked_rounds.push(rounds);
}
attacked_rounds.into_iter().max().unwrap_or(0)
}
fn determine_interpolation_rounds(params: &Parameters) -> usize {
for rounds in 1.. {
let terms = interpolation_terms(params, rounds);
if (terms as f64).log2() >= params.keysize as f64 / 2.3
|| (terms as f64).log2() >= params.data_complexity as f64
{
return rounds;
}
}
unreachable!()
}
fn determine_keyguess_state_rounds(params: &Parameters) -> usize {
params.keysize / (3 * params.sboxes)
}
fn determine_keyguess_bit_rounds(params: &Parameters, dimension: usize) -> usize {
let free_rounds = determine_free_rounds(params, dimension);
let guess_state_rounds = determine_keyguess_state_rounds(params);
free_rounds + guess_state_rounds
}
fn no_good_trail_after_round(
params: &Parameters,
rounds: usize,
cache: &mut Memorizer<u128>,
) -> bool {
let max_active_sboxes = params.data_complexity / 2;
let all_good_trails = all_possible_good_trails(params, max_active_sboxes, rounds, cache);
let inv_realization_probability =
(2_u128.saturating_pow(params.blocksize as u32) - 1).saturating_pow(rounds as u32 - 1);
let threshold = (1.0 / NEGL_PROB) as u128;
let product = threshold.saturating_mul(all_good_trails);
product < inv_realization_probability
}
fn all_possible_good_trails(
params: &Parameters,
max_active_sboxes: usize,
rounds: usize,
cache: &mut Memorizer<u128>,
) -> u128 {
let key = format!(
"trails_{}_{}_{}_{}",
params.blocksize, params.sboxes, max_active_sboxes, rounds
);
cache.get_or_compute(key, || {
let mut current_trails = vec![0u128; max_active_sboxes + 1];
for (active_sboxes, trail) in current_trails.iter_mut().enumerate() {
*trail = one_round_trails(params, active_sboxes);
}
for _ in 2..=rounds {
let mut new_trails = vec![0u128; max_active_sboxes + 1];
for prev_actives in 0..=max_active_sboxes {
for new_actives in 0..=(max_active_sboxes - prev_actives) {
let product = current_trails[prev_actives]
.saturating_mul(one_round_trails(params, new_actives));
new_trails[prev_actives + new_actives] =
new_trails[prev_actives + new_actives].saturating_add(product);
}
}
current_trails = new_trails;
}
current_trails
.iter()
.fold(0u128, |acc, &x| acc.saturating_add(x))
})
}
fn no_good_boomerang_after_round(
params: &Parameters,
rounds: usize,
cache: &mut Memorizer<u128>,
) -> bool {
let max_actives = params.data_complexity / 4;
let top_rounds = rounds / 2;
let bottom_rounds = rounds - top_rounds;
for top_actives in 0..=max_actives {
let bottom_actives = max_actives - top_actives;
let top_good_trails = all_possible_good_trails(params, top_actives, top_rounds, cache);
let bottom_good_trails =
all_possible_good_trails(params, bottom_actives, bottom_rounds, cache);
let inv_top_realization_prob = (2_u128.saturating_pow(params.blocksize as u32) - 1)
.saturating_pow(top_rounds as u32 - 1);
let inv_bottom_realization_prob = (2_u128.saturating_pow(params.blocksize as u32) - 1)
.saturating_pow(bottom_rounds as u32 - 1);
let threshold = (1.0 / NEGL_PROB) as u128;
if threshold.saturating_mul(top_good_trails) >= inv_top_realization_prob
&& threshold.saturating_mul(bottom_good_trails) >= inv_bottom_realization_prob
{
return false;
}
}
true
}
fn one_round_trails(params: &Parameters, active_sboxes: usize) -> u128 {
let activating = activating_vectors(params, active_sboxes);
let power_of_4 = 4_u128.saturating_pow(active_sboxes as u32);
activating.saturating_mul(power_of_4)
}
fn activating_vectors(params: &Parameters, active_sboxes: usize) -> u128 {
let combinations = choose(params.sboxes, active_sboxes);
let power_of_7 = 7_u128.saturating_pow(active_sboxes as u32);
let power_of_2 = 2_u128.saturating_pow(params.identity_bits as u32);
combinations
.saturating_mul(power_of_7)
.saturating_mul(power_of_2)
}
fn determine_free_rounds(params: &Parameters, dimension: usize) -> usize {
if dimension > params.blocksize {
panic!("dimension must not be larger than blocksize");
}
if 3 * params.sboxes == params.blocksize {
0
} else {
(params.blocksize - dimension) / (3 * params.sboxes) + 1
}
}
fn polytopic_listing_rounds(params: &Parameters, ddiff_size: usize) -> usize {
let mut rounds = 0;
let diffusion_per_round = calculate_average_polytopic_diffusion(params, ddiff_size);
let mut diffusion = 0.0;
while diffusion < params.keysize as f64
&& diffusion < ddiff_size as f64 * params.blocksize as f64
{
rounds += 1;
diffusion += diffusion_per_round;
}
rounds
}
fn calculate_average_polytopic_diffusion(params: &Parameters, ddiff_size: usize) -> f64 {
let sboxes = params.sboxes;
let mut all_created_differences = 0.0;
let sbox_two_active = 8_usize.pow(ddiff_size as u32) - 1 - ddiff_size * 7;
for inactive in 0..=sboxes {
for one_active in 0..=(sboxes - inactive) {
let number_of_patterns = choose(sboxes, inactive) as f64
* choose(sboxes - inactive, one_active) as f64
* (ddiff_size * 7).pow(one_active as u32) as f64
* sbox_two_active.pow((sboxes - inactive - one_active) as u32) as f64;
let new_differences = number_of_patterns
* 4_usize.pow(one_active as u32) as f64
* 8_usize.pow((sboxes - inactive - one_active) as u32) as f64;
all_created_differences += new_differences;
}
}
all_created_differences.log2() - 3.0 * sboxes as f64 * ddiff_size as f64
}
fn determine_degree_rounds(params: &Parameters) -> usize {
for rounds in 1.. {
let max_degree = determine_degree_upper_bound(params, rounds);
if max_degree >= params.data_complexity - 1 {
return rounds;
}
}
unreachable!()
}
fn determine_degree_upper_bound(params: &Parameters, rounds: usize) -> usize {
let mut degree = 1;
for _ in 0..rounds {
degree = degree
.min(2 * degree)
.min(params.sboxes + degree)
.min((params.blocksize + degree) / 2);
}
degree
}
fn determine_influence_rounds(params: &Parameters) -> usize {
(params.blocksize as f64 / (7.0 / 8.0 * params.sboxes as f64 * 3.0)).ceil() as usize
}
fn interpolation_terms(params: &Parameters, rounds: usize) -> u128 {
let mut keybit_terms = vec![0u128; params.blocksize + 1];
keybit_terms[0] = 1;
keybit_terms[1] = params.blocksize as u128;
keybit_terms[2] = 3 * params.sboxes as u128;
for _ in 1..rounds {
let mut newkeybit_terms = vec![0u128; params.blocksize + 1];
newkeybit_terms[0] = 1;
newkeybit_terms[1] = params.blocksize as u128;
for degree in 2..=params.blocksize {
let mut terms_of_degree = 0u128;
for degree_1st_factor in 0..=(degree / 2) {
terms_of_degree +=
keybit_terms[degree_1st_factor] * keybit_terms[degree - degree_1st_factor];
}
newkeybit_terms[degree] = terms_of_degree.min(choose(params.blocksize, degree));
}
keybit_terms = newkeybit_terms;
}
let mut terms = 0u128;
for (degree, &val) in keybit_terms
.iter()
.enumerate()
.take(2_usize.pow(rounds as u32).min(params.blocksize) + 1)
{
terms += val.min(terms_with_bounded_degree(
params.keysize,
2_usize.pow(rounds as u32) - degree,
));
}
terms
}
fn terms_with_bounded_degree(variables: usize, max_degree: usize) -> u128 {
let mut terms = 0u128;
for degree in 0..=max_degree {
terms += choose(variables, degree);
}
terms
}
fn parse_program_arguments() -> Parameters {
let args: Vec<String> = env::args().collect();
if args.len() < 5 {
eprintln!(
"Usage: {} <block_size> <sboxes> <data_complexity> <key_size> [options]",
args[0]
);
eprintln!("Options:");
eprintln!(" -v, --verbose Print additional information");
eprintln!(" -q, --quiet Only print the number of total rounds");
eprintln!(" -m, --multiplicity Print multiplicative complexities");
std::process::exit(1);
}
let blocksize: usize = args[1].parse().expect("Invalid block size");
let sboxes: usize = args[2].parse().expect("Invalid number of S-boxes");
let data_complexity: usize = args[3].parse().expect("Invalid data complexity");
let keysize: usize = args[4].parse().expect("Invalid key size");
let mut params = Parameters::new(blocksize, sboxes, data_complexity, keysize);
for arg in &args[5..] {
match arg.as_str() {
"-v" | "--verbose" => params.verbosity = Verbosity::Verbose,
"-q" | "--quiet" => params.verbosity = Verbosity::Quiet,
"-m" | "--multiplicity" => params.with_multiplicity = true,
_ => eprintln!("Unknown option: {}", arg),
}
}
params
}
fn check_parameter_validity(params: &Parameters) {
if params.blocksize < params.data_complexity
|| params.sboxes * 3 > params.blocksize
|| params.data_complexity > params.keysize
|| params.sboxes < 1
|| params.blocksize < 1
|| params.data_complexity < 1
|| params.keysize < 1
{
eprintln!("Invalid parameter set");
std::process::exit(1);
}
}
fn print_rounds(params: &Parameters, distinguishers: &[(&str, usize)]) {
let total_rounds = distinguishers
.iter()
.map(|(_, rounds)| *rounds)
.max()
.unwrap_or(0);
if params.verbosity != Verbosity::Quiet {
println!("{}", "-".repeat(46));
println!("{:<40}{:>6}", "Distinguisher", "Rounds");
println!("{}", "-".repeat(46));
for (name, rounds) in distinguishers {
println!("{:<40}{:>6}", name, rounds);
}
println!("{}", "-".repeat(46));
println!("{:<40}{:>6}", "Secure rounds:", total_rounds);
} else {
println!("{}", total_rounds);
}
if params.with_multiplicity {
println!("{}", "-".repeat(46));
println!(
"{:<40}{:>6}",
"Total number of ANDs:",
total_rounds * 3 * params.sboxes
);
println!(
"{:<40}{:6.2}",
"Number of ANDs per bit:",
total_rounds as f64 * 3.0 * params.sboxes as f64 / params.blocksize as f64
);
println!("{:<40}{:>6}", "AND-depth:", total_rounds);
}
}
fn choose(n: usize, k: usize) -> u128 {
if k > n {
return 0;
}
if k == 0 || k == n {
return 1;
}
let k = k.min(n - k); let mut result = 1u128;
for i in 0..k {
result = result * (n - i) as u128 / (i + 1) as u128;
}
result
}