Skip to main content

fnzero_safe/
lib.rs

1//! # FnzSafe
2//!
3//! A Solana key management library with authenticated password keystores.
4//!
5//! ## Features
6//!
7//! - **Simple Encryption**: Password-based encryption for Solana private keys
8//! - **Triple-Factor Authentication**: Hardware fingerprint + master password + security question
9//! - **2FA Support**: TOTP-based two-factor authentication
10//! - **Cross-Platform**: Works on macOS, Linux, and Windows
11//!
12//! ## Quick Start
13//!
14//! ```rust
15//! use fnzero_safe::{KeyManager, EncryptionResult};
16//!
17//! // Generate a new Solana keypair
18//! let keypair = KeyManager::generate_keypair();
19//!
20//! // Encrypt with password
21//! let encrypted = KeyManager::encrypt_with_password(
22//!     &keypair.to_base58_string(),
23//!     "my_strong_password"
24//! ).unwrap();
25//!
26//! // Decrypt with password
27//! let decrypted = KeyManager::decrypt_with_password(
28//!     &encrypted,
29//!     "my_strong_password"
30//! ).unwrap();
31//! ```
32
33use 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// Re-export modules for advanced usage (conditional compilation)
49#[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
61// Interactive menu module - needed for bot integration
62pub mod interactive;
63
64// Bot helper module for easy bot integration (no CLI dependency)
65pub mod bot_helper;
66
67// Solana operations interactive menu
68pub mod operations;
69
70// Solana utilities for token operations
71#[cfg(any(
72    feature = "solana-ops",
73    feature = "mobile-solana-ops",
74    feature = "sol-trade-sdk"
75))]
76pub mod solana_utils;
77
78// Re-export commonly used types
79pub use solana_sdk::pubkey::Pubkey;
80pub use solana_sdk::signature::{Keypair, Signer};
81
82// ============================================================================
83// Core Encryption/Decryption Functions
84// ============================================================================
85
86/// Legacy XOR encryption/decryption retained for old non-keystore formats.
87/// Do not use this unauthenticated construction for new encrypted data.
88fn xor_encrypt_decrypt(data: &[u8], key: &[u8; 32]) -> Vec<u8> {
89    let mut result = Vec::with_capacity(data.len());
90
91    // Generate keystream from the key
92    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    // XOR operation
104    for (i, &byte) in data.iter().enumerate() {
105        result.push(byte ^ keystream[i % keystream.len()]);
106    }
107
108    result
109}
110
111/// Encrypt a string with the legacy unauthenticated format.
112///
113/// Returns base64-encoded encrypted data
114pub 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
120/// Decrypt a base64-encoded legacy encrypted string with a 32-byte key.
121///
122/// Returns the original plaintext string
123pub 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
130/// Decrypt to raw bytes (used when plaintext may be base58 string or 64-byte keypair).
131pub 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
141/// Strip trailing bytes that are not valid base58 (e.g. 0x00, \n, \r, or non-ASCII).
142fn 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
155/// Minimum password length for encryption/decryption
156pub const MIN_PASSWORD_LENGTH: usize = 10;
157
158/// Maximum password size accepted by v2 operations, measured in UTF-8 bytes.
159pub const MAX_PASSWORD_LENGTH: usize = 1024;
160
161/// Fixed salt retained exclusively for read compatibility with legacy v1 data.
162const PASSWORD_SALT: &[u8] = b"sol-safekey-v1-salt-2025";
163
164/// Derive the legacy v1 key. New password keystores use Argon2id instead.
165pub fn generate_encryption_key_simple(password: &str) -> [u8; 32] {
166    // Combine password with fixed salt
167    let mut salted_password = Zeroizing::new(password.as_bytes().to_vec());
168    salted_password.extend_from_slice(PASSWORD_SALT);
169
170    // Hash the salted password using SHA-256
171    let hash = digest::digest(&digest::SHA256, &salted_password);
172
173    // Take the first 16 bytes of the hash
174    let mut key = [0u8; 32];
175    key[0..16].copy_from_slice(&hash.as_ref()[0..16]);
176
177    // Fill the remaining 16 bytes by repeating the first 16 bytes
178    // This ensures we have a 32-byte key for compatibility
179    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
739// ============================================================================
740// High-Level Key Management API (简单集成用)
741// ============================================================================
742
743/// Result type for encryption operations
744pub type EncryptionResult<T> = Result<T, String>;
745
746/// Main interface for key management operations
747///
748/// This is the recommended API for library integration.
749/// It provides simple, safe methods for common key operations.
750pub struct KeyManager;
751
752impl KeyManager {
753    /// Generate a new Solana keypair
754    ///
755    /// # Example
756    ///
757    /// ```
758    /// use fnzero_safe::KeyManager;
759    /// use solana_sdk::signature::Signer;
760    ///
761    /// let keypair = KeyManager::generate_keypair();
762    /// println!("Public key: {}", keypair.pubkey());
763    /// ```
764    pub fn generate_keypair() -> Keypair {
765        Keypair::new()
766    }
767
768    /// Encrypt a private key with a password
769    ///
770    /// # Arguments
771    ///
772    /// * `private_key` - The private key in base58 string format
773    /// * `password` - The password to use for encryption
774    ///
775    /// # Returns
776    ///
777    /// A serialized authenticated v2 keystore envelope
778    ///
779    /// # Example
780    ///
781    /// ```
782    /// use fnzero_safe::KeyManager;
783    ///
784    /// let keypair = KeyManager::generate_keypair();
785    /// let private_key = keypair.to_base58_string();
786    ///
787    /// let encrypted = KeyManager::encrypt_with_password(
788    ///     &private_key,
789    ///     "my_password"
790    /// ).unwrap();
791    /// ```
792    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    /// Encrypt arbitrary UTF-8 secret material in an authenticated, password-derived envelope.
798    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    /// Decrypt an authenticated UTF-8 secret envelope.
841    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    /// Decrypt a private key with a password
900    ///
901    /// # Arguments
902    ///
903    /// * `encrypted_data` - A v2 envelope or legacy v1 ciphertext
904    /// * `password` - The password used for encryption
905    ///
906    /// # Returns
907    ///
908    /// The original private key in base58 string format
909    ///
910    /// # Example
911    ///
912    /// ```
913    /// use fnzero_safe::KeyManager;
914    ///
915    /// let keypair = KeyManager::generate_keypair();
916    /// let private_key = keypair.to_base58_string();
917    /// let encrypted = KeyManager::encrypt_with_password(
918    ///     &private_key,
919    ///     "my_password"
920    /// ).unwrap();
921    /// let decrypted = KeyManager::decrypt_with_password(
922    ///     &encrypted,
923    ///     "my_password"
924    /// ).unwrap();
925    /// assert_eq!(decrypted, private_key);
926    /// ```
927    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    /// Get public key from a private key
940    ///
941    /// # Arguments
942    ///
943    /// * `private_key` - Private key in base58 string format
944    ///
945    /// # Returns
946    ///
947    /// Public key as a base58 string
948    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    /// Encrypt a keypair to a JSON keystore format
953    ///
954    /// This creates a fnzero-safe v2 password keystore.
955    ///
956    /// # Arguments
957    ///
958    /// * `keypair` - The Solana keypair to encrypt
959    /// * `password` - The password for encryption
960    ///
961    /// # Returns
962    ///
963    /// JSON string containing the encrypted keystore
964    pub fn keypair_to_encrypted_json(
965        keypair: &Keypair,
966        password: &str,
967    ) -> EncryptionResult<String> {
968        encrypt_keystore_v2(keypair, password)
969    }
970
971    /// Encrypt a BIP-39 mnemonic as the primary keystore secret.
972    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    /// Derive a Solana keypair from a BIP-39 mnemonic and absolute path.
981    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    /// Decrypt the mnemonic from a v3 mnemonic keystore.
990    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    /// Detect the supported password-keystore format without decrypting it.
1001    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    /// Decrypt a supported v2 keystore, rejecting legacy unauthenticated formats.
1049    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    /// Decrypt a password keystore. Legacy v1 support is read-only.
1066    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    /// Explicitly decrypt a legacy v1 keystore and re-encrypt it as v2.
1080    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    /// Explicitly migrate legacy v1 and return the public key verified from plaintext.
1090    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// ============================================================================
1112// Advanced 2FA Functions (CLI 工具使用,库集成可选)
1113// ============================================================================
1114
1115// ============================================================================
1116// 2FA Functions (only available with "2fa" feature)
1117// ============================================================================
1118
1119#[cfg(feature = "2fa")]
1120/// Derive a TOTP secret from password
1121///
1122/// This is used internally for deterministic 2FA key generation.
1123#[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]; // 160 bits for TOTP
1137    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")]
1149/// Derive TOTP secret from hardware fingerprint and password
1150///
1151/// This creates a deterministic 2FA key bound to specific hardware.
1152pub 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")]
1179/// Verify a TOTP code
1180fn 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// ============================================================================
1202// Triple-Factor Encryption (only available with "2fa" feature)
1203// ============================================================================
1204
1205#[cfg(feature = "2fa")]
1206/// Generate a triple-factor encryption key
1207///
1208/// Combines hardware fingerprint + master password + security answer
1209pub 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")]
1240/// Encrypt with triple-factor authentication
1241///
1242/// Used by CLI for maximum security with device binding.
1243pub 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")]
1274/// Decrypt with triple-factor authentication and verify 2FA code
1275///
1276/// Used by CLI for unlocking triple-factor encrypted wallets.
1277pub 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 2FA code
1306    verify_current_totp_code(&twofa_secret, twofa_code)?;
1307
1308    Ok((private_key, twofa_secret, question_index))
1309}
1310
1311// ============================================================================
1312// Tests
1313// ============================================================================
1314
1315#[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}