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/src/bin/main.rs b/src/bin/main.rs deleted file mode 100644 index 5efefcd..0000000 --- a/src/bin/main.rs +++ /dev/null @@ -1,136 +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, Read, 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); - } - - 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"); - - 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; - let mut last_frame = None; - - while total_entropy.len() < length { - let (data, _) = stream.next().expect("Failed to capture frame"); - frame_count += 1; - - if frame_count <= 30 { - continue; // Skip the first 30 frames - } - - let grayscale_data = frame_to_grayscale(&data); - - 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) { - 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); - } - - total_entropy.truncate(length); - - let elapsed_time = start_time.elapsed(); - println!( - "Process completed in {:.3} seconds.", - elapsed_time.as_secs_f64() - ); - - let final_shannon_entropy = ocelli.shannon(&total_entropy); - println!("Final Shannon entropy: {:.3}", final_shannon_entropy); - - 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 3fb6d22..4ce89cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,25 +5,19 @@ 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; - // Define bounds for the grid + // Skip 100 pixels at the top and bottom of the frame let start_row = 100; let end_row = height - 100; @@ -31,44 +25,71 @@ 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; - 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)); + // 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; + } + } } - self.bits_to_bytes(&entropy) - + entropy } - pub fn pick_and_flip(&self, data: &[u8], low: u8, high: u8, current_frame_index: usize) -> Vec { + 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; - for &pixel in data { - if (low..=high).contains(&pixel) { // filter bias - let mut lsb = pixel & 1; - if current_frame_index % 2 == 0 { // flip bits - lsb ^= 1; + 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.push(lsb); } } - self.bits_to_bytes(&entropy) + entropy } pub fn shannon(&self, data: &Vec) -> f64 { @@ -164,8 +185,6 @@ 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, @@ -173,7 +192,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, low, high, current_frame_index); + let result = ocelli.pick_and_flip(data, current_frame_index); unsafe { let result_slice = slice::from_raw_parts_mut(result_ptr, result.len()); @@ -182,7 +201,6 @@ pub extern "C" fn pick_and_flip( } } - #[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) }; @@ -216,4 +234,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 +}