use aes_gcm::aead::{Aead, KeyInit};
use aes_gcm::{Aes256Gcm, Key, Nonce};
use argon2::{Argon2, Params};
use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;
use super::error::{CliError, Result};
const SALT_LEN: usize = 16;
const NONCE_LEN: usize = 12;
const ARGON2_M_COST_KIB: u32 = 19_456; const ARGON2_T_COST: u32 = 2; const ARGON2_P_COST: u32 = 1;
#[derive(Serialize, Deserialize)]
pub struct SecretsBundle {
pub nostr_secret_key: String,
pub solana_secret_key: String,
pub llm_api_key: String,
#[serde(default)]
pub customer_llm_api_key: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptionSection {
pub ciphertext: String, pub salt: String, pub nonce: String, }
fn derive_key(password: &str, salt: &[u8]) -> Result<Zeroizing<[u8; 32]>> {
let params = Params::new(ARGON2_M_COST_KIB, ARGON2_T_COST, ARGON2_P_COST, Some(32))
.map_err(|e| CliError::Other(format!("invalid argon2 params: {}", e)))?;
let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
let mut key = Zeroizing::new([0u8; 32]);
argon2
.hash_password_into(password.as_bytes(), salt, &mut *key)
.map_err(|e| CliError::Other(format!("key derivation failed: {}", e)))?;
Ok(key)
}
fn random_bytes<const N: usize>() -> [u8; N] {
let mut buf = [0u8; N];
getrandom::getrandom(&mut buf).expect("failed to generate random bytes");
buf
}
pub fn encrypt_secrets(bundle: &SecretsBundle, password: &str) -> Result<EncryptionSection> {
let plaintext = Zeroizing::new(
serde_json::to_vec(bundle)
.map_err(|e| CliError::Other(format!("failed to serialize secrets: {}", e)))?,
);
let salt = random_bytes::<SALT_LEN>();
let nonce_bytes = random_bytes::<NONCE_LEN>();
let key = derive_key(password, &salt)?;
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&*key));
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, plaintext.as_ref())
.map_err(|e| CliError::Other(format!("encryption failed: {}", e)))?;
Ok(EncryptionSection {
ciphertext: bs58::encode(&ciphertext).into_string(),
salt: bs58::encode(salt).into_string(),
nonce: bs58::encode(nonce_bytes).into_string(),
})
}
pub fn decrypt_secrets(section: &EncryptionSection, password: &str) -> Result<SecretsBundle> {
let ciphertext = bs58::decode(§ion.ciphertext)
.into_vec()
.map_err(|e| CliError::Other(format!("invalid ciphertext encoding: {}", e)))?;
let salt = bs58::decode(§ion.salt)
.into_vec()
.map_err(|e| CliError::Other(format!("invalid salt encoding: {}", e)))?;
let nonce_bytes = bs58::decode(§ion.nonce)
.into_vec()
.map_err(|e| CliError::Other(format!("invalid nonce encoding: {}", e)))?;
if salt.len() != SALT_LEN {
return Err(CliError::Other(format!(
"invalid salt length: expected {} bytes, got {}",
SALT_LEN,
salt.len()
)));
}
if nonce_bytes.len() != NONCE_LEN {
return Err(CliError::Other(format!(
"invalid nonce length: expected {} bytes, got {}",
NONCE_LEN,
nonce_bytes.len()
)));
}
let key = derive_key(password, &salt)?;
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&*key));
let nonce = Nonce::from_slice(&nonce_bytes);
let plaintext = Zeroizing::new(
cipher
.decrypt(nonce, ciphertext.as_ref())
.map_err(|_| CliError::Other("wrong password or corrupted data".into()))?,
);
let bundle: SecretsBundle = serde_json::from_slice(&plaintext)
.map_err(|e| CliError::Other(format!("failed to parse decrypted secrets: {}", e)))?;
Ok(bundle)
}