Skip to main content

aion_context/
crypto.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Cryptographic primitives for AION v2
3//!
4//! This module provides safe, ergonomic wrappers around battle-tested cryptographic
5//! libraries. All operations follow Tiger Style principles: explicit error handling,
6//! constant-time where applicable, and automatic zeroization of sensitive data.
7//!
8//! # Cryptographic Algorithms
9//!
10//! - **Ed25519**: Digital signatures (RFC 8032, 128-bit security level)
11//!   - Used for version signing and author authentication
12//!   - Deterministic signatures, no nonce generation issues
13//!   - Constant-time operations resistant to timing attacks
14//!
15//! - **ChaCha20-Poly1305**: Authenticated encryption (RFC 8439, 256-bit key)
16//!   - AEAD cipher for rules encryption
17//!   - Prevents tampering via authentication tag
18//!   - Used in TLS 1.3 as mandatory cipher suite
19//!
20//! - **BLAKE3**: Cryptographic hashing (256-bit output)
21//!   - 5x faster than SHA-256
22//!   - Parallelizable for large data
23//!   - Used for content hashing and integrity checks
24//!
25//! - **HKDF**: Key derivation function (RFC 5869, NIST approved)
26//!   - HMAC-based key derivation with SHA-256
27//!   - Derives multiple keys from master secret
28//!   - Context separation via info parameter
29//!
30//! # Security Properties
31//!
32//! - **No panics**: All errors explicitly handled
33//! - **Constant-time**: Ed25519 operations resistant to timing attacks
34//! - **Zeroization**: Signing keys automatically cleared on drop
35//! - **Entropy**: OS CSPRNG for key/nonce generation
36//! - **Standards**: RFC-compliant implementations
37//!
38//! # Usage Examples
39//!
40//! ## Digital Signatures
41//!
42//! ```
43//! use aion_context::crypto::SigningKey;
44//!
45//! // Generate a new signing key
46//! let signing_key = SigningKey::generate();
47//! let message = b"Version 1: Updated fraud rules";
48//!
49//! // Sign a message
50//! let signature = signing_key.sign(message);
51//!
52//! // Verify the signature
53//! let verifying_key = signing_key.verifying_key();
54//! assert!(verifying_key.verify(message, &signature).is_ok());
55//! ```
56//!
57//! ## Authenticated Encryption
58//!
59//! ```
60//! use aion_context::crypto::{generate_nonce, encrypt, decrypt};
61//!
62//! let key = [0u8; 32];  // In production, use proper key derivation
63//! let nonce = generate_nonce();
64//! let plaintext = b"sensitive rules data";
65//! let aad = b"version metadata";
66//!
67//! // Encrypt data
68//! let ciphertext = encrypt(&key, &nonce, plaintext, aad).unwrap();
69//!
70//! // Decrypt data
71//! let recovered = decrypt(&key, &nonce, &ciphertext, aad).unwrap();
72//! assert_eq!(recovered, plaintext);
73//! ```
74//!
75//! ## Hashing
76//!
77//! ```
78//! use aion_context::crypto::{hash, keyed_hash};
79//!
80//! // Content hashing
81//! let data = b"file content";
82//! let content_hash = hash(data);
83//!
84//! // Keyed hashing (MAC)
85//! let key = [0u8; 32];
86//! let mac = keyed_hash(&key, data);
87//! ```
88//!
89//! ## Key Derivation
90//!
91//! ```
92//! use aion_context::crypto::derive_key;
93//!
94//! let master_secret = b"high entropy master key";
95//! let salt = b"unique salt value";
96//! let info = b"encryption-key-v1";
97//!
98//! let mut derived_key = [0u8; 32];
99//! derive_key(master_secret, salt, info, &mut derived_key).unwrap();
100//! // derived_key now contains 32 bytes of derived key material
101//! assert_eq!(derived_key.len(), 32);
102//! ```
103
104use crate::{AionError, Result};
105use ed25519_dalek::{Signature as Ed25519Signature, Signer, Verifier};
106use rand::RngCore;
107use zeroize::Zeroizing;
108
109// Re-export commonly used types
110pub use ed25519_dalek::{SigningKey as Ed25519SigningKey, VerifyingKey as Ed25519VerifyingKey};
111
112/// Signing key for Ed25519 signatures
113///
114/// Automatically zeroized on drop to protect key material.
115///
116/// # Security
117///
118/// - 256-bit private key
119/// - Constant-time operations
120/// - Automatically zeroized when dropped
121/// - **Note**: Implements `Clone` for testing convenience, but cloning key material
122///   increases exposure. Use sparingly in production code.
123///
124/// # Examples
125///
126/// ```
127/// use aion_context::crypto::SigningKey;
128///
129/// let key = SigningKey::generate();
130/// let message = b"test message";
131/// let signature = key.sign(message);
132/// ```
133#[derive(Clone)]
134pub struct SigningKey(Zeroizing<[u8; 32]>);
135
136impl SigningKey {
137    /// Generate a new random signing key using OS entropy
138    ///
139    /// Uses the operating system's cryptographically secure random number
140    /// generator (`/dev/urandom` on Unix, `CryptGenRandom` on Windows).
141    #[must_use]
142    pub fn generate() -> Self {
143        let key = Ed25519SigningKey::generate(&mut rand::rngs::OsRng);
144        Self(Zeroizing::new(key.to_bytes()))
145    }
146
147    /// Create a signing key from bytes
148    ///
149    /// # Errors
150    ///
151    /// Returns `AionError::InvalidPrivateKey` if the bytes are not a valid Ed25519 private key
152    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
153        if bytes.len() != 32 {
154            return Err(AionError::InvalidPrivateKey {
155                reason: format!("expected 32 bytes, got {}", bytes.len()),
156            });
157        }
158
159        let mut key_bytes = [0u8; 32];
160        key_bytes.copy_from_slice(bytes);
161
162        // Validate the key by trying to create an Ed25519SigningKey
163        let _validate = Ed25519SigningKey::from_bytes(&key_bytes);
164
165        Ok(Self(Zeroizing::new(key_bytes)))
166    }
167
168    /// Get the bytes of this signing key
169    ///
170    /// Returns a reference to the key bytes
171    #[must_use]
172    pub fn to_bytes(&self) -> &[u8; 32] {
173        &self.0
174    }
175
176    /// Sign a message
177    ///
178    /// Creates an Ed25519 signature over the message using this key.
179    ///
180    /// # Examples
181    ///
182    /// ```
183    /// use aion_context::crypto::SigningKey;
184    ///
185    /// let key = SigningKey::generate();
186    /// let signature = key.sign(b"message");
187    /// assert_eq!(signature.len(), 64);
188    /// ```
189    #[must_use]
190    pub fn sign(&self, message: &[u8]) -> [u8; 64] {
191        let signing_key = Ed25519SigningKey::from_bytes(&self.0);
192        signing_key.sign(message).to_bytes()
193    }
194
195    /// Get the corresponding verifying key
196    #[must_use]
197    pub fn verifying_key(&self) -> VerifyingKey {
198        let signing_key = Ed25519SigningKey::from_bytes(&self.0);
199        VerifyingKey(signing_key.verifying_key())
200    }
201}
202
203/// Verifying key for Ed25519 signatures
204///
205/// Used to verify signatures created by the corresponding `SigningKey`.
206///
207/// # Examples
208///
209/// ```
210/// use aion_context::crypto::{SigningKey, VerifyingKey};
211///
212/// let signing_key = SigningKey::generate();
213/// let verifying_key = signing_key.verifying_key();
214///
215/// let message = b"test";
216/// let signature = signing_key.sign(message);
217///
218/// assert!(verifying_key.verify(message, &signature).is_ok());
219/// ```
220#[derive(Clone, Copy, Debug, PartialEq, Eq)]
221pub struct VerifyingKey(Ed25519VerifyingKey);
222
223impl VerifyingKey {
224    /// Create a verifying key from bytes
225    ///
226    /// # Errors
227    ///
228    /// Returns `AionError::InvalidPublicKey` if the bytes are not a valid Ed25519 public key
229    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
230        if bytes.len() != 32 {
231            return Err(AionError::InvalidPublicKey {
232                reason: format!("expected 32 bytes, got {}", bytes.len()),
233            });
234        }
235
236        let mut key_bytes = [0u8; 32];
237        key_bytes.copy_from_slice(bytes);
238
239        let key = Ed25519VerifyingKey::from_bytes(&key_bytes).map_err(|e| {
240            AionError::InvalidPublicKey {
241                reason: e.to_string(),
242            }
243        })?;
244
245        Ok(Self(key))
246    }
247
248    /// Get the bytes of this verifying key
249    #[must_use]
250    pub fn to_bytes(&self) -> [u8; 32] {
251        self.0.to_bytes()
252    }
253
254    /// Verify a signature on a message
255    ///
256    /// # Errors
257    ///
258    /// Returns `AionError::InvalidSignature` if the signature is invalid or doesn't match the message
259    pub fn verify(&self, message: &[u8], signature: &[u8; 64]) -> Result<()> {
260        let sig = Ed25519Signature::from_bytes(signature);
261
262        self.0
263            .verify(message, &sig)
264            .map_err(|_| AionError::InvalidSignature {
265                reason: "signature verification failed".to_string(),
266            })
267    }
268}
269
270/// Hash a message using BLAKE3
271///
272/// Returns a 32-byte (256-bit) cryptographic hash.
273///
274/// # Examples
275///
276/// ```
277/// use aion_context::crypto::hash;
278///
279/// let hash = hash(b"Hello, AION!");
280/// assert_eq!(hash.len(), 32);
281/// ```
282#[must_use]
283pub fn hash(data: &[u8]) -> [u8; 32] {
284    blake3::hash(data).into()
285}
286
287/// Keyed hash using BLAKE3
288///
289/// Provides message authentication (MAC) using a secret key.
290///
291/// # Examples
292///
293/// ```
294/// use aion_context::crypto::keyed_hash;
295///
296/// let key = [0u8; 32];
297/// let mac = keyed_hash(&key, b"message");
298/// assert_eq!(mac.len(), 32);
299/// ```
300#[must_use]
301pub fn keyed_hash(key: &[u8; 32], data: &[u8]) -> [u8; 32] {
302    blake3::keyed_hash(key, data).into()
303}
304
305/// Derive a key using HKDF-SHA256
306///
307/// Extracts entropy from input key material and expands it into a derived key.
308///
309/// # Arguments
310///
311/// * `ikm` - Input key material
312/// * `salt` - Optional salt value (use empty slice if none)
313/// * `info` - Context and application-specific information
314/// * `output` - Buffer to fill with derived key material
315///
316/// # Examples
317///
318/// ```
319/// use aion_context::crypto::derive_key;
320///
321/// let ikm = b"input key material";
322/// let salt = b"optional salt";
323/// let info = b"application context";
324/// let mut output = [0u8; 32];
325///
326/// derive_key(ikm, salt, info, &mut output).unwrap();
327/// ```
328///
329/// # Errors
330///
331/// Returns `AionError::InvalidPrivateKey` if key derivation fails (should never happen with valid inputs)
332pub fn derive_key(ikm: &[u8], salt: &[u8], info: &[u8], output: &mut [u8]) -> Result<()> {
333    use hkdf::Hkdf;
334    use sha2::Sha256;
335
336    let hk = Hkdf::<Sha256>::new(Some(salt), ikm);
337
338    hk.expand(info, output)
339        .map_err(|_| AionError::InvalidPrivateKey {
340            reason: "HKDF expand failed".to_string(),
341        })?;
342
343    Ok(())
344}
345
346/// Encrypt data using ChaCha20-Poly1305 (AEAD)
347///
348/// # Arguments
349///
350/// * `key` - 32-byte encryption key
351/// * `nonce` - 12-byte nonce (MUST be unique for each encryption with the same key)
352/// * `plaintext` - Data to encrypt
353/// * `aad` - Additional authenticated data (not encrypted, but authenticated)
354///
355/// # Returns
356///
357/// Ciphertext with authentication tag appended (`plaintext.len()` + 16 bytes)
358///
359/// # Errors
360///
361/// Returns `AionError::EncryptionFailed` if encryption fails
362///
363/// # Security
364///
365/// **CRITICAL**: Never reuse a nonce with the same key. Use `generate_nonce()` for each encryption.
366pub fn encrypt(key: &[u8; 32], nonce: &[u8; 12], plaintext: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
367    use chacha20poly1305::{
368        aead::{Aead, KeyInit, Payload},
369        ChaCha20Poly1305,
370    };
371
372    let cipher = ChaCha20Poly1305::new(key.into());
373    let payload = Payload {
374        msg: plaintext,
375        aad,
376    };
377
378    cipher
379        .encrypt(nonce.into(), payload)
380        .map_err(|e| AionError::EncryptionFailed {
381            reason: e.to_string(),
382        })
383}
384
385/// Decrypt data using ChaCha20-Poly1305 (AEAD)
386///
387/// # Arguments
388///
389/// * `key` - 32-byte encryption key (same as used for encryption)
390/// * `nonce` - 12-byte nonce (same as used for encryption)
391/// * `ciphertext` - Encrypted data with authentication tag
392/// * `aad` - Additional authenticated data (must match encryption)
393///
394/// # Returns
395///
396/// Decrypted plaintext
397///
398/// # Errors
399///
400/// Returns `AionError::DecryptionFailed` if:
401/// - Authentication tag is invalid (data was tampered with)
402/// - Wrong key or nonce used
403/// - AAD doesn't match
404pub fn decrypt(key: &[u8; 32], nonce: &[u8; 12], ciphertext: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
405    use chacha20poly1305::{
406        aead::{Aead, KeyInit, Payload},
407        ChaCha20Poly1305,
408    };
409
410    let cipher = ChaCha20Poly1305::new(key.into());
411    let payload = Payload {
412        msg: ciphertext,
413        aad,
414    };
415
416    cipher
417        .decrypt(nonce.into(), payload)
418        .map_err(|e| AionError::DecryptionFailed {
419            reason: e.to_string(),
420        })
421}
422
423/// Generate a random nonce for ChaCha20-Poly1305
424///
425/// Uses OS entropy to generate a cryptographically secure 12-byte nonce.
426///
427/// # Examples
428///
429/// ```
430/// use aion_context::crypto::generate_nonce;
431///
432/// let nonce = generate_nonce();
433/// assert_eq!(nonce.len(), 12);
434/// ```
435#[must_use]
436pub fn generate_nonce() -> [u8; 12] {
437    let mut nonce = [0u8; 12];
438    rand::rngs::OsRng.fill_bytes(&mut nonce);
439    nonce
440}
441
442#[cfg(test)]
443#[allow(clippy::unwrap_used)] // Tests are allowed to panic
444mod tests {
445    use super::*;
446
447    mod signing {
448        use super::*;
449
450        #[test]
451        fn should_generate_signing_key() {
452            let key = SigningKey::generate();
453            let bytes = key.to_bytes();
454            assert_eq!(bytes.len(), 32);
455        }
456
457        #[test]
458        fn should_create_signing_key_from_bytes() {
459            let original = SigningKey::generate();
460            let bytes = *original.to_bytes();
461
462            let restored = SigningKey::from_bytes(&bytes).unwrap();
463            assert_eq!(*original.to_bytes(), *restored.to_bytes());
464        }
465
466        #[test]
467        fn should_reject_invalid_key_length() {
468            let result = SigningKey::from_bytes(&[0u8; 16]);
469            assert!(result.is_err());
470        }
471
472        #[test]
473        fn should_sign_message() {
474            let key = SigningKey::generate();
475            let signature = key.sign(b"test message");
476            assert_eq!(signature.len(), 64);
477        }
478
479        #[test]
480        fn should_verify_valid_signature() {
481            let key = SigningKey::generate();
482            let message = b"test message";
483            let signature = key.sign(message);
484
485            let verifying_key = key.verifying_key();
486            assert!(verifying_key.verify(message, &signature).is_ok());
487        }
488
489        #[test]
490        fn should_reject_invalid_signature() {
491            let key = SigningKey::generate();
492            let message = b"test message";
493            let mut signature = key.sign(message);
494
495            // Tamper with signature
496            signature[0] ^= 1;
497
498            let verifying_key = key.verifying_key();
499            assert!(verifying_key.verify(message, &signature).is_err());
500        }
501
502        #[test]
503        fn should_reject_wrong_message() {
504            let key = SigningKey::generate();
505            let signature = key.sign(b"original message");
506
507            let verifying_key = key.verifying_key();
508            assert!(verifying_key
509                .verify(b"different message", &signature)
510                .is_err());
511        }
512
513        #[test]
514        fn should_serialize_verifying_key() {
515            let key = SigningKey::generate();
516            let verifying_key = key.verifying_key();
517            let bytes = verifying_key.to_bytes();
518            assert_eq!(bytes.len(), 32);
519
520            let restored = VerifyingKey::from_bytes(&bytes).unwrap();
521            assert_eq!(verifying_key.to_bytes(), restored.to_bytes());
522        }
523    }
524
525    mod hashing {
526        use super::*;
527
528        #[test]
529        fn should_hash_data() {
530            let hash1 = hash(b"test data");
531            assert_eq!(hash1.len(), 32);
532
533            // Same input produces same hash
534            let hash2 = hash(b"test data");
535            assert_eq!(hash1, hash2);
536        }
537
538        #[test]
539        fn should_produce_different_hashes_for_different_data() {
540            let hash1 = hash(b"data1");
541            let hash2 = hash(b"data2");
542            assert_ne!(hash1, hash2);
543        }
544
545        #[test]
546        fn should_create_keyed_hash() {
547            let key = [0u8; 32];
548            let mac = keyed_hash(&key, b"message");
549            assert_eq!(mac.len(), 32);
550        }
551
552        #[test]
553        fn should_produce_different_macs_with_different_keys() {
554            let key1 = [0u8; 32];
555            let key2 = [1u8; 32];
556
557            let mac1 = keyed_hash(&key1, b"message");
558            let mac2 = keyed_hash(&key2, b"message");
559            assert_ne!(mac1, mac2);
560        }
561    }
562
563    mod key_derivation {
564        use super::*;
565
566        #[test]
567        fn should_derive_key() {
568            let ikm = b"input key material";
569            let salt = b"salt";
570            let info = b"context";
571            let mut output = [0u8; 32];
572
573            derive_key(ikm, salt, info, &mut output).unwrap();
574
575            // Output should not be all zeros
576            assert_ne!(output, [0u8; 32]);
577        }
578
579        #[test]
580        fn should_produce_deterministic_output() {
581            let ikm = b"input key material";
582            let salt = b"salt";
583            let info = b"context";
584
585            let mut output1 = [0u8; 32];
586            derive_key(ikm, salt, info, &mut output1).unwrap();
587
588            let mut output2 = [0u8; 32];
589            derive_key(ikm, salt, info, &mut output2).unwrap();
590
591            assert_eq!(output1, output2);
592        }
593
594        #[test]
595        fn should_produce_different_keys_for_different_info() {
596            let ikm = b"input key material";
597            let salt = b"salt";
598
599            let mut output1 = [0u8; 32];
600            derive_key(ikm, salt, b"context1", &mut output1).unwrap();
601
602            let mut output2 = [0u8; 32];
603            derive_key(ikm, salt, b"context2", &mut output2).unwrap();
604
605            assert_ne!(output1, output2);
606        }
607    }
608
609    mod encryption {
610        use super::*;
611
612        #[test]
613        fn should_encrypt_and_decrypt() {
614            let key = [0u8; 32];
615            let nonce = generate_nonce();
616            let plaintext = b"secret message";
617            let aad = b"additional data";
618
619            let ciphertext = encrypt(&key, &nonce, plaintext, aad).unwrap();
620            assert_eq!(ciphertext.len(), plaintext.len() + 16); // +16 for auth tag
621
622            let decrypted = decrypt(&key, &nonce, &ciphertext, aad).unwrap();
623            assert_eq!(decrypted, plaintext);
624        }
625
626        #[test]
627        fn should_reject_tampered_ciphertext() {
628            let key = [0u8; 32];
629            let nonce = generate_nonce();
630            let plaintext = b"secret message";
631            let aad = b"additional data";
632
633            let mut ciphertext = encrypt(&key, &nonce, plaintext, aad).unwrap();
634
635            // Tamper with ciphertext
636            if let Some(byte) = ciphertext.get_mut(0) {
637                *byte ^= 1;
638            }
639
640            let result = decrypt(&key, &nonce, &ciphertext, aad);
641            assert!(result.is_err());
642        }
643
644        #[test]
645        fn should_reject_wrong_aad() {
646            let key = [0u8; 32];
647            let nonce = generate_nonce();
648            let plaintext = b"secret message";
649
650            let ciphertext = encrypt(&key, &nonce, plaintext, b"aad1").unwrap();
651
652            let result = decrypt(&key, &nonce, &ciphertext, b"aad2");
653            assert!(result.is_err());
654        }
655
656        #[test]
657        fn should_reject_wrong_key() {
658            let key1 = [0u8; 32];
659            let key2 = [1u8; 32];
660            let nonce = generate_nonce();
661            let plaintext = b"secret message";
662            let aad = b"additional data";
663
664            let ciphertext = encrypt(&key1, &nonce, plaintext, aad).unwrap();
665
666            let result = decrypt(&key2, &nonce, &ciphertext, aad);
667            assert!(result.is_err());
668        }
669
670        #[test]
671        fn should_generate_unique_nonces() {
672            let nonce1 = generate_nonce();
673            let nonce2 = generate_nonce();
674            assert_ne!(nonce1, nonce2);
675        }
676    }
677
678    mod properties {
679        use super::*;
680        use hegel::generators as gs;
681
682        #[hegel::test]
683        fn prop_sign_verify_roundtrip(tc: hegel::TestCase) {
684            let message = tc.draw(gs::binary().max_size(4096));
685            let key = SigningKey::generate();
686            let signature = key.sign(&message);
687            let verifying_key = key.verifying_key();
688            assert!(verifying_key.verify(&message, &signature).is_ok());
689        }
690
691        #[hegel::test]
692        fn prop_verify_rejects_wrong_key(tc: hegel::TestCase) {
693            let message = tc.draw(gs::binary().max_size(4096));
694            let signer = SigningKey::generate();
695            let other = SigningKey::generate();
696            let signature = signer.sign(&message);
697            assert!(other.verifying_key().verify(&message, &signature).is_err());
698        }
699
700        #[hegel::test]
701        fn prop_verify_rejects_tampered_message(tc: hegel::TestCase) {
702            let mut message = tc.draw(gs::binary().min_size(1).max_size(4096));
703            let key = SigningKey::generate();
704            let signature = key.sign(&message);
705            let max = message
706                .len()
707                .checked_sub(1)
708                .unwrap_or_else(|| std::process::abort());
709            let flip_index = tc.draw(gs::integers::<usize>().max_value(max));
710            if let Some(byte) = message.get_mut(flip_index) {
711                *byte ^= 0x01;
712            }
713            assert!(key.verifying_key().verify(&message, &signature).is_err());
714        }
715
716        #[hegel::test]
717        fn prop_hash_is_deterministic(tc: hegel::TestCase) {
718            let data = tc.draw(gs::binary().max_size(8192));
719            assert_eq!(hash(&data), hash(&data));
720        }
721
722        #[hegel::test]
723        fn prop_verifying_key_roundtrip_verifies(tc: hegel::TestCase) {
724            let message = tc.draw(gs::binary().max_size(4096));
725            let signer = SigningKey::generate();
726            let original = signer.verifying_key();
727            let restored = VerifyingKey::from_bytes(&original.to_bytes())
728                .unwrap_or_else(|_| std::process::abort());
729            let signature = signer.sign(&message);
730            assert!(restored.verify(&message, &signature).is_ok());
731        }
732    }
733}