1#![cfg_attr(not(feature = "std"), no_std)]
22#![cfg_attr(not(feature = "hardware"), forbid(unsafe_code))]
23
24#[cfg(not(feature = "std"))]
25extern crate alloc;
26
27#[cfg(feature = "std")]
28use zeroize::Zeroizing;
29
30pub mod evidence;
31pub mod model;
32#[cfg(feature = "std")]
33pub mod phys;
34pub mod pure;
35pub mod traits;
36
37pub use evidence::{Evidence, EvidenceChain, MAX_EVIDENCE_RECORDS};
38pub use model::{Anomaly, AnomalyKind, HumanModel, SequenceStats, ValidationResult};
39#[cfg(feature = "std")]
40pub use phys::PhysJitter;
41pub use pure::PureJitter;
42#[cfg(feature = "std")]
43pub use traits::EntropySource;
44pub use traits::JitterEngine;
45
46pub fn derive_session_secret(master_key: &[u8], context: &[u8]) -> [u8; 32] {
47 use hkdf::Hkdf;
48 use sha2::Sha256;
49
50 let hk = Hkdf::<Sha256>::new(None, master_key);
51 let mut output = [0u8; 32];
52 hk.expand(context, &mut output)
53 .expect("32 bytes is a valid output length for HKDF-SHA256");
54 output
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
58pub struct PhysHash {
59 pub hash: [u8; 32],
60 pub entropy_bits: u8,
61}
62
63impl From<[u8; 32]> for PhysHash {
64 fn from(hash: [u8; 32]) -> Self {
65 Self {
66 hash,
67 entropy_bits: 0,
68 }
69 }
70}
71
72pub type Jitter = u32;
74
75#[derive(Debug)]
76#[cfg_attr(feature = "std", derive(thiserror::Error))]
77pub enum Error {
78 #[cfg_attr(
79 feature = "std",
80 error("Insufficient entropy: required {required} bits, found {found}")
81 )]
82 InsufficientEntropy { required: u8, found: u8 },
83
84 #[cfg_attr(feature = "std", error("Hardware entropy not available: {reason}"))]
85 HardwareUnavailable {
86 #[cfg(feature = "std")]
87 reason: String,
88 #[cfg(not(feature = "std"))]
89 reason: &'static str,
90 },
91
92 #[cfg_attr(feature = "std", error("Invalid input: {0}"))]
93 InvalidInput(
94 #[cfg(feature = "std")] String,
95 #[cfg(not(feature = "std"))] &'static str,
96 ),
97}
98
99#[cfg(not(feature = "std"))]
100impl core::fmt::Display for Error {
101 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
102 match self {
103 Error::InsufficientEntropy { required, found } => {
104 write!(
105 f,
106 "Insufficient entropy: required {} bits, found {}",
107 required, found
108 )
109 }
110 Error::HardwareUnavailable { reason } => {
111 write!(f, "Hardware entropy not available: {}", reason)
112 }
113 Error::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
114 }
115 }
116}
117
118#[cfg(feature = "std")]
119#[derive(Debug, Clone)]
120pub struct HybridEngine {
121 phys: PhysJitter,
122 fallback: PureJitter,
123 min_phys_entropy: u8,
124}
125
126#[cfg(feature = "std")]
127impl Default for HybridEngine {
128 fn default() -> Self {
129 Self::new(PhysJitter::default(), PureJitter::default())
130 }
131}
132
133#[cfg(feature = "std")]
134impl HybridEngine {
135 pub fn new(phys: PhysJitter, fallback: PureJitter) -> Self {
136 Self {
137 phys,
138 fallback,
139 min_phys_entropy: 8,
140 }
141 }
142
143 pub fn with_min_entropy(mut self, bits: u8) -> Self {
144 self.min_phys_entropy = bits;
145 self
146 }
147
148 pub fn sample(&self, secret: &[u8; 32], inputs: &[u8]) -> Result<(Jitter, Evidence), Error> {
149 match self.phys.sample(inputs) {
150 Ok(entropy)
151 if entropy.entropy_bits >= self.min_phys_entropy && self.phys.validate(entropy) =>
152 {
153 let jitter = self.phys.compute_jitter(secret, inputs, entropy);
154 Ok((jitter, Evidence::phys(entropy, jitter)))
155 }
156 _ => {
157 let jitter = self
158 .fallback
159 .compute_jitter(secret, inputs, [0u8; 32].into());
160 Ok((jitter, Evidence::pure(jitter)))
161 }
162 }
163 }
164
165 pub fn phys_available(&self) -> bool {
166 self.phys.sample(b"probe").is_ok()
167 }
168}
169
170#[cfg(feature = "std")]
171#[derive(Debug)]
172pub struct Session {
173 secret: Zeroizing<[u8; 32]>,
174 engine: HybridEngine,
175 evidence: EvidenceChain,
176 model: HumanModel,
177}
178
179#[cfg(feature = "std")]
180impl Session {
181 pub fn new(secret: [u8; 32]) -> Self {
182 Self {
183 secret: Zeroizing::new(secret),
184 engine: HybridEngine::default(),
185 evidence: EvidenceChain::with_secret(secret),
186 model: HumanModel::default(),
187 }
188 }
189
190 pub fn with_engine(secret: [u8; 32], engine: HybridEngine) -> Self {
191 Self {
192 secret: Zeroizing::new(secret),
193 engine,
194 evidence: EvidenceChain::with_secret(secret),
195 model: HumanModel::default(),
196 }
197 }
198
199 #[cfg(feature = "rand")]
200 pub fn random() -> Self {
201 use rand::RngCore;
202 let mut secret = [0u8; 32];
203 rand::thread_rng().fill_bytes(&mut secret);
204 Self::new(secret)
205 }
206
207 pub fn sample(&mut self, inputs: &[u8]) -> Result<Jitter, Error> {
208 let (jitter, evidence) = self.engine.sample(&self.secret, inputs)?;
209 self.evidence.append(evidence);
210 Ok(jitter)
211 }
212
213 pub fn evidence(&self) -> &EvidenceChain {
214 &self.evidence
215 }
216
217 pub fn validate(&self) -> ValidationResult {
218 let jitters: Vec<Jitter> = self.evidence.records.iter().map(|e| e.jitter()).collect();
219 self.model.validate(&jitters)
220 }
221
222 pub fn phys_ratio(&self) -> f64 {
223 self.evidence.phys_ratio()
224 }
225
226 pub fn export_json(&self) -> Result<String, serde_json::Error> {
227 serde_json::to_string_pretty(&self.evidence)
228 }
229}
230
231#[cfg(all(test, feature = "std"))]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn test_hybrid_engine_default() {
237 let engine = HybridEngine::default();
238 let secret = [42u8; 32];
239 let inputs = b"test input";
240
241 let result = engine.sample(&secret, inputs);
242 assert!(result.is_ok());
243
244 let (jitter, evidence) = result.unwrap();
245 assert!(jitter >= 500);
246 assert!(jitter < 3000);
247 assert!(evidence.jitter() == jitter);
248 }
249
250 #[test]
251 fn test_session_workflow() {
252 let secret = [1u8; 32];
253 let mut session = Session::new(secret);
254
255 for i in 0..30 {
256 let input = format!("keystroke {}", i);
257 let jitter = session.sample(input.as_bytes()).unwrap();
258 assert!(jitter >= 500);
259 }
260
261 assert_eq!(session.evidence().records.len(), 30);
262 let validation = session.validate();
263 println!("Validation: {:?}", validation);
264 }
265
266 #[test]
267 fn test_evidence_serialization() {
268 let secret = [2u8; 32];
269 let mut session = Session::new(secret);
270
271 for i in 0..10 {
272 session.sample(format!("key{}", i).as_bytes()).unwrap();
273 }
274
275 let json = session.export_json().unwrap();
276 assert!(json.contains("\"version\""));
277 assert!(json.contains("\"records\""));
278 }
279
280 #[test]
281 fn test_pure_jitter_determinism() {
282 let engine = PureJitter::default();
283 let secret = [99u8; 32];
284 let inputs = b"deterministic test";
285 let entropy: PhysHash = [0u8; 32].into();
286
287 let j1 = engine.compute_jitter(&secret, inputs, entropy);
288 let j2 = engine.compute_jitter(&secret, inputs, entropy);
289
290 assert_eq!(j1, j2, "Pure jitter should be deterministic");
291 }
292
293 #[test]
294 fn test_empty_inputs() {
295 let engine = HybridEngine::default();
296 let secret = [42u8; 32];
297
298 let result = engine.sample(&secret, b"");
299 assert!(result.is_ok());
300 }
301
302 #[test]
303 fn test_large_inputs() {
304 let engine = HybridEngine::default();
305 let secret = [42u8; 32];
306 let large_input = vec![0u8; 10000];
307 let result = engine.sample(&secret, &large_input);
308 assert!(result.is_ok());
309 }
310
311 #[test]
312 fn test_min_phys_entropy_enforced() {
313 let engine = HybridEngine::default().with_min_entropy(255);
314 let secret = [42u8; 32];
315
316 let (_, evidence) = engine.sample(&secret, b"test").unwrap();
317 assert!(
318 !evidence.is_phys(),
319 "Should have fallen back to pure jitter"
320 );
321 }
322}
323
324#[cfg(test)]
325mod no_std_compatible_tests {
326 use super::*;
327
328 #[test]
329 fn test_phys_hash_from_array() {
330 let hash: PhysHash = [42u8; 32].into();
331 assert_eq!(hash.entropy_bits, 0);
332 assert_eq!(hash.hash, [42u8; 32]);
333 }
334
335 #[test]
336 fn test_derive_session_secret() {
337 let master = [1u8; 32];
338 let secret1 = derive_session_secret(&master, b"context1");
339 let secret2 = derive_session_secret(&master, b"context2");
340 assert_ne!(secret1, secret2);
341 }
342
343 #[test]
344 fn test_evidence_with_timestamp() {
345 let evidence = Evidence::pure_with_timestamp(1500, 12345);
346 assert_eq!(evidence.jitter(), 1500);
347 assert_eq!(evidence.timestamp_us(), 12345);
348 assert!(!evidence.is_phys());
349
350 let phys_hash: PhysHash = [1u8; 32].into();
351 let phys_evidence = Evidence::phys_with_timestamp(phys_hash, 2000, 67890);
352 assert_eq!(phys_evidence.jitter(), 2000);
353 assert_eq!(phys_evidence.timestamp_us(), 67890);
354 assert!(phys_evidence.is_phys());
355 }
356}