feat(security): implement Phase 1 of Plausible Deniability hardening
This commit is contained in:
+42
-7
@@ -146,6 +146,7 @@ pub fn generate_dummy_slot() -> (Vec<u8>, [u8; 12], [u8; 16], [u8; 16]) {
|
||||
|
||||
/// Verschlüsselt den Dateinamen für Knoten im Hidden Vault mit AES-256-GCM.
|
||||
/// Verhindert, dass unverschlüsselte Dateinamen in der SQLite-Datenbank forensisch auffindbar sind.
|
||||
/// Verwendet reines Hex-Encoding ohne verräterisches Präfix (12B Nonce + 16B Tag + Ciphertext).
|
||||
pub fn encrypt_node_name(dek: &[u8; 32], name: &str) -> String {
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
OsRng.fill_bytes(&mut nonce_bytes);
|
||||
@@ -154,16 +155,16 @@ pub fn encrypt_node_name(dek: &[u8; 32], name: &str) -> String {
|
||||
let tag = cipher
|
||||
.encrypt_in_place_detached(Nonce::from_slice(&nonce_bytes), b"SANCTUM_NODE_NAME", &mut buffer)
|
||||
.expect("Name encryption");
|
||||
format!(
|
||||
"$h${}${}${}",
|
||||
hex::encode(nonce_bytes),
|
||||
hex::encode(tag.as_slice()),
|
||||
hex::encode(&buffer)
|
||||
)
|
||||
let mut combined = Vec::with_capacity(12 + 16 + buffer.len());
|
||||
combined.extend_from_slice(&nonce_bytes);
|
||||
combined.extend_from_slice(tag.as_slice());
|
||||
combined.extend_from_slice(&buffer);
|
||||
hex::encode(combined)
|
||||
}
|
||||
|
||||
/// Entschlüsselt den Dateinamen eines Knotens im Hidden Vault mit AES-256-GCM.
|
||||
pub fn decrypt_node_name(dek: &[u8; 32], stored: &str) -> Option<String> {
|
||||
// Abwärtskompatibilität für alte v0.2.0 $h$<nonce>$<tag>$<ct> Namen
|
||||
if let Some(rest) = stored.strip_prefix("$h$") {
|
||||
let parts: Vec<&str> = rest.split('$').collect();
|
||||
if parts.len() == 3 {
|
||||
@@ -189,6 +190,33 @@ pub fn decrypt_node_name(dek: &[u8; 32], stored: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// Neuer Standard: Reiner Hex-String ohne Präfix (12B Nonce + 16B Tag + Ciphertext)
|
||||
if stored.len() >= 56 {
|
||||
if let Ok(bytes) = hex::decode(stored) {
|
||||
if bytes.len() >= 28 {
|
||||
let nonce = &bytes[0..12];
|
||||
let tag = &bytes[12..28];
|
||||
let ct = &bytes[28..];
|
||||
|
||||
if let Ok(cipher) = Aes256Gcm::new_from_slice(dek) {
|
||||
let mut buffer = ct.to_vec();
|
||||
if cipher
|
||||
.decrypt_in_place_detached(
|
||||
Nonce::from_slice(nonce),
|
||||
b"SANCTUM_NODE_NAME",
|
||||
&mut buffer,
|
||||
Tag::from_slice(tag),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
return String::from_utf8(buffer).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -482,12 +510,19 @@ mod tests {
|
||||
let dek = generate_dek();
|
||||
let filename = "ultra_geheimes_dokument.pdf";
|
||||
let encrypted = encrypt_node_name(&dek, filename);
|
||||
assert!(encrypted.starts_with("$h$"));
|
||||
// Kein verräterisches Präfix mehr! Reines Hex.
|
||||
assert!(!encrypted.starts_with("$h$"));
|
||||
assert!(!encrypted.contains(filename));
|
||||
assert!(encrypted.len() >= 56);
|
||||
|
||||
let decrypted = decrypt_node_name(&dek, &encrypted).expect("Decrypt name");
|
||||
assert_eq!(decrypted, filename);
|
||||
|
||||
// Abwärtskompatibilität: Legacy $h$<nonce>$<tag>$<ct> Format muss weiter entschlüsselt werden
|
||||
let legacy_format = format!("$h${}${}${}", &encrypted[0..24], &encrypted[24..56], &encrypted[56..]);
|
||||
let decrypted_legacy = decrypt_node_name(&dek, &legacy_format).expect("Decrypt legacy $h$ name");
|
||||
assert_eq!(decrypted_legacy, filename);
|
||||
|
||||
// Mit anderem DEK schlägt Entschlüsselung fehl
|
||||
let other_dek = generate_dek();
|
||||
assert!(decrypt_node_name(&other_dek, &encrypted).is_none());
|
||||
|
||||
Reference in New Issue
Block a user