zoe_wire_protocol/
crypto.rs

1//! Message-level cryptography for the Zoe protocol
2//!
3//! This module provides cryptographic primitives for message encryption, key derivation,
4//! and mnemonic-based key management used in the Zoe messaging protocol.
5//!
6//! # Key Features
7//!
8//! - ChaCha20-Poly1305 encryption for message content
9//! - BIP39 mnemonic phrase support for key derivation
10//! - Ed25519 and ML-DSA key generation from mnemonics
11//! - Self-encryption and ephemeral ECDH patterns
12//! - Argon2 key derivation with configurable parameters
13
14use libcrux_ml_dsa::ml_dsa_65::MLDSA65SigningKey;
15use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret as X25519PrivateKey};
16
17#[cfg(feature = "frb-api")]
18use flutter_rust_bridge::frb;
19
20// ChaCha20-Poly1305 and mnemonic support
21use argon2::{Argon2, PasswordHasher};
22use bip39::{Language, Mnemonic};
23use chacha20poly1305::{
24    aead::{Aead, AeadCore, KeyInit, OsRng},
25    ChaCha20Poly1305, Key, Nonce,
26};
27use rand::{thread_rng, RngCore, SeedableRng};
28
29use serde::{Deserialize, Serialize};
30
31#[derive(Debug, thiserror::Error)]
32pub enum CryptoError {
33    #[error("Parse error: {0}")]
34    ParseError(String),
35
36    #[error("Invalid ML-DSA key: {0:?}")]
37    InvalidMlDsaKey(String),
38
39    #[error("Encryption error: {0}")]
40    EncryptionError(String),
41
42    #[error("Decryption error: {0}")]
43    DecryptionError(String),
44
45    #[error("Mnemonic error: {0}")]
46    MnemonicError(String),
47
48    #[error("Key derivation error: {0}")]
49    KeyDerivationError(String),
50
51    #[error("TLS configuration error: {0}")]
52    TlsError(String),
53}
54
55// ==================== ChaCha20-Poly1305 & Mnemonic Support ====================
56
57/// Mnemonic phrase for key derivation
58#[derive(Debug, Clone)]
59pub struct MnemonicPhrase {
60    pub phrase: String,
61    pub language: Language,
62}
63
64impl MnemonicPhrase {
65    /// Generate a new 24-word mnemonic phrase
66    pub fn generate() -> std::result::Result<Self, CryptoError> {
67        // Generate 32 bytes of entropy for 24 words
68        let mut entropy = [0u8; 32];
69        thread_rng().fill_bytes(&mut entropy);
70
71        let mnemonic = Mnemonic::from_entropy_in(Language::English, &entropy)
72            .map_err(|e| CryptoError::MnemonicError(format!("Failed to generate mnemonic: {e}")))?;
73
74        Ok(Self {
75            phrase: mnemonic.to_string(),
76            language: Language::English,
77        })
78    }
79
80    /// Create from existing phrase
81    pub fn from_phrase(phrase: &str, language: Language) -> std::result::Result<Self, CryptoError> {
82        // Validate the mnemonic
83        Mnemonic::parse_in(language, phrase)
84            .map_err(|e| CryptoError::MnemonicError(format!("Invalid mnemonic phrase: {e}")))?;
85
86        Ok(Self {
87            phrase: phrase.to_string(),
88            language,
89        })
90    }
91
92    /// Derive a seed from the mnemonic with optional passphrase
93    pub fn to_seed(&self, passphrase: &str) -> std::result::Result<[u8; 64], CryptoError> {
94        let mnemonic = Mnemonic::parse_in(self.language, &self.phrase)
95            .map_err(|e| CryptoError::MnemonicError(format!("Invalid mnemonic: {e}")))?;
96
97        Ok(mnemonic.to_seed(passphrase))
98    }
99
100    /// Get the phrase as string (be careful with this!)
101    pub fn phrase(&self) -> &str {
102        &self.phrase
103    }
104}
105
106/// Key derivation methods supported by the system
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
108pub enum KeyDerivationMethod {
109    /// BIP39 mnemonic phrase with Argon2 key derivation
110    ///
111    /// This is the standard method for user-controlled key derivation using
112    /// a BIP39 mnemonic phrase combined with Argon2 for key stretching.
113    Bip39Argon2,
114
115    /// Direct ChaCha20-Poly1305 key generation
116    ///
117    /// Used for fallback scenarios or when no mnemonic is provided.
118    /// Keys are generated directly without mnemonic derivation.
119    ChaCha20Poly1305Keygen,
120}
121
122impl KeyDerivationMethod {
123    /// Get the string representation of this derivation method
124    ///
125    /// This is useful for compatibility with existing string-based systems
126    /// or for display purposes.
127    pub fn as_str(&self) -> &'static str {
128        match self {
129            Self::Bip39Argon2 => "bip39+argon2",
130            Self::ChaCha20Poly1305Keygen => "chacha20-poly1305-keygen",
131        }
132    }
133}
134
135impl std::fmt::Display for KeyDerivationMethod {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        write!(f, "{}", self.as_str())
138    }
139}
140
141impl std::str::FromStr for KeyDerivationMethod {
142    type Err = String;
143
144    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
145        match s {
146            "bip39+argon2" => Ok(Self::Bip39Argon2),
147            "chacha20-poly1305-keygen" => Ok(Self::ChaCha20Poly1305Keygen),
148            _ => Err(format!("Unknown key derivation method: '{s}'")),
149        }
150    }
151}
152
153/// Information about how a key was derived
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
155pub struct KeyDerivationInfo {
156    /// Key derivation method used
157    pub method: KeyDerivationMethod,
158    /// Salt used for derivation
159    pub salt: Vec<u8>,
160    /// Argon2 parameters used
161    pub argon2_params: Argon2Params,
162    /// Context string used for derivation
163    pub context: String, // e.g., "dga-group-key"
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
167pub struct Argon2Params {
168    pub memory: u32,
169    pub iterations: u32,
170    pub parallelism: u32,
171}
172
173impl Default for Argon2Params {
174    fn default() -> Self {
175        Self {
176            memory: 65536,  // 64 MB
177            iterations: 3,  // 3 iterations
178            parallelism: 4, // 4 threads
179        }
180    }
181}
182
183/// ChaCha20-Poly1305 encryption key
184#[cfg_attr(feature = "frb-api", frb(opaque))]
185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
186pub struct EncryptionKey {
187    /// The actual key bytes (32 bytes for ChaCha20)
188    pub key: [u8; 32],
189    /// Key identifier
190    pub key_id: Vec<u8>,
191    /// When this key was created
192    pub created_at: u64,
193    /// Optional derivation info (for mnemonic-derived keys)
194    pub derivation_info: Option<KeyDerivationInfo>,
195}
196
197/// Minimal encrypted content for wire protocol messages
198/// Optimized for space - no key_id since it's determined by channel context
199#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
200pub struct ChaCha20Poly1305Content {
201    /// Encrypted data + authentication tag
202    pub ciphertext: Vec<u8>,
203    /// ChaCha20-Poly1305 nonce (fixed 12 bytes for space efficiency)
204    pub nonce: [u8; 12],
205}
206
207/// Ed25519-derived ChaCha20-Poly1305 encrypted content
208/// Simple self-encryption using only the sender's ed25519 keypair derived from mnemonic
209/// Only the sender can decrypt this content (encrypt-to-self pattern)
210/// Public key is available from message sender field - no need to duplicate
211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
212pub struct Ed25519SelfEncryptedContent {
213    /// Encrypted data + authentication tag
214    pub ciphertext: Vec<u8>,
215    /// ChaCha20-Poly1305 nonce (12 bytes)
216    pub nonce: [u8; 12],
217}
218
219/// Ephemeral ECDH ChaCha20-Poly1305 encrypted content
220/// Simple public key encryption using ephemeral X25519 keys
221/// Anyone can encrypt for the recipient using only their public key
222#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
223pub struct EphemeralEcdhContent {
224    /// Encrypted data + authentication tag
225    pub ciphertext: Vec<u8>,
226    /// ChaCha20-Poly1305 nonce (12 bytes)
227    pub nonce: [u8; 12],
228    /// Ephemeral X25519 public key (generated randomly for each message)
229    pub ephemeral_public: [u8; 32],
230}
231
232/// PQXDH encrypted content for asynchronous secure communication
233///
234/// This supports both initial handshake messages (Phase 2) and ongoing
235/// session messages (Phase 3) of the PQXDH protocol.
236#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
237pub enum PqxdhEncryptedContent {
238    /// Initial PQXDH handshake message that establishes the session
239    /// and delivers the first encrypted payload
240    Initial(crate::inbox::pqxdh::PqxdhInitialMessage),
241
242    /// Follow-up session message using established shared secret
243    /// for efficient ongoing communication
244    Session(crate::inbox::pqxdh::PqxdhSessionMessage),
245}
246
247impl Ed25519SelfEncryptedContent {
248    /// Encrypt data using ed25519 private key (self-encryption)
249    /// Derives a ChaCha20 key from the ed25519 private key deterministically
250    /// Only the same private key can decrypt this content
251    pub fn encrypt(
252        plaintext: &[u8],
253        signing_key: &ed25519_dalek::SigningKey,
254    ) -> std::result::Result<Self, CryptoError> {
255        use chacha20poly1305::aead::{Aead, OsRng};
256        use chacha20poly1305::{AeadCore, ChaCha20Poly1305, Key, KeyInit};
257
258        // Derive ChaCha20 key from ed25519 private key using Blake3
259        let ed25519_private_bytes = signing_key.to_bytes();
260        let mut key_derivation_input = Vec::new();
261        key_derivation_input.extend_from_slice(&ed25519_private_bytes);
262        key_derivation_input.extend_from_slice(b"ed25519-to-chacha20-key-derivation");
263
264        let derived_key_hash = blake3::hash(&key_derivation_input);
265        let chacha_key = Key::from_slice(derived_key_hash.as_bytes());
266        let cipher = ChaCha20Poly1305::new(chacha_key);
267
268        // Generate random nonce
269        let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
270
271        let ciphertext = cipher.encrypt(&nonce, plaintext).map_err(|e| {
272            CryptoError::EncryptionError(format!("Ed25519-derived ChaCha20 encryption failed: {e}"))
273        })?;
274
275        let mut nonce_bytes = [0u8; 12];
276        nonce_bytes.copy_from_slice(&nonce);
277
278        Ok(Self {
279            ciphertext,
280            nonce: nonce_bytes,
281        })
282    }
283
284    /// Decrypt data using ed25519 private key (self-decryption)
285    /// Must be the same private key that was used for encryption
286    pub fn decrypt(
287        &self,
288        signing_key: &ed25519_dalek::SigningKey,
289    ) -> std::result::Result<Vec<u8>, CryptoError> {
290        use chacha20poly1305::aead::Aead;
291        use chacha20poly1305::{ChaCha20Poly1305, Key, KeyInit, Nonce};
292
293        // Derive the same ChaCha20 key from ed25519 private key
294        let ed25519_private_bytes = signing_key.to_bytes();
295        let mut key_derivation_input = Vec::new();
296        key_derivation_input.extend_from_slice(&ed25519_private_bytes);
297        key_derivation_input.extend_from_slice(b"ed25519-to-chacha20-key-derivation");
298
299        let derived_key_hash = blake3::hash(&key_derivation_input);
300        let chacha_key = Key::from_slice(derived_key_hash.as_bytes());
301        let cipher = ChaCha20Poly1305::new(chacha_key);
302
303        let nonce = Nonce::from_slice(&self.nonce);
304
305        cipher
306            .decrypt(nonce, self.ciphertext.as_ref())
307            .map_err(|e| {
308                CryptoError::DecryptionError(format!(
309                    "Ed25519-derived ChaCha20 decryption failed: {e}"
310                ))
311            })
312    }
313}
314
315/// ML-DSA-derived ChaCha20-Poly1305 encrypted content
316/// Simple self-encryption using only the sender's ML-DSA keypair derived from mnemonic
317/// Only the sender can decrypt this content (encrypt-to-self pattern)
318/// Public key is available from message sender field - no need to duplicate
319#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
320pub struct MlDsaSelfEncryptedContent {
321    /// Encrypted data + authentication tag
322    pub ciphertext: Vec<u8>,
323    /// ChaCha20-Poly1305 nonce (12 bytes)
324    pub nonce: [u8; 12],
325}
326
327impl MlDsaSelfEncryptedContent {
328    /// Encrypt data using ML-DSA private key (self-encryption)
329    /// Derives a ChaCha20 key from the ML-DSA private key deterministically
330    /// Only the same private key can decrypt this content
331    pub fn encrypt(
332        plaintext: &[u8],
333        signing_key: &MLDSA65SigningKey,
334    ) -> std::result::Result<Self, CryptoError> {
335        use chacha20poly1305::aead::{Aead, OsRng};
336        use chacha20poly1305::{AeadCore, ChaCha20Poly1305, Key, KeyInit};
337
338        // Derive ChaCha20 key from ML-DSA private key using Blake3
339        let ml_dsa_private_bytes = signing_key.as_slice();
340        let mut key_derivation_input = Vec::new();
341        key_derivation_input.extend_from_slice(ml_dsa_private_bytes);
342        key_derivation_input.extend_from_slice(b"ml-dsa-to-chacha20-key-derivation");
343
344        let derived_key_hash = blake3::hash(&key_derivation_input);
345        let chacha_key = Key::from_slice(derived_key_hash.as_bytes());
346        let cipher = ChaCha20Poly1305::new(chacha_key);
347
348        // Generate random nonce
349        let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
350
351        let ciphertext = cipher.encrypt(&nonce, plaintext).map_err(|e| {
352            CryptoError::EncryptionError(format!("ML-DSA-derived ChaCha20 encryption failed: {e}"))
353        })?;
354
355        let mut nonce_bytes = [0u8; 12];
356        nonce_bytes.copy_from_slice(&nonce);
357
358        Ok(Self {
359            ciphertext,
360            nonce: nonce_bytes,
361        })
362    }
363
364    /// Decrypt data using ML-DSA private key (self-decryption)
365    /// Must be the same private key that was used for encryption
366    pub fn decrypt(
367        &self,
368        signing_key: &MLDSA65SigningKey,
369    ) -> std::result::Result<Vec<u8>, CryptoError> {
370        use chacha20poly1305::aead::Aead;
371        use chacha20poly1305::{ChaCha20Poly1305, Key, KeyInit, Nonce};
372
373        // Derive the same ChaCha20 key from ML-DSA private key
374        let ml_dsa_private_bytes = signing_key.as_slice();
375        let mut key_derivation_input = Vec::new();
376        key_derivation_input.extend_from_slice(ml_dsa_private_bytes);
377        key_derivation_input.extend_from_slice(b"ml-dsa-to-chacha20-key-derivation");
378
379        let derived_key_hash = blake3::hash(&key_derivation_input);
380        let chacha_key = Key::from_slice(derived_key_hash.as_bytes());
381        let cipher = ChaCha20Poly1305::new(chacha_key);
382
383        let nonce = Nonce::from_slice(&self.nonce);
384
385        cipher
386            .decrypt(nonce, self.ciphertext.as_ref())
387            .map_err(|e| {
388                CryptoError::DecryptionError(format!(
389                    "ML-DSA-derived ChaCha20 decryption failed: {e}"
390                ))
391            })
392    }
393}
394
395impl EphemeralEcdhContent {
396    /// Encrypt data using ephemeral X25519 ECDH  
397    /// Generates a random ephemeral key pair for each message
398    /// Anyone can encrypt for the recipient using only their Ed25519 public key
399    pub fn encrypt(
400        plaintext: &[u8],
401        recipient_ed25519_public: &ed25519_dalek::VerifyingKey,
402    ) -> std::result::Result<Self, CryptoError> {
403        use chacha20poly1305::aead::{Aead, OsRng};
404        use chacha20poly1305::{AeadCore, ChaCha20Poly1305, Key, KeyInit};
405
406        // Generate ephemeral X25519 key pair for this message
407        let ephemeral_private = x25519_dalek::StaticSecret::random_from_rng(OsRng);
408        let ephemeral_public = x25519_dalek::PublicKey::from(&ephemeral_private);
409
410        // For ephemeral ECDH, we need a consistent way to derive X25519 public key
411        // from Ed25519 public key. We'll use a deterministic derivation based on the Ed25519 public key bytes.
412        // This creates a "virtual" X25519 public key that will match what the recipient computes.
413        let recipient_x25519_public = {
414            // Use Ed25519 public key bytes as seed for deterministic X25519 public key derivation
415            let ed25519_bytes = recipient_ed25519_public.to_bytes();
416            // Hash the Ed25519 public key to create deterministic X25519 private key
417            let x25519_private_bytes = *blake3::hash(&ed25519_bytes).as_bytes();
418            let x25519_private = x25519_dalek::StaticSecret::from(x25519_private_bytes);
419            x25519_dalek::PublicKey::from(&x25519_private)
420        };
421
422        // Ephemeral ECDH: each message uses a unique ephemeral key pair for perfect forward secrecy
423
424        // Perform ECDH: ephemeral_private + recipient_public → shared secret
425        let shared_secret = ephemeral_private.diffie_hellman(&recipient_x25519_public);
426
427        // Derive ChaCha20 key from shared secret using Blake3
428        let mut key_derivation_input = Vec::new();
429        key_derivation_input.extend_from_slice(shared_secret.as_bytes());
430        key_derivation_input.extend_from_slice(b"ephemeral-ecdh-to-chacha20-key-derivation");
431
432        let derived_key_hash = blake3::hash(&key_derivation_input);
433        let chacha_key = Key::from_slice(derived_key_hash.as_bytes());
434        let cipher = ChaCha20Poly1305::new(chacha_key);
435
436        // Generate random nonce
437        let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
438
439        let ciphertext = cipher.encrypt(&nonce, plaintext).map_err(|e| {
440            CryptoError::EncryptionError(format!("Ephemeral ECDH ChaCha20 encryption failed: {e}"))
441        })?;
442
443        let mut nonce_bytes = [0u8; 12];
444        nonce_bytes.copy_from_slice(&nonce);
445
446        Ok(Self {
447            ciphertext,
448            nonce: nonce_bytes,
449            ephemeral_public: ephemeral_public.to_bytes(),
450        })
451    }
452
453    /// Decrypt data using ephemeral X25519 ECDH
454    /// Recipient uses their Ed25519 private key + stored ephemeral public key
455    pub fn decrypt(
456        &self,
457        recipient_ed25519_key: &ed25519_dalek::SigningKey,
458    ) -> std::result::Result<Vec<u8>, CryptoError> {
459        use chacha20poly1305::aead::Aead;
460        use chacha20poly1305::{ChaCha20Poly1305, Key, KeyInit, Nonce};
461
462        // Use the same deterministic derivation as encryption
463        // Derive X25519 private key from Ed25519 public key (deterministic)
464        let recipient_x25519_private = {
465            let ed25519_public = recipient_ed25519_key.verifying_key();
466            let ed25519_bytes = ed25519_public.to_bytes();
467            // Hash the Ed25519 public key to create deterministic X25519 private key (same as encryption)
468            let x25519_private_bytes = *blake3::hash(&ed25519_bytes).as_bytes();
469            x25519_dalek::StaticSecret::from(x25519_private_bytes)
470        };
471        let _recipient_x25519_public = x25519_dalek::PublicKey::from(&recipient_x25519_private);
472
473        // Extract ephemeral public key from message
474        let ephemeral_public = x25519_dalek::PublicKey::from(self.ephemeral_public);
475
476        // Use same deterministic X25519 derivation to compute shared secret
477
478        // Perform ECDH: recipient_private + ephemeral_public → shared secret (same as encryption)
479        let shared_secret = recipient_x25519_private.diffie_hellman(&ephemeral_public);
480
481        // Derive the same ChaCha20 key from shared secret
482        let mut key_derivation_input = Vec::new();
483        key_derivation_input.extend_from_slice(shared_secret.as_bytes());
484        key_derivation_input.extend_from_slice(b"ephemeral-ecdh-to-chacha20-key-derivation");
485
486        let derived_key_hash = blake3::hash(&key_derivation_input);
487        let chacha_key = Key::from_slice(derived_key_hash.as_bytes());
488        let cipher = ChaCha20Poly1305::new(chacha_key);
489
490        let nonce = Nonce::from_slice(&self.nonce);
491
492        cipher
493            .decrypt(nonce, self.ciphertext.as_ref())
494            .map_err(|e| {
495                CryptoError::DecryptionError(format!(
496                    "Ephemeral ECDH ChaCha20 decryption failed: {e}"
497                ))
498            })
499    }
500}
501
502/// Convert Ed25519 private key to X25519 private key
503/// Both curves use the same underlying Curve25519
504pub fn ed25519_to_x25519_private(
505    ed25519_key: &ed25519_dalek::SigningKey,
506) -> std::result::Result<X25519PrivateKey, CryptoError> {
507    // Ed25519 private key is the same as X25519 private key (both are 32-byte scalars)
508    let ed25519_bytes = ed25519_key.to_bytes();
509    Ok(X25519PrivateKey::from(ed25519_bytes))
510}
511
512/// Convert Ed25519 public key to X25519 public key
513/// Derives X25519 public key from the corresponding Ed25519 private key
514/// Note: This is a simplified approach that requires the private key
515pub fn ed25519_to_x25519_public(
516    ed25519_private_key: &ed25519_dalek::SigningKey,
517) -> std::result::Result<X25519PublicKey, CryptoError> {
518    // Convert Ed25519 private key to X25519 private key, then derive public
519    let x25519_private = ed25519_to_x25519_private(ed25519_private_key)?;
520    Ok(X25519PublicKey::from(&x25519_private))
521}
522
523/// Convert Ed25519 public key (VerifyingKey) to X25519 public key
524/// Uses curve25519-dalek's Edwards to Montgomery conversion to match
525/// the same conversion that happens in the private key derivation path
526pub fn ed25519_to_x25519_public_from_verifying_key(
527    ed25519_public: &ed25519_dalek::VerifyingKey,
528) -> std::result::Result<X25519PublicKey, CryptoError> {
529    // Use curve25519-dalek's conversion which should match the private key approach
530    use curve25519_dalek::edwards::CompressedEdwardsY;
531
532    let compressed_point = CompressedEdwardsY::from_slice(&ed25519_public.to_bytes())
533        .map_err(|_| CryptoError::ParseError("Invalid Ed25519 public key".to_string()))?;
534
535    let edwards_point = compressed_point.decompress().ok_or_else(|| {
536        CryptoError::ParseError("Cannot decompress Ed25519 public key".to_string())
537    })?;
538
539    let montgomery_point = edwards_point.to_montgomery();
540    Ok(X25519PublicKey::from(montgomery_point.to_bytes()))
541}
542
543impl EncryptionKey {
544    /// Generate a random encryption key
545    pub fn generate(timestamp: u64) -> Self {
546        let mut key = [0u8; 32];
547        let mut key_id = vec![0u8; 16];
548        thread_rng().fill_bytes(&mut key);
549        thread_rng().fill_bytes(&mut key_id);
550
551        Self {
552            key,
553            key_id,
554            created_at: timestamp,
555            derivation_info: None,
556        }
557    }
558
559    /// Derive an encryption key from a mnemonic phrase
560    pub fn from_mnemonic(
561        mnemonic: &MnemonicPhrase,
562        passphrase: &str,
563        context: &str, // e.g., "dga-group-key"
564        timestamp: u64,
565    ) -> std::result::Result<Self, CryptoError> {
566        // Generate a random salt
567        let mut salt = [0u8; 32];
568        thread_rng().fill_bytes(&mut salt);
569
570        Self::from_mnemonic_with_salt(mnemonic, passphrase, context, &salt, timestamp)
571    }
572
573    /// Derive an encryption key from a mnemonic phrase with specific salt (for key recovery)
574    pub fn from_mnemonic_with_salt(
575        mnemonic: &MnemonicPhrase,
576        passphrase: &str,
577        context: &str,
578        salt: &[u8; 32],
579        timestamp: u64,
580    ) -> std::result::Result<Self, CryptoError> {
581        // First get the BIP39 seed
582        let seed = mnemonic.to_seed(passphrase)?;
583
584        // Then use Argon2 to derive the actual encryption key
585        let argon2_params = Argon2Params::default();
586        let argon2 = Argon2::new(
587            argon2::Algorithm::Argon2id,
588            argon2::Version::V0x13,
589            argon2::Params::new(
590                argon2_params.memory,
591                argon2_params.iterations,
592                argon2_params.parallelism,
593                Some(32), // output length
594            )
595            .map_err(|e| CryptoError::KeyDerivationError(format!("Invalid Argon2 params: {e}")))?,
596        );
597
598        // Combine seed with context for key derivation
599        let mut input = Vec::new();
600        input.extend_from_slice(&seed);
601        input.extend_from_slice(context.as_bytes());
602
603        // Create salt for argon2 - use first 16 bytes encoded as base64 without padding
604        use base64::Engine;
605        let salt_bytes = &salt[..16]; // argon2 salt should be 16 bytes
606        let salt_b64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(salt_bytes);
607        let salt_ref = argon2::password_hash::Salt::from_b64(&salt_b64)
608            .map_err(|e| CryptoError::KeyDerivationError(format!("Salt error: {e}")))?;
609
610        let password_hash = argon2
611            .hash_password(&input, salt_ref)
612            .map_err(|e| CryptoError::KeyDerivationError(format!("Key derivation failed: {e}")))?;
613
614        // Extract the key bytes
615        let mut key = [0u8; 32];
616        let hash = password_hash.hash.unwrap();
617        let hash_bytes = hash.as_bytes();
618        key.copy_from_slice(&hash_bytes[..32]);
619
620        // Generate key ID from the derivation parameters
621        let mut key_id_input = Vec::new();
622        key_id_input.extend_from_slice(salt);
623        key_id_input.extend_from_slice(context.as_bytes());
624        let key_id = blake3::hash(&key_id_input).as_bytes()[..16].to_vec();
625
626        Ok(Self {
627            key,
628            key_id,
629            created_at: timestamp,
630            derivation_info: Some(KeyDerivationInfo {
631                method: KeyDerivationMethod::Bip39Argon2,
632                salt: salt.to_vec(),
633                argon2_params,
634                context: context.to_string(),
635            }),
636        })
637    }
638
639    /// Encrypt data to minimal ChaCha20Poly1305Content (no key_id for wire protocol)
640    pub fn encrypt_content(
641        &self,
642        plaintext: &[u8],
643    ) -> std::result::Result<ChaCha20Poly1305Content, CryptoError> {
644        let key = Key::from_slice(&self.key);
645        let cipher = ChaCha20Poly1305::new(key);
646
647        // Generate random nonce
648        let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
649
650        let ciphertext = cipher.encrypt(&nonce, plaintext).map_err(|e| {
651            CryptoError::EncryptionError(format!("ChaCha20 encryption failed: {e}"))
652        })?;
653
654        let mut nonce_bytes = [0u8; 12];
655        nonce_bytes.copy_from_slice(&nonce);
656
657        Ok(ChaCha20Poly1305Content {
658            ciphertext,
659            nonce: nonce_bytes,
660        })
661    }
662
663    /// Decrypt ChaCha20Poly1305Content (assumes correct key based on channel context)
664    pub fn decrypt_content(
665        &self,
666        content: &ChaCha20Poly1305Content,
667    ) -> std::result::Result<Vec<u8>, CryptoError> {
668        let key = Key::from_slice(&self.key);
669        let cipher = ChaCha20Poly1305::new(key);
670        let nonce = Nonce::from_slice(&content.nonce);
671
672        cipher
673            .decrypt(nonce, content.ciphertext.as_ref())
674            .map_err(|e| CryptoError::DecryptionError(format!("ChaCha20 decryption failed: {e}")))
675    }
676}
677
678/// Generate an ed25519 signing key from a mnemonic phrase
679pub fn generate_ed25519_from_mnemonic(
680    mnemonic: &MnemonicPhrase,
681    passphrase: &str,
682    context: &str, // e.g., "ed25519-signing-key"
683) -> std::result::Result<ed25519_dalek::SigningKey, CryptoError> {
684    // Get the BIP39 seed
685    let seed = mnemonic.to_seed(passphrase)?;
686
687    // Use Blake3 to derive ed25519 key material from seed + context
688    let mut input = Vec::new();
689    input.extend_from_slice(&seed);
690    input.extend_from_slice(context.as_bytes());
691
692    let key_material = blake3::hash(&input);
693    let key_bytes = key_material.as_bytes();
694
695    // ed25519 keys are 32 bytes - SigningKey::from_bytes doesn't return Result
696    Ok(ed25519_dalek::SigningKey::from_bytes(key_bytes))
697}
698
699/// Recover an ed25519 signing key from a mnemonic phrase (deterministic)
700pub fn recover_ed25519_from_mnemonic(
701    mnemonic: &MnemonicPhrase,
702    passphrase: &str,
703    context: &str,
704) -> std::result::Result<ed25519_dalek::SigningKey, CryptoError> {
705    // Same as generate - it's deterministic
706    generate_ed25519_from_mnemonic(mnemonic, passphrase, context)
707}
708
709/// Generate an ML-DSA signing key from a mnemonic phrase
710pub fn generate_ml_dsa_from_mnemonic(
711    mnemonic: &MnemonicPhrase,
712    passphrase: &str,
713    context: &str, // e.g., "ml-dsa-signing-key"
714) -> std::result::Result<MLDSA65SigningKey, CryptoError> {
715    // Get the BIP39 seed
716    let seed = mnemonic.to_seed(passphrase)?;
717
718    // Use Blake3 to derive ML-DSA key material from seed + context
719    let mut input = Vec::new();
720    input.extend_from_slice(&seed);
721    input.extend_from_slice(context.as_bytes());
722
723    let key_material = blake3::hash(&input);
724
725    // ML-DSA keys need more entropy than 32 bytes, so we expand using Blake3
726    let mut expanded_seed = [0u8; 64]; // Use 64 bytes for better entropy
727    let mut hasher = blake3::Hasher::new();
728    hasher.update(key_material.as_bytes());
729    hasher.update(b"ml-dsa-key-expansion");
730    let expanded_hash = hasher.finalize();
731    expanded_seed[..32].copy_from_slice(expanded_hash.as_bytes());
732
733    // Create second hash for remaining bytes
734    let mut hasher2 = blake3::Hasher::new();
735    hasher2.update(expanded_hash.as_bytes());
736    hasher2.update(b"ml-dsa-key-expansion-2");
737    let second_hash = hasher2.finalize();
738    expanded_seed[32..].copy_from_slice(&second_hash.as_bytes()[..32]);
739
740    // Generate ML-DSA key from expanded seed
741    use libcrux_ml_dsa::{ml_dsa_65, KEY_GENERATION_RANDOMNESS_SIZE};
742    use rand::RngCore;
743    // ChaCha20Rng expects 32 bytes, so use the first 32 bytes
744    let mut seed_32 = [0u8; 32];
745    seed_32.copy_from_slice(&expanded_seed[..32]);
746    let mut rng = rand_chacha::ChaCha20Rng::from_seed(seed_32);
747    let mut randomness = [0u8; KEY_GENERATION_RANDOMNESS_SIZE];
748    rng.fill_bytes(&mut randomness);
749    let keypair = ml_dsa_65::portable::generate_key_pair(randomness);
750    Ok(keypair.signing_key)
751}
752
753/// Recover an ML-DSA signing key from a mnemonic phrase (deterministic)
754pub fn recover_ml_dsa_from_mnemonic(
755    mnemonic: &MnemonicPhrase,
756    passphrase: &str,
757    context: &str,
758) -> std::result::Result<MLDSA65SigningKey, CryptoError> {
759    // Same as generate - it's deterministic
760    generate_ml_dsa_from_mnemonic(mnemonic, passphrase, context)
761}
762
763#[cfg(test)]
764mod tests {
765    use super::*;
766
767    #[test]
768    fn test_ephemeral_ecdh_encrypt_decrypt_roundtrip() {
769        // Test the new ephemeral ECDH pattern used in RPC transport:
770        // Anyone can encrypt for recipient using only their Ed25519 public key
771        // Recipient decrypts using their Ed25519 private key
772
773        let plaintext = b"Hello, Ephemeral ECDH World!";
774
775        // Create recipient Ed25519 key pair (sender doesn't need long-term keys!)
776        let recipient_ed25519_key = ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng);
777        let recipient_ed25519_public = recipient_ed25519_key.verifying_key();
778
779        // Encrypt using only recipient's public key (ephemeral key generated automatically)
780        let encrypted = EphemeralEcdhContent::encrypt(plaintext, &recipient_ed25519_public)
781            .expect("Encryption should succeed");
782
783        // Decrypt using recipient's private key
784        let decrypted = encrypted
785            .decrypt(&recipient_ed25519_key)
786            .expect("Decryption should succeed");
787
788        // Verify roundtrip
789        assert_eq!(
790            plaintext,
791            decrypted.as_slice(),
792            "Roundtrip failed: plaintext != decrypted"
793        );
794    }
795
796    #[test]
797    fn test_mnemonic_generation() {
798        let mnemonic = MnemonicPhrase::generate().unwrap();
799        // Should be 24 words
800        assert_eq!(mnemonic.phrase().split_whitespace().count(), 24);
801    }
802
803    #[test]
804    fn test_chacha20_content_encryption_roundtrip() {
805        let key = EncryptionKey::generate(1640995200);
806        let plaintext = b"Hello, encrypted world!";
807
808        let encrypted = key.encrypt_content(plaintext).unwrap();
809        let decrypted = key.decrypt_content(&encrypted).unwrap();
810
811        assert_eq!(plaintext, decrypted.as_slice());
812        assert_eq!(encrypted.nonce.len(), 12);
813    }
814
815    #[test]
816    fn test_encryption_key_from_mnemonic() {
817        let mnemonic = MnemonicPhrase::from_phrase(
818            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
819            Language::English
820        ).unwrap();
821
822        let key =
823            EncryptionKey::from_mnemonic(&mnemonic, "test passphrase", "test-context", 1640995200)
824                .unwrap();
825
826        assert!(key.derivation_info.is_some());
827        assert_eq!(
828            key.derivation_info.as_ref().unwrap().context,
829            "test-context"
830        );
831    }
832
833    #[test]
834    fn test_key_derivation_method_as_str() {
835        assert_eq!(KeyDerivationMethod::Bip39Argon2.as_str(), "bip39+argon2");
836        assert_eq!(
837            KeyDerivationMethod::ChaCha20Poly1305Keygen.as_str(),
838            "chacha20-poly1305-keygen"
839        );
840    }
841
842    #[test]
843    fn test_key_derivation_method_display() {
844        assert_eq!(KeyDerivationMethod::Bip39Argon2.to_string(), "bip39+argon2");
845        assert_eq!(
846            KeyDerivationMethod::ChaCha20Poly1305Keygen.to_string(),
847            "chacha20-poly1305-keygen"
848        );
849    }
850
851    #[test]
852    fn test_key_derivation_method_from_str() {
853        use std::str::FromStr;
854        assert_eq!(
855            KeyDerivationMethod::from_str("bip39+argon2"),
856            Ok(KeyDerivationMethod::Bip39Argon2)
857        );
858        assert_eq!(
859            KeyDerivationMethod::from_str("chacha20-poly1305-keygen"),
860            Ok(KeyDerivationMethod::ChaCha20Poly1305Keygen)
861        );
862        assert!(KeyDerivationMethod::from_str("unknown").is_err());
863        assert!(KeyDerivationMethod::from_str("").is_err());
864    }
865
866    #[test]
867    fn test_key_derivation_method_round_trip() {
868        use std::str::FromStr;
869        let methods = [
870            KeyDerivationMethod::Bip39Argon2,
871            KeyDerivationMethod::ChaCha20Poly1305Keygen,
872        ];
873
874        for method in methods {
875            let as_str = method.as_str();
876            let parsed = KeyDerivationMethod::from_str(as_str).expect("Should parse back");
877            assert_eq!(method, parsed);
878        }
879    }
880
881    #[test]
882    fn test_key_derivation_info_with_enum() {
883        let derivation_info = KeyDerivationInfo {
884            method: KeyDerivationMethod::Bip39Argon2,
885            salt: vec![1, 2, 3, 4],
886            argon2_params: Argon2Params::default(),
887            context: "test-context".to_string(),
888        };
889
890        assert_eq!(derivation_info.method, KeyDerivationMethod::Bip39Argon2);
891        assert_eq!(derivation_info.method.as_str(), "bip39+argon2");
892        assert_eq!(derivation_info.context, "test-context");
893    }
894
895    #[test]
896    fn test_postcard_serialization_key_derivation_method() {
897        for method in [
898            KeyDerivationMethod::Bip39Argon2,
899            KeyDerivationMethod::ChaCha20Poly1305Keygen,
900        ] {
901            let serialized = postcard::to_stdvec(&method).expect("Failed to serialize");
902            let deserialized: KeyDerivationMethod =
903                postcard::from_bytes(&serialized).expect("Failed to deserialize");
904            assert_eq!(method, deserialized);
905        }
906    }
907
908    #[test]
909    fn test_postcard_serialization_key_derivation_info() {
910        let derivation_info = KeyDerivationInfo {
911            method: KeyDerivationMethod::Bip39Argon2,
912            salt: vec![1, 2, 3, 4, 5, 6, 7, 8],
913            argon2_params: Argon2Params {
914                memory: 65536,
915                iterations: 3,
916                parallelism: 4,
917            },
918            context: "dga-group-key".to_string(),
919        };
920
921        let serialized = postcard::to_stdvec(&derivation_info).expect("Failed to serialize");
922        let deserialized: KeyDerivationInfo =
923            postcard::from_bytes(&serialized).expect("Failed to deserialize");
924        assert_eq!(derivation_info, deserialized);
925    }
926}