Merge remote-tracking branch 'origin/main' into feature/v0.9.0-dev-e-keyfile-ext
This commit is contained in:
@@ -56,7 +56,8 @@ pub fn cmd_init(image: Option<PathBuf>, output: PathBuf, key_file: Option<PathBu
|
||||
// the server would hold both the ciphertext and the key, defeating
|
||||
// the two-factor model.
|
||||
let kf_path = root.join(kf);
|
||||
fs::write(&kf_path, relicario_core::keyfile::keyfile_encode(&secret))
|
||||
let armored = relicario_core::keyfile::keyfile_encode(&secret);
|
||||
fs::write(&kf_path, armored.as_slice())
|
||||
.with_context(|| format!("failed to write key file {}", kf_path.display()))?;
|
||||
let name = kf
|
||||
.file_name()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use assert_cmd::prelude::*;
|
||||
use predicates::prelude::PredicateBooleanExt;
|
||||
use std::process::Command;
|
||||
|
||||
fn relicario(dir: &tempfile::TempDir) -> Command {
|
||||
@@ -139,6 +140,86 @@ fn recovery_qr_generate_works_for_keyfile_vault() {
|
||||
.stdout(predicates::str::contains("Recovery QR generated"));
|
||||
}
|
||||
|
||||
/// `backup export --include-image` on a key-file vault must fail with a clear message.
|
||||
/// (Key-file vaults have no reference JPEG to bundle.)
|
||||
#[test]
|
||||
fn backup_include_image_fails_on_keyfile_vault() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
relicario(&dir)
|
||||
.args(["init", "--key-file", "vault.relkey"])
|
||||
.env("RELICARIO_TEST_PASSPHRASE", "correct horse")
|
||||
.assert()
|
||||
.success();
|
||||
let out_path = dir.path().join("vault.relbak");
|
||||
relicario(&dir)
|
||||
.args(["backup", "export", out_path.to_str().unwrap(), "--include-image"])
|
||||
.env("RELICARIO_TEST_BACKUP_PASSPHRASE", "strong-backup-pass-test-2026")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicates::str::contains("not applicable to a key-file vault"));
|
||||
}
|
||||
|
||||
/// A malformed key file (garbage bytes) must fail with "invalid key file" in stderr.
|
||||
/// This is the `keyfile_decode` → `.context("invalid key file")` path.
|
||||
#[test]
|
||||
fn malformed_keyfile_emits_invalid_key_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
relicario(&dir)
|
||||
.args(["init", "--key-file", "vault.relkey"])
|
||||
.env("RELICARIO_TEST_PASSPHRASE", "correct horse")
|
||||
.assert()
|
||||
.success();
|
||||
// Overwrite the key file with garbage — keyfile_decode will reject the header.
|
||||
std::fs::write(dir.path().join("vault.relkey"), b"not a valid keyfile\n").unwrap();
|
||||
relicario(&dir)
|
||||
.args(["list"])
|
||||
.env("RELICARIO_TEST_PASSPHRASE", "correct horse")
|
||||
.env("RELICARIO_KEYFILE", dir.path().join("vault.relkey"))
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicates::str::contains("invalid key file"));
|
||||
}
|
||||
|
||||
/// A well-formed but WRONG key file (valid armor, wrong 32-byte secret) must produce
|
||||
/// a decryption failure — NOT "invalid key file". There must be no oracle about
|
||||
/// which factor was wrong.
|
||||
#[test]
|
||||
fn wrong_secret_emits_decryption_failed_not_invalid_key_file() {
|
||||
// Vault A: the target vault.
|
||||
let dir_a = tempfile::tempdir().unwrap();
|
||||
relicario(&dir_a)
|
||||
.args(["init", "--key-file", "vault.relkey"])
|
||||
.env("RELICARIO_TEST_PASSPHRASE", "correct horse")
|
||||
.assert()
|
||||
.success();
|
||||
// Add an item so there is encrypted manifest content to exercise.
|
||||
relicario(&dir_a)
|
||||
.args(["add", "login", "--title", "test", "--username", "u", "--password", "p"])
|
||||
.env("RELICARIO_TEST_PASSPHRASE", "correct horse")
|
||||
.env("RELICARIO_KEYFILE", dir_a.path().join("vault.relkey"))
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Vault B: a completely separate vault with a different random 32-byte secret.
|
||||
let dir_b = tempfile::tempdir().unwrap();
|
||||
relicario(&dir_b)
|
||||
.args(["init", "--key-file", "vault.relkey"])
|
||||
.env("RELICARIO_TEST_PASSPHRASE", "correct horse")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Open vault A with vault B's key file: armor parses fine, but the derived
|
||||
// master key is wrong → manifest decryption fails with RelicarioError::Decrypt.
|
||||
relicario(&dir_a)
|
||||
.args(["list"])
|
||||
.env("RELICARIO_TEST_PASSPHRASE", "correct horse")
|
||||
.env("RELICARIO_KEYFILE", dir_b.path().join("vault.relkey"))
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicates::str::contains("decryption failed"))
|
||||
.stderr(predicates::str::contains("invalid key file").not());
|
||||
}
|
||||
|
||||
/// --image and --key-file are mutually exclusive (clap rejects before the handler).
|
||||
#[test]
|
||||
fn init_with_both_factors_fails() {
|
||||
|
||||
@@ -25,8 +25,8 @@ const HEADER: &str = "relicario-keyfile-v1";
|
||||
///
|
||||
/// Output is: `"relicario-keyfile-v1\n<base64>\n"` as UTF-8 bytes.
|
||||
/// The returned bytes are suitable for writing directly to a `.relkey` file.
|
||||
pub fn keyfile_encode(secret: &[u8; 32]) -> Vec<u8> {
|
||||
format!("{HEADER}\n{}\n", STANDARD.encode(secret)).into_bytes()
|
||||
pub fn keyfile_encode(secret: &[u8; 32]) -> Zeroizing<Vec<u8>> {
|
||||
Zeroizing::new(format!("{HEADER}\n{}\n", STANDARD.encode(secret)).into_bytes())
|
||||
}
|
||||
|
||||
/// Decode a `relicario-keyfile-v1` armored file back to the raw 32-byte secret.
|
||||
|
||||
@@ -100,7 +100,7 @@ pub fn unlock_with_secret(
|
||||
pub fn keyfile_encode(secret: &[u8]) -> Result<Vec<u8>, JsError> {
|
||||
let arr: &[u8; 32] = secret.try_into()
|
||||
.map_err(|_| JsError::new("secret must be exactly 32 bytes"))?;
|
||||
Ok(relicario_core::keyfile::keyfile_encode(arr))
|
||||
Ok(relicario_core::keyfile::keyfile_encode(arr).to_vec())
|
||||
}
|
||||
|
||||
/// Decode a `relicario-keyfile-v1` armored file, returning the raw 32-byte secret.
|
||||
|
||||
Reference in New Issue
Block a user