Prepare public release v0.1.0

This commit is contained in:
2026-01-30 09:44:12 +01:00
parent 75be95fdf7
commit 2034281ad7
41 changed files with 2137 additions and 1028 deletions

View File

@@ -2,39 +2,39 @@
//!
//! These tests require camera hardware or will use mocks on non-Linux systems.
use linux_hello_daemon::camera::{enumerate_cameras, Camera, IrEmitterControl};
use linux_hello_common::Result;
use linux_hello_daemon::camera::{enumerate_cameras, Camera, IrEmitterControl};
#[test]
fn test_camera_enumeration() -> Result<()> {
let cameras = enumerate_cameras()?;
// Should at least enumerate (may be empty if no cameras)
println!("Found {} camera(s)", cameras.len());
for cam in &cameras {
println!(" - {}", cam);
}
Ok(())
}
#[test]
fn test_camera_open_and_capture() -> Result<()> {
let cameras = enumerate_cameras()?;
if cameras.is_empty() {
println!("No cameras available, skipping capture test");
return Ok(());
}
// Try to open the first camera
let first_cam = &cameras[0];
println!("Opening camera: {}", first_cam.device_path);
let mut camera = Camera::open(&first_cam.device_path)?;
let (width, height) = camera.resolution();
println!("Camera resolution: {}x{}", width, height);
// Capture a few frames
for i in 0..3 {
let frame = camera.capture_frame()?;
@@ -47,13 +47,13 @@ fn test_camera_open_and_capture() -> Result<()> {
frame.data.len(),
frame.timestamp_us
);
// Verify frame properties
assert_eq!(frame.width, width);
assert_eq!(frame.height, height);
assert!(!frame.data.is_empty());
}
camera.stop();
Ok(())
}
@@ -61,20 +61,20 @@ fn test_camera_open_and_capture() -> Result<()> {
#[test]
fn test_ir_emitter_control() -> Result<()> {
let cameras = enumerate_cameras()?;
// Find an IR camera if available
let ir_camera = cameras.iter().find(|c| c.is_ir);
if let Some(cam) = ir_camera {
println!("Testing IR emitter on: {}", cam.device_path);
let mut emitter = IrEmitterControl::new(&cam.device_path);
// Try to enable
emitter.enable()?;
assert!(emitter.is_active());
println!("IR emitter enabled");
// Try to disable
emitter.disable()?;
assert!(!emitter.is_active());
@@ -82,19 +82,19 @@ fn test_ir_emitter_control() -> Result<()> {
} else {
println!("No IR camera found, skipping IR emitter test");
}
Ok(())
}
#[test]
fn test_camera_info_properties() -> Result<()> {
let cameras = enumerate_cameras()?;
for cam in cameras {
// Verify all cameras have valid properties
assert!(!cam.device_path.is_empty());
assert!(!cam.name.is_empty());
println!(
"Camera: {} (IR: {}, Resolutions: {})",
cam.device_path,
@@ -102,6 +102,6 @@ fn test_camera_info_properties() -> Result<()> {
cam.resolutions.len()
);
}
Ok(())
}

View File

@@ -8,9 +8,12 @@ fn cli_binary() -> String {
let _ = Command::new("cargo")
.args(["build", "--bin", "linux-hello"])
.output();
// Return path to the binary
format!("{}/target/debug/linux-hello", env!("CARGO_MANIFEST_DIR").replace("/linux-hello-tests", ""))
format!(
"{}/target/debug/linux-hello",
env!("CARGO_MANIFEST_DIR").replace("/linux-hello-tests", "")
)
}
#[test]
@@ -19,15 +22,15 @@ fn test_cli_status_command() {
.args(["status"])
.output()
.expect("Failed to execute CLI");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("Status stdout: {}", stdout);
if !stderr.is_empty() {
println!("Status stderr: {}", stderr);
}
// Status should at least run without crashing
// May fail if no cameras, but should handle gracefully
assert!(output.status.code().is_some());
@@ -39,18 +42,21 @@ fn test_cli_config_command() {
.args(["config"])
.output()
.expect("Failed to execute CLI");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("Config output: {}", stdout);
if !stderr.is_empty() {
println!("Config stderr: {}", stderr);
}
// Should output TOML configuration
assert!(stdout.contains("[general]") || stdout.contains("log_level"),
"Expected config output in stdout, got: {}", stdout);
assert!(
stdout.contains("[general]") || stdout.contains("log_level"),
"Expected config output in stdout, got: {}",
stdout
);
}
#[test]
@@ -59,18 +65,21 @@ fn test_cli_config_json_command() {
.args(["config", "--json"])
.output()
.expect("Failed to execute CLI");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("Config JSON output: {}", stdout);
if !stderr.is_empty() {
println!("Config JSON stderr: {}", stderr);
}
// Should output JSON configuration
assert!(stdout.contains("\"general\"") || stdout.contains("log_level"),
"Expected JSON config output in stdout, got: {}", stdout);
assert!(
stdout.contains("\"general\"") || stdout.contains("log_level"),
"Expected JSON config output in stdout, got: {}",
stdout
);
}
#[test]
@@ -78,7 +87,7 @@ fn test_cli_capture_command() {
// Create a temporary directory for output
let temp_dir = std::env::temp_dir().join("linux-hello-test");
std::fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
let output = Command::new(cli_binary())
.args([
"capture",
@@ -89,15 +98,15 @@ fn test_cli_capture_command() {
])
.output()
.expect("Failed to execute CLI");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("Capture stdout: {}", stdout);
if !stderr.is_empty() {
println!("Capture stderr: {}", stderr);
}
// May fail if no camera, but should handle gracefully
// If successful, check for output files
if output.status.success() {
@@ -107,7 +116,7 @@ fn test_cli_capture_command() {
.collect();
println!("Created {} file(s) in temp dir", files.len());
}
// Cleanup
let _ = std::fs::remove_dir_all(&temp_dir);
}
@@ -117,34 +126,33 @@ fn test_cli_detect_command() {
// Create a simple test image
let temp_dir = std::env::temp_dir().join("linux-hello-test-detect");
std::fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
// Create a simple grayscale PNG (100x100, mid-gray)
use image::{GrayImage, Luma};
let img = GrayImage::from_fn(100, 100, |_, _| Luma([128u8]));
let img_path = temp_dir.join("test.png");
img.save(&img_path).expect("Failed to save test image");
let output = Command::new(cli_binary())
.args([
"detect",
"--image",
img_path.to_str().unwrap(),
])
.args(["detect", "--image", img_path.to_str().unwrap()])
.output()
.expect("Failed to execute CLI");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("Detect stdout: {}", stdout);
if !stderr.is_empty() {
println!("Detect stderr: {}", stderr);
}
// Should at least run and report something
assert!(stdout.contains("detected") || stdout.contains("Face") || stdout.contains("No face"),
"Expected detection output, got: {}", stdout);
assert!(
stdout.contains("detected") || stdout.contains("Face") || stdout.contains("No face"),
"Expected detection output, got: {}",
stdout
);
// Cleanup
let _ = std::fs::remove_dir_all(&temp_dir);
}

View File

@@ -1,7 +1,9 @@
//! Integration tests for face detection
use linux_hello_daemon::detection::{detect_face_simple, FaceDetection, FaceDetect, SimpleFaceDetector};
use linux_hello_common::Result;
use linux_hello_daemon::detection::{
detect_face_simple, FaceDetect, FaceDetection, SimpleFaceDetector,
};
#[test]
fn test_simple_face_detection() {
@@ -9,28 +11,31 @@ fn test_simple_face_detection() {
let width = 640u32;
let height = 480u32;
let mut image = Vec::new();
// Create a gradient pattern (simulates a face-like region)
for y in 0..height {
for x in 0..width {
// Center region with higher intensity (face-like)
let center_x = width / 2;
let center_y = height / 2;
let dist = ((x as i32 - center_x as i32).pow(2) + (y as i32 - center_y as i32).pow(2)) as f32;
let dist =
((x as i32 - center_x as i32).pow(2) + (y as i32 - center_y as i32).pow(2)) as f32;
let max_dist = ((width / 2).pow(2) + (height / 2).pow(2)) as f32;
let intensity = (128.0 + (1.0 - dist / max_dist) * 100.0) as u8;
image.push(intensity);
}
}
let detection = detect_face_simple(&image, width, height);
assert!(detection.is_some(), "Should detect face in test image");
if let Some(det) = detection {
println!("Face detected: x={:.2}, y={:.2}, w={:.2}, h={:.2}, conf={:.2}",
det.x, det.y, det.width, det.height, det.confidence);
println!(
"Face detected: x={:.2}, y={:.2}, w={:.2}, h={:.2}, conf={:.2}",
det.x, det.y, det.width, det.height, det.confidence
);
// Verify detection is within image bounds
assert!(det.x >= 0.0 && det.x <= 1.0);
assert!(det.y >= 0.0 && det.y <= 1.0);
@@ -53,7 +58,7 @@ fn test_face_detection_low_contrast() {
let width = 100u32;
let height = 100u32;
let image = vec![10u8; (width * height) as usize];
let detection = detect_face_simple(&image, width, height);
// May or may not detect, but shouldn't crash
if let Some(det) = detection {
@@ -67,7 +72,7 @@ fn test_face_detection_high_contrast() {
let width = 100u32;
let height = 100u32;
let image = vec![255u8; (width * height) as usize];
let detection = detect_face_simple(&image, width, height);
// Should not detect (too bright)
if let Some(det) = detection {
@@ -78,22 +83,22 @@ fn test_face_detection_high_contrast() {
#[test]
fn test_simple_face_detector_trait() -> Result<()> {
let detector = SimpleFaceDetector::new(0.3);
// Test with reasonable image
let width = 200u32;
let height = 200u32;
let image: Vec<u8> = (0..width * height)
.map(|i| ((i % 200) + 50) as u8)
.collect();
let detections = detector.detect(&image, width, height)?;
println!("Detector found {} face(s)", detections.len());
// Test with threshold too high
let strict_detector = SimpleFaceDetector::new(0.9);
let strict_detections = strict_detector.detect(&image, width, height)?;
println!("Strict detector found {} face(s)", strict_detections.len());
Ok(())
}
@@ -106,13 +111,13 @@ fn test_face_detection_pixel_conversion() {
height: 0.8,
confidence: 0.95,
};
let (x, y, w, h) = detection.to_pixels(640, 480);
assert_eq!(x, 160);
assert_eq!(y, 48);
assert_eq!(w, 320);
assert_eq!(h, 384);
// Test edge cases
let edge = FaceDetection {
x: 0.0,

View File

@@ -10,11 +10,11 @@ fn test_authenticate_request_serialization() {
let request = IpcRequest::Authenticate {
user: "testuser".to_string(),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"action\":\"authenticate\""));
assert!(json.contains("\"user\":\"testuser\""));
// Deserialize back
let parsed: IpcRequest = serde_json::from_str(&json).unwrap();
match parsed {
@@ -33,16 +33,20 @@ fn test_enroll_request_serialization() {
label: "default".to_string(),
frame_count: 5,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"action\":\"enroll\""));
assert!(json.contains("\"user\":\"testuser\""));
assert!(json.contains("\"label\":\"default\""));
// Deserialize back
let parsed: IpcRequest = serde_json::from_str(&json).unwrap();
match parsed {
IpcRequest::Enroll { user, label, frame_count } => {
IpcRequest::Enroll {
user,
label,
frame_count,
} => {
assert_eq!(user, "testuser");
assert_eq!(label, "default");
assert_eq!(frame_count, 5);
@@ -57,10 +61,10 @@ fn test_list_request_serialization() {
let request = IpcRequest::List {
user: "testuser".to_string(),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"action\":\"list\""));
let parsed: IpcRequest = serde_json::from_str(&json).unwrap();
match parsed {
IpcRequest::List { user } => {
@@ -79,7 +83,7 @@ fn test_remove_request_serialization() {
label: Some("default".to_string()),
all: false,
};
let json = serde_json::to_string(&request).unwrap();
let parsed: IpcRequest = serde_json::from_str(&json).unwrap();
match parsed {
@@ -90,14 +94,14 @@ fn test_remove_request_serialization() {
}
_ => panic!("Expected Remove request"),
}
// Remove all
let request = IpcRequest::Remove {
user: "testuser".to_string(),
label: None,
all: true,
};
let json = serde_json::to_string(&request).unwrap();
let parsed: IpcRequest = serde_json::from_str(&json).unwrap();
match parsed {
@@ -114,10 +118,10 @@ fn test_remove_request_serialization() {
#[test]
fn test_ping_request_serialization() {
let request = IpcRequest::Ping;
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"action\":\"ping\""));
let parsed: IpcRequest = serde_json::from_str(&json).unwrap();
match parsed {
IpcRequest::Ping => {}
@@ -135,16 +139,16 @@ fn test_response_serialization() {
confidence: Some(0.95),
templates: None,
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"success\":true"));
assert!(json.contains("\"confidence\":0.95"));
// Deserialize
let parsed: IpcResponse = serde_json::from_str(&json).unwrap();
assert!(parsed.success);
assert_eq!(parsed.confidence, Some(0.95));
// Response with templates
let response = IpcResponse {
success: true,
@@ -152,10 +156,13 @@ fn test_response_serialization() {
confidence: None,
templates: Some(vec!["default".to_string(), "glasses".to_string()]),
};
let json = serde_json::to_string(&response).unwrap();
let parsed: IpcResponse = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.templates, Some(vec!["default".to_string(), "glasses".to_string()]));
assert_eq!(
parsed.templates,
Some(vec!["default".to_string(), "glasses".to_string()])
);
}
/// Test error response serialization
@@ -167,11 +174,11 @@ fn test_error_response_serialization() {
confidence: None,
templates: None,
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"success\":false"));
assert!(json.contains("User not enrolled"));
let parsed: IpcResponse = serde_json::from_str(&json).unwrap();
assert!(!parsed.success);
assert!(parsed.message.unwrap().contains("not enrolled"));
@@ -196,9 +203,10 @@ fn test_pam_protocol_compatibility() {
}
_ => panic!("Failed to parse PAM-format request"),
}
// Test response format
let success_response = r#"{"success":true,"message":"Authentication successful","confidence":1.0}"#;
let success_response =
r#"{"success":true,"message":"Authentication successful","confidence":1.0}"#;
let parsed: IpcResponse = serde_json::from_str(success_response).unwrap();
assert!(parsed.success);
}

View File

@@ -5,8 +5,10 @@
use linux_hello_common::{Config, FaceTemplate, TemplateStore};
use linux_hello_daemon::auth::AuthService;
use linux_hello_daemon::embedding::{PlaceholderEmbeddingExtractor, EmbeddingExtractor, cosine_similarity};
use linux_hello_daemon::matching::{match_template, average_embeddings, MatchResult};
use linux_hello_daemon::embedding::{
cosine_similarity, EmbeddingExtractor, PlaceholderEmbeddingExtractor,
};
use linux_hello_daemon::matching::{average_embeddings, match_template, MatchResult};
use tempfile::TempDir;
/// Test template storage operations
@@ -14,10 +16,10 @@ use tempfile::TempDir;
fn test_template_store_operations() {
let temp_dir = TempDir::new().unwrap();
let store = TemplateStore::new(temp_dir.path());
// Initialize store
store.initialize().unwrap();
// Create test template
let template = FaceTemplate {
user: "testuser".to_string(),
@@ -26,29 +28,29 @@ fn test_template_store_operations() {
enrolled_at: 1234567890,
frame_count: 5,
};
// Store template
store.store(&template).unwrap();
// Verify user is enrolled
assert!(store.is_enrolled("testuser"));
assert!(!store.is_enrolled("nonexistent"));
// Load template back
let loaded = store.load("testuser", "default").unwrap();
assert_eq!(loaded.user, "testuser");
assert_eq!(loaded.label, "default");
assert_eq!(loaded.embedding, vec![0.1, 0.2, 0.3, 0.4, 0.5]);
assert_eq!(loaded.frame_count, 5);
// List users
let users = store.list_users().unwrap();
assert!(users.contains(&"testuser".to_string()));
// List templates
let templates = store.list_templates("testuser").unwrap();
assert!(templates.contains(&"default".to_string()));
// Remove template
store.remove("testuser", "default").unwrap();
assert!(!store.is_enrolled("testuser"));
@@ -60,7 +62,7 @@ fn test_multiple_templates_per_user() {
let temp_dir = TempDir::new().unwrap();
let store = TemplateStore::new(temp_dir.path());
store.initialize().unwrap();
// Add multiple templates
for (i, label) in ["default", "glasses", "profile"].iter().enumerate() {
let template = FaceTemplate {
@@ -72,15 +74,15 @@ fn test_multiple_templates_per_user() {
};
store.store(&template).unwrap();
}
// Load all templates
let templates = store.load_all("multiuser").unwrap();
assert_eq!(templates.len(), 3);
// List template labels
let labels = store.list_templates("multiuser").unwrap();
assert_eq!(labels.len(), 3);
// Remove all
store.remove_all("multiuser").unwrap();
assert!(!store.is_enrolled("multiuser"));
@@ -90,23 +92,23 @@ fn test_multiple_templates_per_user() {
#[test]
fn test_embedding_extraction() {
let extractor = PlaceholderEmbeddingExtractor::new(128);
// Create test grayscale image
let mut img = image::GrayImage::new(100, 100);
// Fill with gradient pattern
for y in 0..100 {
for x in 0..100 {
img.put_pixel(x, y, image::Luma([(x + y) as u8 / 2]));
}
}
// Extract embedding
let embedding = extractor.extract(&img).unwrap();
// Check dimension
assert_eq!(embedding.len(), 128);
// Check normalization (should be approximately unit length)
let norm: f32 = embedding.iter().map(|&x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 0.1 || norm < 0.01);
@@ -116,11 +118,11 @@ fn test_embedding_extraction() {
#[test]
fn test_embedding_consistency() {
let extractor = PlaceholderEmbeddingExtractor::new(128);
// Create identical images
let mut img1 = image::GrayImage::new(100, 100);
let mut img2 = image::GrayImage::new(100, 100);
for y in 0..100 {
for x in 0..100 {
let val = ((x * 2 + y * 3) % 256) as u8;
@@ -128,14 +130,18 @@ fn test_embedding_consistency() {
img2.put_pixel(x, y, image::Luma([val]));
}
}
// Extract embeddings
let emb1 = extractor.extract(&img1).unwrap();
let emb2 = extractor.extract(&img2).unwrap();
// Should be identical
let similarity = cosine_similarity(&emb1, &emb2);
assert!((similarity - 1.0).abs() < 0.001, "Identical images should have similarity ~1.0, got {}", similarity);
assert!(
(similarity - 1.0).abs() < 0.001,
"Identical images should have similarity ~1.0, got {}",
similarity
);
}
/// Test cosine similarity
@@ -145,11 +151,11 @@ fn test_cosine_similarity() {
let a = vec![1.0, 0.0, 0.0];
let b = vec![1.0, 0.0, 0.0];
assert!((cosine_similarity(&a, &b) - 1.0).abs() < 0.001);
// Orthogonal vectors
let c = vec![0.0, 1.0, 0.0];
assert!((cosine_similarity(&a, &c) - 0.0).abs() < 0.001);
// Similar vectors
let d = vec![0.9, 0.1, 0.0];
let similarity = cosine_similarity(&a, &d);
@@ -175,17 +181,17 @@ fn test_template_matching() {
frame_count: 1,
},
];
// Exact match
let result = match_template(&vec![1.0, 0.0, 0.0], &templates, 0.5);
assert!(result.matched);
assert_eq!(result.matched_label, Some("default".to_string()));
assert!((result.best_similarity - 1.0).abs() < 0.001);
// Close match (should match glasses template better due to similarity)
let result = match_template(&vec![0.85, 0.15, 0.0], &templates, 0.5);
assert!(result.matched);
// No match (orthogonal)
let result = match_template(&vec![0.0, 0.0, 1.0], &templates, 0.3);
assert!(!result.matched);
@@ -199,16 +205,16 @@ fn test_embedding_averaging() {
vec![0.0, 1.0, 0.0],
vec![0.0, 0.0, 1.0],
];
let averaged = average_embeddings(&embeddings).unwrap();
// Should be 3 dimensional
assert_eq!(averaged.len(), 3);
// Should be normalized
let norm: f32 = averaged.iter().map(|&x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 0.01);
// All components should be roughly equal (1/sqrt(3) normalized)
let expected = 1.0 / 3.0_f32.sqrt();
for val in &averaged {
@@ -229,16 +235,19 @@ fn test_empty_embeddings_error() {
fn test_auth_service_init() {
let config = Config::default();
let auth_service = AuthService::new(config);
// This should succeed (creates template directory if needed)
// Note: May need root permissions in production
// For testing, we can verify it doesn't panic
let result = auth_service.initialize();
// On systems without /var/lib, this might fail
// That's okay for unit testing
if result.is_err() {
println!("Auth service init failed (expected without root): {:?}", result);
println!(
"Auth service init failed (expected without root): {:?}",
result
);
}
}
@@ -251,7 +260,7 @@ fn test_match_result_structure() {
distance_threshold: 0.5,
matched_label: Some("default".to_string()),
};
assert!(result.matched);
assert_eq!(result.best_similarity, 0.95);
assert_eq!(result.distance_threshold, 0.5);
@@ -262,18 +271,18 @@ fn test_match_result_structure() {
#[test]
fn test_embedding_diversity() {
let extractor = PlaceholderEmbeddingExtractor::new(128);
// Create different pattern images
let patterns: Vec<Box<dyn Fn(u32, u32) -> u8>> = vec![
Box::new(|x, _y| x as u8), // Horizontal gradient
Box::new(|_x, y| y as u8), // Vertical gradient
Box::new(|x, y| (x ^ y) as u8), // XOR pattern
Box::new(|_x, _y| 128), // Solid gray
Box::new(|x, _y| x as u8), // Horizontal gradient
Box::new(|_x, y| y as u8), // Vertical gradient
Box::new(|x, y| (x ^ y) as u8), // XOR pattern
Box::new(|_x, _y| 128), // Solid gray
Box::new(|x, y| ((x * y) % 256) as u8), // Multiplication pattern
];
let mut embeddings = Vec::new();
for pattern in &patterns {
let mut img = image::GrayImage::new(100, 100);
for y in 0..100 {
@@ -283,7 +292,7 @@ fn test_embedding_diversity() {
}
embeddings.push(extractor.extract(&img).unwrap());
}
// Check that different patterns produce somewhat different embeddings
// (not all identical)
for i in 0..embeddings.len() {
@@ -306,13 +315,13 @@ fn test_template_serialization() {
enrolled_at: 1704153600,
frame_count: 10,
};
// Serialize
let json = serde_json::to_string(&original).unwrap();
// Deserialize
let restored: FaceTemplate = serde_json::from_str(&json).unwrap();
// Verify
assert_eq!(original.user, restored.user);
assert_eq!(original.label, restored.label);

View File

@@ -2,13 +2,13 @@
//!
//! Tests for TPM storage, secure memory, and anti-spoofing functionality.
use linux_hello_common::FaceTemplate;
use linux_hello_daemon::anti_spoofing::{
AntiSpoofingConfig, AntiSpoofingDetector, AntiSpoofingFrame,
};
use linux_hello_daemon::secure_memory::{SecureBytes, SecureEmbedding, memory_protection};
use linux_hello_daemon::secure_memory::{memory_protection, SecureBytes, SecureEmbedding};
use linux_hello_daemon::tpm::{EncryptedTemplate, SoftwareTpmFallback, TpmStorage};
use linux_hello_daemon::SecureTemplateStore;
use linux_hello_common::FaceTemplate;
use tempfile::TempDir;
// =============================================================================
@@ -19,7 +19,7 @@ use tempfile::TempDir;
fn test_software_tpm_initialization() {
let temp = TempDir::new().unwrap();
let mut storage = SoftwareTpmFallback::new(temp.path());
assert!(storage.is_available());
storage.initialize().unwrap();
}
@@ -29,14 +29,14 @@ fn test_software_tpm_encrypt_decrypt_roundtrip() {
let temp = TempDir::new().unwrap();
let mut storage = SoftwareTpmFallback::new(temp.path());
storage.initialize().unwrap();
let plaintext = b"Sensitive face embedding data for security testing";
let encrypted = storage.encrypt("testuser", plaintext).unwrap();
assert!(!encrypted.ciphertext.is_empty());
assert_ne!(encrypted.ciphertext.as_slice(), plaintext);
assert!(!encrypted.tpm_encrypted); // Software fallback
let decrypted = storage.decrypt("testuser", &encrypted).unwrap();
assert_eq!(decrypted.as_slice(), plaintext);
}
@@ -46,13 +46,13 @@ fn test_software_tpm_user_key_management() {
let temp = TempDir::new().unwrap();
let mut storage = SoftwareTpmFallback::new(temp.path());
storage.initialize().unwrap();
storage.create_user_key("user1").unwrap();
storage.create_user_key("user2").unwrap();
storage.remove_user_key("user1").unwrap();
// user2's key should still exist
// Can still encrypt for both users (key derivation is deterministic)
let encrypted = storage.encrypt("user1", b"test").unwrap();
assert!(!encrypted.ciphertext.is_empty());
@@ -62,7 +62,9 @@ fn test_software_tpm_user_key_management() {
fn test_encrypted_template_structure() {
let template = EncryptedTemplate {
ciphertext: vec![1, 2, 3, 4, 5, 6, 7, 8],
iv: vec![0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88], // 12 bytes for AES-GCM
iv: vec![
0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
], // 12 bytes for AES-GCM
salt: vec![0u8; 32], // 32 bytes for PBKDF2 salt
key_handle: 0x81000001,
tpm_encrypted: true,
@@ -86,7 +88,7 @@ fn test_encrypted_template_structure() {
fn test_secure_embedding_operations() {
let data = vec![0.1, 0.2, 0.3, 0.4, 0.5];
let embedding = SecureEmbedding::new(data.clone());
assert_eq!(embedding.len(), 5);
assert!(!embedding.is_empty());
assert_eq!(embedding.as_slice(), data.as_slice());
@@ -97,13 +99,13 @@ fn test_secure_embedding_similarity_metrics() {
// Identical vectors
let emb1 = SecureEmbedding::new(vec![1.0, 0.0, 0.0, 0.0]);
let emb2 = SecureEmbedding::new(vec![1.0, 0.0, 0.0, 0.0]);
let similarity = emb1.cosine_similarity(&emb2);
assert!((similarity - 1.0).abs() < 0.001);
let distance = emb1.euclidean_distance(&emb2);
assert!(distance.abs() < 0.001);
// Orthogonal vectors
let emb3 = SecureEmbedding::new(vec![0.0, 1.0, 0.0, 0.0]);
let similarity2 = emb1.cosine_similarity(&emb3);
@@ -115,7 +117,7 @@ fn test_secure_embedding_serialization() {
let original = SecureEmbedding::new(vec![1.5, -2.5, 3.14159, 0.0, f32::MAX]);
let bytes = original.to_bytes();
let restored = SecureEmbedding::from_bytes(&bytes).unwrap();
assert_eq!(original.as_slice(), restored.as_slice());
}
@@ -124,7 +126,7 @@ fn test_secure_bytes_constant_time_comparison() {
let secret1 = SecureBytes::new(vec![0x12, 0x34, 0x56, 0x78]);
let secret2 = SecureBytes::new(vec![0x12, 0x34, 0x56, 0x78]);
let secret3 = SecureBytes::new(vec![0x12, 0x34, 0x56, 0x79]);
assert!(secret1.constant_time_eq(&secret2));
assert!(!secret1.constant_time_eq(&secret3));
}
@@ -133,7 +135,7 @@ fn test_secure_bytes_constant_time_comparison() {
fn test_secure_memory_zeroization() {
let mut data = vec![0xAA_u8; 64];
memory_protection::secure_zero(&mut data);
assert!(data.iter().all(|&b| b == 0));
}
@@ -141,7 +143,7 @@ fn test_secure_memory_zeroization() {
fn test_secure_embedding_debug_hides_data() {
let embedding = SecureEmbedding::new(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let debug_str = format!("{:?}", embedding);
assert!(debug_str.contains("REDACTED"));
assert!(!debug_str.contains("1.0"));
assert!(!debug_str.contains("2.0"));
@@ -166,12 +168,12 @@ fn test_secure_template_store_unencrypted() {
let temp = TempDir::new().unwrap();
let mut store = SecureTemplateStore::new(temp.path());
store.initialize(false).unwrap();
assert!(!store.is_encryption_enabled());
let template = create_test_template("alice", "default");
store.store(&template).unwrap();
let loaded = store.load("alice", "default").unwrap();
assert_eq!(loaded.user, "alice");
assert_eq!(loaded.embedding, template.embedding);
@@ -182,12 +184,14 @@ fn test_secure_template_store_enrollment_check() {
let temp = TempDir::new().unwrap();
let mut store = SecureTemplateStore::new(temp.path());
store.initialize(false).unwrap();
assert!(!store.is_enrolled("bob"));
store.store(&create_test_template("bob", "primary")).unwrap();
store
.store(&create_test_template("bob", "primary"))
.unwrap();
assert!(store.is_enrolled("bob"));
store.remove("bob", "primary").unwrap();
assert!(!store.is_enrolled("bob"));
}
@@ -197,11 +201,17 @@ fn test_secure_template_store_load_all() {
let temp = TempDir::new().unwrap();
let mut store = SecureTemplateStore::new(temp.path());
store.initialize(false).unwrap();
store.store(&create_test_template("charlie", "default")).unwrap();
store.store(&create_test_template("charlie", "backup")).unwrap();
store.store(&create_test_template("charlie", "outdoor")).unwrap();
store
.store(&create_test_template("charlie", "default"))
.unwrap();
store
.store(&create_test_template("charlie", "backup"))
.unwrap();
store
.store(&create_test_template("charlie", "outdoor"))
.unwrap();
let templates = store.load_all("charlie").unwrap();
assert_eq!(templates.len(), 3);
}
@@ -211,10 +221,10 @@ fn test_secure_template_store_load_secure_embedding() {
let temp = TempDir::new().unwrap();
let mut store = SecureTemplateStore::new(temp.path());
store.initialize(false).unwrap();
let template = create_test_template("dave", "default");
store.store(&template).unwrap();
let secure = store.load_secure("dave", "default").unwrap();
assert_eq!(secure.len(), template.embedding.len());
assert_eq!(secure.as_slice(), template.embedding.as_slice());
@@ -227,18 +237,18 @@ fn test_secure_template_store_load_secure_embedding() {
fn create_test_frame(brightness: u8, is_ir: bool, width: u32, height: u32) -> AntiSpoofingFrame {
let size = (width * height) as usize;
let mut pixels = vec![brightness; size];
// Add realistic variation
for (i, pixel) in pixels.iter_mut().enumerate() {
let x = (i % width as usize) as u32;
let y = (i / width as usize) as u32;
// Add gradient and noise
let gradient = ((x + y) % 20) as i16 - 10;
let noise = ((i * 17 + 31) % 15) as i16 - 7;
*pixel = (brightness as i16 + gradient + noise).clamp(0, 255) as u8;
}
AntiSpoofingFrame {
pixels,
width,
@@ -253,10 +263,10 @@ fn create_test_frame(brightness: u8, is_ir: bool, width: u32, height: u32) -> An
fn test_anti_spoofing_basic_check() {
let config = AntiSpoofingConfig::default();
let mut detector = AntiSpoofingDetector::new(config);
let frame = create_test_frame(100, true, 200, 200);
let result = detector.check_frame(&frame).unwrap();
assert!(result.score >= 0.0 && result.score <= 1.0);
assert!(result.checks.ir_check.is_some());
assert!(result.checks.depth_check.is_some());
@@ -267,19 +277,19 @@ fn test_anti_spoofing_basic_check() {
fn test_anti_spoofing_ir_verification() {
let config = AntiSpoofingConfig::default();
let mut detector = AntiSpoofingDetector::new(config);
// Normal IR frame (should pass)
let normal_frame = create_test_frame(100, true, 200, 200);
let result1 = detector.check_frame(&normal_frame).unwrap();
let ir_score1 = result1.checks.ir_check.unwrap();
detector.reset();
// Very dark frame (suspicious)
let dark_frame = create_test_frame(10, true, 200, 200);
let result2 = detector.check_frame(&dark_frame).unwrap();
let ir_score2 = result2.checks.ir_check.unwrap();
// Normal frame should score higher than very dark frame
assert!(ir_score1 > ir_score2);
}
@@ -289,9 +299,9 @@ fn test_anti_spoofing_temporal_analysis() {
let mut config = AntiSpoofingConfig::default();
config.enable_movement_check = true;
config.temporal_frames = 5;
let mut detector = AntiSpoofingDetector::new(config);
// Simulate multiple frames with natural movement
for i in 0..6 {
let mut frame = create_test_frame(100, true, 200, 200);
@@ -299,9 +309,9 @@ fn test_anti_spoofing_temporal_analysis() {
// Add slight position variation
let offset = (i % 3) as u32;
frame.face_bbox = Some((50 + offset, 50, 100, 100));
let result = detector.check_frame(&frame).unwrap();
// After enough frames, movement check should be available
if i >= 3 {
assert!(result.checks.movement_check.is_some());
@@ -313,17 +323,17 @@ fn test_anti_spoofing_temporal_analysis() {
fn test_anti_spoofing_reset() {
let mut config = AntiSpoofingConfig::default();
config.enable_movement_check = true;
let mut detector = AntiSpoofingDetector::new(config);
// Process some frames
for _ in 0..5 {
let frame = create_test_frame(100, true, 200, 200);
let _ = detector.check_frame(&frame);
}
detector.reset();
// After reset, first frame should not have movement analysis
let frame = create_test_frame(100, true, 200, 200);
let result = detector.check_frame(&frame).unwrap();
@@ -334,12 +344,12 @@ fn test_anti_spoofing_reset() {
fn test_anti_spoofing_rejection_reasons() {
let mut config = AntiSpoofingConfig::default();
config.threshold = 0.95; // Very high threshold
let mut detector = AntiSpoofingDetector::new(config);
let frame = create_test_frame(100, true, 200, 200);
let result = detector.check_frame(&frame).unwrap();
if !result.is_live {
assert!(result.rejection_reason.is_some());
let reason = result.rejection_reason.unwrap();
@@ -358,11 +368,11 @@ fn test_anti_spoofing_config_customization() {
enable_movement_check: false,
temporal_frames: 10,
};
let mut detector = AntiSpoofingDetector::new(config);
let frame = create_test_frame(100, true, 200, 200);
let result = detector.check_frame(&frame).unwrap();
// Texture check should not be performed
assert!(result.checks.texture_check.is_none());
// IR and depth checks should be performed
@@ -379,7 +389,7 @@ fn test_secure_workflow_enroll_and_verify() {
let temp = TempDir::new().unwrap();
let mut store = SecureTemplateStore::new(temp.path());
store.initialize(false).unwrap();
// Simulate enrollment embedding
let enroll_embedding = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8];
let template = FaceTemplate {
@@ -392,17 +402,17 @@ fn test_secure_workflow_enroll_and_verify() {
.as_secs(),
frame_count: 5,
};
// Store securely
store.store(&template).unwrap();
assert!(store.is_enrolled("secure_user"));
// Load as secure embedding for matching
let stored = store.load_secure("secure_user", "default").unwrap();
// Simulate authentication embedding (slightly different)
let auth_embedding = SecureEmbedding::new(vec![0.11, 0.19, 0.31, 0.39, 0.51, 0.59, 0.71, 0.79]);
// Compare securely
let similarity = stored.cosine_similarity(&auth_embedding);
assert!(similarity > 0.9); // Should be very similar
@@ -414,6 +424,6 @@ fn test_memory_protection_basics() {
let data = vec![0xABu8; 1024];
let result = memory_protection::lock_memory(&data);
assert!(result.is_ok()); // Should not error, even if lock fails
let _ = memory_protection::unlock_memory(&data);
}