diff --git a/crates/relicario-cli/src/commands/init.rs b/crates/relicario-cli/src/commands/init.rs index c5aedc1..f6612b2 100644 --- a/crates/relicario-cli/src/commands/init.rs +++ b/crates/relicario-cli/src/commands/init.rs @@ -65,17 +65,7 @@ pub fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> { fs::write(relicario_dir.join("salt"), salt)?; fs::write( relicario_dir.join("params.json"), - serde_json::to_string_pretty(&ParamsFile { - format_version: 2, - kdf: ParamsKdf { - algorithm: "argon2id-v0x13".into(), - argon2_m: params.argon2_m, - argon2_t: params.argon2_t, - argon2_p: params.argon2_p, - }, - aead: "xchacha20poly1305".into(), - salt_path: ".relicario/salt".into(), - })?, + serde_json::to_string_pretty(&crate::session::ParamsFile::for_new_vault(¶ms))?, )?; let manifest = Manifest::new(); fs::write(root.join("manifest.enc"), encrypt_manifest(&manifest, &master_key)?)?; @@ -106,20 +96,3 @@ pub fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> { eprintln!(" \u{2192} back this file up somewhere safe; it is your second factor."); Ok(()) } - -#[derive(serde::Serialize)] -struct ParamsFile { - format_version: u32, - kdf: ParamsKdf, - aead: String, - salt_path: String, -} - -#[derive(serde::Serialize)] -#[serde(rename_all = "snake_case")] -struct ParamsKdf { - algorithm: String, - argon2_m: u32, - argon2_t: u32, - argon2_p: u32, -} diff --git a/crates/relicario-cli/src/session.rs b/crates/relicario-cli/src/session.rs index ec42f1b..7bbf68e 100644 --- a/crates/relicario-cli/src/session.rs +++ b/crates/relicario-cli/src/session.rs @@ -107,17 +107,52 @@ fn read_salt(root: &Path) -> Result<[u8; 32]> { Ok(salt) } -fn read_params(root: &Path) -> Result { - // params.json layout: { "format_version": 2, "kdf": { "argon2_m": ..., ... }, ... } - // We extract only the "kdf" sub-object and deserialize it as KdfParams. - #[derive(serde::Deserialize)] - struct ParamsFile { - kdf: KdfParams, +#[derive(serde::Serialize, serde::Deserialize)] +pub(crate) struct ParamsFile { + pub format_version: u32, + pub kdf: ParamsKdf, + pub aead: String, + pub salt_path: String, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) struct ParamsKdf { + pub algorithm: String, + pub argon2_m: u32, + pub argon2_t: u32, + pub argon2_p: u32, +} + +impl ParamsFile { + pub fn for_new_vault(params: &KdfParams) -> Self { + Self { + format_version: 2, + kdf: ParamsKdf { + algorithm: "argon2id-v0x13".into(), + argon2_m: params.argon2_m, + argon2_t: params.argon2_t, + argon2_p: params.argon2_p, + }, + aead: "xchacha20poly1305".into(), + salt_path: ".relicario/salt".into(), + } } + + pub fn into_kdf_params(self) -> KdfParams { + KdfParams { + argon2_m: self.kdf.argon2_m, + argon2_t: self.kdf.argon2_t, + argon2_p: self.kdf.argon2_p, + } + } +} + +fn read_params(root: &Path) -> Result { let s = fs::read_to_string(root.join(".relicario").join("params.json")) .context("failed to read .relicario/params.json")?; let pf: ParamsFile = serde_json::from_str(&s).context("failed to parse params.json")?; - Ok(pf.kdf) + Ok(pf.into_kdf_params()) } /// Locate the reference image path via `RELICARIO_IMAGE` env var or interactive prompt. @@ -149,3 +184,72 @@ fn atomic_write(path: &Path, data: &[u8]) -> Result<()> { fs::rename(&tmp, path).with_context(|| format!("failed to rename {}", path.display()))?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + const FIXTURE: &str = r#"{ + "format_version": 2, + "kdf": { + "algorithm": "argon2id-v0x13", + "argon2_m": 65536, + "argon2_t": 3, + "argon2_p": 4 + }, + "aead": "xchacha20poly1305", + "salt_path": ".relicario/salt" +}"#; + + #[test] + fn params_file_round_trips_current_layout() { + let pf: ParamsFile = serde_json::from_str(FIXTURE).expect("parse fixture"); + assert_eq!(pf.format_version, 2); + assert_eq!(pf.kdf.algorithm, "argon2id-v0x13"); + assert_eq!(pf.kdf.argon2_m, 65536); + assert_eq!(pf.kdf.argon2_t, 3); + assert_eq!(pf.kdf.argon2_p, 4); + assert_eq!(pf.aead, "xchacha20poly1305"); + assert_eq!(pf.salt_path, ".relicario/salt"); + + let kdf = ParamsFile { + format_version: pf.format_version, + kdf: ParamsKdf { + algorithm: pf.kdf.algorithm.clone(), + argon2_m: pf.kdf.argon2_m, + argon2_t: pf.kdf.argon2_t, + argon2_p: pf.kdf.argon2_p, + }, + aead: pf.aead.clone(), + salt_path: pf.salt_path.clone(), + } + .into_kdf_params(); + assert_eq!(kdf.argon2_m, 65536); + assert_eq!(kdf.argon2_t, 3); + assert_eq!(kdf.argon2_p, 4); + + let serialized = serde_json::to_string(&pf).expect("re-serialize"); + let pf2: ParamsFile = serde_json::from_str(&serialized).expect("parse re-serialized"); + assert_eq!(pf2.format_version, 2); + assert_eq!(pf2.kdf.algorithm, "argon2id-v0x13"); + assert_eq!(pf2.kdf.argon2_m, 65536); + assert_eq!(pf2.kdf.argon2_t, 3); + assert_eq!(pf2.kdf.argon2_p, 4); + assert_eq!(pf2.aead, "xchacha20poly1305"); + assert_eq!(pf2.salt_path, ".relicario/salt"); + } + + #[test] + fn for_new_vault_produces_expected_shape() { + let params = KdfParams { argon2_m: 65536, argon2_t: 3, argon2_p: 4 }; + let pf = ParamsFile::for_new_vault(¶ms); + let v = serde_json::to_value(&pf).expect("to_value"); + assert_eq!(v["format_version"], 2); + assert_eq!(v["kdf"]["algorithm"], "argon2id-v0x13"); + assert_eq!(v["kdf"]["argon2_m"], 65536); + assert_eq!(v["kdf"]["argon2_t"], 3); + assert_eq!(v["kdf"]["argon2_p"], 4); + assert_eq!(v["aead"], "xchacha20poly1305"); + assert_eq!(v["salt_path"], ".relicario/salt"); + } +}