use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Key, Nonce,
};
use argon2::{Argon2, Params};
use zeroize::Zeroizing;
use crate::error::PqfileError;
const ARGON2_M_COST: u32 = 65536; const ARGON2_T_COST: u32 = 3;
const ARGON2_P_COST: u32 = 4;
const ARGON2_P_COST_LEGACY: u32 = 1;
const SALT_LEN: usize = 16;
const NONCE_LEN: usize = 12;
const SEED_LEN: usize = 64;
const HYBRID_SEED_LEN: usize = 96;
pub const ENCRYPTED_BODY_LEN: usize = SALT_LEN + NONCE_LEN + SEED_LEN + 16;
pub const ENCRYPTED_HYBRID_BODY_LEN: usize = SALT_LEN + NONCE_LEN + HYBRID_SEED_LEN + 16;
pub fn encrypt_seed(seed: &[u8; SEED_LEN], passphrase: &str) -> Result<Vec<u8>, PqfileError> {
let mut salt = [0u8; SALT_LEN];
getrandom::fill(&mut salt).map_err(|_| PqfileError::EncryptionFailure)?;
let key = derive_key(passphrase, &salt)?;
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key.as_ref()));
let mut nonce_bytes = [0u8; NONCE_LEN];
getrandom::fill(&mut nonce_bytes).map_err(|_| PqfileError::EncryptionFailure)?;
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, seed.as_slice())
.map_err(|_| PqfileError::EncryptionFailure)?;
let mut out = Vec::with_capacity(ENCRYPTED_BODY_LEN);
out.extend_from_slice(&salt);
out.extend_from_slice(&nonce_bytes);
out.extend_from_slice(&ciphertext);
Ok(out)
}
pub fn decrypt_seed(
body: &[u8],
passphrase: &str,
) -> Result<Zeroizing<[u8; SEED_LEN]>, PqfileError> {
if body.len() != ENCRYPTED_BODY_LEN {
return Err(PqfileError::InvalidKeyLength {
expected: ENCRYPTED_BODY_LEN,
got: body.len(),
});
}
if let Ok(seed) = try_decrypt_seed(body, passphrase, ARGON2_P_COST) {
return Ok(seed);
}
if try_decrypt_seed(body, passphrase, ARGON2_P_COST_LEGACY).is_ok() {
return Err(PqfileError::LegacyKeyFormat);
}
Err(PqfileError::WrongPassphrase)
}
pub(crate) fn decrypt_seed_legacy(
body: &[u8],
passphrase: &str,
) -> Result<Zeroizing<[u8; SEED_LEN]>, PqfileError> {
if body.len() != ENCRYPTED_BODY_LEN {
return Err(PqfileError::InvalidKeyLength {
expected: ENCRYPTED_BODY_LEN,
got: body.len(),
});
}
try_decrypt_seed(body, passphrase, ARGON2_P_COST_LEGACY)
.map_err(|_| PqfileError::WrongPassphrase)
}
fn try_decrypt_seed(
body: &[u8],
passphrase: &str,
p_cost: u32,
) -> Result<Zeroizing<[u8; SEED_LEN]>, PqfileError> {
let salt = &body[..SALT_LEN];
let nonce_bytes = &body[SALT_LEN..SALT_LEN + NONCE_LEN];
let ciphertext = &body[SALT_LEN + NONCE_LEN..];
let key = derive_key_with_pcost(passphrase, salt, p_cost)?;
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key.as_ref()));
let nonce = Nonce::from_slice(nonce_bytes);
let plaintext = Zeroizing::new(
cipher
.decrypt(nonce, ciphertext)
.map_err(|_| PqfileError::WrongPassphrase)?,
);
if plaintext.len() != SEED_LEN {
return Err(PqfileError::WrongPassphrase);
}
let mut seed = Zeroizing::new([0u8; SEED_LEN]);
seed.copy_from_slice(&plaintext);
Ok(seed)
}
pub fn encrypt_hybrid_seed(
seed: &[u8; HYBRID_SEED_LEN],
passphrase: &str,
) -> Result<Vec<u8>, PqfileError> {
let mut salt = [0u8; SALT_LEN];
getrandom::fill(&mut salt).map_err(|_| PqfileError::EncryptionFailure)?;
let key = derive_key(passphrase, &salt)?;
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key.as_ref()));
let mut nonce_bytes = [0u8; NONCE_LEN];
getrandom::fill(&mut nonce_bytes).map_err(|_| PqfileError::EncryptionFailure)?;
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, seed.as_slice())
.map_err(|_| PqfileError::EncryptionFailure)?;
let mut out = Vec::with_capacity(ENCRYPTED_HYBRID_BODY_LEN);
out.extend_from_slice(&salt);
out.extend_from_slice(&nonce_bytes);
out.extend_from_slice(&ciphertext);
Ok(out)
}
pub fn decrypt_hybrid_seed(
body: &[u8],
passphrase: &str,
) -> Result<Zeroizing<[u8; HYBRID_SEED_LEN]>, PqfileError> {
if body.len() != ENCRYPTED_HYBRID_BODY_LEN {
return Err(PqfileError::InvalidKeyLength {
expected: ENCRYPTED_HYBRID_BODY_LEN,
got: body.len(),
});
}
if let Ok(seed) = try_decrypt_hybrid_seed(body, passphrase, ARGON2_P_COST) {
return Ok(seed);
}
if try_decrypt_hybrid_seed(body, passphrase, ARGON2_P_COST_LEGACY).is_ok() {
return Err(PqfileError::LegacyKeyFormat);
}
Err(PqfileError::WrongPassphrase)
}
pub(crate) fn decrypt_hybrid_seed_legacy(
body: &[u8],
passphrase: &str,
) -> Result<Zeroizing<[u8; HYBRID_SEED_LEN]>, PqfileError> {
if body.len() != ENCRYPTED_HYBRID_BODY_LEN {
return Err(PqfileError::InvalidKeyLength {
expected: ENCRYPTED_HYBRID_BODY_LEN,
got: body.len(),
});
}
try_decrypt_hybrid_seed(body, passphrase, ARGON2_P_COST_LEGACY)
.map_err(|_| PqfileError::WrongPassphrase)
}
fn try_decrypt_hybrid_seed(
body: &[u8],
passphrase: &str,
p_cost: u32,
) -> Result<Zeroizing<[u8; HYBRID_SEED_LEN]>, PqfileError> {
let salt = &body[..SALT_LEN];
let nonce_bytes = &body[SALT_LEN..SALT_LEN + NONCE_LEN];
let ciphertext = &body[SALT_LEN + NONCE_LEN..];
let key = derive_key_with_pcost(passphrase, salt, p_cost)?;
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key.as_ref()));
let nonce = Nonce::from_slice(nonce_bytes);
let plaintext = Zeroizing::new(
cipher
.decrypt(nonce, ciphertext)
.map_err(|_| PqfileError::WrongPassphrase)?,
);
if plaintext.len() != HYBRID_SEED_LEN {
return Err(PqfileError::WrongPassphrase);
}
let mut seed = Zeroizing::new([0u8; HYBRID_SEED_LEN]);
seed.copy_from_slice(&plaintext);
Ok(seed)
}
const SIGNING_SEED_LEN: usize = 32;
pub const ENCRYPTED_SIGNING_BODY_LEN: usize = SALT_LEN + NONCE_LEN + SIGNING_SEED_LEN + 16;
pub fn encrypt_signing_seed(
seed: &[u8; SIGNING_SEED_LEN],
passphrase: &str,
) -> Result<Vec<u8>, PqfileError> {
let mut salt = [0u8; SALT_LEN];
getrandom::fill(&mut salt).map_err(|_| PqfileError::EncryptionFailure)?;
let key = derive_key(passphrase, &salt)?;
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key.as_ref()));
let mut nonce_bytes = [0u8; NONCE_LEN];
getrandom::fill(&mut nonce_bytes).map_err(|_| PqfileError::EncryptionFailure)?;
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, seed.as_slice())
.map_err(|_| PqfileError::EncryptionFailure)?;
let mut out = Vec::with_capacity(ENCRYPTED_SIGNING_BODY_LEN);
out.extend_from_slice(&salt);
out.extend_from_slice(&nonce_bytes);
out.extend_from_slice(&ciphertext);
Ok(out)
}
pub fn decrypt_signing_seed(
body: &[u8],
passphrase: &str,
) -> Result<Zeroizing<[u8; SIGNING_SEED_LEN]>, PqfileError> {
if body.len() != ENCRYPTED_SIGNING_BODY_LEN {
return Err(PqfileError::InvalidKeyLength {
expected: ENCRYPTED_SIGNING_BODY_LEN,
got: body.len(),
});
}
if let Ok(seed) = try_decrypt_signing_seed(body, passphrase, ARGON2_P_COST) {
return Ok(seed);
}
if try_decrypt_signing_seed(body, passphrase, ARGON2_P_COST_LEGACY).is_ok() {
return Err(PqfileError::LegacyKeyFormat);
}
Err(PqfileError::WrongPassphrase)
}
pub(crate) fn decrypt_signing_seed_legacy(
body: &[u8],
passphrase: &str,
) -> Result<Zeroizing<[u8; SIGNING_SEED_LEN]>, PqfileError> {
if body.len() != ENCRYPTED_SIGNING_BODY_LEN {
return Err(PqfileError::InvalidKeyLength {
expected: ENCRYPTED_SIGNING_BODY_LEN,
got: body.len(),
});
}
try_decrypt_signing_seed(body, passphrase, ARGON2_P_COST_LEGACY)
.map_err(|_| PqfileError::WrongPassphrase)
}
fn try_decrypt_signing_seed(
body: &[u8],
passphrase: &str,
p_cost: u32,
) -> Result<Zeroizing<[u8; SIGNING_SEED_LEN]>, PqfileError> {
let salt = &body[..SALT_LEN];
let nonce_bytes = &body[SALT_LEN..SALT_LEN + NONCE_LEN];
let ciphertext = &body[SALT_LEN + NONCE_LEN..];
let key = derive_key_with_pcost(passphrase, salt, p_cost)?;
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key.as_ref()));
let nonce = Nonce::from_slice(nonce_bytes);
let plaintext = Zeroizing::new(
cipher
.decrypt(nonce, ciphertext)
.map_err(|_| PqfileError::WrongPassphrase)?,
);
if plaintext.len() != SIGNING_SEED_LEN {
return Err(PqfileError::WrongPassphrase);
}
let mut seed = Zeroizing::new([0u8; SIGNING_SEED_LEN]);
seed.copy_from_slice(&plaintext);
Ok(seed)
}
fn derive_key(passphrase: &str, salt: &[u8]) -> Result<Zeroizing<[u8; 32]>, PqfileError> {
derive_key_with_pcost(passphrase, salt, ARGON2_P_COST)
}
fn derive_key_with_pcost(
passphrase: &str,
salt: &[u8],
p_cost: u32,
) -> Result<Zeroizing<[u8; 32]>, PqfileError> {
let params = Params::new(ARGON2_M_COST, ARGON2_T_COST, p_cost, Some(32))
.map_err(|_| PqfileError::EncryptionFailure)?;
let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
let mut key = Zeroizing::new([0u8; 32]);
argon2
.hash_password_into(passphrase.as_bytes(), salt, key.as_mut())
.map_err(|_| PqfileError::EncryptionFailure)?;
Ok(key)
}
#[cfg(test)]
mod tests {
use super::*;
fn encrypt_seed_legacy(seed: &[u8; SEED_LEN], passphrase: &str) -> Vec<u8> {
let mut salt = [0u8; SALT_LEN];
getrandom::fill(&mut salt).unwrap();
let key = derive_key_with_pcost(passphrase, &salt, ARGON2_P_COST_LEGACY).unwrap();
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key.as_ref()));
let mut nonce_bytes = [0u8; NONCE_LEN];
getrandom::fill(&mut nonce_bytes).unwrap();
let ct = cipher
.encrypt(Nonce::from_slice(&nonce_bytes), seed.as_slice())
.unwrap();
let mut out = Vec::new();
out.extend_from_slice(&salt);
out.extend_from_slice(&nonce_bytes);
out.extend_from_slice(&ct);
out
}
fn encrypt_signing_seed_legacy(seed: &[u8; SIGNING_SEED_LEN], passphrase: &str) -> Vec<u8> {
let mut salt = [0u8; SALT_LEN];
getrandom::fill(&mut salt).unwrap();
let key = derive_key_with_pcost(passphrase, &salt, ARGON2_P_COST_LEGACY).unwrap();
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key.as_ref()));
let mut nonce_bytes = [0u8; NONCE_LEN];
getrandom::fill(&mut nonce_bytes).unwrap();
let ct = cipher
.encrypt(Nonce::from_slice(&nonce_bytes), seed.as_slice())
.unwrap();
let mut out = Vec::new();
out.extend_from_slice(&salt);
out.extend_from_slice(&nonce_bytes);
out.extend_from_slice(&ct);
out
}
fn encrypt_hybrid_seed_legacy(seed: &[u8; HYBRID_SEED_LEN], passphrase: &str) -> Vec<u8> {
let mut salt = [0u8; SALT_LEN];
getrandom::fill(&mut salt).unwrap();
let key = derive_key_with_pcost(passphrase, &salt, ARGON2_P_COST_LEGACY).unwrap();
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key.as_ref()));
let mut nonce_bytes = [0u8; NONCE_LEN];
getrandom::fill(&mut nonce_bytes).unwrap();
let ct = cipher
.encrypt(Nonce::from_slice(&nonce_bytes), seed.as_slice())
.unwrap();
let mut out = Vec::new();
out.extend_from_slice(&salt);
out.extend_from_slice(&nonce_bytes);
out.extend_from_slice(&ct);
out
}
#[test]
fn roundtrip_correct_passphrase() {
let seed = [0x42u8; SEED_LEN];
let body = encrypt_seed(&seed, "hunter2").unwrap();
assert_eq!(body.len(), ENCRYPTED_BODY_LEN);
let recovered = decrypt_seed(&body, "hunter2").unwrap();
assert_eq!(*recovered, seed);
}
#[test]
fn wrong_passphrase_returns_error() {
let seed = [0x99u8; SEED_LEN];
let body = encrypt_seed(&seed, "correct").unwrap();
assert!(matches!(
decrypt_seed(&body, "wrong"),
Err(PqfileError::WrongPassphrase)
));
}
#[test]
fn different_encryptions_produce_different_bodies() {
let seed = [0x01u8; SEED_LEN];
let a = encrypt_seed(&seed, "pass").unwrap();
let b = encrypt_seed(&seed, "pass").unwrap();
assert_ne!(a, b);
}
#[test]
fn wrong_body_length_returns_error() {
assert!(matches!(
decrypt_seed(&[0u8; 10], "pass"),
Err(PqfileError::InvalidKeyLength { .. })
));
}
#[test]
fn legacy_key_returns_legacy_key_format_error() {
let seed = [0x11u8; SEED_LEN];
let legacy_body = encrypt_seed_legacy(&seed, "correct");
assert!(matches!(
decrypt_seed(&legacy_body, "correct"),
Err(PqfileError::LegacyKeyFormat)
));
}
#[test]
fn legacy_key_wrong_passphrase_returns_wrong_passphrase() {
let seed = [0x22u8; SEED_LEN];
let legacy_body = encrypt_seed_legacy(&seed, "correct");
assert!(matches!(
decrypt_seed(&legacy_body, "wrong"),
Err(PqfileError::WrongPassphrase)
));
}
#[test]
fn decrypt_seed_legacy_roundtrip() {
let seed = [0x33u8; SEED_LEN];
let legacy_body = encrypt_seed_legacy(&seed, "migrate-me");
let recovered = decrypt_seed_legacy(&legacy_body, "migrate-me").unwrap();
assert_eq!(*recovered, seed);
}
#[test]
fn decrypt_seed_legacy_wrong_passphrase() {
let seed = [0x44u8; SEED_LEN];
let legacy_body = encrypt_seed_legacy(&seed, "correct");
assert!(matches!(
decrypt_seed_legacy(&legacy_body, "wrong"),
Err(PqfileError::WrongPassphrase)
));
}
#[test]
fn hybrid_roundtrip_correct_passphrase() {
let seed = [0x77u8; HYBRID_SEED_LEN];
let body = encrypt_hybrid_seed(&seed, "hybrid-pass").unwrap();
assert_eq!(body.len(), ENCRYPTED_HYBRID_BODY_LEN);
let recovered = decrypt_hybrid_seed(&body, "hybrid-pass").unwrap();
assert_eq!(*recovered, seed);
}
#[test]
fn hybrid_wrong_passphrase_returns_error() {
let seed = [0xABu8; HYBRID_SEED_LEN];
let body = encrypt_hybrid_seed(&seed, "correct").unwrap();
assert!(matches!(
decrypt_hybrid_seed(&body, "wrong"),
Err(PqfileError::WrongPassphrase)
));
}
#[test]
fn hybrid_wrong_body_length_returns_error() {
assert!(matches!(
decrypt_hybrid_seed(&[0u8; 10], "pass"),
Err(PqfileError::InvalidKeyLength { .. })
));
}
#[test]
fn hybrid_different_encryptions_produce_different_bodies() {
let seed = [0x55u8; HYBRID_SEED_LEN];
let a = encrypt_hybrid_seed(&seed, "pass").unwrap();
let b = encrypt_hybrid_seed(&seed, "pass").unwrap();
assert_ne!(a, b);
}
#[test]
fn hybrid_legacy_key_returns_legacy_key_format_error() {
let seed = [0xCCu8; HYBRID_SEED_LEN];
let legacy_body = encrypt_hybrid_seed_legacy(&seed, "correct");
assert!(matches!(
decrypt_hybrid_seed(&legacy_body, "correct"),
Err(PqfileError::LegacyKeyFormat)
));
}
#[test]
fn decrypt_hybrid_seed_legacy_roundtrip() {
let seed = [0xDDu8; HYBRID_SEED_LEN];
let legacy_body = encrypt_hybrid_seed_legacy(&seed, "migrate-me");
let recovered = decrypt_hybrid_seed_legacy(&legacy_body, "migrate-me").unwrap();
assert_eq!(*recovered, seed);
}
#[test]
fn signing_roundtrip_correct_passphrase() {
let seed = [0x11u8; SIGNING_SEED_LEN];
let body = encrypt_signing_seed(&seed, "signpass").unwrap();
assert_eq!(body.len(), ENCRYPTED_SIGNING_BODY_LEN);
let recovered = decrypt_signing_seed(&body, "signpass").unwrap();
assert_eq!(*recovered, seed);
}
#[test]
fn signing_wrong_passphrase_returns_error() {
let seed = [0x22u8; SIGNING_SEED_LEN];
let body = encrypt_signing_seed(&seed, "correct").unwrap();
assert!(matches!(
decrypt_signing_seed(&body, "wrong"),
Err(PqfileError::WrongPassphrase)
));
}
#[test]
fn signing_wrong_body_length_returns_error() {
assert!(matches!(
decrypt_signing_seed(&[0u8; 10], "pass"),
Err(PqfileError::InvalidKeyLength { .. })
));
}
#[test]
fn signing_different_encryptions_produce_different_bodies() {
let seed = [0x33u8; SIGNING_SEED_LEN];
let a = encrypt_signing_seed(&seed, "pass").unwrap();
let b = encrypt_signing_seed(&seed, "pass").unwrap();
assert_ne!(a, b);
}
#[test]
fn signing_legacy_key_returns_legacy_key_format_error() {
let seed = [0x55u8; SIGNING_SEED_LEN];
let legacy_body = encrypt_signing_seed_legacy(&seed, "correct");
assert!(matches!(
decrypt_signing_seed(&legacy_body, "correct"),
Err(PqfileError::LegacyKeyFormat)
));
}
#[test]
fn decrypt_signing_seed_legacy_roundtrip() {
let seed = [0x66u8; SIGNING_SEED_LEN];
let legacy_body = encrypt_signing_seed_legacy(&seed, "migrate-me");
let recovered = decrypt_signing_seed_legacy(&legacy_body, "migrate-me").unwrap();
assert_eq!(*recovered, seed);
}
}