1use std::fs;
18use std::path::Path;
19
20use chrono::{DateTime, Utc};
21use hmac::{Hmac, Mac};
22use serde::{Deserialize, Serialize};
23use sha2::Sha256;
24
25use super::secret::Secret;
26use crate::errors::{EnvVaultError, Result};
27
28const MAGIC: &[u8; 4] = b"EVLT";
34
35pub const CURRENT_VERSION: u8 = 1;
37
38const HMAC_LEN: usize = 32;
40
41const PREFIX_LEN: usize = 9;
43
44#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
52pub struct StoredArgon2Params {
53 pub memory_kib: u32,
54 pub iterations: u32,
55 pub parallelism: u32,
56}
57
58impl Default for StoredArgon2Params {
59 fn default() -> Self {
60 Self {
61 memory_kib: 65_536,
62 iterations: 3,
63 parallelism: 4,
64 }
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct VaultHeader {
71 pub version: u8,
73
74 #[serde(serialize_with = "base64_encode", deserialize_with = "base64_decode")]
76 pub salt: Vec<u8>,
77
78 pub created_at: DateTime<Utc>,
80
81 pub environment: String,
83
84 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub argon2_params: Option<StoredArgon2Params>,
88
89 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub keyfile_hash: Option<String>,
93}
94
95pub fn write_vault(
108 path: &Path,
109 header: &VaultHeader,
110 secrets: &[Secret],
111 hmac_key: &[u8],
112) -> Result<()> {
113 let header_bytes = serde_json::to_vec(header)
114 .map_err(|e| EnvVaultError::SerializationError(format!("header: {e}")))?;
115 let secrets_bytes = serde_json::to_vec(secrets)
116 .map_err(|e| EnvVaultError::SerializationError(format!("secrets: {e}")))?;
117
118 let hmac_tag = compute_hmac(hmac_key, &header_bytes, &secrets_bytes)?;
119
120 let header_len = u32::try_from(header_bytes.len()).map_err(|_| {
122 EnvVaultError::SerializationError(format!(
123 "header length {} exceeds u32::MAX",
124 header_bytes.len()
125 ))
126 })?;
127 let total = PREFIX_LEN + header_bytes.len() + secrets_bytes.len() + HMAC_LEN;
128 let mut buf = Vec::with_capacity(total);
129
130 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("."));
141 let tmp_path = parent.join(format!(
142 ".{}.tmp",
143 path.file_name().unwrap_or_default().to_string_lossy()
144 ));
145
146 fs::write(&tmp_path, &buf)?;
147 fs::rename(&tmp_path, path)?;
148
149 Ok(())
150}
151
152pub struct RawVault {
157 pub header: VaultHeader,
158 pub secrets: Vec<Secret>,
159 pub header_bytes: Vec<u8>,
161 pub secrets_bytes: Vec<u8>,
163 pub stored_hmac: Vec<u8>,
165}
166
167pub fn read_vault(path: &Path) -> Result<RawVault> {
173 if !path.exists() {
174 return Err(EnvVaultError::VaultNotFound(path.to_path_buf()));
175 }
176
177 let data = fs::read(path)?;
178
179 let min_size = PREFIX_LEN + HMAC_LEN;
181 if data.len() < min_size {
182 return Err(EnvVaultError::InvalidVaultFormat(
183 "file too small to be a valid vault".into(),
184 ));
185 }
186
187 if &data[0..4] != MAGIC {
190 return Err(EnvVaultError::InvalidVaultFormat(
191 "missing EVLT magic bytes".into(),
192 ));
193 }
194
195 let version = data[4];
196 if version != CURRENT_VERSION {
197 return Err(EnvVaultError::InvalidVaultFormat(format!(
198 "unsupported version {version}, expected {CURRENT_VERSION}"
199 )));
200 }
201
202 let header_len_u32 = u32::from_le_bytes(
203 data[5..9]
204 .try_into()
205 .map_err(|_| EnvVaultError::InvalidVaultFormat("bad header length".into()))?,
206 );
207 let header_len = usize::try_from(header_len_u32).map_err(|_| {
208 EnvVaultError::InvalidVaultFormat(format!(
209 "header length {header_len_u32} exceeds platform address space"
210 ))
211 })?;
212
213 let header_end = PREFIX_LEN + header_len;
214 if header_end + HMAC_LEN > data.len() {
215 return Err(EnvVaultError::InvalidVaultFormat(
216 "header length exceeds file size".into(),
217 ));
218 }
219
220 let header_bytes = data[PREFIX_LEN..header_end].to_vec();
223 let secrets_end = data.len() - HMAC_LEN;
224 let secrets_bytes = data[header_end..secrets_end].to_vec();
225 let stored_hmac = data[secrets_end..].to_vec();
226
227 let header: VaultHeader = serde_json::from_slice(&header_bytes)
230 .map_err(|e| EnvVaultError::InvalidVaultFormat(format!("header JSON: {e}")))?;
231
232 let secrets: Vec<Secret> = serde_json::from_slice(&secrets_bytes)
233 .map_err(|e| EnvVaultError::InvalidVaultFormat(format!("secrets JSON: {e}")))?;
234
235 Ok(RawVault {
236 header,
237 secrets,
238 header_bytes,
239 secrets_bytes,
240 stored_hmac,
241 })
242}
243
244pub fn compute_hmac(hmac_key: &[u8], header_bytes: &[u8], secrets_bytes: &[u8]) -> Result<Vec<u8>> {
246 let mut mac = Hmac::<Sha256>::new_from_slice(hmac_key)
247 .map_err(|e| EnvVaultError::HmacError(format!("invalid HMAC key: {e}")))?;
248
249 mac.update(header_bytes);
250 mac.update(secrets_bytes);
251
252 Ok(mac.finalize().into_bytes().to_vec())
253}
254
255pub fn verify_hmac(
260 hmac_key: &[u8],
261 header_bytes: &[u8],
262 secrets_bytes: &[u8],
263 expected_hmac: &[u8],
264) -> Result<()> {
265 let mut mac = Hmac::<Sha256>::new_from_slice(hmac_key)
266 .map_err(|e| EnvVaultError::HmacError(format!("invalid HMAC key: {e}")))?;
267
268 mac.update(header_bytes);
269 mac.update(secrets_bytes);
270
271 mac.verify_slice(expected_hmac)
272 .map_err(|_| EnvVaultError::HmacMismatch)
273}
274
275use base64::engine::general_purpose::STANDARD as BASE64;
280use base64::Engine;
281
282pub(crate) fn base64_encode<S>(data: &[u8], serializer: S) -> std::result::Result<S::Ok, S::Error>
283where
284 S: serde::Serializer,
285{
286 let encoded = BASE64.encode(data);
287 serializer.serialize_str(&encoded)
288}
289
290pub(crate) fn base64_decode<'de, D>(deserializer: D) -> std::result::Result<Vec<u8>, D::Error>
291where
292 D: serde::Deserializer<'de>,
293{
294 let s = String::deserialize(deserializer)?;
295 BASE64.decode(&s).map_err(serde::de::Error::custom)
296}