feat(security): implement Phase 2 Modell A (Steganografischer Alibi-Carrier für Plausible Deniability)

This commit is contained in:
2026-09-09 20:22:00 +02:00
parent b8e4dcb614
commit 030ce6a1e5
8 changed files with 1971 additions and 73 deletions
+101 -25
View File
@@ -78,11 +78,10 @@ pub fn generate_salt() -> [u8; 16] {
salt
}
/// Verschlüsselt den DEK mit dem KEK via AES-256-GCM.
/// Gibt (wrapped_dek_32_bytes, nonce_12_bytes, tag_16_bytes) zurück.
pub fn wrap_dek(
/// Verschlüsselt beliebige Schlüsseldaten (32B DEK, 40B Slot0-Payload oder 72B Slot1-Payload) via AES-256-GCM.
pub fn wrap_key_payload(
kek: &[u8; 32],
dek: &[u8; 32],
payload: &[u8],
) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
let cipher = Aes256Gcm::new_from_slice(kek)
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
@@ -91,10 +90,10 @@ pub fn wrap_dek(
OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let mut buffer = dek.to_vec();
let mut buffer = payload.to_vec();
let tag = cipher
.encrypt_in_place_detached(nonce, b"SANCTUM_HEADER_DEK", &mut buffer)
.map_err(|e| anyhow::anyhow!("DEK-Wrapping fehlgeschlagen: {e}"))?;
.map_err(|e| anyhow::anyhow!("Key-Wrapping fehlgeschlagen: {e}"))?;
let mut tag_bytes = [0u8; 16];
tag_bytes.copy_from_slice(tag.as_slice());
@@ -102,6 +101,36 @@ pub fn wrap_dek(
Ok((buffer, nonce_bytes, tag_bytes))
}
/// Entschlüsselt beliebige Schlüsseldaten via AES-256-GCM und validiert die Authentizität.
pub fn unwrap_key_payload(
kek: &[u8; 32],
wrapped_payload: &[u8],
nonce_bytes: &[u8; 12],
tag_bytes: &[u8; 16],
) -> Result<Zeroizing<Vec<u8>>> {
let cipher = Aes256Gcm::new_from_slice(kek)
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
let nonce = Nonce::from_slice(nonce_bytes);
let tag = Tag::from_slice(tag_bytes);
let mut buffer = wrapped_payload.to_vec();
cipher
.decrypt_in_place_detached(nonce, b"SANCTUM_HEADER_DEK", &mut buffer, tag)
.map_err(|_| anyhow::anyhow!("Passwort falsch oder Header beschädigt (AEAD Authentifizierungsfehler)"))?;
Ok(Zeroizing::new(buffer))
}
/// Verschlüsselt den DEK (32 Bytes) mit dem KEK via AES-256-GCM.
/// Gibt (wrapped_dek_32_bytes, nonce_12_bytes, tag_16_bytes) zurück.
pub fn wrap_dek(
kek: &[u8; 32],
dek: &[u8; 32],
) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
wrap_key_payload(kek, dek)
}
/// Entschlüsselt den DEK mit dem KEK via AES-256-GCM und validiert die Authentizität.
pub fn unwrap_dek(
kek: &[u8; 32],
@@ -109,31 +138,47 @@ pub fn unwrap_dek(
nonce_bytes: &[u8; 12],
tag_bytes: &[u8; 16],
) -> Result<Zeroizing<[u8; 32]>> {
if wrapped_dek.len() != 32 {
bail!("Ungültige wrapped_dek Länge: erwartet 32 Bytes, erhalten {}", wrapped_dek.len());
let payload = unwrap_key_payload(kek, wrapped_dek, nonce_bytes, tag_bytes)?;
if payload.len() < 32 {
bail!("Ungültige wrapped_dek Länge: erwartet mindestens 32 Bytes, erhalten {}", payload.len());
}
let cipher = Aes256Gcm::new_from_slice(kek)
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
let nonce = Nonce::from_slice(nonce_bytes);
let tag = Tag::from_slice(tag_bytes);
let mut buffer = wrapped_dek.to_vec();
cipher
.decrypt_in_place_detached(nonce, b"SANCTUM_HEADER_DEK", &mut buffer, tag)
.map_err(|_| anyhow::anyhow!("Passwort falsch oder Header beschädigt (AEAD Authentifizierungsfehler)"))?;
let mut dek = Zeroizing::new([0u8; 32]);
dek.copy_from_slice(&buffer);
dek.copy_from_slice(&payload[0..32]);
Ok(dek)
}
/// Verschlüsselt den Slot-0 Payload (32 Bytes DEK_0 || 8 Bytes carrier_node_id Little-Endian).
pub fn wrap_slot0_payload(
kek: &[u8; 32],
dek_0: &[u8; 32],
carrier_node_id: i64,
) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
let mut payload = Vec::with_capacity(40);
payload.extend_from_slice(dek_0);
payload.extend_from_slice(&carrier_node_id.to_le_bytes());
wrap_key_payload(kek, &payload)
}
/// Verschlüsselt den Slot-1 Payload für Modell A (32 Bytes DEK_1 || 32 Bytes DEK_0 || 8 Bytes carrier_node_id Little-Endian).
pub fn wrap_slot1_payload(
kek: &[u8; 32],
dek_1: &[u8; 32],
dek_0: &[u8; 32],
carrier_node_id: i64,
) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
let mut payload = Vec::with_capacity(72);
payload.extend_from_slice(dek_1);
payload.extend_from_slice(dek_0);
payload.extend_from_slice(&carrier_node_id.to_le_bytes());
wrap_key_payload(kek, &payload)
}
/// Erzeugt einen Dummy-Header-Slot mit kryptografisch sicherem Zufallsrauschen derselben Länge wie
/// ein echter KDF/DEK-Slot. Dadurch sind Standard-Container von Containern mit Hidden Vault
/// auf Bitebene und Entropieebene ununterscheidbar (Plausible Deniability).
/// ein echter Modell-A Slot 1 (72 Bytes wrapped Payload). Dadurch sind Standard-Container von
/// Containern mit Hidden Vault auf Bitebene und Entropieebene ununterscheidbar (Plausible Deniability).
pub fn generate_dummy_slot() -> (Vec<u8>, [u8; 12], [u8; 16], [u8; 16]) {
let mut wrapped_dek = vec![0u8; 32];
let mut wrapped_dek = vec![0u8; 72];
let mut nonce = [0u8; 12];
let mut tag = [0u8; 16];
let mut salt = [0u8; 16];
@@ -527,12 +572,43 @@ mod tests {
let other_dek = generate_dek();
assert!(decrypt_node_name(&other_dek, &encrypted).is_none());
// Dummy-Slot hat korrekte Längen
// Dummy-Slot hat korrekte Längen (72 Bytes für Modell A)
let (dummy_dek, dummy_nonce, dummy_tag, dummy_salt) = generate_dummy_slot();
assert_eq!(dummy_dek.len(), 32);
assert_eq!(dummy_dek.len(), 72);
assert_eq!(dummy_nonce.len(), 12);
assert_eq!(dummy_tag.len(), 16);
assert_eq!(dummy_salt.len(), 16);
}
#[test]
fn test_model_a_slot_payloads() {
let kek_0 = derive_kek("DecoyPass123!", &generate_salt(), &KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1 }).unwrap();
let kek_1 = derive_kek("HiddenPass123!", &generate_salt(), &KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1 }).unwrap();
let dek_0 = generate_dek();
let dek_1 = generate_dek();
let carrier_node_id = 42i64;
// Slot 0 Payload: 40 Bytes
let (wrapped_0, nonce_0, tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
assert_eq!(wrapped_0.len(), 40);
let unwrapped_0 = unwrap_key_payload(&kek_0, &wrapped_0, &nonce_0, &tag_0).unwrap();
assert_eq!(unwrapped_0.len(), 40);
assert_eq!(&unwrapped_0[0..32], &*dek_0);
let recovered_cid_0 = i64::from_le_bytes(unwrapped_0[32..40].try_into().unwrap());
assert_eq!(recovered_cid_0, carrier_node_id);
// Slot 1 Payload: 72 Bytes
let (wrapped_1, nonce_1, tag_1) = wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
assert_eq!(wrapped_1.len(), 72);
let unwrapped_1 = unwrap_key_payload(&kek_1, &wrapped_1, &nonce_1, &tag_1).unwrap();
assert_eq!(unwrapped_1.len(), 72);
assert_eq!(&unwrapped_1[0..32], &*dek_1);
assert_eq!(&unwrapped_1[32..64], &*dek_0);
let recovered_cid_1 = i64::from_le_bytes(unwrapped_1[64..72].try_into().unwrap());
assert_eq!(recovered_cid_1, carrier_node_id);
}
}