fix(core): NFC normalize backup passphrase (audit B2)
Backup KDF was passing raw passphrase bytes to Argon2id without NFC normalization, causing cross-platform restore failures for non-ASCII passphrases (macOS NFD vs Linux NFC). Now matches derive_master_key behavior from crypto.rs. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -301,12 +301,20 @@ pub fn unpack_backup(data: &[u8], passphrase: &str) -> Result<BackupOutput> {
|
||||
}
|
||||
|
||||
fn derive_backup_key(passphrase: &[u8], salt: &[u8]) -> Result<Zeroizing<[u8; 32]>> {
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
|
||||
// NFC normalize passphrase (matches derive_master_key in crypto.rs)
|
||||
let nfc_passphrase: Vec<u8> = match std::str::from_utf8(passphrase) {
|
||||
Ok(s) => s.nfc().collect::<String>().into_bytes(),
|
||||
Err(_) => passphrase.to_vec(),
|
||||
};
|
||||
|
||||
let params = Params::new(ARGON2_M_KIB, ARGON2_T, ARGON2_P, Some(32))
|
||||
.map_err(|e| RelicarioError::Kdf(format!("argon2 params: {e}")))?;
|
||||
let argon = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
||||
let mut key = Zeroizing::new([0u8; 32]);
|
||||
argon
|
||||
.hash_password_into(passphrase, salt, key.as_mut_slice())
|
||||
.hash_password_into(&nfc_passphrase, salt, key.as_mut_slice())
|
||||
.map_err(|e| RelicarioError::Kdf(format!("argon2 hash: {e}")))?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
@@ -186,3 +186,30 @@ fn tampered_ciphertext_rejected_as_decrypt_error() {
|
||||
other => panic!("expected Decrypt for tampered tag, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backup_roundtrip_with_nfd_passphrase() {
|
||||
// "café" in NFD (decomposed: e + combining acute accent)
|
||||
let nfd_passphrase = "caf\u{0065}\u{0301}";
|
||||
// "café" in NFC (precomposed é)
|
||||
let nfc_passphrase = "caf\u{00E9}";
|
||||
|
||||
let input = BackupInput {
|
||||
salt: &[0u8; 32],
|
||||
params_json: r#"{"format_version":2,"kdf":{"argon2_m":256,"argon2_t":1,"argon2_p":1},"aead":"xchacha20poly1305","salt_path":".relicario/salt"}"#,
|
||||
devices_json: "[]",
|
||||
manifest_enc: &[1, 2, 3],
|
||||
settings_enc: &[4, 5, 6],
|
||||
items: vec![],
|
||||
attachments: vec![],
|
||||
reference_jpg: None,
|
||||
git_archive: None,
|
||||
};
|
||||
|
||||
// Pack with NFD passphrase
|
||||
let packed = pack_backup(input, nfd_passphrase).unwrap();
|
||||
|
||||
// Unpack with NFC passphrase — should work after fix
|
||||
let unpacked = unpack_backup(&packed, nfc_passphrase).unwrap();
|
||||
assert_eq!(unpacked.manifest_enc, vec![1, 2, 3]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user