1use 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
20use 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#[derive(Debug, Clone)]
59pub struct MnemonicPhrase {
60 pub phrase: String,
61 pub language: Language,
62}
63
64impl MnemonicPhrase {
65 pub fn generate() -> std::result::Result<Self, CryptoError> {
67 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 pub fn from_phrase(phrase: &str, language: Language) -> std::result::Result<Self, CryptoError> {
82 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 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 pub fn phrase(&self) -> &str {
102 &self.phrase
103 }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
108pub enum KeyDerivationMethod {
109 Bip39Argon2,
114
115 ChaCha20Poly1305Keygen,
120}
121
122impl KeyDerivationMethod {
123 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
155pub struct KeyDerivationInfo {
156 pub method: KeyDerivationMethod,
158 pub salt: Vec<u8>,
160 pub argon2_params: Argon2Params,
162 pub context: String, }
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, iterations: 3, parallelism: 4, }
180 }
181}
182
183#[cfg_attr(feature = "frb-api", frb(opaque))]
185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
186pub struct EncryptionKey {
187 pub key: [u8; 32],
189 pub key_id: Vec<u8>,
191 pub created_at: u64,
193 pub derivation_info: Option<KeyDerivationInfo>,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
200pub struct ChaCha20Poly1305Content {
201 pub ciphertext: Vec<u8>,
203 pub nonce: [u8; 12],
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
212pub struct Ed25519SelfEncryptedContent {
213 pub ciphertext: Vec<u8>,
215 pub nonce: [u8; 12],
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
223pub struct EphemeralEcdhContent {
224 pub ciphertext: Vec<u8>,
226 pub nonce: [u8; 12],
228 pub ephemeral_public: [u8; 32],
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
237pub enum PqxdhEncryptedContent {
238 Initial(crate::inbox::pqxdh::PqxdhInitialMessage),
241
242 Session(crate::inbox::pqxdh::PqxdhSessionMessage),
245}
246
247impl Ed25519SelfEncryptedContent {
248 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 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
320pub struct MlDsaSelfEncryptedContent {
321 pub ciphertext: Vec<u8>,
323 pub nonce: [u8; 12],
325}
326
327impl MlDsaSelfEncryptedContent {
328 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 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 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 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 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 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 let ephemeral_private = x25519_dalek::StaticSecret::random_from_rng(OsRng);
408 let ephemeral_public = x25519_dalek::PublicKey::from(&ephemeral_private);
409
410 let recipient_x25519_public = {
414 let ed25519_bytes = recipient_ed25519_public.to_bytes();
416 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 let shared_secret = ephemeral_private.diffie_hellman(&recipient_x25519_public);
426
427 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 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 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 let recipient_x25519_private = {
465 let ed25519_public = recipient_ed25519_key.verifying_key();
466 let ed25519_bytes = ed25519_public.to_bytes();
467 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 let ephemeral_public = x25519_dalek::PublicKey::from(self.ephemeral_public);
475
476 let shared_secret = recipient_x25519_private.diffie_hellman(&ephemeral_public);
480
481 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
502pub fn ed25519_to_x25519_private(
505 ed25519_key: &ed25519_dalek::SigningKey,
506) -> std::result::Result<X25519PrivateKey, CryptoError> {
507 let ed25519_bytes = ed25519_key.to_bytes();
509 Ok(X25519PrivateKey::from(ed25519_bytes))
510}
511
512pub fn ed25519_to_x25519_public(
516 ed25519_private_key: &ed25519_dalek::SigningKey,
517) -> std::result::Result<X25519PublicKey, CryptoError> {
518 let x25519_private = ed25519_to_x25519_private(ed25519_private_key)?;
520 Ok(X25519PublicKey::from(&x25519_private))
521}
522
523pub fn ed25519_to_x25519_public_from_verifying_key(
527 ed25519_public: &ed25519_dalek::VerifyingKey,
528) -> std::result::Result<X25519PublicKey, CryptoError> {
529 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 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 pub fn from_mnemonic(
561 mnemonic: &MnemonicPhrase,
562 passphrase: &str,
563 context: &str, timestamp: u64,
565 ) -> std::result::Result<Self, CryptoError> {
566 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 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 let seed = mnemonic.to_seed(passphrase)?;
583
584 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), )
595 .map_err(|e| CryptoError::KeyDerivationError(format!("Invalid Argon2 params: {e}")))?,
596 );
597
598 let mut input = Vec::new();
600 input.extend_from_slice(&seed);
601 input.extend_from_slice(context.as_bytes());
602
603 use base64::Engine;
605 let salt_bytes = &salt[..16]; 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 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 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 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 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 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
678pub fn generate_ed25519_from_mnemonic(
680 mnemonic: &MnemonicPhrase,
681 passphrase: &str,
682 context: &str, ) -> std::result::Result<ed25519_dalek::SigningKey, CryptoError> {
684 let seed = mnemonic.to_seed(passphrase)?;
686
687 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 Ok(ed25519_dalek::SigningKey::from_bytes(key_bytes))
697}
698
699pub fn recover_ed25519_from_mnemonic(
701 mnemonic: &MnemonicPhrase,
702 passphrase: &str,
703 context: &str,
704) -> std::result::Result<ed25519_dalek::SigningKey, CryptoError> {
705 generate_ed25519_from_mnemonic(mnemonic, passphrase, context)
707}
708
709pub fn generate_ml_dsa_from_mnemonic(
711 mnemonic: &MnemonicPhrase,
712 passphrase: &str,
713 context: &str, ) -> std::result::Result<MLDSA65SigningKey, CryptoError> {
715 let seed = mnemonic.to_seed(passphrase)?;
717
718 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 let mut expanded_seed = [0u8; 64]; 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 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 use libcrux_ml_dsa::{ml_dsa_65, KEY_GENERATION_RANDOMNESS_SIZE};
742 use rand::RngCore;
743 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
753pub fn recover_ml_dsa_from_mnemonic(
755 mnemonic: &MnemonicPhrase,
756 passphrase: &str,
757 context: &str,
758) -> std::result::Result<MLDSA65SigningKey, CryptoError> {
759 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 let plaintext = b"Hello, Ephemeral ECDH World!";
774
775 let recipient_ed25519_key = ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng);
777 let recipient_ed25519_public = recipient_ed25519_key.verifying_key();
778
779 let encrypted = EphemeralEcdhContent::encrypt(plaintext, &recipient_ed25519_public)
781 .expect("Encryption should succeed");
782
783 let decrypted = encrypted
785 .decrypt(&recipient_ed25519_key)
786 .expect("Decryption should succeed");
787
788 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 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}