From e5479f8183f6eba50706ca841ca47fc9e26ee9b0 Mon Sep 17 00:00:00 2001 From: randogoth Date: Sun, 29 Dec 2024 23:41:10 +0200 Subject: [PATCH 01/10] better names --- src/main.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1a8a329..d66e84e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use chrono::Local; struct Ocelli; impl Ocelli { - fn get_entropy(&self, current: &Vec, previous: &Vec, width: usize, minimum_distance: usize) -> Vec { + fn chop_and_tack(&self, current: &Vec, previous: &Vec, width: usize, minimum_distance: usize) -> Vec { let mut entropy = Vec::new(); let mut current_byte = 0u8; let mut bit_count = 0; @@ -21,7 +21,7 @@ impl Ocelli { // Calculate the height of the frame let height = current.len() / width; - // Skip the first and last 100 rows (width * 100 pixels) + // Skip 100 pixels at the top and bottom of the frame let start_row = 100; let end_row = height - 100; @@ -29,9 +29,8 @@ impl Ocelli { panic!("Resolution is too small to apply the grid selection with the given offset."); } - // Iterate through rows, skipping the top and bottom 100 rows for row in start_row..end_row { - // Skip the first 100 pixels in the row + // Skip 100 pixels at the left and right of the frame let row_start = row * width + 100; let row_end = (row + 1) * width - 100; @@ -65,7 +64,7 @@ impl Ocelli { entropy } - fn pick_and_fold(&self, data: &Vec, current_frame_index: usize) -> Vec { + fn pick_and_flip(&self, data: &Vec, current_frame_index: usize) -> Vec { let mut entropy = Vec::new(); let mut current_byte = 0u8; let mut bit_count = 0; @@ -223,10 +222,10 @@ fn main() -> opencv::Result<()> { let mut entropy: Vec = Vec::new(); if uncovered { - entropy = ocelli.pick_and_fold(¤t_frame_data, frame_index); + entropy = ocelli.pick_and_flip(¤t_frame_data, frame_index); frame_index = frame_index + 1; } else { - entropy = ocelli.get_entropy(¤t_frame_data, &previous_frame_data, width, 20); + entropy = ocelli.chop_and_tack(¤t_frame_data, &previous_frame_data, width, 30); previous_frame_data = current_frame_data; } @@ -263,7 +262,7 @@ fn main() -> opencv::Result<()> { // Save the generated entropy to a binary file let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string(); - let method = if uncovered { "pick_and_fold" } else { "get_entropy" }; + let method = if uncovered { "pick_and_flip" } else { "chop_and_tack" }; let whitened = if whiten_flag { "_whitened" } else { "" }; let filename = format!("{}{}_{}.bin", method, whitened, timestamp); From 700f2770b6143083f36d5145ce940e2434048a26 Mon Sep 17 00:00:00 2001 From: randogoth Date: Sun, 29 Dec 2024 23:50:20 +0200 Subject: [PATCH 02/10] update readme --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e23681d..5a41dee 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # ocelli: Camera-Based TRNG -**Ocelli** is a Rust application that generates high-quality entropy using a camera feed. The application supports two entropy generation methods (`get_entropy` and `chop_and_stack`) and optionally applies Van Neumann whitening for enhanced randomness. Generated entropy is saved as a binary file. +**Ocelli** is a Rust application that generates high-quality entropy using a camera feed. The application supports two entropy generation methods (`chop_and_tack` and `pick_and_flip`) and optionally applies Van Neumann whitening for enhanced randomness. Generated entropy is saved as a binary file. ## Features - **Entropy Methods**: - - `get_entropy`: Compares pixel values between frames. - - `chop_and_stack`: Combines and processes pixel data with reversed rows. + - `chop_and_tack`: Compares pixel values between frames. + - `pick_and_flip`: Processes pixel data and flips bits every second frame. - **Van Neumann Whitening**: Optional, enabled via the `-w` flag. - **Shannon Entropy Test**: Ensures the randomness quality of generated entropy. @@ -19,10 +19,10 @@ cargo run --release -- Date: Mon, 30 Dec 2024 18:14:12 +0200 Subject: [PATCH 03/10] ocelli as library, better capture algorithm, high resolution --- Cargo.toml | 9 +- src/bin/main.rs | 144 +++++++++++++++++++++++++ src/lib.rs | 137 ++++++++++++++++++++++++ src/main.rs | 275 ------------------------------------------------ 4 files changed, 288 insertions(+), 277 deletions(-) create mode 100644 src/bin/main.rs create mode 100644 src/lib.rs delete mode 100644 src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 135b75e..bf62e94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,11 @@ name = "ocelli" version = "0.1.0" edition = "2021" +[lib] +name = "ocelli" +path = "src/lib.rs" + [dependencies] -chrono = "0.4.39" -opencv = "0.93.5" +v4l = "0.14" +chrono = "0.4" +image = "0.25.5" diff --git a/src/bin/main.rs b/src/bin/main.rs new file mode 100644 index 0000000..b0607b2 --- /dev/null +++ b/src/bin/main.rs @@ -0,0 +1,144 @@ +use ocelli::Ocelli; +use v4l::prelude::*; +use v4l::video::Capture; +use v4l::buffer::Type; +use v4l::format::FourCC; +use v4l::io::traits::CaptureStream; +use image::ImageReader; +use std::fs::File; +use std::io::{stdin, Write}; +use chrono::Local; +use std::time::Instant; + +fn frame_to_grayscale(data: &[u8]) -> Vec { + let img = ImageReader::new(std::io::Cursor::new(data)) + .with_guessed_format() + .expect("Failed to guess format") + .decode() + .expect("Failed to decode image"); + let gray = img.into_luma8(); // Convert to grayscale + gray.into_raw() // Return raw pixel data as Vec +} + +fn main() -> Result<(), Box> { + let args: Vec = std::env::args().collect(); + if args.len() < 3 { + eprintln!("Usage: {} ", args[0]); + std::process::exit(1); + } + + // Check if the whitening flag is set + let quick = args.contains(&String::from("-q")); + + let camera_index: usize = args[1].parse().expect("Failed to parse camera index as a number"); + let length: usize = args[2].parse().expect("Failed to parse entropy length as a number"); + + let dev = Device::new(camera_index).expect("Failed to open camera"); + + // Set the desired format and resolution + let mut format = dev.format().expect("Failed to get camera format"); + format.fourcc = FourCC::new(b"MJPG"); // Use MJPG for higher resolutions + format.width = 1920; + format.height = 1080; + + if let Err(_) = dev.set_format(&format) { + println!("Failed to set resolution to 1920x1080. Falling back to 1280x720."); + format.width = 1280; + format.height = 720; + dev.set_format(&format) + .expect("Failed to set resolution to 1280x720"); + } + + println!( + "Using resolution: {}x{} (FourCC: {})", + format.width, format.height, format.fourcc + ); + + let mut stream = MmapStream::with_buffers(&dev, Type::VideoCapture, 4) + .expect("Failed to create stream"); + + let ocelli = Ocelli; + let mut total_entropy = Vec::new(); + let start_time = Instant::now(); + let shannon_threshold = 4.0; + let mut frame_count = 0; + + while total_entropy.len() < length { + // Capture first frame + let (data1, _) = stream.next().expect("Failed to capture frame"); + let grayscale_data1 = frame_to_grayscale(&data1); + + if ocelli.is_covered(&grayscale_data1, 50) { + // Skip the first 10 frames + while frame_count < 10 { + let _ = stream.next().expect("Failed to capture frame"); + frame_count += 1; + } + + let entropy: Vec; + + if quick { + // Quicker capture using Pick and Flip + entropy = ocelli.pick_and_flip(&grayscale_data1, frame_count as usize); + } else { + // Capture second frame + let (data2, _) = stream.next().expect("Failed to capture second frame"); + let grayscale_data2 = frame_to_grayscale(&data2); + + // Generate entropy using chop_and_tack + entropy = ocelli.chop_and_tack(&grayscale_data1, &grayscale_data2, format.width as usize, 30); + } + + let shannon_entropy = ocelli.shannon(&entropy); + + if shannon_entropy >= shannon_threshold { + total_entropy.extend(entropy); + println!( + "Collected {} of {} bytes of entropy (Shannon entropy: {:.3})", + total_entropy.len(), + length, + shannon_entropy + ); + } else { + println!("Rejected entropy (Shannon entropy: {:.3})", shannon_entropy); + } + } else { + println!("Camera is not covered. Please cover the camera."); + frame_count = 0; + } + } + + // Trim total_entropy to the exact length + total_entropy.truncate(length); + + // Print elapsed time + let elapsed_time = start_time.elapsed(); + println!( + "Process completed in {:.3} seconds.", + elapsed_time.as_secs_f64() + ); + + // Final Shannon entropy test + let final_shannon_entropy = ocelli.shannon(&total_entropy); + println!("Final Shannon entropy: {:.3}", final_shannon_entropy); + + // Ask if the result should be saved + println!("Save result to a file or print as hex string? (file/print):"); + let mut input = String::new(); + stdin().read_line(&mut input).expect("Failed to read input"); + + if input.trim().eq_ignore_ascii_case("file") { + let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string(); + let filename = format!("entropy_{}.bin", timestamp); + + let mut file = File::create(&filename).expect("Failed to create file"); + file.write_all(&total_entropy).expect("Failed to write data to file"); + + println!("Entropy saved to file: {}", filename); + } else if input.trim().eq_ignore_ascii_case("print") { + let entropy_hex: String = total_entropy.iter().map(|b| format!("{:02x}", b)).collect(); + println!("Generated entropy (hex): {}", entropy_hex); + } + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c90df95 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,137 @@ +use std::collections::{HashMap, HashSet}; + +pub struct Ocelli; + +impl Ocelli { + + pub fn chop_and_tack(&self, current: &Vec, previous: &Vec, width: usize, minimum_distance: usize) -> Vec { + let mut entropy = Vec::new(); + let mut current_byte = 0u8; + let mut bit_count = 0; + + // Calculate the height of the frame + let height = current.len() / width; + + // Skip 100 pixels at the top and bottom of the frame + let start_row = 100; + let end_row = height - 100; + + if start_row >= end_row || width <= 200 { + panic!("Resolution is too small to apply the grid selection with the given offset."); + } + + for row in start_row..end_row { + // Skip 100 pixels at the left and right of the frame + let row_start = row * width + 100; + let row_end = (row + 1) * width - 100; + + // Select pixels in the row based on the step size + for pixel_index in (row_start..row_end).step_by(minimum_distance) { + if pixel_index >= current.len() || pixel_index >= previous.len() { + continue; + } + + let c = current[pixel_index]; + let p = previous[pixel_index]; + + if c > p { + current_byte = (current_byte << 1) | 1; // Append '1' + } else if c < p { + current_byte = current_byte << 1; // Append '0' + } else { + continue; // Skip if equal + } + + bit_count += 1; + + if bit_count == 8 { + entropy.push(current_byte); + current_byte = 0; + bit_count = 0; + } + } + } + + entropy + } + + pub fn pick_and_flip(&self, data: &[u8], current_frame_index: usize) -> Vec { + let mut entropy = Vec::new(); + let mut current_byte = 0u8; + let mut bit_count = 0; + + for &pixel_brightness in data { + if (2..=253).contains(&pixel_brightness) { + let mut lsb = pixel_brightness & 1; + + if current_frame_index % 2 == 0 { + lsb ^= 1; // Flip the bit + } + + current_byte = (current_byte << 1) | lsb; + bit_count += 1; + + if bit_count == 8 { + entropy.push(current_byte); + current_byte = 0; + bit_count = 0; + } + } + } + + entropy + } + + pub fn shannon(&self, data: &Vec) -> f64 { + let mut frequency_map = HashMap::new(); + let data_len = data.len(); + + for &byte in data { + *frequency_map.entry(byte).or_insert(0) += 1; + } + + frequency_map.values().fold(0.0, |entropy, &count| { + let probability = count as f64 / data_len as f64; + entropy - probability * probability.log2() + }) + } + + pub fn whiten(&self, entropy: &[u8]) -> Vec { + let mut whitened_entropy = Vec::new(); + let mut current_byte = 0u8; + let mut bit_count = 0; + + for byte in entropy { + for i in (0..8).step_by(2) { + let bit1 = (byte >> (7 - i)) & 1; + let bit2 = (byte >> (6 - i)) & 1; + + match (bit1, bit2) { + (0, 1) => { + current_byte = (current_byte << 1) | 0; + bit_count += 1; + } + (1, 0) => { + current_byte = (current_byte << 1) | 1; + bit_count += 1; + } + _ => {} + } + + if bit_count == 8 { + whitened_entropy.push(current_byte); + current_byte = 0; + bit_count = 0; + } + } + } + + whitened_entropy + } + + pub fn is_covered(&self, grayscale: &[u8], threshold: usize) -> bool { + let unique_values: HashSet<_> = grayscale.iter().copied().collect(); + // println!("UV: {:?}", unique_values); + unique_values.len() < threshold + } +} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index d66e84e..0000000 --- a/src/main.rs +++ /dev/null @@ -1,275 +0,0 @@ -use opencv::prelude::*; -use opencv::videoio::{VideoCapture, CAP_V4L}; -use opencv::imgproc; -use opencv::core; -use std::collections::HashSet; -use std::collections::HashMap; -use std::env; -use std::time::Instant; -use std::fs::File; -use std::io::Write; -use chrono::Local; - -struct Ocelli; - -impl Ocelli { - fn chop_and_tack(&self, current: &Vec, previous: &Vec, width: usize, minimum_distance: usize) -> Vec { - let mut entropy = Vec::new(); - let mut current_byte = 0u8; - let mut bit_count = 0; - - // Calculate the height of the frame - let height = current.len() / width; - - // Skip 100 pixels at the top and bottom of the frame - let start_row = 100; - let end_row = height - 100; - - if start_row >= end_row || width <= 200 { - panic!("Resolution is too small to apply the grid selection with the given offset."); - } - - for row in start_row..end_row { - // Skip 100 pixels at the left and right of the frame - let row_start = row * width + 100; - let row_end = (row + 1) * width - 100; - - // Select pixels in the row based on the step size - for pixel_index in (row_start..row_end).step_by(minimum_distance) { - if pixel_index >= current.len() || pixel_index >= previous.len() { - continue; - } - - let c = current[pixel_index]; - let p = previous[pixel_index]; - - if c > p { - current_byte = (current_byte << 1) | 1; // Append '1' - } else if c < p { - current_byte = current_byte << 1; // Append '0' - } else { - continue; // Skip if equal - } - - bit_count += 1; - - if bit_count == 8 { - entropy.push(current_byte); - current_byte = 0; - bit_count = 0; - } - } - } - - entropy - } - - fn pick_and_flip(&self, data: &Vec, current_frame_index: usize) -> Vec { - let mut entropy = Vec::new(); - let mut current_byte = 0u8; - let mut bit_count = 0; - - // Iterate through each pixel in the array - for &pixel_brightness in data { - // Check if the brightness is within the valid range [2, 253] - if (2..=253).contains(&pixel_brightness) { - // Extract the least significant bit (LSB) - let mut lsb = pixel_brightness & 1; - - // If the frame index is even, flip the bit - if current_frame_index % 2 == 0 { - lsb ^= 1; // Flip the bit (0 -> 1, 1 -> 0) - } - - // Add the bit to the current byte - current_byte = (current_byte << 1) | lsb; - bit_count += 1; - - // If we have 8 bits, push the byte to the entropy vector - if bit_count == 8 { - entropy.push(current_byte); - current_byte = 0; - bit_count = 0; - } - } - } - - entropy - } - - - /// Apply Van Neumann whitening to a vector of entropy bits - fn whiten(&self, entropy: &Vec) -> Vec { - let mut whitened_entropy = Vec::new(); - let mut current_byte = 0u8; - let mut bit_count = 0; - - // Iterate through the entropy bytes and process bits in pairs - for byte in entropy { - for i in (0..8).step_by(2) { - let bit1 = (byte >> (7 - i)) & 1; - let bit2 = (byte >> (6 - i)) & 1; - - // Apply Van Neumann rules - match (bit1, bit2) { - (0, 1) => { - current_byte = (current_byte << 1) | 0; // Append '0' - bit_count += 1; - } - (1, 0) => { - current_byte = (current_byte << 1) | 1; // Append '1' - bit_count += 1; - } - _ => {} // Discard (0,0) and (1,1) pairs - } - - // If we have a full byte, push it to the output - if bit_count == 8 { - whitened_entropy.push(current_byte); - current_byte = 0; - bit_count = 0; - } - } - } - - whitened_entropy - } - - /// Calculates the Shannon entropy of binary data - fn shannon(&self, data: &Vec) -> f64 { - let mut frequency_map = HashMap::new(); - let data_len = data.len(); - - for &byte in data { - *frequency_map.entry(byte).or_insert(0) += 1; - } - - frequency_map.values().fold(0.0, |entropy, &count| { - let probability = count as f64 / data_len as f64; - entropy - probability * probability.log2() - }) - } - - /// Determines if the camera is covered based on the unique grayscale values - fn is_covered(&self, grayscale: &Vec, threshold: usize) -> bool { - let unique_values: HashSet<_> = grayscale.iter().copied().collect(); - unique_values.len() < threshold - } -} - -fn main() -> opencv::Result<()> { - let args: Vec = env::args().collect(); - if args.len() < 3 { - eprintln!("Usage: {} [-w]", args[0]); - std::process::exit(1); - } - - let camera_index: i32 = args[1].parse().expect("Failed to parse camera index as a number"); - let length: usize = args[2].parse().expect("Failed to parse entropy length as a number"); - let whiten_flag = args.contains(&String::from("-w")); - - let mut cam = VideoCapture::new(camera_index, CAP_V4L)?; - if !cam.is_opened()? { - panic!("Failed to open the camera with index {}", camera_index); - } - - // Capture a single frame to determine resolution - let mut frame = core::Mat::default(); - cam.read(&mut frame)?; - if frame.empty() { - panic!("Failed to capture a frame."); - } - - let width = frame.cols() as usize; - let height = frame.rows() as usize; - - println!( - "Camera index: {}\nCamera resolution detected: {}x{}", - camera_index, width, height - ); - - let ocelli = Ocelli; - - // Convert the frame to grayscale and check if the camera is covered - let mut gray_frame = core::Mat::default(); - imgproc::cvt_color(&frame, &mut gray_frame, imgproc::COLOR_BGR2GRAY, 0)?; - let grayscale_data = gray_frame.data_bytes().expect("Failed to get grayscale data").to_vec(); - - let uncovered = !ocelli.is_covered(&grayscale_data, 50); - - println!( - "Camera is {}covered.", - if uncovered { - "un" - } else { - "" - } - ); - - let mut total_entropy = Vec::new(); - let shannon_threshold = 4.0; - let mut previous_frame_data = grayscale_data.clone(); - - let start_time = Instant::now(); - let mut frame_index = 1; - - while total_entropy.len() < length { - cam.read(&mut frame)?; - let mut gray_frame = core::Mat::default(); - imgproc::cvt_color(&frame, &mut gray_frame, imgproc::COLOR_BGR2GRAY, 0)?; - let current_frame_data = gray_frame.data_bytes().expect("Failed to get grayscale data").to_vec(); - - let mut entropy: Vec = Vec::new(); - - if uncovered { - entropy = ocelli.pick_and_flip(¤t_frame_data, frame_index); - frame_index = frame_index + 1; - } else { - entropy = ocelli.chop_and_tack(¤t_frame_data, &previous_frame_data, width, 30); - previous_frame_data = current_frame_data; - } - - if whiten_flag { - entropy = ocelli.whiten(&entropy); - } - - let shannon_entropy = ocelli.shannon(&entropy); - - if shannon_entropy >= shannon_threshold { - total_entropy.extend(entropy); - } else { - println!( - "Rejected entropy array (Shannon entropy: {:.3}). Retrying...", - shannon_entropy - ); - } - - println!( - "Collected {} of {} bytes of entropy...", - total_entropy.len(), - length - ); - } - - let total_shannon_entropy = ocelli.shannon(&total_entropy); - - let elapsed_time = start_time.elapsed(); - println!( - "Process completed in {:.3} seconds.\nShannon Entropy {:.3}.", - elapsed_time.as_secs_f64(), - total_shannon_entropy - ); - - // Save the generated entropy to a binary file - let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string(); - let method = if uncovered { "pick_and_flip" } else { "chop_and_tack" }; - let whitened = if whiten_flag { "_whitened" } else { "" }; - let filename = format!("{}{}_{}.bin", method, whitened, timestamp); - - let mut file = File::create(&filename).expect("Failed to create file"); - file.write_all(&total_entropy).expect("Failed to write data to file"); - - println!("Entropy saved to file: {}", filename); - - Ok(()) -} From 37df6fc27e4adf87e4ed8c6174ef2aa25e578160 Mon Sep 17 00:00:00 2001 From: randogoth Date: Mon, 30 Dec 2024 22:35:30 +0200 Subject: [PATCH 04/10] raised thresholds --- src/bin/main.rs | 78 +++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index b0607b2..14ded3c 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -60,51 +60,53 @@ fn main() -> Result<(), Box> { let ocelli = Ocelli; let mut total_entropy = Vec::new(); let start_time = Instant::now(); - let shannon_threshold = 4.0; + let shannon_threshold = 7.9; let mut frame_count = 0; while total_entropy.len() < length { // Capture first frame let (data1, _) = stream.next().expect("Failed to capture frame"); - let grayscale_data1 = frame_to_grayscale(&data1); - if ocelli.is_covered(&grayscale_data1, 50) { - // Skip the first 10 frames - while frame_count < 10 { - let _ = stream.next().expect("Failed to capture frame"); - frame_count += 1; - } - - let entropy: Vec; - - if quick { - // Quicker capture using Pick and Flip - entropy = ocelli.pick_and_flip(&grayscale_data1, frame_count as usize); - } else { - // Capture second frame - let (data2, _) = stream.next().expect("Failed to capture second frame"); - let grayscale_data2 = frame_to_grayscale(&data2); - - // Generate entropy using chop_and_tack - entropy = ocelli.chop_and_tack(&grayscale_data1, &grayscale_data2, format.width as usize, 30); - } - - let shannon_entropy = ocelli.shannon(&entropy); - - if shannon_entropy >= shannon_threshold { - total_entropy.extend(entropy); - println!( - "Collected {} of {} bytes of entropy (Shannon entropy: {:.3})", - total_entropy.len(), - length, - shannon_entropy - ); - } else { - println!("Rejected entropy (Shannon entropy: {:.3})", shannon_entropy); - } + // Skip the first 30 frames + if frame_count <= 30 { + frame_count += 1; } else { - println!("Camera is not covered. Please cover the camera."); - frame_count = 0; + + let grayscale_data1 = frame_to_grayscale(&data1); + + if !ocelli.is_covered(&grayscale_data1, 50) { + + let entropy: Vec; + + if quick { + // Quicker capture using Pick and Flip + entropy = ocelli.whiten(&ocelli.pick_and_flip(&grayscale_data1, frame_count as usize)); + } else { + // Capture second frame + let (data2, _) = stream.next().expect("Failed to capture second frame"); + let grayscale_data2 = frame_to_grayscale(&data2); + + // Generate entropy using chop_and_tack + entropy = ocelli.chop_and_tack(&grayscale_data1, &grayscale_data2, format.width as usize, 30); + } + + let shannon_entropy = ocelli.shannon(&entropy); + + if shannon_entropy >= shannon_threshold { + total_entropy.extend(entropy); + println!( + "Collected {} of {} bytes of entropy (Shannon entropy: {:.3})", + total_entropy.len(), + length, + shannon_entropy + ); + } else { + println!("Rejected entropy for frame {} (Shannon entropy: {:.3})", frame_count, shannon_entropy); + } + } else { + println!("Camera is not covered. Please cover the camera."); + frame_count = 0; + } } } From 7ed1e5e4e69a7b1d511f2a72595b85c27ba9b4e9 Mon Sep 17 00:00:00 2001 From: randogoth Date: Mon, 30 Dec 2024 22:54:40 +0200 Subject: [PATCH 05/10] allow pick and flip with open cam --- src/bin/main.rs | 50 ++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index 14ded3c..a71386c 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -27,7 +27,7 @@ fn main() -> Result<(), Box> { std::process::exit(1); } - // Check if the whitening flag is set + // Check if the quick flag is set let quick = args.contains(&String::from("-q")); let camera_index: usize = args[1].parse().expect("Failed to parse camera index as a number"); @@ -74,39 +74,39 @@ fn main() -> Result<(), Box> { let grayscale_data1 = frame_to_grayscale(&data1); - if !ocelli.is_covered(&grayscale_data1, 50) { - - let entropy: Vec; - - if quick { - // Quicker capture using Pick and Flip - entropy = ocelli.whiten(&ocelli.pick_and_flip(&grayscale_data1, frame_count as usize)); - } else { + let mut entropy: Vec = [0].to_vec(); + + if quick { + // Quicker capture using Pick and Flip + entropy = ocelli.whiten(&ocelli.pick_and_flip(&grayscale_data1, frame_count as usize)); + } else { + if ocelli.is_covered(&grayscale_data1, 50) { // Capture second frame let (data2, _) = stream.next().expect("Failed to capture second frame"); let grayscale_data2 = frame_to_grayscale(&data2); // Generate entropy using chop_and_tack entropy = ocelli.chop_and_tack(&grayscale_data1, &grayscale_data2, format.width as usize, 30); - } - - let shannon_entropy = ocelli.shannon(&entropy); - - if shannon_entropy >= shannon_threshold { - total_entropy.extend(entropy); - println!( - "Collected {} of {} bytes of entropy (Shannon entropy: {:.3})", - total_entropy.len(), - length, - shannon_entropy - ); } else { - println!("Rejected entropy for frame {} (Shannon entropy: {:.3})", frame_count, shannon_entropy); + println!("Camera is not covered. Please cover the camera."); + frame_count = 0; } - } else { - println!("Camera is not covered. Please cover the camera."); - frame_count = 0; } + + let shannon_entropy = ocelli.shannon(&entropy); + + if shannon_entropy >= shannon_threshold { + total_entropy.extend(entropy); + println!( + "Collected {} of {} bytes of entropy (Shannon entropy: {:.3})", + total_entropy.len(), + length, + shannon_entropy + ); + } else { + println!("Rejected entropy for frame {} (Shannon entropy: {:.3})", frame_count, shannon_entropy); + } + } } From bdf3e8dde580450b2190ae13b0768717af3684ea Mon Sep 17 00:00:00 2001 From: randogoth Date: Tue, 31 Dec 2024 23:37:57 +0200 Subject: [PATCH 06/10] FFI --- Cargo.toml | 4 +- justfile | 6 +- src/bin/main.rs | 146 ------------------------------------------------ src/lib.rs | 80 ++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 150 deletions(-) delete mode 100644 src/bin/main.rs diff --git a/Cargo.toml b/Cargo.toml index bf62e94..8f24fec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,6 @@ edition = "2021" [lib] name = "ocelli" path = "src/lib.rs" +crate-type = ["cdylib"] [dependencies] -v4l = "0.14" -chrono = "0.4" -image = "0.25.5" diff --git a/justfile b/justfile index 987eabc..98e6851 100644 --- a/justfile +++ b/justfile @@ -1,2 +1,6 @@ diehard file: - dieharder -a -g 201 -f {{file}} \ No newline at end of file + dieharder -a -g 201 -f {{file}} + +build arg: + cargo build {{arg}} + cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 build {{arg}} diff --git a/src/bin/main.rs b/src/bin/main.rs deleted file mode 100644 index a71386c..0000000 --- a/src/bin/main.rs +++ /dev/null @@ -1,146 +0,0 @@ -use ocelli::Ocelli; -use v4l::prelude::*; -use v4l::video::Capture; -use v4l::buffer::Type; -use v4l::format::FourCC; -use v4l::io::traits::CaptureStream; -use image::ImageReader; -use std::fs::File; -use std::io::{stdin, Write}; -use chrono::Local; -use std::time::Instant; - -fn frame_to_grayscale(data: &[u8]) -> Vec { - let img = ImageReader::new(std::io::Cursor::new(data)) - .with_guessed_format() - .expect("Failed to guess format") - .decode() - .expect("Failed to decode image"); - let gray = img.into_luma8(); // Convert to grayscale - gray.into_raw() // Return raw pixel data as Vec -} - -fn main() -> Result<(), Box> { - let args: Vec = std::env::args().collect(); - if args.len() < 3 { - eprintln!("Usage: {} ", args[0]); - std::process::exit(1); - } - - // Check if the quick flag is set - let quick = args.contains(&String::from("-q")); - - let camera_index: usize = args[1].parse().expect("Failed to parse camera index as a number"); - let length: usize = args[2].parse().expect("Failed to parse entropy length as a number"); - - let dev = Device::new(camera_index).expect("Failed to open camera"); - - // Set the desired format and resolution - let mut format = dev.format().expect("Failed to get camera format"); - format.fourcc = FourCC::new(b"MJPG"); // Use MJPG for higher resolutions - format.width = 1920; - format.height = 1080; - - if let Err(_) = dev.set_format(&format) { - println!("Failed to set resolution to 1920x1080. Falling back to 1280x720."); - format.width = 1280; - format.height = 720; - dev.set_format(&format) - .expect("Failed to set resolution to 1280x720"); - } - - println!( - "Using resolution: {}x{} (FourCC: {})", - format.width, format.height, format.fourcc - ); - - let mut stream = MmapStream::with_buffers(&dev, Type::VideoCapture, 4) - .expect("Failed to create stream"); - - let ocelli = Ocelli; - let mut total_entropy = Vec::new(); - let start_time = Instant::now(); - let shannon_threshold = 7.9; - let mut frame_count = 0; - - while total_entropy.len() < length { - // Capture first frame - let (data1, _) = stream.next().expect("Failed to capture frame"); - - // Skip the first 30 frames - if frame_count <= 30 { - frame_count += 1; - } else { - - let grayscale_data1 = frame_to_grayscale(&data1); - - let mut entropy: Vec = [0].to_vec(); - - if quick { - // Quicker capture using Pick and Flip - entropy = ocelli.whiten(&ocelli.pick_and_flip(&grayscale_data1, frame_count as usize)); - } else { - if ocelli.is_covered(&grayscale_data1, 50) { - // Capture second frame - let (data2, _) = stream.next().expect("Failed to capture second frame"); - let grayscale_data2 = frame_to_grayscale(&data2); - - // Generate entropy using chop_and_tack - entropy = ocelli.chop_and_tack(&grayscale_data1, &grayscale_data2, format.width as usize, 30); - } else { - println!("Camera is not covered. Please cover the camera."); - frame_count = 0; - } - } - - let shannon_entropy = ocelli.shannon(&entropy); - - if shannon_entropy >= shannon_threshold { - total_entropy.extend(entropy); - println!( - "Collected {} of {} bytes of entropy (Shannon entropy: {:.3})", - total_entropy.len(), - length, - shannon_entropy - ); - } else { - println!("Rejected entropy for frame {} (Shannon entropy: {:.3})", frame_count, shannon_entropy); - } - - } - } - - // Trim total_entropy to the exact length - total_entropy.truncate(length); - - // Print elapsed time - let elapsed_time = start_time.elapsed(); - println!( - "Process completed in {:.3} seconds.", - elapsed_time.as_secs_f64() - ); - - // Final Shannon entropy test - let final_shannon_entropy = ocelli.shannon(&total_entropy); - println!("Final Shannon entropy: {:.3}", final_shannon_entropy); - - // Ask if the result should be saved - println!("Save result to a file or print as hex string? (file/print):"); - let mut input = String::new(); - stdin().read_line(&mut input).expect("Failed to read input"); - - if input.trim().eq_ignore_ascii_case("file") { - let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string(); - let filename = format!("entropy_{}.bin", timestamp); - - let mut file = File::create(&filename).expect("Failed to create file"); - file.write_all(&total_entropy).expect("Failed to write data to file"); - - println!("Entropy saved to file: {}", filename); - } else if input.trim().eq_ignore_ascii_case("print") { - let entropy_hex: String = total_entropy.iter().map(|b| format!("{:02x}", b)).collect(); - println!("Generated entropy (hex): {}", entropy_hex); - } - - Ok(()) -} diff --git a/src/lib.rs b/src/lib.rs index c90df95..b6be47b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ use std::collections::{HashMap, HashSet}; +use std::slice; pub struct Ocelli; @@ -135,3 +136,82 @@ impl Ocelli { unique_values.len() < threshold } } + +#[no_mangle] +pub extern "C" fn chop_and_tack( + current_ptr: *const u8, + current_len: usize, + previous_ptr: *const u8, + previous_len: usize, + width: usize, + minimum_distance: usize, + result_ptr: *mut u8, + result_len: &mut usize, +) { + let current = unsafe { slice::from_raw_parts(current_ptr, current_len) }; + let previous = unsafe { slice::from_raw_parts(previous_ptr, previous_len) }; + + let ocelli = Ocelli; + let result = ocelli.chop_and_tack(¤t.to_vec(), &previous.to_vec(), width, minimum_distance); + + unsafe { + let result_slice = slice::from_raw_parts_mut(result_ptr, result.len()); + result_slice.copy_from_slice(&result); + *result_len = result.len(); + } +} + +#[no_mangle] +pub extern "C" fn pick_and_flip( + data_ptr: *const u8, + data_len: usize, + current_frame_index: usize, + result_ptr: *mut u8, + result_len: &mut usize, +) { + let data = unsafe { slice::from_raw_parts(data_ptr, data_len) }; + + let ocelli = Ocelli; + let result = ocelli.pick_and_flip(data, current_frame_index); + + unsafe { + let result_slice = slice::from_raw_parts_mut(result_ptr, result.len()); + result_slice.copy_from_slice(&result); + *result_len = result.len(); + } +} + +#[no_mangle] +pub extern "C" fn shannon(data_ptr: *const u8, data_len: usize) -> f64 { + let data = unsafe { slice::from_raw_parts(data_ptr, data_len) }; + + let ocelli = Ocelli; + ocelli.shannon(&data.to_vec()) +} + +#[no_mangle] +pub extern "C" fn whiten( + entropy_ptr: *const u8, + entropy_len: usize, + result_ptr: *mut u8, + result_len: &mut usize, +) { + let entropy = unsafe { slice::from_raw_parts(entropy_ptr, entropy_len) }; + + let ocelli = Ocelli; + let result = ocelli.whiten(entropy); + + unsafe { + let result_slice = slice::from_raw_parts_mut(result_ptr, result.len()); + result_slice.copy_from_slice(&result); + *result_len = result.len(); + } +} + +#[no_mangle] +pub extern "C" fn is_covered(grayscale_ptr: *const u8, grayscale_len: usize, threshold: usize) -> bool { + let grayscale = unsafe { slice::from_raw_parts(grayscale_ptr, grayscale_len) }; + + let ocelli = Ocelli; + ocelli.is_covered(grayscale, threshold) +} From 8507e0fe5d2fb38c525871f0fac095d3ebbdfe60 Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 1 Jan 2025 18:19:30 +0200 Subject: [PATCH 07/10] documentation, cleanup --- README.md | 81 +++++++++++++++++++++++++++++------------------------- justfile | 6 ---- src/lib.rs | 20 ++++++++++++++ 3 files changed, 64 insertions(+), 43 deletions(-) delete mode 100644 justfile diff --git a/README.md b/README.md index 5a41dee..d9f982f 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,57 @@ # ocelli: Camera-Based TRNG -**Ocelli** is a Rust application that generates high-quality entropy using a camera feed. The application supports two entropy generation methods (`chop_and_tack` and `pick_and_flip`) and optionally applies Van Neumann whitening for enhanced randomness. Generated entropy is saved as a binary file. +**Ocelli** is a Rust library with FFI bindings that can be used to generate high-quality entropy using a camera feed. The application supports two entropy generation methods (`chop_and_tack` and `pick_and_flip`) and optionally applies Van Neumann whitening for enhanced randomness. -## Features +### Chop and Tack -- **Entropy Methods**: - - `chop_and_tack`: Compares pixel values between frames. - - `pick_and_flip`: Processes pixel data and flips bits every second frame. -- **Van Neumann Whitening**: Optional, enabled via the `-w` flag. -- **Shannon Entropy Test**: Ensures the randomness quality of generated entropy. +(ported from NoiseBasedCamRng by Andika Wasisto https://github.com/awasisto/camrng) -## Usage +The algorithm extracts entropy from two arrays with 8 bit integers that can be obtained by taking two consecutive frames from a camera feed and reading their brightness levels. It is required to cover the lens of the camera so it only sees uniform blackness. Due to thermal and quantum effects the image sensor will still sense fluctuations in brightness. + +The outer 100 pixel wide edges of each frames are ignored since they can be prone to bias. Entropy is obtained by comparing the remaining pixel values in a grid pattern 30 pixels apart to avoid correlation. + +### Pick and Flip + +(inspired by R. Li, "A True Random Number Generator algorithm from digital camera image noise for varying lighting conditions," SoutheastCon 2015, Fort Lauderdale, FL, USA, 2015, pp. 1-8, doi: 10.1109/SECON.2015.7132901.) + +The algorithm extracts entropy from an array of 8-bit values (camera frame pixel brightness) by analyzing the least significant bit (LSB) of each value. This process leverages the natural variability in pixel brightness across a frame. Entropy is derived by first examining whether the brightness value of a pixel falls within the range of 2 to 253, to avoid bias. The LSB of qualifying pixel values is then used to form a bitstream. To avoid correlations, the bits of every second array are flipped. The resulting bits are sequentially packed into bytes, forming the output entropy. + +## Main Methods + +* **`chop_and_tack`** takes two 8 bit integer arrays `current` and `previous` representing consecutive grayscale image frames, an usize `width` and an usize `height` of the original image frame dimensions, and a `minimum_distance` usize to define the grid distance between qualifying pixels. It returns an array of random 8 bit chunks. + +* **`pick_and_flip`** takes an 8 bit integer array representing a grayscale image frame and an usize `current_frame_index` representing a frame count of which every even number triggers flipped bits for the frame for the output array of random 8 bit chunks. + +## Helper Methods + +* **`is_covered`** can be used to check if a camera lens is covered (a requirement for the Chop and Tack method to work properly). It takes an 8 bit integer array and an 8 bit integer `threshold` value. It checks how many unique values are present in the array. If the number of unique values lies under the threshold the method returns `true`. + +* **`shannon`** can be used to calculate the Shannon Entropy value for an array of 8 bit integers. + +* **`whiten`** applies Van Neumann whitening to an array of 8 bit values, halving its size but increasing the entropy amount by filtering out bias. + +## Recommended Use + +1. If *Chop and Tack* is to be used, utilize the `is_covered` method with a threshold of 50 to determine if the camera sensor is covered. +2. Read the desired amount of frames from the camera and extract the brightness levels as 8 bit integers into arrays. +3. Feed the arrays into one of the main methods and make sure to provide all required arguments. +4. Whitening can be applied using the `whiten` method to filter out bias and increase the entropy of the result +5. It is recommended to check the resulting entropy quality using the `shannon` method and drop the result if it falls below a threshold (e.g. 7.9). +6. Loop through the previous steps and accumulate the resulting entropy until the desired amount of random bytes is reached. + +### Build ```bash -cargo run --release -- [-w] +cargo build --release ``` -### Example - +Android ```bash -cargo run --release -- 1 1024 -w +cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 build --result ``` -This generates 1024 bytes of whitened entropy using camera 1. - -## Requirements - -- OpenCV 4.x - -## Installation - -1. Install Rust: https://www.rust-lang.org/tools/install -2. Install OpenCV: Follow the [official guide](https://docs.opencv.org/). -3. Clone the repository: - ```bash - git clone - cd ocelli-entropy-generator - ``` -4. Run the application: - ```bash - cargo run --release -- - ``` - -## Output - -Generated entropy files are saved in the current directory with a name format: -``` -[_whitened]_YYYYMMDD_HHMMSS.bin +iOS +```bash +cargo build --release --target aarch64-apple-ios +cargo build --release --target x86_64-apple-ios ``` \ No newline at end of file diff --git a/justfile b/justfile deleted file mode 100644 index 98e6851..0000000 --- a/justfile +++ /dev/null @@ -1,6 +0,0 @@ -diehard file: - dieharder -a -g 201 -f {{file}} - -build arg: - cargo build {{arg}} - cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 build {{arg}} diff --git a/src/lib.rs b/src/lib.rs index b6be47b..4ce89cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,10 @@ pub struct Ocelli; impl Ocelli { pub fn chop_and_tack(&self, current: &Vec, previous: &Vec, width: usize, minimum_distance: usize) -> Vec { + // Extracts entropy from two frames by comparing pixel values in a specific grid pattern. + // The resulting entropy is constructed by appending 1s or 0s based on pixel differences. + // Algorithm ported from NoiseBasedCamRng by Andika Wasisto https://github.com/awasisto/camrng + let mut entropy = Vec::new(); let mut current_byte = 0u8; let mut bit_count = 0; @@ -57,6 +61,11 @@ impl Ocelli { } pub fn pick_and_flip(&self, data: &[u8], current_frame_index: usize) -> Vec { + // Extracts the least significant bit (LSB) of each pixel brightness, flipping it based on the frame index. + // Generates entropy by combining these bits into bytes. + // Algorithm is a simplified version of R. Li, "A True Random Number Generator algorithm from + // digital camera image noise for varying lighting conditions," doi: 10.1109/SECON.2015.7132901. + let mut entropy = Vec::new(); let mut current_byte = 0u8; let mut bit_count = 0; @@ -84,6 +93,9 @@ impl Ocelli { } pub fn shannon(&self, data: &Vec) -> f64 { + // Calculates the Shannon entropy of a given byte vector to measure its randomness. + // Uses a frequency map to compute probabilities and their contributions to entropy. + let mut frequency_map = HashMap::new(); let data_len = data.len(); @@ -98,6 +110,9 @@ impl Ocelli { } pub fn whiten(&self, entropy: &[u8]) -> Vec { + // Applies von Neumann whitening to reduce bias in the input entropy. + // Pairs of bits are analyzed, and only unbiased pairs are used to construct the output. + let mut whitened_entropy = Vec::new(); let mut current_byte = 0u8; let mut bit_count = 0; @@ -131,6 +146,11 @@ impl Ocelli { } pub fn is_covered(&self, grayscale: &[u8], threshold: usize) -> bool { + // Checks if the grayscale image contains fewer unique values than the specified threshold. + // Useful for ensuring the chop and tack method only sees noise and no image data, resulting + // in higher quality entropy. + // Recommended default threshold is 50 + let unique_values: HashSet<_> = grayscale.iter().copied().collect(); // println!("UV: {:?}", unique_values); unique_values.len() < threshold From 982603e64f3a312467ed4b68aba89c3deff0190c Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 1 Jan 2025 20:43:00 +0200 Subject: [PATCH 08/10] recovered main.rs --- Cargo.toml | 4 +- src/bin/main.rs | 146 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 src/bin/main.rs diff --git a/Cargo.toml b/Cargo.toml index 8f24fec..bf62e94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" [lib] name = "ocelli" path = "src/lib.rs" -crate-type = ["cdylib"] [dependencies] +v4l = "0.14" +chrono = "0.4" +image = "0.25.5" diff --git a/src/bin/main.rs b/src/bin/main.rs new file mode 100644 index 0000000..a71386c --- /dev/null +++ b/src/bin/main.rs @@ -0,0 +1,146 @@ +use ocelli::Ocelli; +use v4l::prelude::*; +use v4l::video::Capture; +use v4l::buffer::Type; +use v4l::format::FourCC; +use v4l::io::traits::CaptureStream; +use image::ImageReader; +use std::fs::File; +use std::io::{stdin, Write}; +use chrono::Local; +use std::time::Instant; + +fn frame_to_grayscale(data: &[u8]) -> Vec { + let img = ImageReader::new(std::io::Cursor::new(data)) + .with_guessed_format() + .expect("Failed to guess format") + .decode() + .expect("Failed to decode image"); + let gray = img.into_luma8(); // Convert to grayscale + gray.into_raw() // Return raw pixel data as Vec +} + +fn main() -> Result<(), Box> { + let args: Vec = std::env::args().collect(); + if args.len() < 3 { + eprintln!("Usage: {} ", args[0]); + std::process::exit(1); + } + + // Check if the quick flag is set + let quick = args.contains(&String::from("-q")); + + let camera_index: usize = args[1].parse().expect("Failed to parse camera index as a number"); + let length: usize = args[2].parse().expect("Failed to parse entropy length as a number"); + + let dev = Device::new(camera_index).expect("Failed to open camera"); + + // Set the desired format and resolution + let mut format = dev.format().expect("Failed to get camera format"); + format.fourcc = FourCC::new(b"MJPG"); // Use MJPG for higher resolutions + format.width = 1920; + format.height = 1080; + + if let Err(_) = dev.set_format(&format) { + println!("Failed to set resolution to 1920x1080. Falling back to 1280x720."); + format.width = 1280; + format.height = 720; + dev.set_format(&format) + .expect("Failed to set resolution to 1280x720"); + } + + println!( + "Using resolution: {}x{} (FourCC: {})", + format.width, format.height, format.fourcc + ); + + let mut stream = MmapStream::with_buffers(&dev, Type::VideoCapture, 4) + .expect("Failed to create stream"); + + let ocelli = Ocelli; + let mut total_entropy = Vec::new(); + let start_time = Instant::now(); + let shannon_threshold = 7.9; + let mut frame_count = 0; + + while total_entropy.len() < length { + // Capture first frame + let (data1, _) = stream.next().expect("Failed to capture frame"); + + // Skip the first 30 frames + if frame_count <= 30 { + frame_count += 1; + } else { + + let grayscale_data1 = frame_to_grayscale(&data1); + + let mut entropy: Vec = [0].to_vec(); + + if quick { + // Quicker capture using Pick and Flip + entropy = ocelli.whiten(&ocelli.pick_and_flip(&grayscale_data1, frame_count as usize)); + } else { + if ocelli.is_covered(&grayscale_data1, 50) { + // Capture second frame + let (data2, _) = stream.next().expect("Failed to capture second frame"); + let grayscale_data2 = frame_to_grayscale(&data2); + + // Generate entropy using chop_and_tack + entropy = ocelli.chop_and_tack(&grayscale_data1, &grayscale_data2, format.width as usize, 30); + } else { + println!("Camera is not covered. Please cover the camera."); + frame_count = 0; + } + } + + let shannon_entropy = ocelli.shannon(&entropy); + + if shannon_entropy >= shannon_threshold { + total_entropy.extend(entropy); + println!( + "Collected {} of {} bytes of entropy (Shannon entropy: {:.3})", + total_entropy.len(), + length, + shannon_entropy + ); + } else { + println!("Rejected entropy for frame {} (Shannon entropy: {:.3})", frame_count, shannon_entropy); + } + + } + } + + // Trim total_entropy to the exact length + total_entropy.truncate(length); + + // Print elapsed time + let elapsed_time = start_time.elapsed(); + println!( + "Process completed in {:.3} seconds.", + elapsed_time.as_secs_f64() + ); + + // Final Shannon entropy test + let final_shannon_entropy = ocelli.shannon(&total_entropy); + println!("Final Shannon entropy: {:.3}", final_shannon_entropy); + + // Ask if the result should be saved + println!("Save result to a file or print as hex string? (file/print):"); + let mut input = String::new(); + stdin().read_line(&mut input).expect("Failed to read input"); + + if input.trim().eq_ignore_ascii_case("file") { + let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string(); + let filename = format!("entropy_{}.bin", timestamp); + + let mut file = File::create(&filename).expect("Failed to create file"); + file.write_all(&total_entropy).expect("Failed to write data to file"); + + println!("Entropy saved to file: {}", filename); + } else if input.trim().eq_ignore_ascii_case("print") { + let entropy_hex: String = total_entropy.iter().map(|b| format!("{:02x}", b)).collect(); + println!("Generated entropy (hex): {}", entropy_hex); + } + + Ok(()) +} From 92fd5531e94bb9068542a73a32052b9bbbde7e0b Mon Sep 17 00:00:00 2001 From: randogoth Date: Sun, 5 Jan 2025 15:15:37 +0200 Subject: [PATCH 09/10] updated method and sequential read --- src/bin/main.rs | 105 ++++++++++++++++++++++-------------------- src/lib.rs | 120 ++++++++++++++++++++++++++++-------------------- 2 files changed, 124 insertions(+), 101 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index a71386c..3668ad4 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -6,7 +6,7 @@ use v4l::format::FourCC; use v4l::io::traits::CaptureStream; use image::ImageReader; use std::fs::File; -use std::io::{stdin, Write}; +use std::io::{stdin, Read, Write}; use chrono::Local; use std::time::Instant; @@ -27,15 +27,14 @@ fn main() -> Result<(), Box> { std::process::exit(1); } - // Check if the quick flag is set let quick = args.contains(&String::from("-q")); + let pp = args.contains(&String::from("-p")); let camera_index: usize = args[1].parse().expect("Failed to parse camera index as a number"); let length: usize = args[2].parse().expect("Failed to parse entropy length as a number"); let dev = Device::new(camera_index).expect("Failed to open camera"); - // Set the desired format and resolution let mut format = dev.format().expect("Failed to get camera format"); format.fourcc = FourCC::new(b"MJPG"); // Use MJPG for higher resolutions format.width = 1920; @@ -45,8 +44,7 @@ fn main() -> Result<(), Box> { println!("Failed to set resolution to 1920x1080. Falling back to 1280x720."); format.width = 1280; format.height = 720; - dev.set_format(&format) - .expect("Failed to set resolution to 1280x720"); + dev.set_format(&format).expect("Failed to set resolution to 1280x720"); } println!( @@ -62,69 +60,76 @@ fn main() -> Result<(), Box> { let start_time = Instant::now(); let shannon_threshold = 7.9; let mut frame_count = 0; + let mut last_frame = None; while total_entropy.len() < length { - // Capture first frame - let (data1, _) = stream.next().expect("Failed to capture frame"); + let (data, _) = stream.next().expect("Failed to capture frame"); + frame_count += 1; - // Skip the first 30 frames if frame_count <= 30 { - frame_count += 1; - } else { - - let grayscale_data1 = frame_to_grayscale(&data1); - - let mut entropy: Vec = [0].to_vec(); - - if quick { - // Quicker capture using Pick and Flip - entropy = ocelli.whiten(&ocelli.pick_and_flip(&grayscale_data1, frame_count as usize)); - } else { - if ocelli.is_covered(&grayscale_data1, 50) { - // Capture second frame - let (data2, _) = stream.next().expect("Failed to capture second frame"); - let grayscale_data2 = frame_to_grayscale(&data2); - - // Generate entropy using chop_and_tack - entropy = ocelli.chop_and_tack(&grayscale_data1, &grayscale_data2, format.width as usize, 30); - } else { - println!("Camera is not covered. Please cover the camera."); - frame_count = 0; - } - } - - let shannon_entropy = ocelli.shannon(&entropy); - - if shannon_entropy >= shannon_threshold { - total_entropy.extend(entropy); - println!( - "Collected {} of {} bytes of entropy (Shannon entropy: {:.3})", - total_entropy.len(), - length, - shannon_entropy - ); - } else { - println!("Rejected entropy for frame {} (Shannon entropy: {:.3})", frame_count, shannon_entropy); - } - + continue; // Skip the first 30 frames } + + let grayscale_data = frame_to_grayscale(&data); + + let entropy: Vec = if pp { + let mut low = 10; + let mut high = 245; + + if ocelli.is_covered(&grayscale_data, 50) { + low = 3; + high = 252; + } + + if let Some(last) = last_frame { + ocelli.whiten(&ocelli.tune_and_prune(&grayscale_data, &last, low, high)) + } else { + Vec::new() + } + } else if quick { + ocelli.whiten(&ocelli.pick_and_flip(&grayscale_data, frame_count as usize)) + } else { + if ocelli.is_covered(&grayscale_data, 50) { + if let Some(last) = last_frame { + ocelli.chop_and_tack(&last, &grayscale_data, format.width as usize, 30) + } else { + Vec::new() + } + } else { + println!("Camera is not covered. Please cover the camera."); + frame_count = 0; + continue; + } + }; + + let shannon_entropy = ocelli.shannon(&entropy); + + if shannon_entropy >= shannon_threshold { + total_entropy.extend(entropy); + println!( + "Collected {} of {} bytes of entropy (Shannon entropy: {:.3})", + total_entropy.len(), + length, + shannon_entropy + ); + } else { + println!("Rejected entropy for frame {} (Shannon entropy: {:.3})", frame_count, shannon_entropy); + } + + last_frame = Some(grayscale_data); } - // Trim total_entropy to the exact length total_entropy.truncate(length); - // Print elapsed time let elapsed_time = start_time.elapsed(); println!( "Process completed in {:.3} seconds.", elapsed_time.as_secs_f64() ); - // Final Shannon entropy test let final_shannon_entropy = ocelli.shannon(&total_entropy); println!("Final Shannon entropy: {:.3}", final_shannon_entropy); - // Ask if the result should be saved println!("Save result to a file or print as hex string? (file/print):"); let mut input = String::new(); stdin().read_line(&mut input).expect("Failed to read input"); diff --git a/src/lib.rs b/src/lib.rs index 4ce89cd..e328e9a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,19 +5,25 @@ pub struct Ocelli; impl Ocelli { + fn bits_to_bytes(&self, bits: &[u8]) -> Vec { + bits.chunks(8).filter_map(|chunk| { + if chunk.len() == 8 { + Some(chunk.iter().fold(0, |byte, &bit| (byte << 1) | bit)) + } else { + None + } + }).collect() + } + pub fn chop_and_tack(&self, current: &Vec, previous: &Vec, width: usize, minimum_distance: usize) -> Vec { // Extracts entropy from two frames by comparing pixel values in a specific grid pattern. // The resulting entropy is constructed by appending 1s or 0s based on pixel differences. // Algorithm ported from NoiseBasedCamRng by Andika Wasisto https://github.com/awasisto/camrng let mut entropy = Vec::new(); - let mut current_byte = 0u8; - let mut bit_count = 0; - - // Calculate the height of the frame let height = current.len() / width; - // Skip 100 pixels at the top and bottom of the frame + // Define bounds for the grid let start_row = 100; let end_row = height - 100; @@ -25,39 +31,23 @@ impl Ocelli { panic!("Resolution is too small to apply the grid selection with the given offset."); } + // Process the grid within the defined bounds for row in start_row..end_row { - // Skip 100 pixels at the left and right of the frame let row_start = row * width + 100; let row_end = (row + 1) * width - 100; - // Select pixels in the row based on the step size - for pixel_index in (row_start..row_end).step_by(minimum_distance) { - if pixel_index >= current.len() || pixel_index >= previous.len() { - continue; - } - - let c = current[pixel_index]; - let p = previous[pixel_index]; - - if c > p { - current_byte = (current_byte << 1) | 1; // Append '1' - } else if c < p { - current_byte = current_byte << 1; // Append '0' - } else { - continue; // Skip if equal - } - - bit_count += 1; - - if bit_count == 8 { - entropy.push(current_byte); - current_byte = 0; - bit_count = 0; - } - } + entropy.extend((row_start..row_end).step_by(minimum_distance) + .filter(|&pixel_index| pixel_index < current.len() && pixel_index < previous.len()) + .map(|pixel_index| { + let c = current[pixel_index]; + let p = previous[pixel_index]; + (c > p) as u8 - (c < p) as u8 // 1 for c > p, 0 for c < p, skips if equal + }) + .filter(|&bit| bit <= 1)); } - entropy + self.bits_to_bytes(&entropy) + } pub fn pick_and_flip(&self, data: &[u8], current_frame_index: usize) -> Vec { @@ -67,29 +57,32 @@ impl Ocelli { // digital camera image noise for varying lighting conditions," doi: 10.1109/SECON.2015.7132901. let mut entropy = Vec::new(); - let mut current_byte = 0u8; - let mut bit_count = 0; - for &pixel_brightness in data { - if (2..=253).contains(&pixel_brightness) { - let mut lsb = pixel_brightness & 1; - - if current_frame_index % 2 == 0 { - lsb ^= 1; // Flip the bit - } - - current_byte = (current_byte << 1) | lsb; - bit_count += 1; - - if bit_count == 8 { - entropy.push(current_byte); - current_byte = 0; - bit_count = 0; + for &pixel in data { + if (2..=253).contains(&pixel) { // filter bias + let mut lsb = pixel & 1; + if current_frame_index % 2 == 0 { // flip bits + lsb ^= 1; } + entropy.push(lsb); } } - entropy + self.bits_to_bytes(&entropy) + } + + pub fn tune_and_prune(&self, current: &Vec, previous: &Vec, low: u8, high: u8) -> Vec { + // Extracts the least significant bit (LSB) of each pixel brightness while filtering out bias + // It's a combination of methods from the other two entropy extraction algorithms + let mut entropy = Vec::new(); + + for (&c, &p) in current.iter().zip(previous.iter()) { + if c != p && (low..=high).contains(&c) { + entropy.push(c & 1); + } + } + + self.bits_to_bytes(&entropy) } pub fn shannon(&self, data: &Vec) -> f64 { @@ -201,6 +194,31 @@ pub extern "C" fn pick_and_flip( } } +#[no_mangle] +pub extern "C" fn tune_and_prune( + current_ptr: *const u8, + current_len: usize, + previous_ptr: *const u8, + previous_len: usize, + low: u8, + high: u8, + result_ptr: *mut u8, + result_len: &mut usize, +) { + let current = unsafe { slice::from_raw_parts(current_ptr, current_len) }; + let previous = unsafe { slice::from_raw_parts(previous_ptr, previous_len) }; + + let ocelli = Ocelli; + let result = ocelli.tune_and_prune(¤t.to_vec(), &previous.to_vec(), low, high); + + unsafe { + let result_slice = slice::from_raw_parts_mut(result_ptr, result.len()); + result_slice.copy_from_slice(&result); + *result_len = result.len(); + } +} + + #[no_mangle] pub extern "C" fn shannon(data_ptr: *const u8, data_len: usize) -> f64 { let data = unsafe { slice::from_raw_parts(data_ptr, data_len) }; @@ -234,4 +252,4 @@ pub extern "C" fn is_covered(grayscale_ptr: *const u8, grayscale_len: usize, thr let ocelli = Ocelli; ocelli.is_covered(grayscale, threshold) -} +} \ No newline at end of file From 8b277ff53bc81b59fdf41ec3aed29e37ab96a118 Mon Sep 17 00:00:00 2001 From: randogoth Date: Sun, 5 Jan 2025 15:37:42 +0200 Subject: [PATCH 10/10] sequential & bandpass --- src/bin/main.rs | 17 +---------------- src/lib.rs | 46 +++++----------------------------------------- 2 files changed, 6 insertions(+), 57 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index 3668ad4..5efefcd 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -28,7 +28,6 @@ fn main() -> Result<(), Box> { } let quick = args.contains(&String::from("-q")); - let pp = args.contains(&String::from("-p")); let camera_index: usize = args[1].parse().expect("Failed to parse camera index as a number"); let length: usize = args[2].parse().expect("Failed to parse entropy length as a number"); @@ -72,21 +71,7 @@ fn main() -> Result<(), Box> { let grayscale_data = frame_to_grayscale(&data); - let entropy: Vec = if pp { - let mut low = 10; - let mut high = 245; - - if ocelli.is_covered(&grayscale_data, 50) { - low = 3; - high = 252; - } - - if let Some(last) = last_frame { - ocelli.whiten(&ocelli.tune_and_prune(&grayscale_data, &last, low, high)) - } else { - Vec::new() - } - } else if quick { + let entropy: Vec = if quick { ocelli.whiten(&ocelli.pick_and_flip(&grayscale_data, frame_count as usize)) } else { if ocelli.is_covered(&grayscale_data, 50) { diff --git a/src/lib.rs b/src/lib.rs index e328e9a..3fb6d22 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,7 +50,7 @@ impl Ocelli { } - pub fn pick_and_flip(&self, data: &[u8], current_frame_index: usize) -> Vec { + pub fn pick_and_flip(&self, data: &[u8], low: u8, high: u8, current_frame_index: usize) -> Vec { // Extracts the least significant bit (LSB) of each pixel brightness, flipping it based on the frame index. // Generates entropy by combining these bits into bytes. // Algorithm is a simplified version of R. Li, "A True Random Number Generator algorithm from @@ -59,7 +59,7 @@ impl Ocelli { let mut entropy = Vec::new(); for &pixel in data { - if (2..=253).contains(&pixel) { // filter bias + if (low..=high).contains(&pixel) { // filter bias let mut lsb = pixel & 1; if current_frame_index % 2 == 0 { // flip bits lsb ^= 1; @@ -71,20 +71,6 @@ impl Ocelli { self.bits_to_bytes(&entropy) } - pub fn tune_and_prune(&self, current: &Vec, previous: &Vec, low: u8, high: u8) -> Vec { - // Extracts the least significant bit (LSB) of each pixel brightness while filtering out bias - // It's a combination of methods from the other two entropy extraction algorithms - let mut entropy = Vec::new(); - - for (&c, &p) in current.iter().zip(previous.iter()) { - if c != p && (low..=high).contains(&c) { - entropy.push(c & 1); - } - } - - self.bits_to_bytes(&entropy) - } - pub fn shannon(&self, data: &Vec) -> f64 { // Calculates the Shannon entropy of a given byte vector to measure its randomness. // Uses a frequency map to compute probabilities and their contributions to entropy. @@ -178,6 +164,8 @@ pub extern "C" fn chop_and_tack( pub extern "C" fn pick_and_flip( data_ptr: *const u8, data_len: usize, + low: u8, + high: u8, current_frame_index: usize, result_ptr: *mut u8, result_len: &mut usize, @@ -185,31 +173,7 @@ pub extern "C" fn pick_and_flip( let data = unsafe { slice::from_raw_parts(data_ptr, data_len) }; let ocelli = Ocelli; - let result = ocelli.pick_and_flip(data, current_frame_index); - - unsafe { - let result_slice = slice::from_raw_parts_mut(result_ptr, result.len()); - result_slice.copy_from_slice(&result); - *result_len = result.len(); - } -} - -#[no_mangle] -pub extern "C" fn tune_and_prune( - current_ptr: *const u8, - current_len: usize, - previous_ptr: *const u8, - previous_len: usize, - low: u8, - high: u8, - result_ptr: *mut u8, - result_len: &mut usize, -) { - let current = unsafe { slice::from_raw_parts(current_ptr, current_len) }; - let previous = unsafe { slice::from_raw_parts(previous_ptr, previous_len) }; - - let ocelli = Ocelli; - let result = ocelli.tune_and_prune(¤t.to_vec(), &previous.to_vec(), low, high); + let result = ocelli.pick_and_flip(data, low, high, current_frame_index); unsafe { let result_slice = slice::from_raw_parts_mut(result_ptr, result.len());