use aes_gcm::aead::{Aead, KeyInit};
use aes_gcm::{Aes256Gcm, Key, Nonce};
use anyhow::{Context, Result};
use argon2::{Argon2, Params};
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
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(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptionSection {
pub ciphertext: String, pub salt: String, pub nonce: String, }
#[derive(Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
pub struct SecretsBundle {
pub nostr_secret_key: String,
pub solana_secret_key: String,
#[serde(default)]
pub llm_api_key: String,
#[serde(default)]
pub customer_llm_api_key: Option<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| anyhow::anyhow!("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| anyhow::anyhow!("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 decrypt_secrets(section: &EncryptionSection, password: &str) -> Result<SecretsBundle> {
let ciphertext = bs58::decode(§ion.ciphertext)
.into_vec()
.context("invalid ciphertext encoding")?;
let salt = bs58::decode(§ion.salt)
.into_vec()
.context("invalid salt encoding")?;
let nonce_bytes: [u8; NONCE_LEN] = bs58::decode(§ion.nonce)
.into_vec()
.context("invalid nonce encoding")?
.try_into()
.map_err(|v: Vec<u8>| anyhow::anyhow!("nonce must be {NONCE_LEN} bytes, got {}", v.len()))?;
anyhow::ensure!(salt.len() == SALT_LEN, "salt must be {SALT_LEN} bytes");
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(|_| anyhow::anyhow!("wrong password or corrupted data"))?,
);
let bundle: SecretsBundle = serde_json::from_slice(&plaintext)
.context("failed to parse decrypted secrets")?;
Ok(bundle)
}
pub fn encrypt_secrets(bundle: &SecretsBundle, password: &str) -> Result<EncryptionSection> {
let plaintext = Zeroizing::new(
serde_json::to_vec(bundle).context("failed to serialize secrets")?,
);
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| anyhow::anyhow!("encryption failed: {e}"))?;
Ok(EncryptionSection {
ciphertext: bs58::encode(&ciphertext).into_string(),
salt: bs58::encode(salt).into_string(),
nonce: bs58::encode(nonce_bytes).into_string(),
})
}