Initial commit: Sanctum encrypted single-file container for Windows
This commit is contained in:
+248
@@ -0,0 +1,248 @@
|
||||
use aes_gcm::{
|
||||
aead::{AeadInPlace, KeyInit},
|
||||
Aes256Gcm, Nonce, Tag,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use argon2::{Algorithm, Argon2, Params, Version};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
pub const MAGIC_BYTES: &[u8; 8] = b"SANCTUM\0";
|
||||
pub const FORMAT_VERSION: u32 = 1;
|
||||
pub const CHUNK_SIZE: usize = 1024 * 1024; // 1 MB
|
||||
|
||||
pub const DEFAULT_MEMORY_COST_KIB: u32 = 64 * 1024; // 64 MB
|
||||
pub const DEFAULT_TIME_COST: u32 = 3;
|
||||
pub const DEFAULT_PARALLELISM: u32 = 4;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct KdfParams {
|
||||
pub memory_cost: u32,
|
||||
pub time_cost: u32,
|
||||
pub parallelism: u32,
|
||||
}
|
||||
|
||||
impl Default for KdfParams {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
memory_cost: DEFAULT_MEMORY_COST_KIB,
|
||||
time_cost: DEFAULT_TIME_COST,
|
||||
parallelism: DEFAULT_PARALLELISM,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Leitet aus dem Master-Passwort und dem Salt einen 256-Bit Key Encryption Key (KEK) via Argon2id ab.
|
||||
pub fn derive_kek(
|
||||
password: &str,
|
||||
salt: &[u8],
|
||||
params: &KdfParams,
|
||||
) -> Result<Zeroizing<[u8; 32]>> {
|
||||
let argon2_params = Params::new(
|
||||
params.memory_cost,
|
||||
params.time_cost,
|
||||
params.parallelism,
|
||||
Some(32),
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("Ungültige Argon2-Parameter: {e}"))?;
|
||||
|
||||
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
|
||||
let mut kek = Zeroizing::new([0u8; 32]);
|
||||
|
||||
argon2
|
||||
.hash_password_into(password.as_bytes(), salt, &mut *kek)
|
||||
.map_err(|e| anyhow::anyhow!("Argon2id KDF-Berechnung fehlgeschlagen: {e}"))?;
|
||||
|
||||
Ok(kek)
|
||||
}
|
||||
|
||||
/// Generiert einen kryptografisch sicheren 256-Bit Data Encryption Key (DEK).
|
||||
pub fn generate_dek() -> Zeroizing<[u8; 32]> {
|
||||
let mut dek = Zeroizing::new([0u8; 32]);
|
||||
OsRng.fill_bytes(&mut *dek);
|
||||
dek
|
||||
}
|
||||
|
||||
/// Generiert ein kryptografisch sicheres 16-Byte KDF-Salt.
|
||||
pub fn generate_salt() -> [u8; 16] {
|
||||
let mut salt = [0u8; 16];
|
||||
OsRng.fill_bytes(&mut salt);
|
||||
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(
|
||||
kek: &[u8; 32],
|
||||
dek: &[u8; 32],
|
||||
) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
|
||||
let cipher = Aes256Gcm::new_from_slice(kek)
|
||||
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
|
||||
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
OsRng.fill_bytes(&mut nonce_bytes);
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
let mut buffer = dek.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}"))?;
|
||||
|
||||
let mut tag_bytes = [0u8; 16];
|
||||
tag_bytes.copy_from_slice(tag.as_slice());
|
||||
|
||||
Ok((buffer, nonce_bytes, tag_bytes))
|
||||
}
|
||||
|
||||
/// Entschlüsselt den DEK mit dem KEK via AES-256-GCM und validiert die Authentizität.
|
||||
pub fn unwrap_dek(
|
||||
kek: &[u8; 32],
|
||||
wrapped_dek: &[u8],
|
||||
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 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);
|
||||
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]
|
||||
pub fn build_chunk_aad(node_id: i64, chunk_index: u32) -> [u8; 16] {
|
||||
let mut aad = [0u8; 16];
|
||||
aad[..8].copy_from_slice(&node_id.to_le_bytes());
|
||||
aad[8..].copy_from_slice(&(chunk_index as u64).to_le_bytes());
|
||||
aad
|
||||
}
|
||||
|
||||
/// Verschlüsselt einen Payload-Chunk mit dem DEK via AES-256-GCM unter Einbindung von AAD.
|
||||
/// Gibt (ciphertext, nonce_12_bytes, tag_16_bytes) zurück.
|
||||
pub fn encrypt_chunk(
|
||||
dek: &[u8; 32],
|
||||
node_id: i64,
|
||||
chunk_index: u32,
|
||||
plaintext: &[u8],
|
||||
) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
|
||||
let cipher = Aes256Gcm::new_from_slice(dek)
|
||||
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
|
||||
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
OsRng.fill_bytes(&mut nonce_bytes);
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
let aad = build_chunk_aad(node_id, chunk_index);
|
||||
|
||||
let mut buffer = plaintext.to_vec();
|
||||
let tag = cipher
|
||||
.encrypt_in_place_detached(nonce, &aad, &mut buffer)
|
||||
.map_err(|e| anyhow::anyhow!("Chunk-Verschlüsselung fehlgeschlagen: {e}"))?;
|
||||
|
||||
let mut tag_bytes = [0u8; 16];
|
||||
tag_bytes.copy_from_slice(tag.as_slice());
|
||||
|
||||
Ok((buffer, nonce_bytes, tag_bytes))
|
||||
}
|
||||
|
||||
/// Entschlüsselt und authentifiziert einen Payload-Chunk mit dem DEK via AES-256-GCM.
|
||||
pub fn decrypt_chunk(
|
||||
dek: &[u8; 32],
|
||||
node_id: i64,
|
||||
chunk_index: u32,
|
||||
ciphertext: &[u8],
|
||||
nonce_bytes: &[u8; 12],
|
||||
tag_bytes: &[u8; 16],
|
||||
) -> Result<Vec<u8>> {
|
||||
let cipher = Aes256Gcm::new_from_slice(dek)
|
||||
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
|
||||
|
||||
let nonce = Nonce::from_slice(nonce_bytes);
|
||||
let tag = Tag::from_slice(tag_bytes);
|
||||
let aad = build_chunk_aad(node_id, chunk_index);
|
||||
|
||||
let mut buffer = ciphertext.to_vec();
|
||||
cipher
|
||||
.decrypt_in_place_detached(nonce, &aad, &mut buffer, tag)
|
||||
.map_err(|_| anyhow::anyhow!("Chunk-Integritätsprüfung fehlgeschlagen (AEAD Auth-Fehler oder Swap-Angriff)"))?;
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_kdf_and_dek_wrapping() {
|
||||
let password = "SuperSecretMasterPassword123!";
|
||||
let salt = generate_salt();
|
||||
let params = KdfParams {
|
||||
memory_cost: 1024, // Schneller für Unit-Test
|
||||
time_cost: 1,
|
||||
parallelism: 1,
|
||||
};
|
||||
|
||||
let kek = derive_kek(password, &salt, ¶ms).unwrap();
|
||||
let dek = generate_dek();
|
||||
|
||||
let (wrapped, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||
assert_eq!(wrapped.len(), 32);
|
||||
|
||||
// Erfolgreiche Entschlüsselung
|
||||
let unwrapped = unwrap_dek(&kek, &wrapped, &nonce, &tag).unwrap();
|
||||
assert_eq!(*dek, *unwrapped);
|
||||
|
||||
// Falscher KEK schlägt fehl
|
||||
let wrong_kek = derive_kek("WrongPassword!", &salt, ¶ms).unwrap();
|
||||
assert!(unwrap_dek(&wrong_kek, &wrapped, &nonce, &tag).is_err());
|
||||
|
||||
// Manipulierter Tag schlägt fehl
|
||||
let mut tampered_tag = tag;
|
||||
tampered_tag[0] ^= 0xFF;
|
||||
assert!(unwrap_dek(&kek, &wrapped, &nonce, &tampered_tag).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chunk_encryption_and_swap_protection() {
|
||||
let dek = generate_dek();
|
||||
let plaintext = b"Hello, Sanctum Encrypted Storage World!";
|
||||
let node_id = 42i64;
|
||||
let chunk_index = 0u32;
|
||||
|
||||
let (ciphertext, nonce, tag) = encrypt_chunk(&dek, node_id, chunk_index, plaintext).unwrap();
|
||||
|
||||
// Reguläre Entschlüsselung
|
||||
let decrypted = decrypt_chunk(&dek, node_id, chunk_index, &ciphertext, &nonce, &tag).unwrap();
|
||||
assert_eq!(decrypted, plaintext);
|
||||
|
||||
// Swap Attack 1: Falsche node_id (Chunk in andere Datei verschoben)
|
||||
let swap_node_err = decrypt_chunk(&dek, 99i64, chunk_index, &ciphertext, &nonce, &tag);
|
||||
assert!(swap_node_err.is_err());
|
||||
|
||||
// Swap Attack 2: Falscher chunk_index (Chunk innerhalb derselben Datei verschoben)
|
||||
let swap_idx_err = decrypt_chunk(&dek, node_id, 1u32, &ciphertext, &nonce, &tag);
|
||||
assert!(swap_idx_err.is_err());
|
||||
|
||||
// Manipulation des Ciphertexts
|
||||
let mut tampered_ct = ciphertext.clone();
|
||||
tampered_ct[0] ^= 0x01;
|
||||
assert!(decrypt_chunk(&dek, node_id, chunk_index, &tampered_ct, &nonce, &tag).is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user