diff --git a/Cargo.toml b/Cargo.toml index 6217541..b715908 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,10 @@ progress_bar = ["dep:indicatif"] quicklog = ["dep:thiserror"] [dependencies] -serde_json = "1.0.115" +serde_json = "1.0.132" hex = "0.4.3" npyz = "0.8.3" -ndarray = "0.16.1" +ndarray = { version = "0.16.1", features = ["serde"] } rayon = "1.10.0" indicatif = { version = "0.17.8", optional = true } ndarray-npy ="0.9.1" @@ -25,6 +25,7 @@ itertools = "0.13.0" thiserror = { version = "1.0.58", optional = true } 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" diff --git a/src/distinguishers/cpa.rs b/src/distinguishers/cpa.rs index 3e87393..8c4eba4 100644 --- a/src/distinguishers/cpa.rs +++ b/src/distinguishers/cpa.rs @@ -4,6 +4,7 @@ use rayon::{ iter::ParallelBridge, prelude::{IntoParallelIterator, ParallelIterator}, }; +use serde::{Deserialize, Serialize}; use std::{iter::zip, ops::Add}; /// Compute the [`Cpa`] of the given traces using [`CpaProcessor`]. @@ -119,6 +120,7 @@ impl Cpa { /// It implements algorithm 4 from [^1]. /// /// [^1]: +#[derive(Debug, PartialEq)] pub struct CpaProcessor where F: Fn(usize, usize) -> usize, @@ -285,10 +287,69 @@ where } } +/// This type implements [`Deserialize`] on the subset of fields of [`CpaProcessor`] that are +/// serializable. +/// +/// [`CpaProcessor`] cannot implement [`Deserialize`] for every type `F` as it does not +/// implement [`Default`]. One solution would be to erase the type, but that would add an +/// indirection which could hurt the performance (not benchmarked though). +#[derive(Serialize, Deserialize)] +pub struct CpaProcessorSerdeAdapter { + num_samples: usize, + target_byte: usize, + guess_range: usize, + sum_traces: Array1, + sum_square_traces: Array1, + guess_sum_traces: Array1, + guess_sum_squares_traces: Array1, + plaintext_sum_traces: Array2, + num_traces: usize, +} + +impl CpaProcessorSerdeAdapter { + pub fn with(self, leakage_func: F) -> CpaProcessor + where + F: Fn(usize, usize) -> usize, + { + CpaProcessor { + num_samples: self.num_samples, + target_byte: self.target_byte, + guess_range: self.guess_range, + sum_traces: self.sum_traces, + sum_square_traces: self.sum_square_traces, + guess_sum_traces: self.guess_sum_traces, + guess_sum_squares_traces: self.guess_sum_squares_traces, + plaintext_sum_traces: self.plaintext_sum_traces, + leakage_func, + num_traces: self.num_traces, + } + } +} + +impl From<&CpaProcessor> for CpaProcessorSerdeAdapter +where + F: Fn(usize, usize) -> usize, +{ + fn from(processor: &CpaProcessor) -> Self { + Self { + num_samples: processor.num_samples, + target_byte: processor.target_byte, + guess_range: processor.guess_range, + sum_traces: processor.sum_traces.clone(), + sum_square_traces: processor.sum_square_traces.clone(), + guess_sum_traces: processor.guess_sum_traces.clone(), + guess_sum_squares_traces: processor.guess_sum_squares_traces.clone(), + plaintext_sum_traces: processor.plaintext_sum_traces.clone(), + num_traces: processor.num_traces, + } + } +} + #[cfg(test)] mod tests { - use super::{cpa, CpaProcessor}; + use super::{cpa, CpaProcessor, CpaProcessorSerdeAdapter}; use ndarray::array; + use serde::Deserialize; #[test] fn test_cpa_helper() { @@ -316,4 +377,56 @@ mod tests { 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, leakage_model); + for i in 0..traces.shape()[0] { + processor.update(traces.row(i), plaintexts.row(i)); + } + + let serialized = + serde_json::to_string(&CpaProcessorSerdeAdapter::from(&processor)).unwrap(); + let mut deserializer = serde_json::Deserializer::from_str(serialized.as_str()); + let restored_processor = CpaProcessorSerdeAdapter::deserialize(&mut deserializer) + .unwrap() + .with(leakage_model); + + 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..c91b4d6 100644 --- a/src/distinguishers/cpa_normal.rs +++ b/src/distinguishers/cpa_normal.rs @@ -1,5 +1,6 @@ use ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis}; use rayon::iter::{ParallelBridge, ParallelIterator}; +use serde::{Deserialize, Serialize}; use std::{iter::zip, ops::Add}; use crate::distinguishers::cpa::Cpa; @@ -246,12 +247,74 @@ where } } +/// This type implements [`Deserialize`] on the subset of fields of [`CpaProcessor`] that are +/// serializable. +/// +/// [`CpaProcessor`] cannot implement [`Deserialize`] for every type `F` as it does not +/// implement [`Default`]. One solution would be to erase the type, but that would add an +/// indirection which could hurt the performance (not benchmarked though). +#[derive(Serialize, Deserialize)] +pub struct CpaProcessorSerdeAdapter { + num_samples: usize, + guess_range: usize, + sum_traces: Array1, + sum_traces2: Array1, + guess_sum_traces: Array1, + guess_sum_traces2: Array1, + values: Array2, + cov: Array2, + batch_size: usize, + num_traces: usize, +} + +impl CpaProcessorSerdeAdapter { + pub fn with(self, leakage_func: F) -> CpaProcessor + where + F: Fn(ArrayView1, usize) -> usize, + { + CpaProcessor { + num_samples: self.num_samples, + guess_range: self.guess_range, + sum_traces: self.sum_traces, + sum_traces2: self.sum_traces2, + guess_sum_traces: self.guess_sum_traces, + guess_sum_traces2: self.guess_sum_traces2, + values: self.values, + cov: self.cov, + batch_size: self.batch_size, + leakage_func, + num_traces: self.num_traces, + } + } +} + +impl From<&CpaProcessor> for CpaProcessorSerdeAdapter +where + F: Fn(ArrayView1, usize) -> usize, +{ + fn from(processor: &CpaProcessor) -> Self { + Self { + num_samples: processor.num_samples, + guess_range: processor.guess_range, + sum_traces: processor.sum_traces.clone(), + sum_traces2: processor.sum_traces2.clone(), + guess_sum_traces: processor.guess_sum_traces.clone(), + guess_sum_traces2: processor.guess_sum_traces2.clone(), + values: processor.values.clone(), + cov: processor.cov.clone(), + batch_size: processor.batch_size, + num_traces: processor.num_traces, + } + } +} + #[cfg(test)] mod tests { use std::iter::zip; - use super::{cpa, CpaProcessor}; + use super::{cpa, CpaProcessor, CpaProcessorSerdeAdapter}; use ndarray::{array, ArrayView1, Axis}; + use serde::Deserialize; #[test] fn test_cpa_helper() { @@ -289,4 +352,54 @@ 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, leakage_model); + 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()); + } + + let serialized = + serde_json::to_string(&CpaProcessorSerdeAdapter::from(&processor)).unwrap(); + let mut deserializer = serde_json::Deserializer::from_str(serialized.as_str()); + let restored_processor = CpaProcessorSerdeAdapter::deserialize(&mut deserializer) + .unwrap() + .with(leakage_model); + + 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..4f834f5 100644 --- a/src/distinguishers/dpa.rs +++ b/src/distinguishers/dpa.rs @@ -1,5 +1,6 @@ use ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis}; use rayon::iter::{ParallelBridge, ParallelIterator}; +use serde::{Deserialize, Serialize}; use std::{iter::zip, marker::PhantomData, ops::Add}; use crate::util::{argmax_by, argsort_by, max_per_row}; @@ -246,10 +247,64 @@ where } } +/// This type implements [`Deserialize`] on the subset of fields of [`DpaProcessor`] that are +/// serializable. +/// +/// [`DpaProcessor`] cannot implement [`Deserialize`] for every type `F` as it does not +/// implement [`Default`]. One solution would be to erase the type, but that would add an +/// indirection which could hurt the performance (not benchmarked though). +#[derive(Serialize, Deserialize)] +pub struct DpaProcessorSerdeAdapter { + num_samples: usize, + guess_range: usize, + sum_0: Array2, + sum_1: Array2, + count_0: Array1, + count_1: Array1, + num_traces: usize, +} + +impl DpaProcessorSerdeAdapter { + pub fn with(self, selection_function: F) -> DpaProcessor + where + F: Fn(M, usize) -> bool, + { + DpaProcessor { + num_samples: self.num_samples, + guess_range: self.guess_range, + sum_0: self.sum_0, + sum_1: self.sum_1, + count_0: self.count_0, + count_1: self.count_1, + selection_function, + num_traces: self.num_traces, + _metadata: PhantomData, + } + } +} + +impl From<&DpaProcessor> for DpaProcessorSerdeAdapter +where + F: Fn(M, usize) -> bool, +{ + fn from(processor: &DpaProcessor) -> Self { + Self { + num_samples: processor.num_samples, + guess_range: processor.guess_range, + sum_0: processor.sum_0.clone(), + sum_1: processor.sum_1.clone(), + count_0: processor.count_0.clone(), + count_1: processor.count_1.clone(), + num_traces: processor.num_traces, + } + } +} + #[cfg(test)] mod tests { - use super::{dpa, DpaProcessor}; + use super::{dpa, DpaProcessor, DpaProcessorSerdeAdapter}; use ndarray::{array, Array1, ArrayView1}; + use serde::Deserialize; #[test] fn test_dpa_helper() { @@ -289,4 +344,43 @@ 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, selection_function); + for i in 0..traces.shape()[0] { + processor.update(traces.row(i).map(|&x| x as f32).view(), plaintexts.row(i)); + } + + let serialized = + serde_json::to_string(&DpaProcessorSerdeAdapter::from(&processor)).unwrap(); + let mut deserializer = serde_json::Deserializer::from_str(serialized.as_str()); + let restored_processor = DpaProcessorSerdeAdapter::deserialize(&mut deserializer) + .unwrap() + .with(selection_function); + + 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/leakage_detection.rs b/src/leakage_detection.rs index be54398..dd06f19 100644 --- a/src/leakage_detection.rs +++ b/src/leakage_detection.rs @@ -2,6 +2,7 @@ use crate::processors::MeanVar; use ndarray::{s, Array1, Array2, ArrayView1, ArrayView2, Axis}; use rayon::iter::{ParallelBridge, ParallelIterator}; +use serde::{Deserialize, Serialize}; use std::{iter::zip, ops::Add}; /// 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 @@ -235,7 +236,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, 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,