feat(security): implement Phase 2 Modell A (Steganografischer Alibi-Carrier für Plausible Deniability)
This commit is contained in:
+101
-25
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user