Skip to main content

lib_q_core/
api.rs

1//! Unified API for lib-Q cryptographic operations
2//!
3//! This module provides a consistent, secure API that works identically
4//! whether used as a Rust crate or compiled to WASM.
5
6// PhantomData import removed - no longer needed after removing old Context<T>
7
8use crate::error::Result;
9#[cfg(feature = "alloc")]
10use crate::traits::*;
11
12#[cfg(feature = "alloc")]
13extern crate alloc;
14#[cfg(feature = "alloc")]
15use alloc::{
16    format,
17    string::String,
18    vec::Vec,
19};
20
21#[cfg(feature = "getrandom")]
22#[allow(unused_imports)] // Used in getrandom::fill() call
23use getrandom;
24pub use lib_q_types::{
25    Algorithm,
26    AlgorithmCategory,
27    SecurityLevel,
28};
29// Hash function imports
30// #[cfg(feature = "hash")]
31// use lib_q_sha3::{
32//     Digest,
33//     Sha3_224,
34//     Sha3_256,
35//     Sha3_384,
36//     Sha3_512,
37//     Shake128,
38//     Shake256,
39//     digest::ExtendableOutput,
40// };
41#[cfg(any(feature = "getrandom", feature = "rand"))]
42#[allow(unused_imports)]
43use rand_core::Rng;
44use subtle::ConstantTimeEq;
45
46// Define cryptographic operation traits for dependency injection
47// This allows implementations to be provided by higher-level crates
48
49/// Key Encapsulation Mechanism operations
50#[cfg(feature = "alloc")]
51pub trait KemOperations {
52    fn generate_keypair(
53        &self,
54        algorithm: Algorithm,
55        randomness: Option<&[u8]>,
56    ) -> Result<KemKeypair>;
57    fn encapsulate(
58        &self,
59        algorithm: Algorithm,
60        public_key: &KemPublicKey,
61        randomness: Option<&[u8]>,
62    ) -> Result<(Vec<u8>, Vec<u8>)>;
63    fn decapsulate(
64        &self,
65        algorithm: Algorithm,
66        secret_key: &KemSecretKey,
67        ciphertext: &[u8],
68    ) -> Result<Vec<u8>>;
69    fn derive_public_key(
70        &self,
71        algorithm: Algorithm,
72        secret_key: &KemSecretKey,
73    ) -> Result<KemPublicKey>;
74}
75
76/// Digital Signature operations
77#[cfg(feature = "alloc")]
78pub trait SignatureOperations {
79    fn generate_keypair(
80        &self,
81        algorithm: Algorithm,
82        randomness: Option<&[u8]>,
83    ) -> Result<SigKeypair>;
84    fn sign(
85        &self,
86        algorithm: Algorithm,
87        secret_key: &SigSecretKey,
88        message: &[u8],
89        randomness: Option<&[u8]>,
90    ) -> Result<Vec<u8>>;
91    fn verify(
92        &self,
93        algorithm: Algorithm,
94        public_key: &SigPublicKey,
95        message: &[u8],
96        signature: &[u8],
97    ) -> Result<bool>;
98}
99
100/// Hash operations
101#[cfg(feature = "alloc")]
102pub trait HashOperations {
103    fn hash(&self, algorithm: Algorithm, data: &[u8]) -> Result<Vec<u8>>;
104}
105
106/// AEAD operations (Layer A — `Result` only)
107///
108/// This trait mirrors [`crate::traits::Aead`] at the algorithm-dispatch boundary: `decrypt`
109/// returns [`Result`] only. Semantic decrypt ([`crate::AeadDecryptSemantic`],
110/// [`crate::DecryptSemanticOutcome`]) is **not** part of this object-safe surface; use a
111/// concrete AEAD type when Layer B is required. See `docs/adr/003-aead-decrypt-layers.md`.
112#[cfg(feature = "alloc")]
113pub trait AeadOperations {
114    fn encrypt(
115        &self,
116        algorithm: Algorithm,
117        key: &AeadKey,
118        nonce: &Nonce,
119        plaintext: &[u8],
120        associated_data: Option<&[u8]>,
121    ) -> Result<Vec<u8>>;
122    fn decrypt(
123        &self,
124        algorithm: Algorithm,
125        key: &AeadKey,
126        nonce: &Nonce,
127        ciphertext: &[u8],
128        associated_data: Option<&[u8]>,
129    ) -> Result<Vec<u8>>;
130}
131
132/// Cryptographic provider that supplies implementations
133pub trait CryptoProvider: Send + Sync {
134    #[cfg(feature = "alloc")]
135    fn kem(&self) -> Option<&dyn KemOperations>;
136    #[cfg(feature = "alloc")]
137    fn signature(&self) -> Option<&dyn SignatureOperations>;
138    #[cfg(feature = "alloc")]
139    fn hash(&self) -> Option<&dyn HashOperations>;
140    #[cfg(feature = "alloc")]
141    fn aead(&self) -> Option<&dyn AeadOperations>;
142}
143
144// Old Context<T> struct removed - use the new modular contexts instead
145// The new architecture provides better separation of concerns and security validation
146
147// KEM context is now implemented in the contexts module
148// Re-export for backward compatibility
149#[cfg(feature = "alloc")]
150pub use crate::contexts::KemContext;
151
152// Old DefaultCryptoProvider removed - use LibQCryptoProvider from providers module instead
153
154// Old Default*Impl structs and implementations removed
155// Use the new LibQCryptoProvider and its implementations from the providers module instead
156
157// Context implementations are now in the contexts module
158// These re-exports are maintained for API consistency
159
160/// The core API provides a clean interface that:
161/// - Defines cryptographic operation traits (KemOperations, SignatureOperations, etc.)
162/// - Uses dependency injection via CryptoProvider trait
163/// - Returns [`ProviderNotConfigured`](crate::error::Error::ProviderNotConfigured) when no provider is set on a context
164/// - Maintains no circular dependencies with implementation crates
165/// - Provides proper validation and error handling
166///
167/// Real implementations are provided by the main lib-q crate through LibQCryptoProvider.
168///
169/// Utility functions that work consistently across platforms
170pub struct Utils;
171
172impl Utils {
173    /// Generate cryptographically secure random bytes
174    ///
175    /// This function works in both std and no_std environments:
176    /// - In std environments with the "rand" feature: Uses rand::rng()
177    /// - In no_std environments with the "getrandom" feature: Uses getrandom directly
178    /// - In no_std environments without getrandom: Returns an error
179    #[cfg(feature = "rand")]
180    pub fn random_bytes(length: usize) -> Result<Vec<u8>> {
181        const MIN_RANDOM_SIZE: usize = 1;
182        const MAX_RANDOM_SIZE: usize = 1024 * 1024; // 1MB limit
183        if !(MIN_RANDOM_SIZE..=MAX_RANDOM_SIZE).contains(&length) {
184            return Err(crate::error::Error::RandomBytesLengthInvalid {
185                min: MIN_RANDOM_SIZE,
186                max: MAX_RANDOM_SIZE,
187                requested: length,
188            });
189        }
190
191        let mut bytes = alloc::vec![0u8; length];
192
193        // Use rand for cryptographically secure random generation
194        let mut rng = rand::rng();
195        rng.fill_bytes(&mut bytes);
196
197        // Zeroize the bytes on error paths (handled by Vec's Drop implementation)
198        Ok(bytes)
199    }
200
201    #[cfg(all(feature = "getrandom", not(feature = "rand")))]
202    #[cfg(feature = "alloc")]
203    pub fn random_bytes(length: usize) -> Result<Vec<u8>> {
204        const MIN_RANDOM_SIZE: usize = 1;
205        const MAX_RANDOM_SIZE: usize = 1024 * 1024; // 1MB limit
206        if !(MIN_RANDOM_SIZE..=MAX_RANDOM_SIZE).contains(&length) {
207            return Err(crate::error::Error::RandomBytesLengthInvalid {
208                min: MIN_RANDOM_SIZE,
209                max: MAX_RANDOM_SIZE,
210                requested: length,
211            });
212        }
213
214        let mut bytes = alloc::vec![0u8; length];
215
216        // Generate cryptographically secure random bytes using getrandom
217        // This works across all platforms including WASM (using crypto.getRandomValues())
218        // The getrandom crate automatically selects the appropriate entropy source:
219        // - Native: OS entropy sources (e.g., /dev/urandom, CryptGenRandom)
220        // - WASM: crypto.getRandomValues() in browsers, WebCrypto API in Node.js
221        getrandom::fill(&mut bytes).map_err(|_| crate::error::Error::RandomGenerationFailed {
222            operation: String::from("random_bytes"),
223        })?;
224
225        // Zeroize the bytes on error paths (handled by Vec's Drop implementation)
226        Ok(bytes)
227    }
228
229    #[cfg(all(feature = "getrandom", not(feature = "rand")))]
230    #[cfg(not(feature = "alloc"))]
231    pub fn random_bytes(length: usize) -> Result<&'static [u8]> {
232        const MIN_RANDOM_SIZE: usize = 1;
233        const MAX_RANDOM_SIZE: usize = 1024; // Limit for no_std without alloc
234        if !(MIN_RANDOM_SIZE..=MAX_RANDOM_SIZE).contains(&length) {
235            return Err(crate::error::Error::RandomBytesLengthInvalid {
236                min: MIN_RANDOM_SIZE,
237                max: MAX_RANDOM_SIZE,
238                requested: length,
239            });
240        }
241
242        // For no_std without alloc, we need to handle platform-specific RNG
243        // This provides a graceful fallback for platforms where getrandom is not available.
244        // WASM builds should enable the root crate's `wasm` or `wasm_js` feature so that
245        // lib-q-core/wasm_getrandom is enabled and getrandom works (this path is then avoided).
246        #[cfg(target_arch = "wasm32")]
247        {
248            // For WASM targets, getrandom might not be available
249            return Err(crate::error::Error::RandomGenerationFailed {
250                operation: "random_bytes",
251            });
252        }
253
254        #[cfg(not(target_arch = "wasm32"))]
255        {
256            // For native targets, getrandom might not be available in this configuration
257            // Note: This is a simplified approach - in production, you'd want proper platform detection
258            return Err(crate::error::Error::RandomGenerationFailed {
259                operation: "random_bytes",
260            });
261        }
262    }
263
264    #[cfg(not(any(feature = "rand", feature = "getrandom")))]
265    #[cfg(feature = "alloc")]
266    pub fn random_bytes(_length: usize) -> Result<Vec<u8>> {
267        Err(crate::error::Error::RandomGenerationFailed {
268            operation: String::from("random_bytes"),
269        })
270    }
271
272    #[cfg(not(any(feature = "rand", feature = "getrandom")))]
273    #[cfg(not(feature = "alloc"))]
274    pub fn random_bytes(_length: usize) -> Result<&'static [u8]> {
275        Err(crate::error::Error::RandomGenerationFailed {
276            operation: "random_bytes",
277        })
278    }
279
280    /// Convert bytes to hex string
281    #[cfg(feature = "alloc")]
282    pub fn bytes_to_hex(bytes: &[u8]) -> String {
283        let mut hex = String::new();
284        for &byte in bytes {
285            hex.push_str(&format!("{:02x}", byte));
286        }
287        hex
288    }
289
290    #[cfg(not(feature = "alloc"))]
291    pub fn bytes_to_hex(_bytes: &[u8]) -> &'static str {
292        "hex conversion not available in no_std without alloc"
293    }
294
295    /// Convert hex string to bytes
296    ///
297    /// # Errors
298    ///
299    /// Returns [`crate::error::Error::HexDecode`] with a [`crate::error::HexDecodeError`] reason when the
300    /// trimmed input is not valid hexadecimal (odd length or non-hex digit).
301    #[cfg(feature = "alloc")]
302    pub fn hex_to_bytes(hex: &str) -> Result<Vec<u8>> {
303        use crate::error::HexDecodeError;
304
305        let hex = hex.trim();
306
307        if !hex.len().is_multiple_of(2) {
308            return Err(crate::error::Error::HexDecode(HexDecodeError::OddLength {
309                char_count: hex.len(),
310            }));
311        }
312
313        let mut bytes = Vec::with_capacity(hex.len() / 2);
314        for i in (0..hex.len()).step_by(2) {
315            let byte = u8::from_str_radix(&hex[i..i + 2], 16).map_err(|_| {
316                crate::error::Error::HexDecode(HexDecodeError::InvalidDigit {
317                    pair_start: i,
318                    char_count: hex.len(),
319                })
320            })?;
321            bytes.push(byte);
322        }
323
324        Ok(bytes)
325    }
326
327    #[cfg(not(feature = "alloc"))]
328    pub fn hex_to_bytes(_hex: &str) -> Result<&'static [u8]> {
329        Err(crate::error::Error::MemoryAllocationFailed {
330            operation: "hex_to_bytes",
331        })
332    }
333
334    /// Constant-time comparison of two byte slices
335    pub fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
336        if a.len() != b.len() {
337            return false;
338        }
339        a.ct_eq(b).into()
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    #[cfg(feature = "alloc")]
347    use crate::contexts::{
348        HashContext,
349        SignatureContext,
350    };
351
352    #[test]
353    fn test_provider_architecture() {
354        #[cfg(feature = "std")]
355        {
356            // Test that default provider is properly configured
357            let mut ctx = KemContext::with_default_provider();
358
359            // Stub core provider: NotImplemented if configured, or ProviderNotConfigured if init failed
360            let result = ctx.generate_keypair(Algorithm::MlKem512, None);
361            assert!(result.is_err());
362
363            match result {
364                Err(crate::error::Error::NotImplemented { feature }) => {
365                    assert!(
366                        feature.contains(
367                            "ML-KEM implementations are provided by the main lib-q crate"
368                        )
369                    );
370                }
371                Err(crate::error::Error::ProviderNotConfigured { operation }) => {
372                    assert_eq!(operation, "KEM");
373                }
374                _ => panic!("Expected NotImplemented or ProviderNotConfigured"),
375            }
376        }
377
378        // Test that context without provider returns clear error
379        #[cfg(feature = "alloc")]
380        {
381            let mut ctx = KemContext::new();
382            let result = ctx.generate_keypair(Algorithm::MlKem512, None);
383            assert!(result.is_err());
384
385            if let Err(crate::error::Error::ProviderNotConfigured { operation }) = result {
386                assert_eq!(operation, "KEM");
387            } else {
388                panic!("Expected ProviderNotConfigured error, got different error type");
389            }
390        }
391    }
392
393    #[test]
394    fn test_algorithm_security_levels() {
395        assert_eq!(Algorithm::MlKem512.security_level(), 1);
396        assert_eq!(Algorithm::MlKem768.security_level(), 3);
397        assert_eq!(Algorithm::MlKem1024.security_level(), 4);
398        assert_eq!(Algorithm::MlDsa44.security_level(), 1);
399        assert_eq!(Algorithm::MlDsa65.security_level(), 3);
400        assert_eq!(Algorithm::MlDsa87.security_level(), 4);
401    }
402
403    #[test]
404    fn test_algorithm_categories() {
405        assert_eq!(Algorithm::MlKem512.category(), AlgorithmCategory::Kem);
406        assert_eq!(Algorithm::MlDsa44.category(), AlgorithmCategory::Signature);
407        assert_eq!(Algorithm::Shake256.category(), AlgorithmCategory::Hash);
408    }
409
410    #[test]
411    #[cfg(feature = "alloc")]
412    fn test_kem_context() {
413        let mut ctx = KemContext::new();
414        let result = ctx.generate_keypair(Algorithm::MlKem512, None);
415        assert!(result.is_err());
416        if let Err(crate::error::Error::ProviderNotConfigured { operation }) = result {
417            assert_eq!(operation, "KEM");
418        } else {
419            panic!("Expected ProviderNotConfigured error");
420        }
421    }
422
423    #[test]
424    #[cfg(feature = "alloc")]
425    fn test_signature_context() {
426        let mut ctx = SignatureContext::new();
427        let result = ctx.generate_keypair(Algorithm::MlDsa65, None);
428        assert!(result.is_err());
429        if let Err(crate::error::Error::ProviderNotConfigured { operation }) = result {
430            assert_eq!(operation, "signature");
431        } else {
432            panic!("Expected ProviderNotConfigured error");
433        }
434    }
435
436    #[test]
437    #[cfg(feature = "alloc")]
438    fn test_hash_context() {
439        let mut ctx = HashContext::new();
440        let result = ctx.hash(Algorithm::Shake256, b"test");
441        assert!(result.is_err());
442        if let Err(crate::error::Error::ProviderNotConfigured { operation }) = result {
443            assert_eq!(operation, "hash");
444        } else {
445            panic!("Expected ProviderNotConfigured error");
446        }
447    }
448
449    #[test]
450    fn test_utils() {
451        #[cfg(feature = "getrandom")]
452        {
453            let bytes = Utils::random_bytes(32).unwrap();
454            assert_eq!(bytes.len(), 32);
455        }
456
457        #[cfg(feature = "alloc")]
458        {
459            let hex = Utils::bytes_to_hex(&[0x01, 0x23, 0x45, 0x67]);
460            assert_eq!(hex, "01234567");
461
462            let decoded = Utils::hex_to_bytes(&hex).unwrap();
463            assert_eq!(decoded, alloc::vec![0x01, 0x23, 0x45, 0x67]);
464        }
465    }
466
467    #[test]
468    fn test_random_bytes_generation() {
469        // Test that random_bytes generates different values when available
470        match Utils::random_bytes(32) {
471            Ok(bytes1) => {
472                let bytes2 = Utils::random_bytes(32).expect("Should generate random bytes");
473                assert_eq!(bytes1.len(), 32);
474                assert_eq!(bytes2.len(), 32);
475
476                // Verify that we get different bytes on subsequent calls
477                // (This test has a very small probability of failure, but it's acceptable for testing)
478                assert_ne!(
479                    bytes1, bytes2,
480                    "Random bytes should be different on subsequent calls"
481                );
482
483                // Test that all bytes are not zero (very unlikely with proper RNG)
484                let all_zero1 = bytes1.iter().all(|&b| b == 0);
485                let all_zero2 = bytes2.iter().all(|&b| b == 0);
486
487                assert!(!all_zero1, "Random bytes should not all be zero");
488                assert!(!all_zero2, "Random bytes should not all be zero");
489            }
490            Err(crate::error::Error::RandomGenerationFailed { .. }) => {
491                // This is expected in no_std mode without getrandom feature
492                // The test passes by not panicking
493            }
494            Err(e) => {
495                panic!("Unexpected error: {:?}", e);
496            }
497        }
498    }
499
500    #[test]
501    fn test_constant_time_compare() {
502        assert!(Utils::constant_time_compare(b"hello", b"hello"));
503        assert!(!Utils::constant_time_compare(b"hello", b"world"));
504        assert!(!Utils::constant_time_compare(b"hello", b"hell"));
505    }
506
507    #[cfg(feature = "getrandom")]
508    #[test]
509    fn test_random_bytes_entropy_quality() {
510        // Test entropy quality by checking byte distribution
511        const NUM_SAMPLES: usize = 1000;
512        const BYTE_LENGTH: usize = 32;
513
514        let mut byte_counts = [0u32; 256];
515        let mut total_bytes = 0u32;
516
517        for _ in 0..NUM_SAMPLES {
518            let bytes = Utils::random_bytes(BYTE_LENGTH).expect("Should generate random bytes");
519            for &byte in &bytes {
520                byte_counts[byte as usize] += 1;
521                total_bytes += 1;
522            }
523        }
524
525        // Check that no byte value is completely absent (extremely unlikely with good RNG)
526        let zero_count = byte_counts.iter().filter(|&&count| count == 0).count();
527        assert!(
528            zero_count < 50,
529            "Too many byte values are missing from random generation"
530        );
531
532        // Chi-square goodness-of-fit test for uniform byte distribution (χ² with ν=255).
533        // Wilson-Hilferty approximation converts χ² to z; reject if z > 5 (false positive ~2.9e-7).
534        let expected_per_byte = total_bytes as f64 / 256.0;
535        let chi_sq: f64 = byte_counts
536            .iter()
537            .map(|&count| {
538                let d = count as f64 - expected_per_byte;
539                d * d / expected_per_byte
540            })
541            .sum();
542        const NU: f64 = 255.0;
543        let z =
544            ((chi_sq / NU).powf(1.0 / 3.0) - (1.0 - 2.0 / (9.0 * NU))) / (2.0 / (9.0 * NU)).sqrt();
545        assert!(
546            z <= 5.0,
547            "Random bytes show poor entropy distribution (chi-square z = {})",
548            z
549        );
550    }
551
552    #[cfg(feature = "getrandom")]
553    #[test]
554    fn test_random_bytes_uniformity() {
555        // Test that random bytes are uniformly distributed
556        const NUM_SAMPLES: usize = 10000;
557        const BYTE_LENGTH: usize = 16;
558
559        let mut all_bytes = alloc::vec![0u8; NUM_SAMPLES * BYTE_LENGTH];
560        let mut offset = 0;
561
562        for _ in 0..NUM_SAMPLES {
563            let bytes = Utils::random_bytes(BYTE_LENGTH).expect("Should generate random bytes");
564            all_bytes[offset..offset + BYTE_LENGTH].copy_from_slice(&bytes);
565            offset += BYTE_LENGTH;
566        }
567
568        // Test for patterns that would indicate poor randomness
569        // Check for runs of identical bytes (should be rare)
570        let mut max_run_length = 0;
571        let mut current_run_length = 1;
572
573        for i in 1..all_bytes.len() {
574            if all_bytes[i] == all_bytes[i - 1] {
575                current_run_length += 1;
576                max_run_length = max_run_length.max(current_run_length);
577            } else {
578                current_run_length = 1;
579            }
580        }
581
582        // Runs longer than 4 identical bytes are suspicious
583        assert!(
584            max_run_length <= 4,
585            "Random bytes show suspicious patterns (run length: {})",
586            max_run_length
587        );
588    }
589
590    #[cfg(any(feature = "rand", all(feature = "getrandom", feature = "alloc")))]
591    #[test]
592    fn test_random_bytes_size_limits() {
593        const MAX_SIZE: usize = 1024 * 1024; // 1MB
594        assert_eq!(
595            Utils::random_bytes(0),
596            Err(crate::error::Error::RandomBytesLengthInvalid {
597                min: 1,
598                max: MAX_SIZE,
599                requested: 0,
600            }),
601            "zero length"
602        );
603
604        assert!(
605            Utils::random_bytes(MAX_SIZE).is_ok(),
606            "Should accept maximum size"
607        );
608        assert_eq!(
609            Utils::random_bytes(MAX_SIZE + 1),
610            Err(crate::error::Error::RandomBytesLengthInvalid {
611                min: 1,
612                max: MAX_SIZE,
613                requested: MAX_SIZE + 1,
614            }),
615            "oversized request"
616        );
617
618        // Test reasonable sizes
619        for size in [1, 16, 32, 64, 128, 256, 512, 1024] {
620            let bytes = Utils::random_bytes(size).expect("Should generate random bytes");
621            assert_eq!(bytes.len(), size, "Should generate exactly {} bytes", size);
622        }
623    }
624
625    #[test]
626    #[cfg(feature = "alloc")]
627    fn test_hex_to_bytes_decode_errors() {
628        use crate::error::{
629            Error,
630            HexDecodeError,
631        };
632
633        assert_eq!(
634            Utils::hex_to_bytes("123").unwrap_err(),
635            Error::HexDecode(HexDecodeError::OddLength { char_count: 3 })
636        );
637        assert_eq!(
638            Utils::hex_to_bytes("12g3").unwrap_err(),
639            Error::HexDecode(HexDecodeError::InvalidDigit {
640                pair_start: 2,
641                char_count: 4,
642            })
643        );
644    }
645}