1use aes_gcm::{
34 aead::{rand_core::RngCore, Aead, AeadCore, KeyInit, OsRng, Payload},
35 Aes256Gcm,
36};
37use argon2::{Algorithm, Argon2, Block, Params, Version as Argon2Version};
38use base64::{engine::general_purpose, Engine};
39use bip39::{Language, Mnemonic};
40use ring::digest;
41use serde::{Deserialize, Serialize};
42use solana_derivation_path::DerivationPath;
43use solana_sdk::signer::keypair::keypair_from_seed_and_derivation_path;
44use std::str::FromStr;
45use std::sync::{Condvar, Mutex, OnceLock};
46use zeroize::Zeroizing;
47
48#[cfg(feature = "2fa")]
50pub mod totp;
51
52#[cfg(feature = "2fa")]
53pub mod secure_totp;
54
55#[cfg(feature = "2fa")]
56pub mod hardware_fingerprint;
57
58#[cfg(feature = "2fa")]
59pub mod security_question;
60
61pub mod interactive;
63
64pub mod bot_helper;
66
67pub mod operations;
69
70#[cfg(any(
72 feature = "solana-ops",
73 feature = "mobile-solana-ops",
74 feature = "sol-trade-sdk"
75))]
76pub mod solana_utils;
77
78pub use solana_sdk::pubkey::Pubkey;
80pub use solana_sdk::signature::{Keypair, Signer};
81
82fn xor_encrypt_decrypt(data: &[u8], key: &[u8; 32]) -> Vec<u8> {
89 let mut result = Vec::with_capacity(data.len());
90
91 let mut keystream = Zeroizing::new(Vec::new());
93 let mut i: u32 = 0;
94 while keystream.len() < data.len() {
95 let mut ctx = digest::Context::new(&digest::SHA256);
96 ctx.update(key);
97 ctx.update(&i.to_le_bytes());
98 let hash = ctx.finish();
99 keystream.extend_from_slice(hash.as_ref());
100 i += 1;
101 }
102
103 for (i, &byte) in data.iter().enumerate() {
105 result.push(byte ^ keystream[i % keystream.len()]);
106 }
107
108 result
109}
110
111pub fn encrypt_key(secret_key: &str, encryption_key: &[u8; 32]) -> Result<String, String> {
115 let data = secret_key.as_bytes();
116 let encrypted = xor_encrypt_decrypt(data, encryption_key);
117 Ok(general_purpose::STANDARD.encode(encrypted))
118}
119
120pub fn decrypt_key(encrypted_data: &str, encryption_key: &[u8; 32]) -> Result<String, String> {
124 let decrypted = Zeroizing::new(decrypt_key_to_bytes(encrypted_data, encryption_key)?);
125 std::str::from_utf8(decrypted.as_slice())
126 .map(ToOwned::to_owned)
127 .map_err(|_| "Invalid UTF-8 data in decrypted content".to_string())
128}
129
130pub fn decrypt_key_to_bytes(
132 encrypted_data: &str,
133 encryption_key: &[u8; 32],
134) -> Result<Vec<u8>, String> {
135 let ciphertext = general_purpose::STANDARD
136 .decode(encrypted_data)
137 .map_err(|_| "Invalid encrypted data format".to_string())?;
138 Ok(xor_encrypt_decrypt(&ciphertext, encryption_key))
139}
140
141fn trim_trailing_non_base58(bytes: &[u8]) -> &[u8] {
143 let mut end = bytes.len();
144 while end > 0 {
145 let b = bytes[end - 1];
146 if b == 0x00 || b == b'\n' || b == b'\r' || b > 127 {
147 end -= 1;
148 } else {
149 break;
150 }
151 }
152 &bytes[..end]
153}
154
155pub const MIN_PASSWORD_LENGTH: usize = 10;
157
158pub const MAX_PASSWORD_LENGTH: usize = 1024;
160
161const PASSWORD_SALT: &[u8] = b"sol-safekey-v1-salt-2025";
163
164pub fn generate_encryption_key_simple(password: &str) -> [u8; 32] {
166 let mut salted_password = Zeroizing::new(password.as_bytes().to_vec());
168 salted_password.extend_from_slice(PASSWORD_SALT);
169
170 let hash = digest::digest(&digest::SHA256, &salted_password);
172
173 let mut key = [0u8; 32];
175 key[0..16].copy_from_slice(&hash.as_ref()[0..16]);
176
177 key[16..32].copy_from_slice(&hash.as_ref()[0..16]);
180
181 key
182}
183
184pub const KEYSTORE_V2_VERSION: u8 = 2;
185pub const KEYSTORE_V3_VERSION: u8 = 3;
186pub const MAX_KEYSTORE_JSON_BYTES: usize = 128 * 1024;
187
188const KEYSTORE_ENCRYPTION_TYPE: &str = "password_only";
189const KEYSTORE_V2_KDF: &str = "argon2id";
190const KEYSTORE_V2_CIPHER: &str = "aes-256-gcm";
191const KEYSTORE_V2_AAD_DOMAIN: &[u8] = b"sol-safekey-keystore";
192const SECRET_ENVELOPE_AAD: &[u8] = b"fnzsafe-secret-envelope-v1";
193const SECRET_ENVELOPE_MAX_BYTES: usize = 16 * 1024;
194const KEYSTORE_V2_ARGON2_MEMORY_KIB: u32 = 64 * 1024;
195const KEYSTORE_V2_ARGON2_ITERATIONS: u32 = 3;
196const KEYSTORE_V2_ARGON2_PARALLELISM: u32 = 1;
197const KEYSTORE_V2_SALT_BYTES: usize = 16;
198const KEYSTORE_V2_NONCE_BYTES: usize = 12;
199const KEYSTORE_V2_PLAINTEXT_BYTES: usize = 64;
200const KEYSTORE_V2_TAG_BYTES: usize = 16;
201const KEYSTORE_V2_SALT_BASE64_CHARS: usize = 24;
202const KEYSTORE_V2_NONCE_BASE64_CHARS: usize = 16;
203const KEYSTORE_V2_CIPHERTEXT_BASE64_CHARS: usize = 108;
204const KEYSTORE_V2_MAX_CONCURRENT_KDFS: usize = 2;
205
206struct KeystoreKdfLimiter {
207 in_flight: Mutex<usize>,
208 available: Condvar,
209}
210
211struct KeystoreKdfPermit {
212 limiter: &'static KeystoreKdfLimiter,
213}
214
215static KEYSTORE_V2_KDF_LIMITER: OnceLock<KeystoreKdfLimiter> = OnceLock::new();
216
217fn acquire_keystore_kdf_permit() -> EncryptionResult<KeystoreKdfPermit> {
218 let limiter = KEYSTORE_V2_KDF_LIMITER.get_or_init(|| KeystoreKdfLimiter {
219 in_flight: Mutex::new(0),
220 available: Condvar::new(),
221 });
222 let mut in_flight = limiter
223 .in_flight
224 .lock()
225 .map_err(|_| "Keystore KDF concurrency limiter is unavailable".to_string())?;
226 while *in_flight >= KEYSTORE_V2_MAX_CONCURRENT_KDFS {
227 in_flight = limiter
228 .available
229 .wait(in_flight)
230 .map_err(|_| "Keystore KDF concurrency limiter is unavailable".to_string())?;
231 }
232 *in_flight += 1;
233 Ok(KeystoreKdfPermit { limiter })
234}
235
236impl Drop for KeystoreKdfPermit {
237 fn drop(&mut self) {
238 if let Ok(mut in_flight) = self.limiter.in_flight.lock() {
239 *in_flight = in_flight.saturating_sub(1);
240 self.limiter.available.notify_one();
241 }
242 }
243}
244
245#[derive(Clone, Copy, Debug, Eq, PartialEq)]
246pub enum KeystoreVersion {
247 LegacyV1,
248 V2,
249 V3,
250}
251
252#[derive(Deserialize, Serialize)]
253struct KeystoreV2 {
254 version: u8,
255 public_key: String,
256 encryption_type: String,
257 crypto: KeystoreCryptoV2,
258 created_at: String,
259}
260
261#[derive(Deserialize, Serialize)]
262struct KeystoreV3 {
263 version: u8,
264 public_key: String,
265 secret_type: String,
266 derivation_path: String,
267 encryption_type: String,
268 crypto: KeystoreCryptoV2,
269 created_at: String,
270}
271
272struct DecryptedMnemonicKeystore {
273 mnemonic: Zeroizing<String>,
274 keypair: Keypair,
275}
276
277#[derive(Deserialize, Serialize)]
278struct KeystoreCryptoV2 {
279 kdf: String,
280 kdf_params: KeystoreKdfParamsV2,
281 cipher: String,
282 nonce: String,
283 ciphertext: String,
284}
285
286#[derive(Deserialize, Serialize)]
287struct KeystoreKdfParamsV2 {
288 memory_kib: u32,
289 iterations: u32,
290 parallelism: u32,
291 salt: String,
292}
293
294#[derive(Deserialize, Serialize)]
295struct SecretEnvelopeV1 {
296 version: u8,
297 kind: String,
298 crypto: KeystoreCryptoV2,
299 created_at: String,
300}
301
302fn validate_v2_password_for_creation(password: &str) -> EncryptionResult<()> {
303 if password.chars().count() < MIN_PASSWORD_LENGTH {
304 return Err(format!(
305 "Password must contain at least {MIN_PASSWORD_LENGTH} characters"
306 ));
307 }
308 validate_password_size(password)
309}
310
311fn validate_password_size(password: &str) -> EncryptionResult<()> {
312 if password.len() > MAX_PASSWORD_LENGTH {
313 return Err(format!(
314 "Password must not exceed {MAX_PASSWORD_LENGTH} UTF-8 bytes"
315 ));
316 }
317 Ok(())
318}
319
320fn keystore_v2_aad(public_key: &str) -> Vec<u8> {
321 let mut aad = Vec::with_capacity(KEYSTORE_V2_AAD_DOMAIN.len() + public_key.len() + 3);
322 aad.extend_from_slice(KEYSTORE_V2_AAD_DOMAIN);
323 aad.push(0);
324 aad.push(KEYSTORE_V2_VERSION);
325 aad.push(0);
326 aad.extend_from_slice(public_key.as_bytes());
327 aad
328}
329
330fn keystore_v3_aad(public_key: &str, secret_type: &str, derivation_path: &str) -> Vec<u8> {
331 let mut aad = Vec::with_capacity(
332 KEYSTORE_V2_AAD_DOMAIN.len()
333 + public_key.len()
334 + secret_type.len()
335 + derivation_path.len()
336 + 7,
337 );
338 aad.extend_from_slice(KEYSTORE_V2_AAD_DOMAIN);
339 aad.push(0);
340 aad.push(KEYSTORE_V3_VERSION);
341 aad.push(0);
342 aad.extend_from_slice(public_key.as_bytes());
343 aad.push(0);
344 aad.extend_from_slice(secret_type.as_bytes());
345 aad.push(0);
346 aad.extend_from_slice(derivation_path.as_bytes());
347 aad
348}
349
350fn derive_keystore_v2_key(
351 password: &str,
352 salt: &[u8; KEYSTORE_V2_SALT_BYTES],
353) -> EncryptionResult<Zeroizing<[u8; 32]>> {
354 validate_password_size(password)?;
355 let _permit = acquire_keystore_kdf_permit()?;
356 let params = Params::new(
357 KEYSTORE_V2_ARGON2_MEMORY_KIB,
358 KEYSTORE_V2_ARGON2_ITERATIONS,
359 KEYSTORE_V2_ARGON2_PARALLELISM,
360 Some(32),
361 )
362 .map_err(|_| "Invalid Argon2id parameters".to_string())?;
363 let block_count = params.block_count();
364 let argon2 = Argon2::new(Algorithm::Argon2id, Argon2Version::V0x13, params);
365 let mut key = Zeroizing::new([0u8; 32]);
366 let mut memory = Vec::new();
367 memory
368 .try_reserve_exact(block_count)
369 .map_err(|_| "Unable to allocate Argon2id memory".to_string())?;
370 memory.resize(block_count, Block::default());
371 let mut memory = Zeroizing::new(memory);
372 argon2
373 .hash_password_into_with_memory(
374 password.as_bytes(),
375 salt,
376 key.as_mut(),
377 memory.as_mut_slice(),
378 )
379 .map_err(|_| "Argon2id key derivation failed".to_string())?;
380 Ok(key)
381}
382
383fn parse_base58_keypair(private_key: &str) -> EncryptionResult<Keypair> {
384 let private_key = private_key.trim();
385 if private_key.is_empty() {
386 return Err("Private key cannot be empty".to_string());
387 }
388 let bytes = Zeroizing::new(
389 bs58::decode(private_key)
390 .into_vec()
391 .map_err(|_| "Private key is not valid base58".to_string())?,
392 );
393 if bytes.len() != KEYSTORE_V2_PLAINTEXT_BYTES {
394 return Err("Private key must decode to exactly 64 bytes".to_string());
395 }
396 Keypair::try_from(bytes.as_slice())
397 .map_err(|_| "Private key bytes are invalid or inconsistent".to_string())
398}
399
400fn parse_decrypted_keypair(bytes: &[u8]) -> EncryptionResult<Keypair> {
401 if bytes.len() == KEYSTORE_V2_PLAINTEXT_BYTES {
402 if let Ok(keypair) = Keypair::try_from(bytes) {
403 return Ok(keypair);
404 }
405 }
406
407 let trimmed = trim_trailing_non_base58(bytes);
408 let private_key = std::str::from_utf8(trimmed)
409 .map_err(|_| "Decrypted private key is not valid UTF-8".to_string())?
410 .trim();
411 parse_base58_keypair(private_key)
412}
413
414fn claimed_public_key(
415 value: &serde_json::Value,
416 required: bool,
417) -> EncryptionResult<Option<Pubkey>> {
418 match value.get("public_key") {
419 Some(serde_json::Value::String(public_key)) => {
420 let parsed = Pubkey::from_str(public_key)
421 .map_err(|_| "Keystore public_key is invalid".to_string())?;
422 if parsed.to_string() != *public_key {
423 return Err("Keystore public_key is not canonical".to_string());
424 }
425 Ok(Some(parsed))
426 }
427 Some(_) => Err("Keystore public_key must be a string".to_string()),
428 None if required => Err("Keystore is missing public_key".to_string()),
429 None => Ok(None),
430 }
431}
432
433fn decrypt_legacy_keystore(json_data: &str, password: &str) -> EncryptionResult<Keypair> {
434 let value: serde_json::Value =
435 serde_json::from_str(json_data).map_err(|_| "Invalid JSON format".to_string())?;
436 if value
437 .get("encryption_type")
438 .and_then(serde_json::Value::as_str)
439 .is_some_and(|encryption_type| encryption_type != KEYSTORE_ENCRYPTION_TYPE)
440 {
441 return Err("Unsupported legacy keystore encryption_type".to_string());
442 }
443 let encrypted = value
444 .get("encrypted_private_key")
445 .and_then(serde_json::Value::as_str)
446 .ok_or_else(|| "Missing encrypted_private_key field".to_string())?;
447 let key = Zeroizing::new(generate_encryption_key_simple(password));
448 let plaintext = Zeroizing::new(
449 decrypt_key_to_bytes(encrypted, &key)
450 .map_err(|error| format!("Legacy keystore decryption failed: {error}"))?,
451 );
452 let keypair = parse_decrypted_keypair(plaintext.as_slice())
453 .map_err(|_| "Legacy keystore password or ciphertext is invalid".to_string())?;
454 if let Some(public_key) = claimed_public_key(&value, false)? {
455 if keypair.pubkey() != public_key {
456 return Err("Legacy keystore public_key does not match decrypted keypair".to_string());
457 }
458 }
459 Ok(keypair)
460}
461
462fn encrypt_keystore_v2(keypair: &Keypair, password: &str) -> EncryptionResult<String> {
463 use chrono::Utc;
464
465 validate_v2_password_for_creation(password)?;
466 let public_key = keypair.pubkey().to_string();
467 let mut rng = OsRng;
468 let mut salt = [0u8; KEYSTORE_V2_SALT_BYTES];
469 rng.fill_bytes(&mut salt);
470 let nonce = Aes256Gcm::generate_nonce(&mut rng);
471 let key = derive_keystore_v2_key(password, &salt)?;
472 let cipher = Aes256Gcm::new_from_slice(key.as_ref())
473 .map_err(|_| "AES-256-GCM initialization failed".to_string())?;
474 let plaintext = Zeroizing::new(keypair.to_bytes());
475 let aad = keystore_v2_aad(&public_key);
476 let ciphertext = cipher
477 .encrypt(
478 &nonce,
479 Payload {
480 msg: plaintext.as_ref(),
481 aad: &aad,
482 },
483 )
484 .map_err(|_| "AES-256-GCM encryption failed".to_string())?;
485
486 serde_json::to_string(&KeystoreV2 {
487 version: KEYSTORE_V2_VERSION,
488 public_key,
489 encryption_type: KEYSTORE_ENCRYPTION_TYPE.to_string(),
490 crypto: KeystoreCryptoV2 {
491 kdf: KEYSTORE_V2_KDF.to_string(),
492 kdf_params: KeystoreKdfParamsV2 {
493 memory_kib: KEYSTORE_V2_ARGON2_MEMORY_KIB,
494 iterations: KEYSTORE_V2_ARGON2_ITERATIONS,
495 parallelism: KEYSTORE_V2_ARGON2_PARALLELISM,
496 salt: general_purpose::STANDARD.encode(salt),
497 },
498 cipher: KEYSTORE_V2_CIPHER.to_string(),
499 nonce: general_purpose::STANDARD.encode(nonce),
500 ciphertext: general_purpose::STANDARD.encode(ciphertext),
501 },
502 created_at: Utc::now().to_rfc3339(),
503 })
504 .map_err(|_| "Failed to serialize v2 keystore".to_string())
505}
506
507fn decrypt_keystore_v2(json_data: &str, password: &str) -> EncryptionResult<Keypair> {
508 let keystore: KeystoreV2 =
509 serde_json::from_str(json_data).map_err(|_| "Invalid v2 keystore JSON".to_string())?;
510 if keystore.version != KEYSTORE_V2_VERSION {
511 return Err(format!("Unsupported keystore version {}", keystore.version));
512 }
513 if keystore.encryption_type != KEYSTORE_ENCRYPTION_TYPE {
514 return Err("Unsupported v2 keystore encryption_type".to_string());
515 }
516 if keystore.crypto.kdf != KEYSTORE_V2_KDF || keystore.crypto.cipher != KEYSTORE_V2_CIPHER {
517 return Err("Unsupported v2 keystore cryptography".to_string());
518 }
519 if keystore.crypto.kdf_params.memory_kib != KEYSTORE_V2_ARGON2_MEMORY_KIB
520 || keystore.crypto.kdf_params.iterations != KEYSTORE_V2_ARGON2_ITERATIONS
521 || keystore.crypto.kdf_params.parallelism != KEYSTORE_V2_ARGON2_PARALLELISM
522 {
523 return Err("Unsupported v2 Argon2id parameters".to_string());
524 }
525
526 let expected_public_key = Pubkey::from_str(&keystore.public_key)
527 .map_err(|_| "Keystore public_key is invalid".to_string())?;
528 if expected_public_key.to_string() != keystore.public_key {
529 return Err("Keystore public_key is not canonical".to_string());
530 }
531 if keystore.crypto.kdf_params.salt.len() != KEYSTORE_V2_SALT_BASE64_CHARS {
532 return Err("Keystore salt has an invalid encoded length".to_string());
533 }
534 if keystore.crypto.nonce.len() != KEYSTORE_V2_NONCE_BASE64_CHARS {
535 return Err("Keystore nonce has an invalid encoded length".to_string());
536 }
537 if keystore.crypto.ciphertext.len() != KEYSTORE_V2_CIPHERTEXT_BASE64_CHARS {
538 return Err("Keystore ciphertext has an invalid encoded length".to_string());
539 }
540 let salt: [u8; KEYSTORE_V2_SALT_BYTES] = general_purpose::STANDARD
541 .decode(keystore.crypto.kdf_params.salt.as_bytes())
542 .map_err(|_| "Keystore salt is not valid base64".to_string())?
543 .try_into()
544 .map_err(|_| "Keystore salt has an invalid length".to_string())?;
545 let nonce_bytes: [u8; KEYSTORE_V2_NONCE_BYTES] = general_purpose::STANDARD
546 .decode(keystore.crypto.nonce.as_bytes())
547 .map_err(|_| "Keystore nonce is not valid base64".to_string())?
548 .try_into()
549 .map_err(|_| "Keystore nonce has an invalid length".to_string())?;
550 let ciphertext = general_purpose::STANDARD
551 .decode(keystore.crypto.ciphertext.as_bytes())
552 .map_err(|_| "Keystore ciphertext is not valid base64".to_string())?;
553 if ciphertext.len() != KEYSTORE_V2_PLAINTEXT_BYTES + KEYSTORE_V2_TAG_BYTES {
554 return Err("Keystore ciphertext has an invalid length".to_string());
555 }
556
557 let key = derive_keystore_v2_key(password, &salt)?;
558 let cipher = Aes256Gcm::new_from_slice(key.as_ref())
559 .map_err(|_| "AES-256-GCM initialization failed".to_string())?;
560 let nonce = aes_gcm::Nonce::from(nonce_bytes);
561 let aad = keystore_v2_aad(&keystore.public_key);
562 let plaintext = Zeroizing::new(
563 cipher
564 .decrypt(
565 &nonce,
566 Payload {
567 msg: &ciphertext,
568 aad: &aad,
569 },
570 )
571 .map_err(|_| "V2 keystore password or authenticated data is invalid".to_string())?,
572 );
573 if plaintext.len() != KEYSTORE_V2_PLAINTEXT_BYTES {
574 return Err("Decrypted v2 keypair has an invalid length".to_string());
575 }
576 let keypair = Keypair::try_from(plaintext.as_slice())
577 .map_err(|_| "Decrypted v2 keypair bytes are invalid".to_string())?;
578 if keypair.pubkey() != expected_public_key {
579 return Err("V2 keystore public_key does not match decrypted keypair".to_string());
580 }
581 Ok(keypair)
582}
583
584fn normalize_mnemonic(mnemonic: &str) -> EncryptionResult<String> {
585 let normalized = mnemonic.split_whitespace().collect::<Vec<_>>().join(" ");
586 Mnemonic::parse_in_normalized(Language::English, &normalized)
587 .map_err(|_| "Mnemonic checksum failed; check the words and order".to_string())?;
588 Ok(normalized)
589}
590
591fn keypair_from_mnemonic_and_path(
592 mnemonic: &str,
593 derivation_path: &str,
594) -> EncryptionResult<Keypair> {
595 let mnemonic = Mnemonic::parse_in_normalized(Language::English, mnemonic)
596 .map_err(|_| "Mnemonic checksum failed; check the words and order".to_string())?;
597 let derivation_path = DerivationPath::from_absolute_path_str(derivation_path)
598 .map_err(|error| format!("Invalid Solana derivation path: {error}"))?;
599 let seed = Zeroizing::new(mnemonic.to_seed(""));
600 keypair_from_seed_and_derivation_path(seed.as_ref(), Some(derivation_path))
601 .map_err(|error| format!("Failed to derive Solana keypair from mnemonic: {error}"))
602}
603
604fn encrypt_mnemonic_keystore_v3(
605 mnemonic: &str,
606 derivation_path: &str,
607 password: &str,
608) -> EncryptionResult<String> {
609 validate_v2_password_for_creation(password)?;
610 let mnemonic = Zeroizing::new(normalize_mnemonic(mnemonic)?);
611 let derivation_path = derivation_path.trim().to_string();
612 DerivationPath::from_absolute_path_str(&derivation_path)
613 .map_err(|error| format!("Invalid Solana derivation path: {error}"))?;
614 let keypair = keypair_from_mnemonic_and_path(&mnemonic, &derivation_path)?;
615 let public_key = keypair.pubkey().to_string();
616 let mut rng = OsRng;
617 let mut salt = [0u8; KEYSTORE_V2_SALT_BYTES];
618 rng.fill_bytes(&mut salt);
619 let nonce = Aes256Gcm::generate_nonce(&mut rng);
620 let key = derive_keystore_v2_key(password, &salt)?;
621 let cipher = Aes256Gcm::new_from_slice(key.as_ref())
622 .map_err(|_| "AES-256-GCM initialization failed".to_string())?;
623 let aad = keystore_v3_aad(&public_key, "mnemonic", &derivation_path);
624 let ciphertext = cipher
625 .encrypt(
626 &nonce,
627 Payload {
628 msg: mnemonic.as_bytes(),
629 aad: &aad,
630 },
631 )
632 .map_err(|_| "AES-256-GCM encryption failed".to_string())?;
633
634 serde_json::to_string(&KeystoreV3 {
635 version: KEYSTORE_V3_VERSION,
636 public_key,
637 secret_type: "mnemonic".to_string(),
638 derivation_path,
639 encryption_type: KEYSTORE_ENCRYPTION_TYPE.to_string(),
640 crypto: KeystoreCryptoV2 {
641 kdf: KEYSTORE_V2_KDF.to_string(),
642 kdf_params: KeystoreKdfParamsV2 {
643 memory_kib: KEYSTORE_V2_ARGON2_MEMORY_KIB,
644 iterations: KEYSTORE_V2_ARGON2_ITERATIONS,
645 parallelism: KEYSTORE_V2_ARGON2_PARALLELISM,
646 salt: general_purpose::STANDARD.encode(salt),
647 },
648 cipher: KEYSTORE_V2_CIPHER.to_string(),
649 nonce: general_purpose::STANDARD.encode(nonce),
650 ciphertext: general_purpose::STANDARD.encode(ciphertext),
651 },
652 created_at: chrono::Utc::now().to_rfc3339(),
653 })
654 .map_err(|_| "Failed to serialize v3 mnemonic keystore".to_string())
655}
656
657fn decrypt_mnemonic_keystore_v3(
658 json_data: &str,
659 password: &str,
660) -> EncryptionResult<DecryptedMnemonicKeystore> {
661 let keystore: KeystoreV3 = serde_json::from_str(json_data)
662 .map_err(|_| "Invalid v3 mnemonic keystore JSON".to_string())?;
663 if keystore.version != KEYSTORE_V3_VERSION
664 || keystore.secret_type != "mnemonic"
665 || keystore.encryption_type != KEYSTORE_ENCRYPTION_TYPE
666 {
667 return Err("Unsupported v3 mnemonic keystore".to_string());
668 }
669 if keystore.crypto.kdf != KEYSTORE_V2_KDF
670 || keystore.crypto.cipher != KEYSTORE_V2_CIPHER
671 || keystore.crypto.kdf_params.memory_kib != KEYSTORE_V2_ARGON2_MEMORY_KIB
672 || keystore.crypto.kdf_params.iterations != KEYSTORE_V2_ARGON2_ITERATIONS
673 || keystore.crypto.kdf_params.parallelism != KEYSTORE_V2_ARGON2_PARALLELISM
674 {
675 return Err("Unsupported v3 mnemonic keystore cryptography".to_string());
676 }
677 let expected_public_key = Pubkey::from_str(&keystore.public_key)
678 .map_err(|_| "Keystore public_key is invalid".to_string())?;
679 if expected_public_key.to_string() != keystore.public_key {
680 return Err("Keystore public_key is not canonical".to_string());
681 }
682 DerivationPath::from_absolute_path_str(&keystore.derivation_path)
683 .map_err(|error| format!("Invalid Solana derivation path: {error}"))?;
684 if keystore.derivation_path.trim() != keystore.derivation_path {
685 return Err("Keystore derivation_path is not canonical".to_string());
686 }
687 let salt: [u8; KEYSTORE_V2_SALT_BYTES] = general_purpose::STANDARD
688 .decode(keystore.crypto.kdf_params.salt.as_bytes())
689 .map_err(|_| "Keystore salt is not valid base64".to_string())?
690 .try_into()
691 .map_err(|_| "Keystore salt has an invalid length".to_string())?;
692 let nonce_bytes: [u8; KEYSTORE_V2_NONCE_BYTES] = general_purpose::STANDARD
693 .decode(keystore.crypto.nonce.as_bytes())
694 .map_err(|_| "Keystore nonce is not valid base64".to_string())?
695 .try_into()
696 .map_err(|_| "Keystore nonce has an invalid length".to_string())?;
697 let ciphertext = general_purpose::STANDARD
698 .decode(keystore.crypto.ciphertext.as_bytes())
699 .map_err(|_| "Keystore ciphertext is not valid base64".to_string())?;
700 if ciphertext.len() <= KEYSTORE_V2_TAG_BYTES
701 || ciphertext.len() > SECRET_ENVELOPE_MAX_BYTES + KEYSTORE_V2_TAG_BYTES
702 {
703 return Err("Keystore ciphertext has an invalid length".to_string());
704 }
705 let key = derive_keystore_v2_key(password, &salt)?;
706 let cipher = Aes256Gcm::new_from_slice(key.as_ref())
707 .map_err(|_| "AES-256-GCM initialization failed".to_string())?;
708 let aad = keystore_v3_aad(
709 &keystore.public_key,
710 &keystore.secret_type,
711 &keystore.derivation_path,
712 );
713 let plaintext = Zeroizing::new(
714 cipher
715 .decrypt(
716 aes_gcm::Nonce::from_slice(&nonce_bytes),
717 Payload {
718 msg: &ciphertext,
719 aad: &aad,
720 },
721 )
722 .map_err(|_| "V3 keystore password or authenticated data is invalid".to_string())?,
723 );
724 let mnemonic = Zeroizing::new(
725 String::from_utf8(plaintext.to_vec())
726 .map_err(|_| "Decrypted mnemonic is not valid UTF-8".to_string())?,
727 );
728 let normalized = Zeroizing::new(normalize_mnemonic(&mnemonic)?);
729 if normalized.as_str() != mnemonic.as_str() {
730 return Err("Decrypted mnemonic is not normalized".to_string());
731 }
732 let keypair = keypair_from_mnemonic_and_path(&mnemonic, &keystore.derivation_path)?;
733 if keypair.pubkey() != expected_public_key {
734 return Err("V3 keystore public_key does not match the derived keypair".to_string());
735 }
736 Ok(DecryptedMnemonicKeystore { mnemonic, keypair })
737}
738
739pub type EncryptionResult<T> = Result<T, String>;
745
746pub struct KeyManager;
751
752impl KeyManager {
753 pub fn generate_keypair() -> Keypair {
765 Keypair::new()
766 }
767
768 pub fn encrypt_with_password(private_key: &str, password: &str) -> EncryptionResult<String> {
793 let keypair = parse_base58_keypair(private_key)?;
794 encrypt_keystore_v2(&keypair, password)
795 }
796
797 pub fn encrypt_secret_with_password(secret: &str, password: &str) -> EncryptionResult<String> {
799 validate_v2_password_for_creation(password)?;
800 if secret.is_empty() || secret.len() > SECRET_ENVELOPE_MAX_BYTES {
801 return Err("Secret is empty or too large".to_string());
802 }
803 let mut rng = OsRng;
804 let mut salt = [0u8; KEYSTORE_V2_SALT_BYTES];
805 rng.fill_bytes(&mut salt);
806 let nonce = Aes256Gcm::generate_nonce(&mut rng);
807 let key = derive_keystore_v2_key(password, &salt)?;
808 let cipher = Aes256Gcm::new_from_slice(key.as_ref())
809 .map_err(|_| "AES-256-GCM initialization failed".to_string())?;
810 let plaintext = Zeroizing::new(secret.as_bytes().to_vec());
811 let ciphertext = cipher
812 .encrypt(
813 &nonce,
814 Payload {
815 msg: plaintext.as_ref(),
816 aad: SECRET_ENVELOPE_AAD,
817 },
818 )
819 .map_err(|_| "Secret encryption failed".to_string())?;
820 serde_json::to_string(&SecretEnvelopeV1 {
821 version: 1,
822 kind: "password-secret".to_string(),
823 crypto: KeystoreCryptoV2 {
824 kdf: KEYSTORE_V2_KDF.to_string(),
825 kdf_params: KeystoreKdfParamsV2 {
826 memory_kib: KEYSTORE_V2_ARGON2_MEMORY_KIB,
827 iterations: KEYSTORE_V2_ARGON2_ITERATIONS,
828 parallelism: KEYSTORE_V2_ARGON2_PARALLELISM,
829 salt: general_purpose::STANDARD.encode(salt),
830 },
831 cipher: KEYSTORE_V2_CIPHER.to_string(),
832 nonce: general_purpose::STANDARD.encode(nonce),
833 ciphertext: general_purpose::STANDARD.encode(ciphertext),
834 },
835 created_at: chrono::Utc::now().to_rfc3339(),
836 })
837 .map_err(|_| "Failed to serialize secret envelope".to_string())
838 }
839
840 pub fn decrypt_secret_with_password(
842 encrypted_data: &str,
843 password: &str,
844 ) -> EncryptionResult<String> {
845 validate_password_size(password)?;
846 if encrypted_data.len() > MAX_KEYSTORE_JSON_BYTES {
847 return Err("Secret envelope is too large".to_string());
848 }
849 let envelope: SecretEnvelopeV1 = serde_json::from_str(encrypted_data)
850 .map_err(|_| "Invalid secret envelope".to_string())?;
851 if envelope.version != 1
852 || envelope.kind != "password-secret"
853 || envelope.crypto.kdf != KEYSTORE_V2_KDF
854 || envelope.crypto.cipher != KEYSTORE_V2_CIPHER
855 || envelope.crypto.kdf_params.memory_kib != KEYSTORE_V2_ARGON2_MEMORY_KIB
856 || envelope.crypto.kdf_params.iterations != KEYSTORE_V2_ARGON2_ITERATIONS
857 || envelope.crypto.kdf_params.parallelism != KEYSTORE_V2_ARGON2_PARALLELISM
858 {
859 return Err("Unsupported secret envelope parameters".to_string());
860 }
861 let salt_bytes = general_purpose::STANDARD
862 .decode(&envelope.crypto.kdf_params.salt)
863 .map_err(|_| "Invalid secret envelope salt".to_string())?;
864 let salt: [u8; KEYSTORE_V2_SALT_BYTES] = salt_bytes
865 .try_into()
866 .map_err(|_| "Invalid secret envelope salt".to_string())?;
867 let nonce_bytes = general_purpose::STANDARD
868 .decode(&envelope.crypto.nonce)
869 .map_err(|_| "Invalid secret envelope nonce".to_string())?;
870 if nonce_bytes.len() != KEYSTORE_V2_NONCE_BYTES {
871 return Err("Invalid secret envelope nonce".to_string());
872 }
873 let ciphertext = general_purpose::STANDARD
874 .decode(&envelope.crypto.ciphertext)
875 .map_err(|_| "Invalid secret envelope ciphertext".to_string())?;
876 if ciphertext.len() <= KEYSTORE_V2_TAG_BYTES
877 || ciphertext.len() > SECRET_ENVELOPE_MAX_BYTES + KEYSTORE_V2_TAG_BYTES
878 {
879 return Err("Invalid secret envelope ciphertext".to_string());
880 }
881 let key = derive_keystore_v2_key(password, &salt)?;
882 let cipher = Aes256Gcm::new_from_slice(key.as_ref())
883 .map_err(|_| "AES-256-GCM initialization failed".to_string())?;
884 let plaintext = Zeroizing::new(
885 cipher
886 .decrypt(
887 aes_gcm::Nonce::from_slice(&nonce_bytes),
888 Payload {
889 msg: &ciphertext,
890 aad: SECRET_ENVELOPE_AAD,
891 },
892 )
893 .map_err(|_| "Secret password or authenticated data is invalid".to_string())?,
894 );
895 String::from_utf8(plaintext.to_vec())
896 .map_err(|_| "Decrypted secret is not valid UTF-8".to_string())
897 }
898
899 pub fn decrypt_with_password(encrypted_data: &str, password: &str) -> EncryptionResult<String> {
928 if encrypted_data.trim_start().starts_with('{') {
929 return Self::keypair_from_encrypted_json(encrypted_data, password)
930 .map(|keypair| keypair.to_base58_string());
931 }
932
933 validate_password_size(password)?;
934 let key = Zeroizing::new(generate_encryption_key_simple(password));
935 let plaintext = Zeroizing::new(decrypt_key_to_bytes(encrypted_data, &key)?);
936 parse_decrypted_keypair(plaintext.as_slice()).map(|keypair| keypair.to_base58_string())
937 }
938
939 pub fn get_public_key(private_key: &str) -> EncryptionResult<String> {
949 parse_base58_keypair(private_key).map(|keypair| keypair.pubkey().to_string())
950 }
951
952 pub fn keypair_to_encrypted_json(
965 keypair: &Keypair,
966 password: &str,
967 ) -> EncryptionResult<String> {
968 encrypt_keystore_v2(keypair, password)
969 }
970
971 pub fn mnemonic_to_encrypted_json(
973 mnemonic: &str,
974 derivation_path: &str,
975 password: &str,
976 ) -> EncryptionResult<String> {
977 encrypt_mnemonic_keystore_v3(mnemonic, derivation_path, password)
978 }
979
980 pub fn keypair_from_mnemonic(
982 mnemonic: &str,
983 derivation_path: &str,
984 ) -> EncryptionResult<Keypair> {
985 let mnemonic = Zeroizing::new(normalize_mnemonic(mnemonic)?);
986 keypair_from_mnemonic_and_path(&mnemonic, derivation_path.trim())
987 }
988
989 pub fn mnemonic_from_encrypted_json(
991 json_data: &str,
992 password: &str,
993 ) -> EncryptionResult<Zeroizing<String>> {
994 if Self::keystore_version(json_data)? != KeystoreVersion::V3 {
995 return Err("Keystore does not contain a primary mnemonic secret".to_string());
996 }
997 decrypt_mnemonic_keystore_v3(json_data, password).map(|decrypted| decrypted.mnemonic)
998 }
999
1000 pub fn keystore_version(json_data: &str) -> EncryptionResult<KeystoreVersion> {
1002 if json_data.len() > MAX_KEYSTORE_JSON_BYTES {
1003 return Err("Keystore JSON is too large".to_string());
1004 }
1005 let value: serde_json::Value =
1006 serde_json::from_str(json_data).map_err(|_| "Invalid JSON format".to_string())?;
1007 let version = value.get("version");
1008 match version {
1009 Some(serde_json::Value::Number(number)) if number.as_u64() == Some(3) => {
1010 if value.get("crypto").is_none()
1011 || value.get("secret_type").and_then(serde_json::Value::as_str)
1012 != Some("mnemonic")
1013 {
1014 return Err("V3 mnemonic keystore is missing required fields".to_string());
1015 }
1016 Ok(KeystoreVersion::V3)
1017 }
1018 Some(serde_json::Value::Number(number)) if number.as_u64() == Some(2) => {
1019 if value.get("crypto").is_none() {
1020 return Err("V2 keystore is missing crypto".to_string());
1021 }
1022 Ok(KeystoreVersion::V2)
1023 }
1024 Some(serde_json::Value::Number(number)) if number.as_u64() == Some(1) => {
1025 if value
1026 .get("encrypted_private_key")
1027 .and_then(serde_json::Value::as_str)
1028 .is_none()
1029 {
1030 return Err("Legacy keystore is missing encrypted_private_key".to_string());
1031 }
1032 Ok(KeystoreVersion::LegacyV1)
1033 }
1034 None => {
1035 if value
1036 .get("encrypted_private_key")
1037 .and_then(serde_json::Value::as_str)
1038 .is_none()
1039 {
1040 return Err("Unrecognized keystore format".to_string());
1041 }
1042 Ok(KeystoreVersion::LegacyV1)
1043 }
1044 Some(_) => Err("Unsupported keystore version".to_string()),
1045 }
1046 }
1047
1048 pub fn keypair_from_encrypted_json_v2(
1050 json_data: &str,
1051 password: &str,
1052 ) -> EncryptionResult<Keypair> {
1053 match Self::keystore_version(json_data)? {
1054 KeystoreVersion::V2 => decrypt_keystore_v2(json_data, password),
1055 KeystoreVersion::V3 => {
1056 Err("V3 mnemonic keystore is not a v2 private-key keystore".to_string())
1057 }
1058 KeystoreVersion::LegacyV1 => Err(
1059 "Legacy v1 keystore is not allowed for this operation; migrate it explicitly"
1060 .to_string(),
1061 ),
1062 }
1063 }
1064
1065 pub fn keypair_from_encrypted_json(
1067 json_data: &str,
1068 password: &str,
1069 ) -> EncryptionResult<Keypair> {
1070 match Self::keystore_version(json_data)? {
1071 KeystoreVersion::LegacyV1 => decrypt_legacy_keystore(json_data, password),
1072 KeystoreVersion::V2 => decrypt_keystore_v2(json_data, password),
1073 KeystoreVersion::V3 => {
1074 decrypt_mnemonic_keystore_v3(json_data, password).map(|decrypted| decrypted.keypair)
1075 }
1076 }
1077 }
1078
1079 pub fn migrate_encrypted_json_to_v2(
1081 json_data: &str,
1082 legacy_password: &str,
1083 new_password: &str,
1084 ) -> EncryptionResult<String> {
1085 Self::migrate_encrypted_json_to_v2_with_public_key(json_data, legacy_password, new_password)
1086 .map(|(json, _)| json)
1087 }
1088
1089 pub fn migrate_encrypted_json_to_v2_with_public_key(
1091 json_data: &str,
1092 legacy_password: &str,
1093 new_password: &str,
1094 ) -> EncryptionResult<(String, String)> {
1095 if Self::keystore_version(json_data)? != KeystoreVersion::LegacyV1 {
1096 return Err("Only legacy v1 keystores can be migrated".to_string());
1097 }
1098 let value: serde_json::Value =
1099 serde_json::from_str(json_data).map_err(|_| "Invalid JSON format".to_string())?;
1100 let claimed = claimed_public_key(&value, true)?
1101 .ok_or_else(|| "Legacy keystore is missing public_key".to_string())?;
1102 let keypair = decrypt_legacy_keystore(json_data, legacy_password)?;
1103 if keypair.pubkey() != claimed {
1104 return Err("Legacy keystore public_key does not match decrypted keypair".to_string());
1105 }
1106 let verified_public_key = keypair.pubkey().to_string();
1107 encrypt_keystore_v2(&keypair, new_password).map(|json| (json, verified_public_key))
1108 }
1109}
1110
1111#[cfg(feature = "2fa")]
1120#[allow(dead_code)]
1124fn derive_totp_secret_from_password(
1125 password: &str,
1126 account: &str,
1127 issuer: &str,
1128) -> Result<String, String> {
1129 use data_encoding::BASE32_NOPAD;
1130 use ring::pbkdf2;
1131 use std::num::NonZeroU32;
1132
1133 let salt = format!("sol-safekey-totp-{}-{}", issuer, account);
1134 let iterations = NonZeroU32::new(100_000).ok_or("Invalid iteration count")?;
1135
1136 let mut secret = [0u8; 20]; pbkdf2::derive(
1138 pbkdf2::PBKDF2_HMAC_SHA256,
1139 iterations,
1140 salt.as_bytes(),
1141 password.as_bytes(),
1142 &mut secret,
1143 );
1144
1145 Ok(BASE32_NOPAD.encode(&secret))
1146}
1147
1148#[cfg(feature = "2fa")]
1149pub fn derive_totp_secret_from_hardware_and_password(
1153 hardware_fingerprint: &str,
1154 master_password: &str,
1155 account: &str,
1156 issuer: &str,
1157) -> Result<String, String> {
1158 use data_encoding::BASE32_NOPAD;
1159 use ring::pbkdf2;
1160 use std::num::NonZeroU32;
1161
1162 let key_material = format!("{}::{}", hardware_fingerprint, master_password);
1163 let salt = format!("sol-safekey-2fa-{}-{}", issuer, account);
1164 let iterations = NonZeroU32::new(100_000).ok_or("Invalid iteration count")?;
1165
1166 let mut secret = [0u8; 20];
1167 pbkdf2::derive(
1168 pbkdf2::PBKDF2_HMAC_SHA256,
1169 iterations,
1170 salt.as_bytes(),
1171 key_material.as_bytes(),
1172 &mut secret,
1173 );
1174
1175 Ok(BASE32_NOPAD.encode(&secret))
1176}
1177
1178#[cfg(feature = "2fa")]
1179fn verify_current_totp_code(totp_secret: &str, current_code: &str) -> Result<(), String> {
1181 use crate::totp::{TOTPConfig, TOTPManager};
1182
1183 let config = TOTPConfig {
1184 secret: totp_secret.to_string(),
1185 account: "wallet".to_string(),
1186 issuer: "FnzSafe".to_string(),
1187 algorithm: "SHA1".to_string(),
1188 digits: 6,
1189 step: 30,
1190 };
1191
1192 let totp_manager = TOTPManager::new(config);
1193
1194 match totp_manager.verify_code(current_code) {
1195 Ok(true) => Ok(()),
1196 Ok(false) => Err("验证失败,请检查主密码、安全问题答案或2FA验证码".to_string()),
1197 Err(e) => Err(format!("验证失败: {}", e)),
1198 }
1199}
1200
1201#[cfg(feature = "2fa")]
1206pub fn generate_triple_factor_key(
1210 hardware_fingerprint: &str,
1211 master_password: &str,
1212 security_answer: &str,
1213) -> [u8; 32] {
1214 use ring::pbkdf2;
1215 use std::num::NonZeroU32;
1216
1217 let key_material = format!(
1218 "HW:{}|PASS:{}|QA:{}",
1219 hardware_fingerprint,
1220 master_password,
1221 security_answer.trim().to_lowercase()
1222 );
1223
1224 let salt = b"sol-safekey-triple-factor-v1";
1225 let iterations = NonZeroU32::new(200_000).unwrap();
1226
1227 let mut key = [0u8; 32];
1228 pbkdf2::derive(
1229 pbkdf2::PBKDF2_HMAC_SHA256,
1230 iterations,
1231 salt,
1232 key_material.as_bytes(),
1233 &mut key,
1234 );
1235
1236 key
1237}
1238
1239#[cfg(feature = "2fa")]
1240pub fn encrypt_with_triple_factor(
1244 private_key: &str,
1245 twofa_secret: &str,
1246 hardware_fingerprint: &str,
1247 master_password: &str,
1248 question_index: usize,
1249 security_answer: &str,
1250) -> Result<String, String> {
1251 use serde_json::json;
1252
1253 let encryption_key =
1254 generate_triple_factor_key(hardware_fingerprint, master_password, security_answer);
1255
1256 let data_package = json!({
1257 "private_key": private_key,
1258 "twofa_secret": twofa_secret,
1259 "question_index": question_index,
1260 "version": "triple_factor_v1",
1261 "created_at": std::time::SystemTime::now()
1262 .duration_since(std::time::UNIX_EPOCH)
1263 .unwrap()
1264 .as_secs()
1265 });
1266
1267 let package_str = data_package.to_string();
1268 let encrypted = encrypt_key(&package_str, &encryption_key)?;
1269
1270 Ok(encrypted)
1271}
1272
1273#[cfg(feature = "2fa")]
1274pub fn decrypt_with_triple_factor_and_2fa(
1278 encrypted_data: &str,
1279 hardware_fingerprint: &str,
1280 master_password: &str,
1281 security_answer: &str,
1282 twofa_code: &str,
1283) -> Result<(String, String, usize), String> {
1284 let decryption_key =
1285 generate_triple_factor_key(hardware_fingerprint, master_password, security_answer);
1286
1287 let decrypted = decrypt_key(encrypted_data, &decryption_key)
1288 .map_err(|_| "解密失败,请检查主密码、安全问题答案是否正确")?;
1289
1290 let data: serde_json::Value = serde_json::from_str(&decrypted)
1291 .map_err(|_| "解密失败,请检查主密码、安全问题答案是否正确")?;
1292
1293 let private_key = data["private_key"]
1294 .as_str()
1295 .ok_or("缺少私钥数据")?
1296 .to_string();
1297
1298 let twofa_secret = data["twofa_secret"]
1299 .as_str()
1300 .ok_or("缺少2FA密钥数据")?
1301 .to_string();
1302
1303 let question_index = data["question_index"].as_u64().ok_or("缺少安全问题索引")? as usize;
1304
1305 verify_current_totp_code(&twofa_secret, twofa_code)?;
1307
1308 Ok((private_key, twofa_secret, question_index))
1309}
1310
1311#[cfg(test)]
1316mod tests {
1317 use super::*;
1318 use serde_json::json;
1319
1320 fn legacy_keystore_json(keypair: &Keypair, password: &str, public_key: Option<&str>) -> String {
1321 let key = Zeroizing::new(generate_encryption_key_simple(password));
1322 let private_key = Zeroizing::new(keypair.to_base58_string());
1323 let encrypted = encrypt_key(private_key.as_str(), &key).unwrap();
1324 let mut value = json!({
1325 "encrypted_private_key": encrypted,
1326 "encryption_type": KEYSTORE_ENCRYPTION_TYPE,
1327 });
1328 if let Some(public_key) = public_key {
1329 value["public_key"] = serde_json::Value::String(public_key.to_string());
1330 }
1331 value.to_string()
1332 }
1333
1334 #[test]
1335 fn test_generate_keypair() {
1336 let keypair = KeyManager::generate_keypair();
1337 assert_eq!(keypair.to_bytes().len(), 64);
1338 }
1339
1340 #[test]
1341 fn test_encrypt_decrypt_with_password() {
1342 let keypair = KeyManager::generate_keypair();
1343 let private_key = keypair.to_base58_string();
1344 let password = "test_password_123";
1345
1346 let encrypted = KeyManager::encrypt_with_password(&private_key, password).unwrap();
1347 assert_eq!(
1348 KeyManager::keystore_version(&encrypted).unwrap(),
1349 KeystoreVersion::V2
1350 );
1351 let decrypted = KeyManager::decrypt_with_password(&encrypted, password).unwrap();
1352
1353 assert_eq!(private_key, decrypted);
1354 }
1355
1356 #[test]
1357 fn test_get_public_key() {
1358 let keypair = KeyManager::generate_keypair();
1359 let private_key = keypair.to_base58_string();
1360 let expected_pubkey = keypair.pubkey().to_string();
1361
1362 let pubkey = KeyManager::get_public_key(&private_key).unwrap();
1363 assert_eq!(pubkey, expected_pubkey);
1364
1365 for invalid in ["", "not base58", "11111111111111111111111111111111"] {
1366 let result = std::panic::catch_unwind(|| KeyManager::get_public_key(invalid));
1367 assert!(result.is_ok());
1368 assert!(result.unwrap().is_err());
1369 }
1370 }
1371
1372 #[test]
1373 fn test_keystore_json_round_trip() {
1374 let keypair = KeyManager::generate_keypair();
1375 let password = "secure_password";
1376
1377 let json = KeyManager::keypair_to_encrypted_json(&keypair, password).unwrap();
1378 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1379 assert_eq!(value["version"], KEYSTORE_V2_VERSION);
1380 assert_eq!(value["crypto"]["kdf"], KEYSTORE_V2_KDF);
1381 assert_eq!(value["crypto"]["cipher"], KEYSTORE_V2_CIPHER);
1382 assert!(value.get("encrypted_private_key").is_none());
1383 let restored_keypair = KeyManager::keypair_from_encrypted_json_v2(&json, password).unwrap();
1384
1385 assert_eq!(keypair.to_bytes(), restored_keypair.to_bytes());
1386 }
1387
1388 #[test]
1389 fn mnemonic_keystore_v3_round_trip_and_metadata_authentication() {
1390 const MNEMONIC: &str =
1391 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
1392 const PATH: &str = "m/44'/501'/0'/0'";
1393 let password = "secure_password";
1394 let json = KeyManager::mnemonic_to_encrypted_json(MNEMONIC, PATH, password).unwrap();
1395 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1396
1397 assert_eq!(value["version"], KEYSTORE_V3_VERSION);
1398 assert_eq!(value["secret_type"], "mnemonic");
1399 assert_eq!(value["derivation_path"], PATH);
1400 assert!(!json.contains(MNEMONIC));
1401 assert_eq!(
1402 KeyManager::keystore_version(&json).unwrap(),
1403 KeystoreVersion::V3
1404 );
1405 assert_eq!(
1406 KeyManager::mnemonic_from_encrypted_json(&json, password)
1407 .unwrap()
1408 .as_str(),
1409 MNEMONIC
1410 );
1411 let restored = KeyManager::keypair_from_encrypted_json(&json, password).unwrap();
1412 assert_eq!(restored.pubkey().to_string(), value["public_key"]);
1413
1414 for field in ["public_key", "secret_type", "derivation_path"] {
1415 let mut tampered = value.clone();
1416 tampered[field] = match field {
1417 "public_key" => serde_json::Value::String(Keypair::new().pubkey().to_string()),
1418 "secret_type" => serde_json::Value::String("private_key".to_string()),
1419 _ => serde_json::Value::String("m/44'/501'/1'/0'".to_string()),
1420 };
1421 assert!(
1422 KeyManager::keypair_from_encrypted_json(&tampered.to_string(), password).is_err()
1423 );
1424 }
1425 }
1426
1427 #[test]
1428 fn test_wrong_password_fails() {
1429 let keypair = KeyManager::generate_keypair();
1430 let private_key = keypair.to_base58_string();
1431
1432 let encrypted =
1433 KeyManager::encrypt_with_password(&private_key, "correct-password").unwrap();
1434 let result = KeyManager::decrypt_with_password(&encrypted, "wrong-password");
1435
1436 assert!(result.is_err());
1437 }
1438
1439 #[test]
1440 fn v2_uses_fresh_salt_and_nonce() {
1441 let keypair = KeyManager::generate_keypair();
1442 let first: serde_json::Value = serde_json::from_str(
1443 &KeyManager::keypair_to_encrypted_json(&keypair, "secure_password").unwrap(),
1444 )
1445 .unwrap();
1446 let second: serde_json::Value = serde_json::from_str(
1447 &KeyManager::keypair_to_encrypted_json(&keypair, "secure_password").unwrap(),
1448 )
1449 .unwrap();
1450
1451 assert_ne!(
1452 first["crypto"]["kdf_params"]["salt"],
1453 second["crypto"]["kdf_params"]["salt"]
1454 );
1455 assert_ne!(first["crypto"]["nonce"], second["crypto"]["nonce"]);
1456 assert_ne!(
1457 first["crypto"]["ciphertext"],
1458 second["crypto"]["ciphertext"]
1459 );
1460 }
1461
1462 #[test]
1463 fn v2_rejects_tampered_ciphertext_and_public_key() {
1464 let keypair = KeyManager::generate_keypair();
1465 let password = "secure_password";
1466 let json = KeyManager::keypair_to_encrypted_json(&keypair, password).unwrap();
1467 let mut tampered_ciphertext: serde_json::Value = serde_json::from_str(&json).unwrap();
1468 let mut ciphertext = general_purpose::STANDARD
1469 .decode(
1470 tampered_ciphertext["crypto"]["ciphertext"]
1471 .as_str()
1472 .unwrap(),
1473 )
1474 .unwrap();
1475 ciphertext[0] ^= 1;
1476 tampered_ciphertext["crypto"]["ciphertext"] =
1477 serde_json::Value::String(general_purpose::STANDARD.encode(ciphertext));
1478 assert!(KeyManager::keypair_from_encrypted_json_v2(
1479 &tampered_ciphertext.to_string(),
1480 password
1481 )
1482 .is_err());
1483
1484 let mut tampered_public_key: serde_json::Value = serde_json::from_str(&json).unwrap();
1485 tampered_public_key["public_key"] =
1486 serde_json::Value::String(Keypair::new().pubkey().to_string());
1487 assert!(KeyManager::keypair_from_encrypted_json_v2(
1488 &tampered_public_key.to_string(),
1489 password
1490 )
1491 .is_err());
1492 }
1493
1494 #[test]
1495 fn v2_rejects_unbounded_or_unknown_kdf_parameters_before_derivation() {
1496 let keypair = KeyManager::generate_keypair();
1497 let password = "secure_password";
1498 let json = KeyManager::keypair_to_encrypted_json(&keypair, password).unwrap();
1499 let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
1500 value["crypto"]["kdf_params"]["memory_kib"] = json!(u32::MAX);
1501
1502 assert!(KeyManager::keypair_from_encrypted_json_v2(&value.to_string(), password).is_err());
1503 }
1504
1505 #[test]
1506 fn legacy_keystore_is_read_only_and_migrates_explicitly() {
1507 let keypair = KeyManager::generate_keypair();
1508 let legacy_password = "old";
1509 let legacy = legacy_keystore_json(
1510 &keypair,
1511 legacy_password,
1512 Some(&keypair.pubkey().to_string()),
1513 );
1514 assert_eq!(
1515 KeyManager::keystore_version(&legacy).unwrap(),
1516 KeystoreVersion::LegacyV1
1517 );
1518 assert_eq!(
1519 KeyManager::keypair_from_encrypted_json(&legacy, legacy_password)
1520 .unwrap()
1521 .pubkey(),
1522 keypair.pubkey()
1523 );
1524 assert!(KeyManager::keypair_from_encrypted_json_v2(&legacy, legacy_password).is_err());
1525
1526 let (migrated, verified_public_key) =
1527 KeyManager::migrate_encrypted_json_to_v2_with_public_key(
1528 &legacy,
1529 legacy_password,
1530 "new-secure-password",
1531 )
1532 .unwrap();
1533 assert_eq!(verified_public_key, keypair.pubkey().to_string());
1534 assert_eq!(
1535 KeyManager::keystore_version(&migrated).unwrap(),
1536 KeystoreVersion::V2
1537 );
1538 assert_eq!(
1539 KeyManager::keypair_from_encrypted_json_v2(&migrated, "new-secure-password")
1540 .unwrap()
1541 .pubkey(),
1542 keypair.pubkey()
1543 );
1544 }
1545
1546 #[test]
1547 fn migration_rejects_missing_or_mismatched_public_key() {
1548 let keypair = KeyManager::generate_keypair();
1549 let password = "legacy-password";
1550 let missing = legacy_keystore_json(&keypair, password, None);
1551 assert!(KeyManager::migrate_encrypted_json_to_v2(
1552 &missing,
1553 password,
1554 "new-secure-password"
1555 )
1556 .is_err());
1557
1558 let mismatch = legacy_keystore_json(
1559 &keypair,
1560 password,
1561 Some(&Keypair::new().pubkey().to_string()),
1562 );
1563 assert!(KeyManager::migrate_encrypted_json_to_v2(
1564 &mismatch,
1565 password,
1566 "new-secure-password"
1567 )
1568 .is_err());
1569 }
1570
1571 #[test]
1572 fn v2_creation_enforces_password_bounds() {
1573 let keypair = KeyManager::generate_keypair();
1574 assert!(KeyManager::keypair_to_encrypted_json(&keypair, "123456789").is_err());
1575 assert!(KeyManager::keypair_to_encrypted_json(
1576 &keypair,
1577 &"x".repeat(MAX_PASSWORD_LENGTH + 1)
1578 )
1579 .is_err());
1580 }
1581
1582 #[test]
1583 fn version_detection_rejects_unknown_or_malformed_formats() {
1584 assert!(KeyManager::keystore_version("not-json").is_err());
1585 assert!(KeyManager::keystore_version(r#"{"version":3}"#).is_err());
1586 assert!(KeyManager::keystore_version(r#"{"version":2}"#).is_err());
1587 assert!(KeyManager::keystore_version("{}").is_err());
1588 }
1589
1590 #[test]
1591 fn authenticated_secret_envelope_round_trips_and_rejects_tampering() {
1592 let password = "secure-secret-password";
1593 let secret = "test test test test test test test test test test test junk";
1594 let encrypted = KeyManager::encrypt_secret_with_password(secret, password).unwrap();
1595 assert_eq!(
1596 KeyManager::decrypt_secret_with_password(&encrypted, password).unwrap(),
1597 secret
1598 );
1599 assert!(KeyManager::decrypt_secret_with_password(&encrypted, "wrong-password").is_err());
1600 let mut value: serde_json::Value = serde_json::from_str(&encrypted).unwrap();
1601 value["kind"] = json!("different-secret-kind");
1602 assert!(KeyManager::decrypt_secret_with_password(&value.to_string(), password).is_err());
1603 }
1604}