diff --git a/Cargo.toml b/Cargo.toml index a2445f0..f302cce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,25 +11,26 @@ readme = "README.md" [features] progress_bar = ["dep:indicatif"] -quicklog = ["dep:thiserror"] +quicklog = [] [dependencies] -serde_json = "1.0.115" +serde_json = "1.0.132" hex = "0.4.3" -npyz = "0.7.4" -ndarray = "0.15.6" +npyz = "0.8.3" +ndarray = { version = "0.16.1", features = ["serde"] } rayon = "1.10.0" indicatif = { version = "0.17.8", optional = true } -ndarray-npy ="0.8.1" -itertools = "0.12.1" -thiserror = { version = "1.0.58", optional = true } +ndarray-npy ="0.9.1" +itertools = "0.13.0" +thiserror = { version = "1.0.58" } dtw = { git = "https://github.com/Ledger-Donjon/dtw.git", rev = "0f8d7ec3bbdf2ca4ec8ea35feddb8d1db73e7d54" } num-traits = "0.2.19" +serde = { version = "1.0.214", features = ["derive"] } [dev-dependencies] criterion = "0.5.1" -ndarray-rand = "0.14.0" -anyhow = "1.0.81" +ndarray-rand = "0.15.0" +anyhow = "1.0.92" muscat = { path = ".", features = ["progress_bar"] } [[example]] diff --git a/benches/cpa.rs b/benches/cpa.rs index a8cefa8..3fb0a4e 100644 --- a/benches/cpa.rs +++ b/benches/cpa.rs @@ -8,38 +8,42 @@ use ndarray_rand::rand_distr::Uniform; use ndarray_rand::RandomExt; use std::iter::zip; -pub fn leakage_model(value: usize, guess: usize) -> usize { - hw(sbox((value ^ guess) as u8) as usize) +pub fn leakage_model(plaintext: usize, guess: usize) -> usize { + hw(sbox((plaintext ^ guess) as u8) as usize) } fn cpa_sequential(traces: &Array2, plaintexts: &Array2) -> Cpa { - let mut cpa = CpaProcessor::new(traces.shape()[1], 256, 0, leakage_model); + let mut cpa = CpaProcessor::new(traces.shape()[1], 256, 0); for i in 0..traces.shape()[0] { cpa.update( traces.row(i).map(|&x| x as usize).view(), plaintexts.row(i).map(|&y| y as usize).view(), + leakage_model, ); } - cpa.finalize() + cpa.finalize(leakage_model) } -pub fn leakage_model_normal(value: ArrayView1, guess: usize) -> usize { - hw(sbox((value[1] ^ guess) as u8) as usize) +pub fn leakage_model_normal(plaintext: ArrayView1, guess: usize) -> usize { + hw(sbox((plaintext[1] ^ guess) as u8) as usize) } fn cpa_normal_sequential(traces: &Array2, plaintexts: &Array2) -> Cpa { let batch_size = 500; - let mut cpa = - cpa_normal::CpaProcessor::new(traces.shape()[1], batch_size, 256, leakage_model_normal); + let mut cpa = cpa_normal::CpaProcessor::new(traces.shape()[1], batch_size, 256); for (trace_batch, plaintext_batch) in zip( traces.axis_chunks_iter(Axis(0), batch_size), plaintexts.axis_chunks_iter(Axis(0), batch_size), ) { - cpa.update(trace_batch.map(|&x| x as f32).view(), plaintext_batch); + cpa.update( + trace_batch.map(|&x| x as f32).view(), + plaintext_batch, + leakage_model_normal, + ); } cpa.finalize() diff --git a/benches/dpa.rs b/benches/dpa.rs index 3c945fc..bb0dbb6 100644 --- a/benches/dpa.rs +++ b/benches/dpa.rs @@ -11,10 +11,10 @@ fn selection_function(metadata: ArrayView1, guess: usize) -> bool { } fn dpa_sequential(traces: &Array2, plaintexts: &Array2) -> Dpa { - let mut dpa = DpaProcessor::new(traces.shape()[1], 256, selection_function); + let mut dpa = DpaProcessor::new(traces.shape()[1], 256); for i in 0..traces.shape()[0] { - dpa.update(traces.row(i), plaintexts.row(i)); + dpa.update(traces.row(i), plaintexts.row(i), selection_function); } dpa.finalize() diff --git a/examples/cpa.rs b/examples/cpa.rs index c7fe1b6..f37b4ca 100644 --- a/examples/cpa.rs +++ b/examples/cpa.rs @@ -35,20 +35,17 @@ fn cpa() -> Result<()> { .progress_with(progress_bar(len_traces)) .par_bridge() .map(|row_number| { - let mut cpa = CpaProcessor::new(size, batch, guess_range, leakage_model); + let mut cpa = CpaProcessor::new(size, batch, guess_range); let range_rows = row_number..row_number + batch; let range_samples = start_sample..end_sample; let sample_traces = traces .slice(s![range_rows.clone(), range_samples]) .map(|l| *l as f32); let sample_metadata = plaintext.slice(s![range_rows, ..]).map(|p| *p as usize); - cpa.update(sample_traces.view(), sample_metadata.view()); + cpa.update(sample_traces.view(), sample_metadata.view(), leakage_model); cpa }) - .reduce( - || CpaProcessor::new(size, batch, guess_range, leakage_model), - |x, y| x + y, - ); + .reduce(|| CpaProcessor::new(size, batch, guess_range), |x, y| x + y); let cpa = cpa_parallel.finalize(); println!("Guessed key = {}", cpa.best_guess()); @@ -69,7 +66,7 @@ fn success() -> Result<()> { let nfiles = 13; // Number of files in the directory. TBD: Automating this value let rank_traces = 1000; - let mut cpa = CpaProcessor::new(size, batch, guess_range, leakage_model); + let mut cpa = CpaProcessor::new(size, batch, guess_range); let mut rank = Array1::zeros(guess_range); let mut processed_traces = 0; @@ -87,7 +84,7 @@ fn success() -> Result<()> { .map(|l| *l as f32); let sample_metadata = plaintext.slice(s![range_rows, range_metadata]); - cpa.update(sample_traces.view(), sample_metadata); + cpa.update(sample_traces.view(), sample_metadata, leakage_model); processed_traces += sample_traces.len(); if processed_traces % rank_traces == 0 { // rank can be saved to get its evolution diff --git a/examples/cpa_partioned.rs b/examples/cpa_partioned.rs index 49e61f7..50feedf 100644 --- a/examples/cpa_partioned.rs +++ b/examples/cpa_partioned.rs @@ -34,21 +34,22 @@ fn cpa() -> Result<()> { }) .par_bridge() .map(|batch| { - let mut c = CpaProcessor::new(size, guess_range, target_byte, leakage_model); + let mut c = CpaProcessor::new(size, guess_range, target_byte); for i in 0..batch.0.shape()[0] { c.update( batch.0.row(i).map(|x| *x as usize).view(), batch.1.row(i).map(|y| *y as usize).view(), + leakage_model, ); } c }) .reduce( - || CpaProcessor::new(size, guess_range, target_byte, leakage_model), + || CpaProcessor::new(size, guess_range, target_byte), |a, b| a + b, ); - let cpa_result = cpa.finalize(); + let cpa_result = cpa.finalize(leakage_model); println!("Guessed key = {}", cpa_result.best_guess()); // save corr key curves in npy diff --git a/examples/dpa.rs b/examples/dpa.rs index a9d5a7f..dee00dd 100644 --- a/examples/dpa.rs +++ b/examples/dpa.rs @@ -25,14 +25,18 @@ fn dpa() -> Result<()> { let traces = read_array2_from_npy_file::(&dir_l)?; let plaintext = read_array2_from_npy_file::(&dir_p)?; let len_traces = 20000; //traces.shape()[0]; - let mut dpa_proc = DpaProcessor::new(size, guess_range, selection_function); + let mut dpa_proc = DpaProcessor::new(size, guess_range); for i in (0..len_traces).progress() { let tmp_trace = traces .row(i) .slice(s![start_sample..end_sample]) .mapv(|t| t as f32); let tmp_metadata = plaintext.row(i); - dpa_proc.update(tmp_trace.view(), tmp_metadata.to_owned()); + dpa_proc.update( + tmp_trace.view(), + tmp_metadata.to_owned(), + selection_function, + ); } let dpa = dpa_proc.finalize(); println!("Guessed key = {:02x}", dpa.best_guess()); @@ -53,7 +57,7 @@ fn dpa_success() -> Result<()> { let traces = read_array2_from_npy_file::(&dir_l)?; let plaintext = read_array2_from_npy_file::(&dir_p)?; let len_traces = traces.shape()[0]; - let mut dpa_proc = DpaProcessor::new(size, guess_range, selection_function); + let mut dpa_proc = DpaProcessor::new(size, guess_range); let rank_traces: usize = 100; let mut rank = Array1::zeros(guess_range); @@ -63,7 +67,7 @@ fn dpa_success() -> Result<()> { .slice(s![start_sample..end_sample]) .mapv(|t| t as f32); let tmp_metadata = plaintext.row(i).to_owned(); - dpa_proc.update(tmp_trace.view(), tmp_metadata); + dpa_proc.update(tmp_trace.view(), tmp_metadata, selection_function); if i % rank_traces == 0 { // rank can be saved to get its evolution @@ -102,18 +106,15 @@ fn dpa_parallel() -> Result<()> { .slice(s![range_rows..range_rows + batch, ..]) .to_owned(); - let mut dpa_inner = DpaProcessor::new(size, guess_range, selection_function); + let mut dpa_inner = DpaProcessor::new(size, guess_range); for i in 0..batch { let trace = tmp_traces.row(i); let metadata = tmp_metadata.row(i).to_owned(); - dpa_inner.update(trace, metadata); + dpa_inner.update(trace, metadata, selection_function); } dpa_inner }) - .reduce( - || DpaProcessor::new(size, guess_range, selection_function), - |x, y| x + y, - ) + .reduce(|| DpaProcessor::new(size, guess_range), |x, y| x + y) .finalize(); println!("{:2x}", dpa.best_guess()); diff --git a/examples/rank.rs b/examples/rank.rs index b1a49f1..a70621a 100644 --- a/examples/rank.rs +++ b/examples/rank.rs @@ -22,7 +22,7 @@ fn rank() -> Result<()> { let folder = String::from("../../data"); let nfiles = 5; let batch_size = 3000; - let mut rank = CpaProcessor::new(size, guess_range, target_byte, leakage_model); + let mut rank = CpaProcessor::new(size, guess_range, target_byte); for file in (0..nfiles).progress_with(progress_bar(nfiles)) { let dir_l = format!("{folder}/l{file}.npy"); let dir_p = format!("{folder}/p{file}.npy"); @@ -37,24 +37,25 @@ fn rank() -> Result<()> { let x = (0..batch_size) .par_bridge() .fold( - || CpaProcessor::new(size, guess_range, target_byte, leakage_model), + || CpaProcessor::new(size, guess_range, target_byte), |mut r, n| { r.update( l_sample.row(n).map(|l| *l as usize).view(), p_sample.row(n).map(|p| *p as usize).view(), + leakage_model, ); r }, ) .reduce( - || CpaProcessor::new(size, guess_range, target_byte, leakage_model), + || CpaProcessor::new(size, guess_range, target_byte), |lhs, rhs| lhs + rhs, ); rank = rank + x; } } - let rank = rank.finalize(); + let rank = rank.finalize(leakage_model); // save rank key curves in npy save_array("../results/rank.npy", &rank.rank().map(|&x| x as u64))?; diff --git a/src/distinguishers/cpa.rs b/src/distinguishers/cpa.rs index 3e87393..a8e1949 100644 --- a/src/distinguishers/cpa.rs +++ b/src/distinguishers/cpa.rs @@ -1,10 +1,14 @@ -use crate::util::{argmax_by, argsort_by, max_per_row}; +use crate::{ + util::{argmax_by, argsort_by, max_per_row}, + Error, +}; use ndarray::{s, Array1, Array2, ArrayView1, ArrayView2, Axis}; use rayon::{ iter::ParallelBridge, prelude::{IntoParallelIterator, ParallelIterator}, }; -use std::{iter::zip, ops::Add}; +use serde::{Deserialize, Serialize}; +use std::{fs::File, iter::zip, ops::Add, path::Path}; /// Compute the [`Cpa`] of the given traces using [`CpaProcessor`]. /// @@ -38,7 +42,7 @@ use std::{iter::zip, ops::Add}; /// [2, 1], /// [2, 1], /// ]; -/// let cpa = cpa(traces.view(), plaintexts.view(), 256, 0, |key, guess| sbox((key ^ guess) as u8) as usize, 2); +/// let cpa = cpa(traces.view(), plaintexts.view(), 256, 0, |plaintext, guess| sbox((plaintext ^ guess) as u8) as usize, 2); /// ``` /// /// # Panics @@ -49,7 +53,7 @@ pub fn cpa( plaintexts: ArrayView2

, guess_range: usize, target_byte: usize, - leakage_func: F, + leakage_model: F, batch_size: usize, ) -> Cpa where @@ -67,10 +71,10 @@ where ) .par_bridge() .fold( - || CpaProcessor::new(traces.shape()[1], guess_range, target_byte, leakage_func), + || CpaProcessor::new(traces.shape()[1], guess_range, target_byte), |mut cpa, (trace_batch, plaintext_batch)| { for i in 0..trace_batch.shape()[0] { - cpa.update(trace_batch.row(i), plaintext_batch.row(i)); + cpa.update(trace_batch.row(i), plaintext_batch.row(i), leakage_model); } cpa @@ -78,7 +82,7 @@ where ) .reduce_with(|a, b| a + b) .unwrap() - .finalize() + .finalize(leakage_model) } /// Result of the CPA[^1] on some traces. @@ -119,10 +123,8 @@ impl Cpa { /// It implements algorithm 4 from [^1]. /// /// [^1]: -pub struct CpaProcessor -where - F: Fn(usize, usize) -> usize, -{ +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct CpaProcessor { /// Number of samples per trace num_samples: usize, /// Target byte index in a block @@ -140,22 +142,12 @@ where /// Sum of traces per plaintext used /// See 4.3 in plaintext_sum_traces: Array2, - /// Leakage model - leakage_func: F, /// Number of traces processed num_traces: usize, } -impl CpaProcessor -where - F: Fn(usize, usize) -> usize + Sync, -{ - pub fn new( - num_samples: usize, - guess_range: usize, - target_byte: usize, - leakage_func: F, - ) -> Self { +impl CpaProcessor { + pub fn new(num_samples: usize, guess_range: usize, target_byte: usize) -> Self { Self { num_samples, target_byte, @@ -165,17 +157,21 @@ where guess_sum_traces: Array1::zeros(guess_range), guess_sum_squares_traces: Array1::zeros(guess_range), plaintext_sum_traces: Array2::zeros((guess_range, num_samples)), - leakage_func, num_traces: 0, } } /// # Panics /// Panic in debug if `trace.shape()[0] != self.num_samples`. - pub fn update(&mut self, trace: ArrayView1, plaintext: ArrayView1

) - where + pub fn update( + &mut self, + trace: ArrayView1, + plaintext: ArrayView1

, + leakage_model: F, + ) where T: Into + Copy, P: Into + Copy, + F: Fn(usize, usize) -> usize, { debug_assert_eq!(trace.shape()[0], self.num_samples); @@ -188,7 +184,7 @@ where } for guess in 0..self.guess_range { - let value = (self.leakage_func)(plaintext[self.target_byte].into(), guess); + let value = leakage_model(plaintext[self.target_byte].into(), guess); self.guess_sum_traces[guess] += value; self.guess_sum_squares_traces[guess] += value * value; } @@ -197,13 +193,16 @@ where } /// Finalize the calculation after feeding the overall traces. - pub fn finalize(&self) -> Cpa { + pub fn finalize(&self, leakage_model: F) -> Cpa + where + F: Fn(usize, usize) -> usize, + { let mut modeled_leakages = Array1::zeros(self.guess_range); let mut corr = Array2::zeros((self.guess_range, self.num_samples)); for guess in 0..self.guess_range { for u in 0..self.guess_range { - modeled_leakages[u] = (self.leakage_func)(u, guess); + modeled_leakages[u] = leakage_model(u, guess); } let mean_key = self.guess_sum_traces[guess] as f32 / self.num_traces as f32; @@ -242,12 +241,33 @@ where a.dot(&b) } + /// Save the [`CpaProcessor`] to a file. + /// + /// # Warning + /// The file format is not stable as muscat is active development. Thus, the format might + /// change between versions. + pub fn save>(&self, path: P) -> Result<(), Error> { + let file = File::create(path)?; + serde_json::to_writer(file, self)?; + + Ok(()) + } + + /// Load a [`CpaProcessor`] from a file. + /// + /// # Warning + /// The file format is not stable as muscat is active development. Thus, the format might + /// change between versions. + pub fn load>(path: P) -> Result { + let file = File::open(path)?; + let p: CpaProcessor = serde_json::from_reader(file)?; + + Ok(p) + } + /// Determine if two [`CpaProcessor`] are compatible for addition. /// /// If they were created with the same parameters, they are compatible. - /// - /// Note: [`CpaProcessor::leakage_func`] cannot be checked for equality, but they must have the - /// same leakage functions in order to be compatible. fn is_compatible_with(&self, other: &Self) -> bool { self.num_samples == other.num_samples && self.target_byte == other.target_byte @@ -255,10 +275,7 @@ where } } -impl Add for CpaProcessor -where - F: Fn(usize, usize) -> usize + Sync, -{ +impl Add for CpaProcessor { type Output = Self; /// Merge computations of two [`CpaProcessor`]. Processors need to be compatible to be merged @@ -279,7 +296,6 @@ where guess_sum_traces: self.guess_sum_traces + rhs.guess_sum_traces, guess_sum_squares_traces: self.guess_sum_squares_traces + rhs.guess_sum_squares_traces, plaintext_sum_traces: self.plaintext_sum_traces + rhs.plaintext_sum_traces, - leakage_func: self.leakage_func, num_traces: self.num_traces + rhs.num_traces, } } @@ -289,6 +305,7 @@ where mod tests { use super::{cpa, CpaProcessor}; use ndarray::array; + use serde::Deserialize; #[test] fn test_cpa_helper() { @@ -306,14 +323,63 @@ mod tests { ]; let plaintexts = array![[1usize], [3], [1], [2], [3], [2], [2], [1], [3], [1]]; - let leakage_model = |value, guess| value ^ guess; - let mut processor = CpaProcessor::new(traces.shape()[1], 256, 0, leakage_model); + let leakage_model = |plaintext, guess| plaintext ^ guess; + let mut processor = CpaProcessor::new(traces.shape()[1], 256, 0); for i in 0..traces.shape()[0] { - processor.update(traces.row(i), plaintexts.row(i)); + processor.update(traces.row(i), plaintexts.row(i), leakage_model); } assert_eq!( - processor.finalize().corr(), + processor.finalize(leakage_model).corr(), cpa(traces.view(), plaintexts.view(), 256, 0, leakage_model, 2).corr() ); } + + #[test] + fn test_serialize_deserialize_processor() { + let traces = array![ + [77usize, 137, 51, 91], + [72, 61, 91, 83], + [39, 49, 52, 23], + [26, 114, 63, 45], + [30, 8, 97, 91], + [13, 68, 7, 45], + [17, 181, 60, 34], + [43, 88, 76, 78], + [0, 36, 35, 0], + [93, 191, 49, 26], + ]; + let plaintexts = array![[1usize], [3], [1], [2], [3], [2], [2], [1], [3], [1]]; + + let leakage_model = |value, guess| value ^ guess; + let mut processor = CpaProcessor::new(traces.shape()[1], 256, 0); + for i in 0..traces.shape()[0] { + processor.update(traces.row(i), plaintexts.row(i), leakage_model); + } + + let serialized = serde_json::to_string(&processor).unwrap(); + let mut deserializer = serde_json::Deserializer::from_str(serialized.as_str()); + let restored_processor = CpaProcessor::deserialize(&mut deserializer).unwrap(); + + assert_eq!(processor.num_samples, restored_processor.num_samples); + assert_eq!(processor.target_byte, restored_processor.target_byte); + assert_eq!(processor.guess_range, restored_processor.guess_range); + assert_eq!(processor.sum_traces, restored_processor.sum_traces); + assert_eq!( + processor.sum_square_traces, + restored_processor.sum_square_traces + ); + assert_eq!( + processor.guess_sum_traces, + restored_processor.guess_sum_traces + ); + assert_eq!( + processor.guess_sum_squares_traces, + restored_processor.guess_sum_squares_traces + ); + assert_eq!( + processor.plaintext_sum_traces, + restored_processor.plaintext_sum_traces + ); + assert_eq!(processor.num_traces, restored_processor.num_traces); + } } diff --git a/src/distinguishers/cpa_normal.rs b/src/distinguishers/cpa_normal.rs index efb7b1f..548f9c5 100644 --- a/src/distinguishers/cpa_normal.rs +++ b/src/distinguishers/cpa_normal.rs @@ -1,8 +1,9 @@ use ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis}; use rayon::iter::{ParallelBridge, ParallelIterator}; -use std::{iter::zip, ops::Add}; +use serde::{Deserialize, Serialize}; +use std::{fs::File, iter::zip, ops::Add, path::Path}; -use crate::distinguishers::cpa::Cpa; +use crate::{distinguishers::cpa::Cpa, Error}; /// Compute the [`Cpa`] of the given traces using [`CpaProcessor`]. /// @@ -36,7 +37,7 @@ use crate::distinguishers::cpa::Cpa; /// [2, 1], /// [2, 1], /// ]; -/// let cpa = cpa(traces.map(|&x| x as f32).view(), plaintexts.view(), 256, |key, guess| sbox((key[0] ^ guess) as u8) as usize, 2); +/// let cpa = cpa(traces.map(|&x| x as f32).view(), plaintexts.view(), 256, |plaintext, guess| sbox((plaintext[0] ^ guess) as u8) as usize, 2); /// ``` /// /// # Panics @@ -46,7 +47,7 @@ pub fn cpa( traces: ArrayView2, plaintexts: ArrayView2

, guess_range: usize, - leakage_func: F, + leakage_model: F, batch_size: usize, ) -> Cpa where @@ -63,9 +64,9 @@ where ) .par_bridge() .fold( - || CpaProcessor::new(traces.shape()[1], batch_size, guess_range, leakage_func), + || CpaProcessor::new(traces.shape()[1], batch_size, guess_range), |mut cpa, (trace_batch, plaintext_batch)| { - cpa.update(trace_batch, plaintext_batch); + cpa.update(trace_batch, plaintext_batch, leakage_model); cpa }, @@ -78,10 +79,8 @@ where /// A processor that computes the [`Cpa`] of the given traces. /// /// [^1]: -pub struct CpaProcessor -where - F: Fn(ArrayView1, usize) -> usize, -{ +#[derive(Serialize, Deserialize)] +pub struct CpaProcessor { /// Number of samples per trace num_samples: usize, /// Guess range upper excluded bound @@ -98,17 +97,12 @@ where cov: Array2, /// Batch size batch_size: usize, - /// Leakage model - leakage_func: F, /// Number of traces processed num_traces: usize, } -impl CpaProcessor -where - F: Fn(ArrayView1, usize) -> usize, -{ - pub fn new(num_samples: usize, batch_size: usize, guess_range: usize, leakage_func: F) -> Self { +impl CpaProcessor { + pub fn new(num_samples: usize, batch_size: usize, guess_range: usize) -> Self { Self { num_samples, guess_range, @@ -119,7 +113,6 @@ where values: Array2::zeros((batch_size, guess_range)), cov: Array2::zeros((guess_range, num_samples)), batch_size, - leakage_func, num_traces: 0, } } @@ -127,10 +120,15 @@ where /// # Panics /// - Panic in debug if `trace_batch.shape()[0] != plaintext_batch.shape()[0]`. /// - Panic in debug if `trace_batch.shape()[1] != self.num_samples`. - pub fn update(&mut self, trace_batch: ArrayView2, plaintext_batch: ArrayView2

) - where + pub fn update( + &mut self, + trace_batch: ArrayView2, + plaintext_batch: ArrayView2

, + leakage_model: F, + ) where T: Into + Copy, P: Into + Copy, + F: Fn(ArrayView1, usize) -> usize, { debug_assert_eq!(trace_batch.shape()[0], plaintext_batch.shape()[0]); debug_assert_eq!(trace_batch.shape()[1], self.num_samples); @@ -140,23 +138,31 @@ where let trace_batch = trace_batch.mapv(|t| t.into()); let plaintext_batch = plaintext_batch.mapv(|m| m.into()); - self.update_values(trace_batch.view(), plaintext_batch.view(), self.guess_range); + self.update_values( + trace_batch.view(), + plaintext_batch.view(), + self.guess_range, + leakage_model, + ); self.update_key_leakages(trace_batch.view(), self.guess_range); self.num_traces += self.batch_size; } - fn update_values( + fn update_values( /* This function generates the values and cov arrays */ &mut self, trace: ArrayView2, metadata: ArrayView2, guess_range: usize, - ) { + leakage_model: F, + ) where + F: Fn(ArrayView1, usize) -> usize, + { for row in 0..self.batch_size { for guess in 0..guess_range { let pass_to_leakage = metadata.row(row); - self.values[[row, guess]] = (self.leakage_func)(pass_to_leakage, guess) as f32; + self.values[[row, guess]] = leakage_model(pass_to_leakage, guess) as f32; } } @@ -202,12 +208,33 @@ where Cpa { corr } } + /// Save the [`CpaProcessor`] to a file. + /// + /// # Warning + /// The file format is not stable as muscat is active development. Thus, the format might + /// change between versions. + pub fn save>(&self, path: P) -> Result<(), Error> { + let file = File::create(path)?; + serde_json::to_writer(file, self)?; + + Ok(()) + } + + /// Load a [`CpaProcessor`] from a file. + /// + /// # Warning + /// The file format is not stable as muscat is active development. Thus, the format might + /// change between versions. + pub fn load>(path: P) -> Result { + let file = File::open(path)?; + let p: CpaProcessor = serde_json::from_reader(file)?; + + Ok(p) + } + /// Determine if two [`CpaProcessor`] are compatible for addition. /// /// If they were created with the same parameters, they are compatible. - /// - /// Note: [`CpaProcessor::leakage_func`] cannot be checked for equality, but they must have the - /// same leakage functions in order to be compatible. fn is_compatible_with(&self, other: &Self) -> bool { self.num_samples == other.num_samples && self.batch_size == other.batch_size @@ -215,10 +242,7 @@ where } } -impl Add for CpaProcessor -where - F: Fn(ArrayView1, usize) -> usize, -{ +impl Add for CpaProcessor { type Output = Self; /// Merge computations of two [`CpaProcessor`]. Processors need to be compatible to be merged @@ -240,7 +264,6 @@ where values: self.values + rhs.values, cov: self.cov + rhs.cov, batch_size: self.batch_size, - leakage_func: self.leakage_func, num_traces: self.num_traces + rhs.num_traces, } } @@ -252,6 +275,7 @@ mod tests { use super::{cpa, CpaProcessor}; use ndarray::{array, ArrayView1, Axis}; + use serde::Deserialize; #[test] fn test_cpa_helper() { @@ -270,12 +294,16 @@ mod tests { let plaintexts = array![[1usize], [3], [1], [2], [3], [2], [2], [1], [3], [1]]; let leakage_model = |plaintext: ArrayView1, guess| plaintext[0] ^ guess; - let mut processor = CpaProcessor::new(traces.shape()[1], 1, 256, leakage_model); + let mut processor = CpaProcessor::new(traces.shape()[1], 1, 256); for (trace, plaintext) in zip( traces.axis_chunks_iter(Axis(0), 1), plaintexts.axis_chunks_iter(Axis(0), 1), ) { - processor.update(trace.map(|&x| x as f32).view(), plaintext.view()); + processor.update( + trace.map(|&x| x as f32).view(), + plaintext.view(), + leakage_model, + ); } assert_eq!( processor.finalize().corr(), @@ -289,4 +317,56 @@ mod tests { .corr() ); } + + #[test] + fn test_serialize_deserialize_processor() { + let traces = array![ + [77usize, 137, 51, 91], + [72, 61, 91, 83], + [39, 49, 52, 23], + [26, 114, 63, 45], + [30, 8, 97, 91], + [13, 68, 7, 45], + [17, 181, 60, 34], + [43, 88, 76, 78], + [0, 36, 35, 0], + [93, 191, 49, 26], + ]; + let plaintexts = array![[1usize], [3], [1], [2], [3], [2], [2], [1], [3], [1]]; + + let leakage_model = |plaintext: ArrayView1, guess| plaintext[0] ^ guess; + let mut processor = CpaProcessor::new(traces.shape()[1], 1, 256); + for (trace, plaintext) in zip( + traces.axis_chunks_iter(Axis(0), 1), + plaintexts.axis_chunks_iter(Axis(0), 1), + ) { + processor.update( + trace.map(|&x| x as f32).view(), + plaintext.view(), + leakage_model, + ); + } + + let serialized = serde_json::to_string(&processor).unwrap(); + let mut deserializer: serde_json::Deserializer> = + serde_json::Deserializer::from_str(serialized.as_str()); + let restored_processor = CpaProcessor::deserialize(&mut deserializer).unwrap(); + + assert_eq!(processor.num_samples, restored_processor.num_samples); + assert_eq!(processor.guess_range, restored_processor.guess_range); + assert_eq!(processor.sum_traces, restored_processor.sum_traces); + assert_eq!(processor.sum_traces2, restored_processor.sum_traces2); + assert_eq!( + processor.guess_sum_traces, + restored_processor.guess_sum_traces + ); + assert_eq!( + processor.guess_sum_traces2, + restored_processor.guess_sum_traces2 + ); + assert_eq!(processor.values, restored_processor.values); + assert_eq!(processor.cov, restored_processor.cov); + assert_eq!(processor.batch_size, restored_processor.batch_size); + assert_eq!(processor.num_traces, restored_processor.num_traces); + } } diff --git a/src/distinguishers/dpa.rs b/src/distinguishers/dpa.rs index 98fd422..712acb5 100644 --- a/src/distinguishers/dpa.rs +++ b/src/distinguishers/dpa.rs @@ -1,8 +1,12 @@ use ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis}; use rayon::iter::{ParallelBridge, ParallelIterator}; -use std::{iter::zip, marker::PhantomData, ops::Add}; +use serde::{Deserialize, Serialize}; +use std::{fs::File, iter::zip, marker::PhantomData, ops::Add, path::Path}; -use crate::util::{argmax_by, argsort_by, max_per_row}; +use crate::{ + util::{argmax_by, argsort_by, max_per_row}, + Error, +}; /// Compute the [`Dpa`] of the given traces using [`DpaProcessor`]. /// @@ -45,7 +49,7 @@ use crate::util::{argmax_by, argsort_by, max_per_row}; /// .collect::>>() /// .view(), /// 256, -/// |key: Array1, guess| sbox(key[0] ^ guess as u8) & 1 == 1, +/// |plaintext: Array1, guess| sbox(plaintext[0] ^ guess as u8) & 1 == 1, /// 2 /// ); /// ``` @@ -72,10 +76,14 @@ where ) .par_bridge() .fold( - || DpaProcessor::new(traces.shape()[1], guess_range, selection_function), + || DpaProcessor::new(traces.shape()[1], guess_range), |mut dpa, (trace_batch, metadata_batch)| { for i in 0..trace_batch.shape()[0] { - dpa.update(trace_batch.row(i), metadata_batch[i].clone()); + dpa.update( + trace_batch.row(i), + metadata_batch[i].clone(), + selection_function, + ); } dpa @@ -122,10 +130,8 @@ impl Dpa { /// /// [^1]: /// [^2]: -pub struct DpaProcessor -where - F: Fn(M, usize) -> bool, -{ +#[derive(Serialize, Deserialize)] +pub struct DpaProcessor { /// Number of samples per trace num_samples: usize, /// Guess range upper excluded bound @@ -138,18 +144,16 @@ where count_0: Array1, /// Number of traces processed for which the selection function equals true count_1: Array1, - selection_function: F, /// Number of traces processed num_traces: usize, _metadata: PhantomData, } -impl DpaProcessor +impl DpaProcessor where M: Clone, - F: Fn(M, usize) -> bool, { - pub fn new(num_samples: usize, guess_range: usize, selection_function: F) -> Self { + pub fn new(num_samples: usize, guess_range: usize) -> Self { Self { num_samples, guess_range, @@ -157,7 +161,6 @@ where sum_1: Array2::zeros((guess_range, num_samples)), count_0: Array1::zeros(guess_range), count_1: Array1::zeros(guess_range), - selection_function, num_traces: 0, _metadata: PhantomData, } @@ -165,14 +168,15 @@ where /// # Panics /// Panic in debug if `trace.shape()[0] != self.num_samples`. - pub fn update(&mut self, trace: ArrayView1, metadata: M) + pub fn update(&mut self, trace: ArrayView1, metadata: M, selection_function: F) where T: Into + Copy, + F: Fn(M, usize) -> bool, { debug_assert_eq!(trace.shape()[0], self.num_samples); for guess in 0..self.guess_range { - if (self.selection_function)(metadata.clone(), guess) { + if selection_function(metadata.clone(), guess) { for i in 0..self.num_samples { self.sum_1[[guess, i]] += trace[i].into(); } @@ -205,20 +209,40 @@ where } } + /// Save the [`DpaProcessor`] to a file. + /// + /// # Warning + /// The file format is not stable as muscat is active development. Thus, the format might + /// change between versions. + pub fn save>(&self, path: P) -> Result<(), Error> { + let file = File::create(path)?; + serde_json::to_writer(file, self)?; + + Ok(()) + } + + /// Load a [`DpaProcessor`] from a file. + /// + /// # Warning + /// The file format is not stable as muscat is active development. Thus, the format might + /// change between versions. + pub fn load>(path: P) -> Result { + let file = File::open(path)?; + let p: DpaProcessor = serde_json::from_reader(file)?; + + Ok(p) + } + /// Determine if two [`DpaProcessor`] are compatible for addition. /// /// If they were created with the same parameters, they are compatible. - /// - /// Note: [`DpaProcessor::selection_function`] cannot be checked for equality, but they must - /// have the same selection functions in order to be compatible. fn is_compatible_with(&self, other: &Self) -> bool { self.num_samples == other.num_samples && self.guess_range == other.guess_range } } -impl Add for DpaProcessor +impl Add for DpaProcessor where - F: Fn(M, usize) -> bool, M: Clone, { type Output = Self; @@ -239,7 +263,6 @@ where sum_1: self.sum_1 + rhs.sum_1, count_0: self.count_0 + rhs.count_0, count_1: self.count_1 + rhs.count_1, - selection_function: self.selection_function, num_traces: self.num_traces + rhs.num_traces, _metadata: PhantomData, } @@ -250,6 +273,7 @@ where mod tests { use super::{dpa, DpaProcessor}; use ndarray::{array, Array1, ArrayView1}; + use serde::Deserialize; #[test] fn test_dpa_helper() { @@ -269,9 +293,13 @@ mod tests { let selection_function = |plaintext: ArrayView1, guess| (plaintext[0] as usize ^ guess) & 1 == 1; - let mut processor = DpaProcessor::new(traces.shape()[1], 256, selection_function); + let mut processor = DpaProcessor::new(traces.shape()[1], 256); for i in 0..traces.shape()[0] { - processor.update(traces.row(i).map(|&x| x as f32).view(), plaintexts.row(i)); + processor.update( + traces.row(i).map(|&x| x as f32).view(), + plaintexts.row(i), + selection_function, + ); } assert_eq!( processor.finalize().differential_curves(), @@ -289,4 +317,45 @@ mod tests { .differential_curves() ); } + + #[test] + fn test_serialize_deserialize_processor() { + let traces = array![ + [77usize, 137, 51, 91], + [72, 61, 91, 83], + [39, 49, 52, 23], + [26, 114, 63, 45], + [30, 8, 97, 91], + [13, 68, 7, 45], + [17, 181, 60, 34], + [43, 88, 76, 78], + [0, 36, 35, 0], + [93, 191, 49, 26], + ]; + let plaintexts = array![[1], [3], [1], [2], [3], [2], [2], [1], [3], [1]]; + + let selection_function = + |plaintext: ArrayView1, guess| (plaintext[0] as usize ^ guess) & 1 == 1; + let mut processor = DpaProcessor::new(traces.shape()[1], 256); + for i in 0..traces.shape()[0] { + processor.update( + traces.row(i).map(|&x| x as f32).view(), + plaintexts.row(i), + selection_function, + ); + } + + let serialized = serde_json::to_string(&processor).unwrap(); + let mut deserializer = serde_json::Deserializer::from_str(serialized.as_str()); + let restored_processor = + DpaProcessor::>::deserialize(&mut deserializer).unwrap(); + + assert_eq!(processor.num_samples, restored_processor.num_samples); + assert_eq!(processor.guess_range, restored_processor.guess_range); + assert_eq!(processor.sum_0, restored_processor.sum_0); + assert_eq!(processor.sum_1, restored_processor.sum_1); + assert_eq!(processor.count_0, restored_processor.count_0); + assert_eq!(processor.count_1, restored_processor.count_1); + assert_eq!(processor.num_traces, restored_processor.num_traces); + } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..e7db06b --- /dev/null +++ b/src/error.rs @@ -0,0 +1,10 @@ +use std::io; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Failed to save/load muscat data")] + SaveLoadError(#[from] serde_json::Error), + #[error(transparent)] + IoError(#[from] io::Error), +} diff --git a/src/leakage_detection.rs b/src/leakage_detection.rs index be54398..4cbe45d 100644 --- a/src/leakage_detection.rs +++ b/src/leakage_detection.rs @@ -1,8 +1,9 @@ //! Leakage detection methods -use crate::processors::MeanVar; +use crate::{processors::MeanVar, Error}; use ndarray::{s, Array1, Array2, ArrayView1, ArrayView2, Axis}; use rayon::iter::{ParallelBridge, ParallelIterator}; -use std::{iter::zip, ops::Add}; +use serde::{Deserialize, Serialize}; +use std::{fs::File, iter::zip, ops::Add, path::Path}; /// Compute the SNR of the given traces using [`SnrProcessor`]. /// @@ -74,7 +75,7 @@ where } /// A Processor that computes the Signal-to-Noise Ratio of the given traces -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct SnrProcessor { mean_var: MeanVar, /// Sum of traces per class @@ -104,6 +105,7 @@ impl SnrProcessor { /// - Panics in debug if the length of the trace is different from the size of [`SnrProcessor`]. pub fn process + Copy>(&mut self, trace: ArrayView1, class: usize) { debug_assert!(trace.len() == self.size()); + debug_assert!(class < self.num_classes()); self.mean_var.process(trace); @@ -149,6 +151,30 @@ impl SnrProcessor { self.classes_count.len() } + /// Save the [`SnrProcessor`] to a file. + /// + /// # Warning + /// The file format is not stable as muscat is active development. Thus, the format might + /// change between versions. + pub fn save>(&self, path: P) -> Result<(), Error> { + let file = File::create(path)?; + serde_json::to_writer(file, self)?; + + Ok(()) + } + + /// Load a [`SnrProcessor`] from a file. + /// + /// # Warning + /// The file format is not stable as muscat is active development. Thus, the format might + /// change between versions. + pub fn load>(path: P) -> Result { + let file = File::open(path)?; + let p = serde_json::from_reader(file)?; + + Ok(p) + } + /// Determine if two [`SnrProcessor`] are compatible for addition. /// /// If they were created with the same parameters, they are compatible. @@ -235,7 +261,7 @@ where } /// A Processor that computes the Welch's T-Test of the given traces. -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub struct TTestProcessor { mean_var_1: MeanVar, mean_var_2: MeanVar, @@ -288,6 +314,30 @@ impl TTestProcessor { self.mean_var_1.size() } + /// Save the [`TTestProcessor`] to a file. + /// + /// # Warning + /// The file format is not stable as muscat is active development. Thus, the format might + /// change between versions. + pub fn save>(&self, path: P) -> Result<(), Error> { + let file = File::create(path)?; + serde_json::to_writer(file, self)?; + + Ok(()) + } + + /// Load a [`TTestProcessor`] from a file. + /// + /// # Warning + /// The file format is not stable as muscat is active development. Thus, the format might + /// change between versions. + pub fn load>(path: P) -> Result { + let file = File::open(path)?; + let p = serde_json::from_reader(file)?; + + Ok(p) + } + /// Determine if two [`TTestProcessor`] are compatible for addition. /// /// If they were created with the same parameters, they are compatible. diff --git a/src/lib.rs b/src/lib.rs index fb8ba05..ec27cba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod distinguishers; +pub mod error; pub mod leakage_detection; pub mod leakage_model; pub mod preprocessors; @@ -6,5 +7,7 @@ pub mod processors; pub mod trace; pub mod util; +pub use crate::error::Error; + #[cfg(feature = "quicklog")] pub mod quicklog; diff --git a/src/processors.rs b/src/processors.rs index 1099aa2..1527937 100644 --- a/src/processors.rs +++ b/src/processors.rs @@ -1,9 +1,10 @@ //! Traces processing algorithms use ndarray::{Array1, ArrayView1}; +use serde::{Deserialize, Serialize}; use std::{iter::zip, ops::Add}; /// Processes traces to calculate mean and variance. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct MeanVar { /// Sum of traces sum: Array1,