1use crate::error::{Result, WalletError};
8use qp_rusty_crystals_dilithium::ml_dsa_87::{Keypair, PublicKey, SecretKey};
9#[cfg(test)]
10use qp_rusty_crystals_hdwallet::SensitiveBytes32;
11use serde::{Deserialize, Serialize};
12#[cfg(test)]
13use sp_core::crypto::Ss58AddressFormat;
14use sp_core::{
15 crypto::{AccountId32, Ss58Codec},
16 ByteArray,
17};
18use aes_gcm::{
20 aead::{Aead, AeadCore, KeyInit, OsRng as AesOsRng},
21 Aes256Gcm, Key, Nonce,
22};
23use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
24use rand::{rng, RngCore};
25
26use std::path::Path;
27
28use qp_dilithium_crypto::types::{DilithiumPair, DilithiumPublic};
29use sp_runtime::traits::IdentifyAccount;
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct QuantumKeyPair {
34 pub public_key: Vec<u8>,
35 pub private_key: Vec<u8>,
36}
37
38impl QuantumKeyPair {
39 pub fn from_dilithium_keypair(keypair: &Keypair) -> Self {
41 Self {
42 public_key: keypair.public.to_bytes().to_vec(),
43 private_key: keypair.secret.to_bytes().to_vec(),
44 }
45 }
46
47 #[allow(dead_code)]
49 pub fn to_dilithium_keypair(&self) -> Result<Keypair> {
50 Ok(Keypair {
54 public: PublicKey::from_bytes(&self.public_key).expect("Failed to parse public key"),
55 secret: SecretKey::from_bytes(&self.private_key).expect("Failed to parse private key"),
56 })
57 }
58
59 pub fn to_resonance_pair(&self) -> Result<DilithiumPair> {
61 Ok(DilithiumPair::from_raw(&self.public_key, &self.private_key)
63 .map_err(|_| crate::error::WalletError::KeyGeneration)?)
64 }
65
66 pub fn from_resonance_pair(keypair: &DilithiumPair) -> Self {
67 use sp_core::Pair;
68 Self {
69 public_key: keypair.public().as_ref().to_vec(),
70 private_key: keypair.secret_bytes().to_vec(),
71 }
72 }
73
74 pub fn to_account_id_32(&self) -> AccountId32 {
75 let resonance_public =
77 DilithiumPublic::from_slice(&self.public_key).expect("Invalid public key");
78 resonance_public.into_account()
79 }
80
81 pub fn to_account_id_ss58check(&self) -> String {
82 use crate::cli::address_format::quantus_ss58_format;
83 let account = self.to_account_id_32();
84 account.to_ss58check_with_version(quantus_ss58_format())
85 }
86
87 pub fn to_subxt_signer(&self) -> Result<qp_dilithium_crypto::types::DilithiumPair> {
89 let resonance_pair = self.to_resonance_pair()?;
91
92 Ok(resonance_pair)
93 }
94
95 #[allow(dead_code)]
96 pub fn ss58_to_account_id(s: &str) -> Vec<u8> {
97 AsRef::<[u8]>::as_ref(&AccountId32::from_ss58check_with_version(s).unwrap().0).to_vec()
101 }
102}
103
104#[derive(Debug, Serialize, Deserialize)]
106pub struct EncryptedWallet {
107 pub name: String,
108 pub address: String, pub encrypted_data: Vec<u8>,
110 pub kyber_ciphertext: Vec<u8>, pub kyber_public_key: Vec<u8>, pub argon2_salt: Vec<u8>, pub argon2_params: String, pub aes_nonce: Vec<u8>, pub encryption_version: u32, pub created_at: chrono::DateTime<chrono::Utc>,
117}
118
119#[derive(Debug, Serialize, Deserialize)]
121pub struct WalletData {
122 pub name: String,
123 pub keypair: QuantumKeyPair,
124 pub mnemonic: Option<String>,
125 pub derivation_path: String,
126 pub metadata: std::collections::HashMap<String, String>,
127}
128
129pub struct Keystore {
131 storage_path: std::path::PathBuf,
132}
133
134impl Keystore {
135 pub fn new<P: AsRef<Path>>(storage_path: P) -> Self {
137 Self { storage_path: storage_path.as_ref().to_path_buf() }
138 }
139
140 pub fn save_wallet(&self, wallet: &EncryptedWallet) -> Result<()> {
142 let wallet_file = self.storage_path.join(format!("{}.json", wallet.name));
143 let wallet_json = serde_json::to_string_pretty(wallet)?;
144 std::fs::write(wallet_file, wallet_json)?;
145 Ok(())
146 }
147
148 pub fn load_wallet(&self, name: &str) -> Result<Option<EncryptedWallet>> {
150 let wallet_file = self.storage_path.join(format!("{name}.json"));
151
152 if !wallet_file.exists() {
153 return Ok(None);
154 }
155
156 let wallet_json = std::fs::read_to_string(wallet_file)?;
157 let wallet: EncryptedWallet = serde_json::from_str(&wallet_json)?;
158 Ok(Some(wallet))
159 }
160
161 pub fn list_wallets(&self) -> Result<Vec<String>> {
163 let mut wallets = Vec::new();
164
165 if !self.storage_path.exists() {
166 return Ok(wallets);
167 }
168
169 for entry in std::fs::read_dir(&self.storage_path)? {
170 let entry = entry?;
171 let path = entry.path();
172
173 if path.extension().and_then(|s| s.to_str()) == Some("json") {
174 if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
175 wallets.push(name.to_string());
176 }
177 }
178 }
179
180 Ok(wallets)
181 }
182
183 pub fn delete_wallet(&self, name: &str) -> Result<bool> {
185 let wallet_file = self.storage_path.join(format!("{name}.json"));
186
187 if wallet_file.exists() {
188 std::fs::remove_file(wallet_file)?;
189 Ok(true)
190 } else {
191 Ok(false)
192 }
193 }
194
195 pub fn encrypt_wallet_data(
198 &self,
199 data: &WalletData,
200 password: &str,
201 ) -> Result<EncryptedWallet> {
202 let mut argon2_salt = [0u8; 16];
204 rng().fill_bytes(&mut argon2_salt);
205
206 let argon2 = Argon2::default();
208 let salt_string = argon2::password_hash::SaltString::encode_b64(&argon2_salt)
209 .map_err(|e| WalletError::Encryption(e.to_string()))?;
210 let password_hash = argon2
211 .hash_password(password.as_bytes(), &salt_string)
212 .map_err(|e| WalletError::Encryption(e.to_string()))?;
213
214 let hash_bytes = password_hash.hash.as_ref().unwrap().as_bytes();
216 let aes_key = Key::<Aes256Gcm>::from(<[u8; 32]>::try_from(&hash_bytes[..32]).unwrap());
217 let cipher = Aes256Gcm::new(&aes_key);
218
219 let nonce = Aes256Gcm::generate_nonce(&mut AesOsRng);
221 let serialized_data = serde_json::to_vec(data)?;
222 let encrypted_data = cipher
223 .encrypt(&nonce, serialized_data.as_ref())
224 .map_err(|e| WalletError::Encryption(e.to_string()))?;
225
226 Ok(EncryptedWallet {
227 name: data.name.clone(),
228 address: data.keypair.to_account_id_ss58check(), encrypted_data,
230 kyber_ciphertext: vec![], kyber_public_key: vec![], argon2_salt: argon2_salt.to_vec(),
233 argon2_params: password_hash.to_string(),
234 aes_nonce: nonce.to_vec(),
235 encryption_version: 1, created_at: chrono::Utc::now(),
237 })
238 }
239
240 pub fn decrypt_wallet_data(
242 &self,
243 encrypted: &EncryptedWallet,
244 password: &str,
245 ) -> Result<WalletData> {
246 let argon2 = Argon2::default();
248 let password_hash = PasswordHash::new(&encrypted.argon2_params)
249 .map_err(|_| WalletError::InvalidPassword)?;
250
251 argon2
252 .verify_password(password.as_bytes(), &password_hash)
253 .map_err(|_| WalletError::InvalidPassword)?;
254
255 let hash_bytes = password_hash.hash.as_ref().unwrap().as_bytes();
257 let aes_key = Key::<Aes256Gcm>::from(<[u8; 32]>::try_from(&hash_bytes[..32]).unwrap());
258 let cipher = Aes256Gcm::new(&aes_key);
259
260 let nonce = Nonce::from(<[u8; 12]>::try_from(&encrypted.aes_nonce[..]).unwrap());
262 let decrypted_data = cipher
263 .decrypt(&nonce, encrypted.encrypted_data.as_ref())
264 .map_err(|_| WalletError::Decryption)?;
265
266 let wallet_data: WalletData = serde_json::from_slice(&decrypted_data)?;
268
269 Ok(wallet_data)
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use qp_dilithium_crypto::{crystal_alice, crystal_charlie, dilithium_bob};
277 use qp_rusty_crystals_dilithium::ml_dsa_87::Keypair;
278 use sp_core::Pair;
279 use tempfile::TempDir;
280
281 #[test]
282 fn test_quantum_keypair_from_dilithium_keypair() {
283 let mut entropy = [1u8; 32];
285 let dilithium_keypair = Keypair::generate(SensitiveBytes32::from(&mut entropy));
286
287 let quantum_keypair = QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
289
290 assert_eq!(quantum_keypair.public_key, dilithium_keypair.public.to_bytes().to_vec());
292 assert_eq!(quantum_keypair.private_key, dilithium_keypair.secret.to_bytes().to_vec());
293 }
294
295 #[test]
296 fn test_quantum_keypair_to_dilithium_keypair_roundtrip() {
297 let mut entropy = [2u8; 32];
299 let original_keypair = Keypair::generate(SensitiveBytes32::from(&mut entropy));
300
301 let quantum_keypair = QuantumKeyPair::from_dilithium_keypair(&original_keypair);
303 let converted_keypair =
304 quantum_keypair.to_dilithium_keypair().expect("Conversion should succeed");
305
306 assert_eq!(original_keypair.public.to_bytes(), converted_keypair.public.to_bytes());
308 assert_eq!(original_keypair.secret.to_bytes(), converted_keypair.secret.to_bytes());
309 }
310
311 #[test]
312 fn test_quantum_keypair_from_resonance_pair() {
313 let resonance_pair = crystal_alice();
315 let quantum_keypair = QuantumKeyPair::from_resonance_pair(&resonance_pair);
316
317 assert_eq!(quantum_keypair.public_key, resonance_pair.public().as_ref().to_vec());
319 assert_eq!(quantum_keypair.private_key.as_slice(), resonance_pair.secret_bytes());
320 }
321
322 #[test]
323 fn test_quantum_keypair_to_resonance_pair_roundtrip() {
324 let original_pair = dilithium_bob();
326 let quantum_keypair = QuantumKeyPair::from_resonance_pair(&original_pair);
327 let converted_pair =
328 quantum_keypair.to_resonance_pair().expect("Conversion should succeed");
329
330 assert_eq!(original_pair.public().as_ref(), converted_pair.public().as_ref());
332 assert_eq!(original_pair.secret_bytes(), converted_pair.secret_bytes());
333 }
334
335 #[test]
338 fn test_quantum_keypair_address_generation() {
339 sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));
340 let test_pairs = vec![
342 ("crystal_alice", crystal_alice()),
343 ("crystal_bob", dilithium_bob()),
344 ("crystal_charlie", crystal_charlie()),
345 ];
346
347 for (name, resonance_pair) in test_pairs {
348 let quantum_keypair = QuantumKeyPair::from_resonance_pair(&resonance_pair);
349
350 let account_id = quantum_keypair.to_account_id_32();
352 let ss58_address = quantum_keypair.to_account_id_ss58check();
353
354 assert!(
356 ss58_address.starts_with("qz"),
357 "SS58 address for {name} should start with qz (Quantus prefix 189)"
358 );
359 assert!(
360 ss58_address.len() >= 47,
361 "SS58 address for {name} should be at least 47 characters"
362 );
363
364 use crate::cli::address_format::quantus_ss58_format;
366 assert_eq!(
367 account_id.to_ss58check_with_version(quantus_ss58_format()),
368 ss58_address,
369 "Address methods should be consistent for {name}"
370 );
371
372 let chain_expected_address = resonance_pair
375 .public()
376 .into_account()
377 .to_ss58check_with_version(quantus_ss58_format());
378 assert_eq!(
379 ss58_address, chain_expected_address,
380 "Wallet address for {name} must match chain dev account (same derivation and SS58 189)"
381 );
382 }
383 }
384
385 #[test]
386 fn test_ss58_to_account_id_conversion() {
387 sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));
388 use crate::cli::address_format::quantus_ss58_format;
390 let test_cases = vec![
391 crystal_alice()
392 .public()
393 .into_account()
394 .to_ss58check_with_version(quantus_ss58_format()),
395 dilithium_bob()
396 .public()
397 .into_account()
398 .to_ss58check_with_version(quantus_ss58_format()),
399 crystal_charlie()
400 .public()
401 .into_account()
402 .to_ss58check_with_version(quantus_ss58_format()),
403 ];
404
405 for ss58_address in test_cases {
406 let account_bytes = QuantumKeyPair::ss58_to_account_id(&ss58_address);
408
409 assert_eq!(account_bytes.len(), 32, "Account ID should be 32 bytes");
411
412 let account_id =
414 AccountId32::from_slice(&account_bytes).expect("Should create valid AccountId32");
415 let round_trip_address =
416 account_id.to_ss58check_with_version(Ss58AddressFormat::custom(189));
417 assert_eq!(
418 ss58_address, round_trip_address,
419 "Round-trip conversion should preserve address"
420 );
421 }
422 }
423
424 #[test]
425 fn test_address_consistency_across_conversions() {
426 sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));
428
429 let mut entropy = [3u8; 32];
430 let dilithium_keypair = Keypair::generate(SensitiveBytes32::from(&mut entropy));
431
432 let quantum_from_dilithium = QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
434 let resonance_from_quantum =
435 quantum_from_dilithium.to_resonance_pair().expect("Should convert");
436 let quantum_from_resonance = QuantumKeyPair::from_resonance_pair(&resonance_from_quantum);
437
438 let addr1 = quantum_from_dilithium.to_account_id_ss58check();
440 let addr2 = quantum_from_resonance.to_account_id_ss58check();
441 let addr3 = resonance_from_quantum
442 .public()
443 .into_account()
444 .to_ss58check_with_version(Ss58AddressFormat::custom(189));
445
446 assert_eq!(addr1, addr2, "Addresses should be consistent across conversion paths");
447 assert_eq!(addr2, addr3, "Address should match direct DilithiumPair calculation");
448 }
449
450 #[test]
451 fn test_known_test_wallet_addresses() {
452 sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));
454 let alice_pair = crystal_alice();
455 let bob_pair = dilithium_bob();
456 let charlie_pair = crystal_charlie();
457
458 let alice_quantum = QuantumKeyPair::from_resonance_pair(&alice_pair);
459 let bob_quantum = QuantumKeyPair::from_resonance_pair(&bob_pair);
460 let charlie_quantum = QuantumKeyPair::from_resonance_pair(&charlie_pair);
461
462 let alice_addr = alice_quantum.to_account_id_ss58check();
463 let bob_addr = bob_quantum.to_account_id_ss58check();
464 let charlie_addr = charlie_quantum.to_account_id_ss58check();
465
466 assert_ne!(alice_addr, bob_addr, "Alice and Bob should have different addresses");
468 assert_ne!(bob_addr, charlie_addr, "Bob and Charlie should have different addresses");
469 assert_ne!(alice_addr, charlie_addr, "Alice and Charlie should have different addresses");
470
471 assert!(alice_addr.starts_with("qz"), "Alice address should be valid SS58");
473 assert!(bob_addr.starts_with("qz"), "Bob address should be valid SS58");
474 assert!(charlie_addr.starts_with("qz"), "Charlie address should be valid SS58");
475
476 println!("Test wallet addresses:");
477 println!(" Alice: {alice_addr}");
478 println!(" Bob: {bob_addr}");
479 println!(" Charlie: {charlie_addr}");
480 }
481
482 #[test]
483 fn test_invalid_ss58_address_handling() {
484 let invalid_addresses = vec![
486 "invalid",
487 "5", "1234567890", "", ];
491
492 for invalid_addr in invalid_addresses {
493 let result =
494 std::panic::catch_unwind(|| QuantumKeyPair::ss58_to_account_id(invalid_addr));
495 assert!(result.is_err(), "Should panic on invalid address: {invalid_addr}");
496 }
497 }
498
499 #[test]
500 fn test_stored_wallet_address_generation() {
501 sp_core::crypto::set_default_ss58_version(sp_core::crypto::Ss58AddressFormat::custom(189));
502
503 let alice_pair = crystal_alice();
508 let quantum_keypair = QuantumKeyPair::from_resonance_pair(&alice_pair);
509
510 let mut metadata = std::collections::HashMap::new();
512 metadata.insert("version".to_string(), "1.0.0".to_string());
513 metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
514 metadata.insert("test_wallet".to_string(), "true".to_string());
515
516 let wallet_data = WalletData {
517 name: "test_crystal_alice".to_string(),
518 keypair: quantum_keypair.clone(),
519 mnemonic: None,
520 derivation_path: "m/".to_string(),
521 metadata,
522 };
523
524 let result = std::panic::catch_unwind(|| wallet_data.keypair.to_account_id_ss58check());
526
527 match result {
528 Ok(address) => {
529 println!("✅ Address generation successful: {address}");
530 let expected = alice_pair
532 .public()
533 .into_account()
534 .to_ss58check_with_version(Ss58AddressFormat::custom(189));
535 assert_eq!(address, expected, "Stored wallet should generate correct address");
536 },
537 Err(_) => {
538 panic!("❌ Address generation failed - this is the bug we need to fix!");
539 },
540 }
541 }
542
543 #[test]
544 fn test_encrypted_wallet_address_generation() {
545 let temp_dir = tempfile::TempDir::new().expect("Failed to create temp directory");
549 let keystore = Keystore::new(temp_dir.path());
550
551 let alice_pair = crystal_alice();
553 let quantum_keypair = QuantumKeyPair::from_resonance_pair(&alice_pair);
554
555 let mut metadata = std::collections::HashMap::new();
556 metadata.insert("version".to_string(), "1.0.0".to_string());
557 metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
558 metadata.insert("test_wallet".to_string(), "true".to_string());
559
560 let wallet_data = WalletData {
561 name: "test_crystal_alice".to_string(),
562 keypair: quantum_keypair,
563 mnemonic: None,
564 derivation_path: "m/".to_string(),
565 metadata,
566 };
567
568 let encrypted_wallet = keystore
570 .encrypt_wallet_data(&wallet_data, "")
571 .expect("Encryption should succeed");
572
573 keystore.save_wallet(&encrypted_wallet).expect("Save should succeed");
575 let loaded_wallet = keystore
576 .load_wallet("test_crystal_alice")
577 .expect("Load should succeed")
578 .expect("Wallet should exist");
579
580 let decrypted_data = keystore
582 .decrypt_wallet_data(&loaded_wallet, "")
583 .expect("Decryption should succeed");
584
585 let result = std::panic::catch_unwind(|| decrypted_data.keypair.to_account_id_ss58check());
587
588 match result {
589 Ok(address) => {
590 println!("✅ Encrypted wallet address generation successful: {address}");
591 let expected = alice_pair
593 .public()
594 .into_account()
595 .to_ss58check_with_version(Ss58AddressFormat::custom(189));
596 assert_eq!(address, expected, "Decrypted wallet should generate correct address");
597 },
598 Err(_) => {
599 panic!("❌ Encrypted wallet address generation failed - this reproduces the send command bug!");
600 },
601 }
602 }
603
604 #[test]
605 fn test_send_command_wallet_loading_flow() {
606 let temp_dir = TempDir::new().expect("Failed to create temp directory");
611 let keystore = Keystore::new(temp_dir.path());
612
613 let alice_pair = crystal_alice();
615 let quantum_keypair = QuantumKeyPair::from_resonance_pair(&alice_pair);
616
617 let mut metadata = std::collections::HashMap::new();
618 metadata.insert("version".to_string(), "1.0.0".to_string());
619 metadata.insert("algorithm".to_string(), "ML-DSA-87".to_string());
620 metadata.insert("test_wallet".to_string(), "true".to_string());
621
622 let wallet_data = WalletData {
623 name: "crystal_alice".to_string(),
624 keypair: quantum_keypair,
625 mnemonic: None,
626 derivation_path: "m/".to_string(),
627 metadata,
628 };
629
630 let encrypted_wallet = keystore
632 .encrypt_wallet_data(&wallet_data, "")
633 .expect("Encryption should succeed");
634 keystore.save_wallet(&encrypted_wallet).expect("Save should succeed");
635
636 use crate::wallet::WalletManager;
639 let wallet_manager = WalletManager { wallets_dir: temp_dir.path().to_path_buf() };
640 let loaded_wallet_data =
641 wallet_manager.load_wallet("crystal_alice", "").expect("Should load wallet");
642
643 let result = std::panic::catch_unwind(|| {
645 loaded_wallet_data.keypair.to_account_id_ss58check()
647 });
648
649 match result {
650 Ok(address) => {
651 println!("✅ Send command flow works: {address}");
652 let expected = alice_pair
654 .public()
655 .into_account()
656 .to_ss58check_with_version(Ss58AddressFormat::custom(189));
657 assert_eq!(address, expected, "Loaded wallet should generate correct address");
658 },
659 Err(_) => {
660 println!("❌ Send command flow failed - this reproduces the bug!");
661 panic!(
663 "This test reproduces the send command bug - load_wallet returns dummy data!"
664 );
665 },
666 }
667 }
668
669 #[test]
670 fn test_keypair_data_integrity() {
671 for i in 0..5 {
673 let mut entropy = [i as u8; 32];
674 let dilithium_keypair = Keypair::generate(SensitiveBytes32::from(&mut entropy));
675 let quantum_keypair = QuantumKeyPair::from_dilithium_keypair(&dilithium_keypair);
676
677 if i == 0 {
679 println!("Actual public key size: {}", quantum_keypair.public_key.len());
680 println!("Actual private key size: {}", quantum_keypair.private_key.len());
681 }
682
683 assert!(
685 quantum_keypair.public_key.len() > 1000,
686 "Public key should be reasonably large (actual: {})",
687 quantum_keypair.public_key.len()
688 );
689 assert!(
690 quantum_keypair.private_key.len() > 2000,
691 "Private key should be reasonably large (actual: {})",
692 quantum_keypair.private_key.len()
693 );
694
695 assert!(
697 quantum_keypair.public_key.iter().any(|&b| b != 0),
698 "Public key should not be all zeros"
699 );
700 assert!(
701 quantum_keypair.private_key.iter().any(|&b| b != 0),
702 "Private key should not be all zeros"
703 );
704 }
705 }
706}