use std::fs;
use std::path::Path;
use chrono::{DateTime, Utc};
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use super::secret::Secret;
use crate::errors::{EnvVaultError, Result};
const MAGIC: &[u8; 4] = b"EVLT";
pub const CURRENT_VERSION: u8 = 1;
const HMAC_LEN: usize = 32;
const PREFIX_LEN: usize = 9;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct StoredArgon2Params {
pub memory_kib: u32,
pub iterations: u32,
pub parallelism: u32,
}
impl Default for StoredArgon2Params {
fn default() -> Self {
Self {
memory_kib: 65_536,
iterations: 3,
parallelism: 4,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultHeader {
pub version: u8,
#[serde(serialize_with = "base64_encode", deserialize_with = "base64_decode")]
pub salt: Vec<u8>,
pub created_at: DateTime<Utc>,
pub environment: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub argon2_params: Option<StoredArgon2Params>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub keyfile_hash: Option<String>,
}
pub fn write_vault(
path: &Path,
header: &VaultHeader,
secrets: &[Secret],
hmac_key: &[u8],
) -> Result<()> {
let header_bytes = serde_json::to_vec(header)
.map_err(|e| EnvVaultError::SerializationError(format!("header: {e}")))?;
let secrets_bytes = serde_json::to_vec(secrets)
.map_err(|e| EnvVaultError::SerializationError(format!("secrets: {e}")))?;
let hmac_tag = compute_hmac(hmac_key, &header_bytes, &secrets_bytes)?;
let header_len = u32::try_from(header_bytes.len()).map_err(|_| {
EnvVaultError::SerializationError(format!(
"header length {} exceeds u32::MAX",
header_bytes.len()
))
})?;
let total = PREFIX_LEN + header_bytes.len() + secrets_bytes.len() + HMAC_LEN;
let mut buf = Vec::with_capacity(total);
buf.extend_from_slice(MAGIC); buf.push(CURRENT_VERSION); buf.extend_from_slice(&header_len.to_le_bytes()); buf.extend_from_slice(&header_bytes); buf.extend_from_slice(&secrets_bytes); buf.extend_from_slice(&hmac_tag);
let parent = path.parent().unwrap_or(Path::new("."));
let tmp_path = parent.join(format!(
".{}.tmp",
path.file_name().unwrap_or_default().to_string_lossy()
));
fs::write(&tmp_path, &buf)?;
fs::rename(&tmp_path, path)?;
Ok(())
}
pub struct RawVault {
pub header: VaultHeader,
pub secrets: Vec<Secret>,
pub header_bytes: Vec<u8>,
pub secrets_bytes: Vec<u8>,
pub stored_hmac: Vec<u8>,
}
pub fn read_vault(path: &Path) -> Result<RawVault> {
if !path.exists() {
return Err(EnvVaultError::VaultNotFound(path.to_path_buf()));
}
let data = fs::read(path)?;
let min_size = PREFIX_LEN + HMAC_LEN;
if data.len() < min_size {
return Err(EnvVaultError::InvalidVaultFormat(
"file too small to be a valid vault".into(),
));
}
if &data[0..4] != MAGIC {
return Err(EnvVaultError::InvalidVaultFormat(
"missing EVLT magic bytes".into(),
));
}
let version = data[4];
if version != CURRENT_VERSION {
return Err(EnvVaultError::InvalidVaultFormat(format!(
"unsupported version {version}, expected {CURRENT_VERSION}"
)));
}
let header_len_u32 = u32::from_le_bytes(
data[5..9]
.try_into()
.map_err(|_| EnvVaultError::InvalidVaultFormat("bad header length".into()))?,
);
let header_len = usize::try_from(header_len_u32).map_err(|_| {
EnvVaultError::InvalidVaultFormat(format!(
"header length {header_len_u32} exceeds platform address space"
))
})?;
let header_end = PREFIX_LEN + header_len;
if header_end + HMAC_LEN > data.len() {
return Err(EnvVaultError::InvalidVaultFormat(
"header length exceeds file size".into(),
));
}
let header_bytes = data[PREFIX_LEN..header_end].to_vec();
let secrets_end = data.len() - HMAC_LEN;
let secrets_bytes = data[header_end..secrets_end].to_vec();
let stored_hmac = data[secrets_end..].to_vec();
let header: VaultHeader = serde_json::from_slice(&header_bytes)
.map_err(|e| EnvVaultError::InvalidVaultFormat(format!("header JSON: {e}")))?;
let secrets: Vec<Secret> = serde_json::from_slice(&secrets_bytes)
.map_err(|e| EnvVaultError::InvalidVaultFormat(format!("secrets JSON: {e}")))?;
Ok(RawVault {
header,
secrets,
header_bytes,
secrets_bytes,
stored_hmac,
})
}
pub fn compute_hmac(hmac_key: &[u8], header_bytes: &[u8], secrets_bytes: &[u8]) -> Result<Vec<u8>> {
let mut mac = Hmac::<Sha256>::new_from_slice(hmac_key)
.map_err(|e| EnvVaultError::HmacError(format!("invalid HMAC key: {e}")))?;
mac.update(header_bytes);
mac.update(secrets_bytes);
Ok(mac.finalize().into_bytes().to_vec())
}
pub fn verify_hmac(
hmac_key: &[u8],
header_bytes: &[u8],
secrets_bytes: &[u8],
expected_hmac: &[u8],
) -> Result<()> {
let mut mac = Hmac::<Sha256>::new_from_slice(hmac_key)
.map_err(|e| EnvVaultError::HmacError(format!("invalid HMAC key: {e}")))?;
mac.update(header_bytes);
mac.update(secrets_bytes);
mac.verify_slice(expected_hmac)
.map_err(|_| EnvVaultError::HmacMismatch)
}
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
pub(crate) fn base64_encode<S>(data: &[u8], serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let encoded = BASE64.encode(data);
serializer.serialize_str(&encoded)
}
pub(crate) fn base64_decode<'de, D>(deserializer: D) -> std::result::Result<Vec<u8>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
BASE64.decode(&s).map_err(serde::de::Error::custom)
}