use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Nonce,
};
use argon2::{Algorithm, Argon2, Params, Version};
use base64::engine::general_purpose::STANDARD as B64;
use base64::Engine;
use rand::{rngs::OsRng, RngCore};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error;
use zeroize::Zeroize;
use std::io::{Read, Write};
const MAGIC_V2: &[u8; 4] = b"VLT2";
const VERSION_V2: u8 = 0x01;
const KDF_ARGON2ID: u8 = 0x02;
const ARGON_MEM_KIB_DEFAULT: u32 = 64 * 1024;
const ARGON_ITERS_DEFAULT: u32 = 3;
const ARGON_LANES_DEFAULT: u32 = 1;
#[derive(Debug, Error)]
pub enum VaultCryptoError {
#[error("invalid header")]
InvalidHeader,
#[error("unsupported version")]
UnsupportedVersion,
#[error("unsupported kdf")]
UnsupportedKdf,
#[error("crypto failure")]
CryptoFailure,
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("format error: {0}")]
Format(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LegacyMetadata {
pub salt: String,
pub iv: String,
pub tag: String,
#[serde(default)]
pub iterations: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LegacyJson {
pub metadata: LegacyMetadata,
pub data: String,
}
pub fn encrypt_v2(password: &str, plaintext: &[u8]) -> Result<Vec<u8>, VaultCryptoError> {
let mut salt = [0u8; 16];
OsRng.fill_bytes(&mut salt);
let params = Params::new(
ARGON_MEM_KIB_DEFAULT,
ARGON_ITERS_DEFAULT,
ARGON_LANES_DEFAULT,
None,
)
.map_err(|_| VaultCryptoError::CryptoFailure)?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut pw_buf = Sha256::digest(password.as_bytes()).to_vec();
let mut key_material = [0u8; 32];
argon2
.hash_password_into(&pw_buf, &salt, &mut key_material)
.map_err(|_| VaultCryptoError::CryptoFailure)?;
pw_buf.zeroize();
let cipher =
Aes256Gcm::new_from_slice(&key_material).map_err(|_| VaultCryptoError::CryptoFailure)?;
let mut nonce = [0u8; 12];
OsRng.fill_bytes(&mut nonce);
let ct = cipher
.encrypt(Nonce::from_slice(&nonce), plaintext)
.map_err(|_| VaultCryptoError::CryptoFailure)?;
key_material.zeroize();
let mut out = Vec::with_capacity(4 + 1 + 1 + 1 + 16 + 4 + 4 + 1 + 12 + ct.len());
out.extend_from_slice(MAGIC_V2);
out.push(VERSION_V2);
out.push(KDF_ARGON2ID);
out.push(16u8);
out.extend_from_slice(&salt);
out.extend_from_slice(&ARGON_MEM_KIB_DEFAULT.to_le_bytes());
out.extend_from_slice(&ARGON_ITERS_DEFAULT.to_le_bytes());
out.push(ARGON_LANES_DEFAULT as u8);
out.extend_from_slice(&nonce);
out.extend_from_slice(&ct);
Ok(out)
}
pub fn decrypt_auto(password: &str, blob: &[u8]) -> Result<Vec<u8>, VaultCryptoError> {
if blob.len() >= 4 && &blob[0..4] == MAGIC_V2 {
return decrypt_v2(password, blob);
}
if looks_like_json(blob) {
return decrypt_legacy_json(password, blob);
}
if blob.len() >= 12 {
return decrypt_v1_raw(password, blob);
}
Err(VaultCryptoError::InvalidHeader)
}
fn decrypt_v2(password: &str, blob: &[u8]) -> Result<Vec<u8>, VaultCryptoError> {
let mut idx = 0usize;
if blob.len() < 4 + 1 + 1 + 1 {
return Err(VaultCryptoError::InvalidHeader);
}
if &blob[idx..idx + 4] != MAGIC_V2 {
return Err(VaultCryptoError::InvalidHeader);
}
idx += 4;
let ver = blob[idx];
idx += 1;
if ver != VERSION_V2 {
return Err(VaultCryptoError::UnsupportedVersion);
}
let kdf = blob[idx];
idx += 1;
if kdf != KDF_ARGON2ID {
return Err(VaultCryptoError::UnsupportedKdf);
}
let salt_len = blob[idx] as usize;
idx += 1;
if salt_len != 16 || blob.len() < idx + salt_len {
return Err(VaultCryptoError::InvalidHeader);
}
let salt = &blob[idx..idx + salt_len];
idx += salt_len;
if blob.len() < idx + 4 + 4 + 1 + 12 {
return Err(VaultCryptoError::InvalidHeader);
}
let mem_kib = u32::from_le_bytes(blob[idx..idx + 4].try_into().unwrap());
idx += 4;
let iters = u32::from_le_bytes(blob[idx..idx + 4].try_into().unwrap());
idx += 4;
let lanes = blob[idx] as u32;
idx += 1;
let nonce = &blob[idx..idx + 12];
idx += 12;
let ct = &blob[idx..];
let params =
Params::new(mem_kib, iters, lanes, None).map_err(|_| VaultCryptoError::CryptoFailure)?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut pw_buf = Sha256::digest(password.as_bytes()).to_vec();
let mut key_material = [0u8; 32];
argon2
.hash_password_into(&pw_buf, salt, &mut key_material)
.map_err(|_| VaultCryptoError::CryptoFailure)?;
pw_buf.zeroize();
let cipher =
Aes256Gcm::new_from_slice(&key_material).map_err(|_| VaultCryptoError::CryptoFailure)?;
let pt = cipher
.decrypt(Nonce::from_slice(nonce), ct)
.map_err(|_| VaultCryptoError::CryptoFailure)?;
key_material.zeroize();
Ok(pt)
}
fn decrypt_v1_raw(password: &str, blob: &[u8]) -> Result<Vec<u8>, VaultCryptoError> {
if blob.len() < 12 + 16 {
return Err(VaultCryptoError::Format("v1-raw too short".into()));
}
let (nonce, ct) = blob.split_at(12);
let mut key = Sha256::digest(password.as_bytes()).to_vec();
let cipher = Aes256Gcm::new_from_slice(&key).map_err(|_| VaultCryptoError::CryptoFailure)?;
let pt = cipher
.decrypt(Nonce::from_slice(nonce), ct)
.map_err(|_| VaultCryptoError::CryptoFailure)?;
key.zeroize();
Ok(pt)
}
fn decrypt_legacy_json(password: &str, blob: &[u8]) -> Result<Vec<u8>, VaultCryptoError> {
let s = std::str::from_utf8(blob).map_err(|_| VaultCryptoError::Format("bad utf8".into()))?;
let parsed: LegacyJson = serde_json::from_str(s)
.map_err(|e| VaultCryptoError::Format(format!("json parse: {e}")))?;
let salt = B64
.decode(parsed.metadata.salt.as_bytes())
.map_err(|_| VaultCryptoError::Format("bad salt b64".into()))?;
let iv = B64
.decode(parsed.metadata.iv.as_bytes())
.map_err(|_| VaultCryptoError::Format("bad iv b64".into()))?;
let tag = B64
.decode(parsed.metadata.tag.as_bytes())
.map_err(|_| VaultCryptoError::Format("bad tag b64".into()))?;
let data = B64
.decode(parsed.data.as_bytes())
.map_err(|_| VaultCryptoError::Format("bad data b64".into()))?;
if iv.len() != 12 || tag.len() != 16 {
return Err(VaultCryptoError::Format("iv/tag wrong len".into()));
}
let iters = parsed.metadata.iterations.unwrap_or(100_000);
let mut dk = [0u8; 32];
pbkdf2::pbkdf2_hmac::<sha2::Sha256>(password.as_bytes(), &salt, iters, &mut dk);
let mut ct = Vec::with_capacity(data.len() + tag.len());
ct.extend_from_slice(&data);
ct.extend_from_slice(&tag);
let cipher = Aes256Gcm::new_from_slice(&dk).map_err(|_| VaultCryptoError::CryptoFailure)?;
let pt = cipher
.decrypt(Nonce::from_slice(&iv), ct.as_ref())
.map_err(|_| VaultCryptoError::CryptoFailure)?;
dk.zeroize();
Ok(pt)
}
fn looks_like_json(buf: &[u8]) -> bool {
let first_non_ws = buf.iter().copied().find(|b| !b" \n\r\t".contains(b));
matches!(first_non_ws, Some(b'{'))
}
pub fn write_vault_v2_file(
path: &std::path::Path,
password: &str,
plaintext: &[u8],
) -> Result<(), VaultCryptoError> {
let blob = encrypt_v2(password, plaintext)?;
let mut f = std::fs::File::create(path)?;
f.write_all(&blob)?;
Ok(())
}
pub fn read_vault_auto_file(
path: &std::path::Path,
password: &str,
) -> Result<Vec<u8>, VaultCryptoError> {
let mut buf = Vec::new();
std::fs::File::open(path)?.read_to_end(&mut buf)?;
decrypt_auto(password, &buf)
}
mod pbkdf2 {
pub use pbkdf2::pbkdf2_hmac;
}
pub use encrypt_v2 as encrypt_vault;
use crate::vault::vault_structs::VaultPayload;
pub fn decrypt_vault(path: &str, password: &str) -> Result<VaultPayload, VaultCryptoError> {
let bytes = read_vault_auto_file(std::path::Path::new(path), password)?;
let payload: VaultPayload = serde_json::from_slice(&bytes)
.map_err(|e| VaultCryptoError::Format(format!("payload json: {e}")))?;
Ok(payload)
}
pub fn encrypt_and_save_vault(
out_path: &str,
password: &str,
plaintext: &[u8],
) -> Result<(), VaultCryptoError> {
write_vault_v2_file(std::path::Path::new(out_path), password, plaintext)
}
pub fn must_get_password() -> String {
if let Ok(pw) = std::env::var("VIOS_VAULT_PASSWORD") {
return pw;
}
rpassword::prompt_password("Enter password: ").expect("failed to read password")
}