feat(core+cli): SecondFactor hint on ParamsFile (default image, back-compat)
Add SecondFactor enum (Image | Keyfile, serde lowercase) to relicario-core::crypto, re-export from lib.rs alongside KdfParams. Add #[serde(default)] second_factor field to ParamsFile in relicario-cli::session so existing params.json files (no field) still parse as Image. for_new_vault() initialises the field to Image. No unlock logic changed; that is deferred to Task 4. TDD: tests written RED first, then GREEN. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
This commit is contained in:
@@ -12,7 +12,7 @@ use zeroize::Zeroizing;
|
|||||||
use relicario_core::{
|
use relicario_core::{
|
||||||
decrypt_item, decrypt_manifest, decrypt_settings,
|
decrypt_item, decrypt_manifest, decrypt_settings,
|
||||||
derive_master_key, encrypt_item, encrypt_manifest, encrypt_settings,
|
derive_master_key, encrypt_item, encrypt_manifest, encrypt_settings,
|
||||||
imgsecret, Item, ItemId, KdfParams, Manifest, VaultSettings,
|
imgsecret, Item, ItemId, KdfParams, Manifest, SecondFactor, VaultSettings,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::helpers::vault_dir;
|
use crate::helpers::vault_dir;
|
||||||
@@ -119,6 +119,11 @@ pub(crate) struct ParamsFile {
|
|||||||
pub kdf: ParamsKdf,
|
pub kdf: ParamsKdf,
|
||||||
pub aead: String,
|
pub aead: String,
|
||||||
pub salt_path: String,
|
pub salt_path: String,
|
||||||
|
/// Which second-factor container this vault uses to supply its 256-bit secret.
|
||||||
|
/// Defaults to `Image` (reference JPEG via DCT stego) for back-compat with
|
||||||
|
/// params.json files written before keyfile support was introduced.
|
||||||
|
#[serde(default)]
|
||||||
|
pub second_factor: SecondFactor,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
@@ -142,6 +147,7 @@ impl ParamsFile {
|
|||||||
},
|
},
|
||||||
aead: "xchacha20poly1305".into(),
|
aead: "xchacha20poly1305".into(),
|
||||||
salt_path: ".relicario/salt".into(),
|
salt_path: ".relicario/salt".into(),
|
||||||
|
second_factor: SecondFactor::Image,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,6 +254,32 @@ mod tests {
|
|||||||
assert_eq!(v["salt_path"], ".relicario/salt");
|
assert_eq!(v["salt_path"], ".relicario/salt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn params_file_second_factor_backcompat() {
|
||||||
|
// An old params.json without second_factor must deserialize with Image (back-compat).
|
||||||
|
let pf: ParamsFile = serde_json::from_str(FIXTURE).expect("parse old fixture");
|
||||||
|
assert_eq!(pf.second_factor, relicario_core::SecondFactor::Image);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn params_file_second_factor_keyfile() {
|
||||||
|
// A params.json WITH "second_factor":"keyfile" must deserialize as Keyfile.
|
||||||
|
let with_keyfile = r#"{
|
||||||
|
"format_version": 2,
|
||||||
|
"kdf": {
|
||||||
|
"algorithm": "argon2id-v0x13",
|
||||||
|
"argon2_m": 65536,
|
||||||
|
"argon2_t": 3,
|
||||||
|
"argon2_p": 4
|
||||||
|
},
|
||||||
|
"aead": "xchacha20poly1305",
|
||||||
|
"salt_path": ".relicario/salt",
|
||||||
|
"second_factor": "keyfile"
|
||||||
|
}"#;
|
||||||
|
let pf: ParamsFile = serde_json::from_str(with_keyfile).expect("parse keyfile fixture");
|
||||||
|
assert_eq!(pf.second_factor, relicario_core::SecondFactor::Keyfile);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn after_manifest_change_writes_manifest_and_groups_cache() {
|
fn after_manifest_change_writes_manifest_and_groups_cache() {
|
||||||
let dir = tempfile::TempDir::new().unwrap();
|
let dir = tempfile::TempDir::new().unwrap();
|
||||||
|
|||||||
@@ -55,6 +55,29 @@ use zeroize::Zeroizing;
|
|||||||
|
|
||||||
use crate::error::{RelicarioError, Result};
|
use crate::error::{RelicarioError, Result};
|
||||||
|
|
||||||
|
/// Which second-factor container a vault uses to supply its 256-bit secret.
|
||||||
|
///
|
||||||
|
/// This is a non-secret hint stored in params.json so that the CLI (and future
|
||||||
|
/// clients) know which unlock branch to follow. It does **not** influence the
|
||||||
|
/// KDF itself; only the container that delivers `image_secret` changes.
|
||||||
|
///
|
||||||
|
/// Serialized as lowercase via `serde(rename_all = "lowercase")`:
|
||||||
|
/// - `"image"` — default; secret embedded in a reference JPEG via DCT stego
|
||||||
|
/// - `"keyfile"` — secret stored in a plain key file (Task 4+)
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum SecondFactor {
|
||||||
|
/// The secret is embedded in a reference JPEG via DCT steganography.
|
||||||
|
/// This is the original and default mode for every vault created before
|
||||||
|
/// keyfile support was introduced.
|
||||||
|
#[default]
|
||||||
|
Image,
|
||||||
|
/// The secret is stored in a plain key file on disk.
|
||||||
|
/// Vaults with this hint require a key-file path at unlock time instead of
|
||||||
|
/// a reference image.
|
||||||
|
Keyfile,
|
||||||
|
}
|
||||||
|
|
||||||
/// Current binary format version. Increment this if the ciphertext layout changes.
|
/// Current binary format version. Increment this if the ciphertext layout changes.
|
||||||
pub const VERSION_BYTE: u8 = 0x02;
|
pub const VERSION_BYTE: u8 = 0x02;
|
||||||
|
|
||||||
@@ -417,6 +440,17 @@ mod tests {
|
|||||||
assert_eq!(VERSION_BYTE, 0x02);
|
assert_eq!(VERSION_BYTE, 0x02);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn second_factor_serde_behavior() {
|
||||||
|
assert_eq!(serde_json::to_string(&SecondFactor::Image).unwrap(), "\"image\"");
|
||||||
|
assert_eq!(serde_json::to_string(&SecondFactor::Keyfile).unwrap(), "\"keyfile\"");
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_str::<SecondFactor>("\"keyfile\"").unwrap(),
|
||||||
|
SecondFactor::Keyfile
|
||||||
|
);
|
||||||
|
assert_eq!(SecondFactor::default(), SecondFactor::Image);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn decrypt_rejects_v1_blob_with_typed_error() {
|
fn decrypt_rejects_v1_blob_with_typed_error() {
|
||||||
// Construct a v1-style blob: [0x01][24 nonce bytes][16 tag bytes].
|
// Construct a v1-style blob: [0x01][24 nonce bytes][16 tag bytes].
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ pub mod error;
|
|||||||
pub use error::{RelicarioError, Result};
|
pub use error::{RelicarioError, Result};
|
||||||
|
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub use crypto::{decrypt, derive_master_key, encrypt, KdfParams, VERSION_BYTE};
|
pub use crypto::{decrypt, derive_master_key, encrypt, KdfParams, SecondFactor, VERSION_BYTE};
|
||||||
|
|
||||||
pub mod ids;
|
pub mod ids;
|
||||||
pub use ids::{AttachmentId, FieldId, ItemId};
|
pub use ids::{AttachmentId, FieldId, ItemId};
|
||||||
|
|||||||
Reference in New Issue
Block a user