use crate::CoreError;
use aes_gcm::aead::{Aead, KeyInit, OsRng};
use aes_gcm::{AeadCore, Aes256Gcm, Key, Nonce};
use argon2::{Algorithm, Argon2, Params, Version};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use pbkdf2::pbkdf2_hmac;
use rand::Rng;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use zeroize::{Zeroize, ZeroizeOnDrop};
pub const AES_256_KEY_SIZE: usize = 32;
pub const AES_GCM_NONCE_SIZE: usize = 12;
pub const PBKDF2_SALT_SIZE: usize = 16;
pub const MIN_ENCRYPTED_HEADER_SIZE: usize = PBKDF2_SALT_SIZE + AES_GCM_NONCE_SIZE;
pub const PBKDF2_ITERATIONS: u32 = 600_000;
pub const PBKDF2_ITERATIONS_LEGACY: u32 = 100_000;
const ENCRYPTED_PRIVATE_KEY_VERSION_V2: u8 = 2;
const ARGON2ID_MEMORY_COST_KIB: u32 = 19_456;
const ARGON2ID_TIME_COST: u32 = 2;
const ARGON2ID_PARALLELISM: u32 = 1;
#[derive(Clone)]
pub struct ZeroizingVec(Vec<u8>);
impl ZeroizingVec {
pub fn new(data: Vec<u8>) -> Self {
ZeroizingVec(data)
}
pub fn as_slice(&self) -> &[u8] {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl AsRef<[u8]> for ZeroizingVec {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl Zeroize for ZeroizingVec {
fn zeroize(&mut self) {
self.0.zeroize();
}
}
impl Drop for ZeroizingVec {
fn drop(&mut self) {
self.zeroize();
}
}
impl ZeroizeOnDrop for ZeroizingVec {}
impl std::fmt::Debug for ZeroizingVec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ZeroizingVec([REDACTED, {} bytes])", self.0.len())
}
}
#[derive(Debug, Serialize, Deserialize)]
struct KdfEnvelope {
name: String,
version: u32,
m_cost_kib: u32,
t_cost: u32,
p_cost: u32,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct EncryptedPrivateKeyEnvelope {
jacs_encrypted_private_key_version: u8,
cipher: String,
kdf: KdfEnvelope,
salt: String,
nonce: String,
ciphertext: String,
}
fn default_argon2id_kdf() -> KdfEnvelope {
KdfEnvelope {
name: "Argon2id".to_string(),
version: 19,
m_cost_kib: ARGON2ID_MEMORY_COST_KIB,
t_cost: ARGON2ID_TIME_COST,
p_cost: ARGON2ID_PARALLELISM,
}
}
fn derive_argon2id_key(
password: &str,
salt: &[u8],
kdf: &KdfEnvelope,
) -> Result<[u8; AES_256_KEY_SIZE], CoreError> {
if kdf.name != "Argon2id" || kdf.version != 19 {
return Err(CoreError::UnsupportedAlgorithm(format!(
"private key KDF '{}'/version {}",
kdf.name, kdf.version
)));
}
let params = Params::new(
kdf.m_cost_kib,
kdf.t_cost,
kdf.p_cost,
Some(AES_256_KEY_SIZE),
)
.map_err(|e| CoreError::MalformedEnvelope(format!("invalid Argon2id parameters: {e}")))?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut key = [0u8; AES_256_KEY_SIZE];
argon2
.hash_password_into(password.as_bytes(), salt, &mut key)
.map_err(|e| CoreError::DecryptionFailed(format!("Argon2id key derivation failed: {e}")))?;
Ok(key)
}
pub fn encrypt_v2_envelope(data: &[u8], password: &str) -> Result<Vec<u8>, CoreError> {
let mut salt = [0u8; PBKDF2_SALT_SIZE];
rand::rng().fill(&mut salt[..]);
let kdf = default_argon2id_kdf();
let mut key = derive_argon2id_key(password, &salt, &kdf)?;
let cipher_key = Key::<Aes256Gcm>::from_slice(&key);
let cipher = Aes256Gcm::new(cipher_key);
key.zeroize();
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let encrypted = cipher
.encrypt(&nonce, data)
.map_err(|e| CoreError::EncryptionFailed(format!("AES-GCM encryption failed: {e}")))?;
let envelope = EncryptedPrivateKeyEnvelope {
jacs_encrypted_private_key_version: ENCRYPTED_PRIVATE_KEY_VERSION_V2,
cipher: "AES-256-GCM".to_string(),
kdf,
salt: URL_SAFE_NO_PAD.encode(salt),
nonce: URL_SAFE_NO_PAD.encode(nonce.as_slice()),
ciphertext: URL_SAFE_NO_PAD.encode(encrypted),
};
serde_json::to_vec(&envelope).map_err(|e| {
CoreError::EncryptionFailed(format!("failed to serialize encrypted key envelope: {e}"))
})
}
pub fn decrypt_v2_envelope(
encrypted_data: &[u8],
password: &str,
) -> Result<Option<Vec<u8>>, CoreError> {
let first_non_ws = encrypted_data
.iter()
.copied()
.find(|b| !b.is_ascii_whitespace());
if first_non_ws != Some(b'{') {
return Ok(None);
}
let envelope: EncryptedPrivateKeyEnvelope = serde_json::from_slice(encrypted_data)
.map_err(|e| CoreError::MalformedEnvelope(format!("invalid V2 envelope JSON: {e}")))?;
if envelope.jacs_encrypted_private_key_version != ENCRYPTED_PRIVATE_KEY_VERSION_V2 {
return Err(CoreError::UnsupportedAlgorithm(format!(
"encrypted private key envelope version {}",
envelope.jacs_encrypted_private_key_version
)));
}
if envelope.cipher != "AES-256-GCM" {
return Err(CoreError::UnsupportedAlgorithm(format!(
"encrypted private key cipher '{}'",
envelope.cipher
)));
}
let salt = URL_SAFE_NO_PAD
.decode(envelope.salt.as_bytes())
.map_err(|e| CoreError::MalformedEnvelope(format!("invalid envelope salt: {e}")))?;
let nonce = URL_SAFE_NO_PAD
.decode(envelope.nonce.as_bytes())
.map_err(|e| CoreError::MalformedEnvelope(format!("invalid envelope nonce: {e}")))?;
let ciphertext = URL_SAFE_NO_PAD
.decode(envelope.ciphertext.as_bytes())
.map_err(|e| CoreError::MalformedEnvelope(format!("invalid envelope ciphertext: {e}")))?;
if nonce.len() != AES_GCM_NONCE_SIZE {
return Err(CoreError::MalformedEnvelope(format!(
"invalid envelope nonce length: expected {}, got {}",
AES_GCM_NONCE_SIZE,
nonce.len()
)));
}
let mut key = derive_argon2id_key(password, &salt, &envelope.kdf)?;
let cipher_key = Key::<Aes256Gcm>::from_slice(&key);
let cipher = Aes256Gcm::new(cipher_key);
key.zeroize();
let plaintext = cipher
.decrypt(Nonce::from_slice(&nonce), ciphertext.as_ref())
.map_err(|_| CoreError::InvalidPassword)?;
Ok(Some(plaintext))
}
pub fn derive_key_with_iterations(
password: &str,
salt: &[u8],
iterations: u32,
) -> [u8; AES_256_KEY_SIZE] {
let mut key = [0u8; AES_256_KEY_SIZE];
pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, iterations, &mut key);
key
}
pub fn derive_key_from_password(password: &str, salt: &[u8]) -> [u8; AES_256_KEY_SIZE] {
derive_key_with_iterations(password, salt, PBKDF2_ITERATIONS)
}
pub fn encrypt_private_key(private_key: &[u8], password: &str) -> Result<Vec<u8>, CoreError> {
encrypt_v2_envelope(private_key, password)
}
fn reserved_magic_prefix(input: &[u8]) -> Option<&str> {
if input.len() < 4 {
return None;
}
let head = &input[..4];
if head[0] == b'J'
&& head[1].is_ascii_uppercase()
&& head[2].is_ascii_uppercase()
&& head[3].is_ascii_digit()
{
Some(std::str::from_utf8(head).expect("ascii prefix is valid utf8"))
} else {
None
}
}
pub fn decrypt_private_key(
encrypted_key_with_salt_and_nonce: &[u8],
password: &str,
) -> Result<ZeroizingVec, CoreError> {
if let Some(decrypted) = decrypt_v2_envelope(encrypted_key_with_salt_and_nonce, password)? {
return Ok(ZeroizingVec::new(decrypted));
}
if let Some(prefix) = reserved_magic_prefix(encrypted_key_with_salt_and_nonce) {
return Err(CoreError::UnsupportedAlgorithm(prefix.to_string()));
}
if encrypted_key_with_salt_and_nonce.len() < MIN_ENCRYPTED_HEADER_SIZE {
return Err(CoreError::MalformedEnvelope(format!(
"envelope is truncated: expected at least {} bytes, got {}",
MIN_ENCRYPTED_HEADER_SIZE,
encrypted_key_with_salt_and_nonce.len()
)));
}
let (salt, rest) = encrypted_key_with_salt_and_nonce.split_at(PBKDF2_SALT_SIZE);
let (nonce, encrypted_data) = rest.split_at(AES_GCM_NONCE_SIZE);
let nonce_slice = Nonce::from_slice(nonce);
let mut key = derive_key_from_password(password, salt);
let cipher_key = Key::<Aes256Gcm>::from_slice(&key);
let cipher = Aes256Gcm::new(cipher_key);
key.zeroize();
if let Ok(decrypted) = cipher.decrypt(nonce_slice, encrypted_data) {
return Ok(ZeroizingVec::new(decrypted));
}
let mut legacy_key = derive_key_with_iterations(password, salt, PBKDF2_ITERATIONS_LEGACY);
let legacy_cipher_key = Key::<Aes256Gcm>::from_slice(&legacy_key);
let legacy_cipher = Aes256Gcm::new(legacy_cipher_key);
legacy_key.zeroize();
let decrypted = legacy_cipher
.decrypt(nonce_slice, encrypted_data)
.map_err(|_| CoreError::InvalidPassword)?;
Ok(ZeroizingVec::new(decrypted))
}