feat(recovery): implement header backup/restore, BIP-39 recovery key, and integrity verification
This commit is contained in:
@@ -129,6 +129,37 @@ pub fn unwrap_dek(
|
||||
Ok(dek)
|
||||
}
|
||||
|
||||
/// Kodiert den 32-Byte (256-Bit) DEK in eine 24-Wort BIP-39 Notfall-Wiederherstellungsphrase (englisch) mit 8-Bit Checksumme.
|
||||
pub fn dek_to_mnemonic(dek: &[u8; 32]) -> Result<String> {
|
||||
let mnemonic = bip39::Mnemonic::from_entropy(dek)
|
||||
.map_err(|e| anyhow::anyhow!("Fehler beim Erzeugen der BIP-39 Notfallphrase: {e}"))?;
|
||||
Ok(mnemonic.to_string())
|
||||
}
|
||||
|
||||
/// Dekodiert eine 24-Wort BIP-39 Notfall-Wiederherstellungsphrase zurück in den 32-Byte DEK.
|
||||
/// Validiert dabei Wörter und die integrierte BIP-39 Prüfsumme.
|
||||
pub fn mnemonic_to_dek(phrase: &str) -> Result<Zeroizing<[u8; 32]>> {
|
||||
let cleaned = phrase
|
||||
.split_whitespace()
|
||||
.collect::<Vec<&str>>()
|
||||
.join(" ");
|
||||
|
||||
let mnemonic = bip39::Mnemonic::parse_normalized(&cleaned)
|
||||
.map_err(|e| anyhow::anyhow!("Ungültige BIP-39 Notfallphrase (Wortfehler oder ungültige Prüfsumme): {e}"))?;
|
||||
|
||||
let entropy = mnemonic.to_entropy();
|
||||
if entropy.len() != 32 {
|
||||
bail!(
|
||||
"Ungültige Entropielänge aus Mnemonic: erwartet 32 Bytes (24 Wörter), erhalten {}",
|
||||
entropy.len()
|
||||
);
|
||||
}
|
||||
|
||||
let mut dek = Zeroizing::new([0u8; 32]);
|
||||
dek.copy_from_slice(&entropy);
|
||||
Ok(dek)
|
||||
}
|
||||
|
||||
/// Erzeugt die 16-Byte Associated Data (AAD) für einen Chunk, um Swap-Angriffe zu verhindern:
|
||||
/// node_id (8 Bytes Little-Endian) || chunk_index (8 Bytes Little-Endian).
|
||||
#[inline]
|
||||
@@ -342,4 +373,44 @@ mod tests {
|
||||
assert_eq!(decrypted, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bip39_recovery_phrase_roundtrip() {
|
||||
let dek = generate_dek();
|
||||
let mnemonic_str = dek_to_mnemonic(&dek).expect("Generate mnemonic");
|
||||
let words: Vec<&str> = mnemonic_str.split_whitespace().collect();
|
||||
assert_eq!(words.len(), 24, "Mnemonic must have exactly 24 words");
|
||||
|
||||
let recovered_dek = mnemonic_to_dek(&mnemonic_str).expect("Recover DEK");
|
||||
assert_eq!(*dek, *recovered_dek, "Recovered DEK must match original DEK");
|
||||
|
||||
// Whitespace-Toleranz (z. B. doppelte Leerzeichen, Zeilenumbrüche)
|
||||
let messy_phrase = format!(" {} \n\t {} ", words[0..12].join(" "), words[12..24].join(" \n "));
|
||||
let recovered_messy = mnemonic_to_dek(&messy_phrase).expect("Recover messy");
|
||||
assert_eq!(*dek, *recovered_messy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bip39_invalid_words_and_checksum() {
|
||||
// 1. Nicht im Wörterbuch enthaltenes Wort
|
||||
let invalid_word_phrase = "abandon amount anchor animal archive arm armed army armor arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow fakeinvalidword";
|
||||
assert!(mnemonic_to_dek(invalid_word_phrase).is_err());
|
||||
|
||||
// 2. Falsche Wortanzahl (z. B. 23 statt 24)
|
||||
let short_phrase = "abandon amount anchor animal archive arm armed army armor arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow";
|
||||
assert!(mnemonic_to_dek(short_phrase).is_err());
|
||||
|
||||
// 3. Gültige Wörter, aber Prüfsumme ungültig (letztes Wort verändert)
|
||||
let dek = generate_dek();
|
||||
let mut words: Vec<String> = dek_to_mnemonic(&dek)
|
||||
.unwrap()
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
// Tausche das letzte Wort gegen ein anderes gültiges BIP-39 Wort
|
||||
let original_last = words[23].clone();
|
||||
words[23] = if original_last == "abandon" { "zoo".to_string() } else { "abandon".to_string() };
|
||||
let corrupted_phrase = words.join(" ");
|
||||
assert!(mnemonic_to_dek(&corrupted_phrase).is_err(), "Checksum check must fail");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user