fix(crypto): K-02 — format v3 chunk replay protection with generation aad
This commit is contained in:
@@ -115,6 +115,7 @@ pub fn read_carrier_block(
|
|||||||
&chunk_rec.nonce,
|
&chunk_rec.nonce,
|
||||||
&chunk_rec.tag,
|
&chunk_rec.tag,
|
||||||
format_version,
|
format_version,
|
||||||
|
chunk_rec.generation,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Das outer_decrypted enthält: inner_nonce (12B) || inner_tag (16B) || inner_ct_len (4B LE) || inner_ct || CSPRNG-Padding
|
// Das outer_decrypted enthält: inner_nonce (12B) || inner_tag (16B) || inner_ct_len (4B LE) || inner_ct || CSPRNG-Padding
|
||||||
@@ -149,6 +150,7 @@ pub fn read_carrier_block(
|
|||||||
&inner_nonce,
|
&inner_nonce,
|
||||||
&inner_tag,
|
&inner_tag,
|
||||||
format_version,
|
format_version,
|
||||||
|
chunk_rec.generation,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(inner_plaintext)
|
Ok(inner_plaintext)
|
||||||
@@ -167,6 +169,8 @@ pub fn write_carrier_block(
|
|||||||
plaintext: &[u8],
|
plaintext: &[u8],
|
||||||
format_version: u32,
|
format_version: u32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let gen = 0u64;
|
||||||
|
|
||||||
// 1. Innere Schicht verschlüsseln (mit dek_inner = DEK_1)
|
// 1. Innere Schicht verschlüsseln (mit dek_inner = DEK_1)
|
||||||
let (inner_ct, inner_nonce, inner_tag) = encrypt_chunk(
|
let (inner_ct, inner_nonce, inner_tag) = encrypt_chunk(
|
||||||
dek_inner,
|
dek_inner,
|
||||||
@@ -174,6 +178,7 @@ pub fn write_carrier_block(
|
|||||||
block_idx,
|
block_idx,
|
||||||
plaintext,
|
plaintext,
|
||||||
format_version,
|
format_version,
|
||||||
|
gen,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let inner_ct_len = inner_ct.len() as u32;
|
let inner_ct_len = inner_ct.len() as u32;
|
||||||
@@ -208,12 +213,14 @@ pub fn write_carrier_block(
|
|||||||
block_idx,
|
block_idx,
|
||||||
&outer_plaintext,
|
&outer_plaintext,
|
||||||
format_version,
|
format_version,
|
||||||
|
gen,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// 4. In SQLite schreiben (in-place Überschreiben des bestehenden Chunks)
|
// 4. In SQLite schreiben (in-place Überschreiben des bestehenden Chunks)
|
||||||
db.write_chunk(
|
db.write_chunk(
|
||||||
carrier_node_id,
|
carrier_node_id,
|
||||||
block_idx,
|
block_idx,
|
||||||
|
gen,
|
||||||
&outer_nonce,
|
&outer_nonce,
|
||||||
&outer_tag,
|
&outer_tag,
|
||||||
&outer_ct,
|
&outer_ct,
|
||||||
@@ -234,17 +241,20 @@ pub fn shred_carrier_block(
|
|||||||
let mut noise = vec![0u8; CHUNK_SIZE];
|
let mut noise = vec![0u8; CHUNK_SIZE];
|
||||||
OsRng.fill_bytes(&mut noise);
|
OsRng.fill_bytes(&mut noise);
|
||||||
|
|
||||||
|
let gen = 0u64;
|
||||||
let (outer_ct, outer_nonce, outer_tag) = encrypt_chunk(
|
let (outer_ct, outer_nonce, outer_tag) = encrypt_chunk(
|
||||||
dek_outer,
|
dek_outer,
|
||||||
carrier_node_id,
|
carrier_node_id,
|
||||||
block_idx,
|
block_idx,
|
||||||
&noise,
|
&noise,
|
||||||
format_version,
|
format_version,
|
||||||
|
gen,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
db.write_chunk(
|
db.write_chunk(
|
||||||
carrier_node_id,
|
carrier_node_id,
|
||||||
block_idx,
|
block_idx,
|
||||||
|
gen,
|
||||||
&outer_nonce,
|
&outer_nonce,
|
||||||
&outer_tag,
|
&outer_tag,
|
||||||
&outer_ct,
|
&outer_ct,
|
||||||
|
|||||||
+137
-10
@@ -642,9 +642,21 @@ pub fn build_chunk_aad(node_id: i64, chunk_index: u32) -> [u8; 16] {
|
|||||||
aad
|
aad
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Erzeugt die 24-Byte Associated Data (AAD) für einen Chunk in Format V3 (K-02 Chunk-Replay-Schutz):
|
||||||
|
/// node_id (8 Bytes Little-Endian) || chunk_index (8 Bytes Little-Endian) || generation (8 Bytes Little-Endian).
|
||||||
|
#[inline]
|
||||||
|
pub fn build_chunk_aad_v3(node_id: i64, chunk_index: u32, generation: u64) -> [u8; 24] {
|
||||||
|
let mut aad = [0u8; 24];
|
||||||
|
aad[..8].copy_from_slice(&node_id.to_le_bytes());
|
||||||
|
aad[8..16].copy_from_slice(&(chunk_index as u64).to_le_bytes());
|
||||||
|
aad[16..24].copy_from_slice(&generation.to_le_bytes());
|
||||||
|
aad
|
||||||
|
}
|
||||||
|
|
||||||
/// Verschlüsselt einen Payload-Chunk mit dem DEK via AES-256-GCM unter Einbindung von AAD.
|
/// Verschlüsselt einen Payload-Chunk mit dem DEK via AES-256-GCM unter Einbindung von AAD.
|
||||||
/// In Formatversion >= 2 wird der Chunk vor der Verschlüsselung transparent mit LZ4 komprimiert,
|
/// In Formatversion >= 2 wird der Chunk vor der Verschlüsselung transparent mit LZ4 komprimiert,
|
||||||
/// sofern dadurch eine Größenreduktion erzielt wird.
|
/// sofern dadurch eine Größenreduktion erzielt wird.
|
||||||
|
/// In Formatversion >= 3 wird ein 24-Byte AAD inklusive des Generationszählers verwendet (K-02).
|
||||||
/// Gibt (ciphertext, nonce_12_bytes, tag_16_bytes) zurück.
|
/// Gibt (ciphertext, nonce_12_bytes, tag_16_bytes) zurück.
|
||||||
pub fn encrypt_chunk(
|
pub fn encrypt_chunk(
|
||||||
dek: &[u8; 32],
|
dek: &[u8; 32],
|
||||||
@@ -652,6 +664,7 @@ pub fn encrypt_chunk(
|
|||||||
chunk_index: u32,
|
chunk_index: u32,
|
||||||
plaintext: &[u8],
|
plaintext: &[u8],
|
||||||
format_version: u32,
|
format_version: u32,
|
||||||
|
generation: u64,
|
||||||
) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
|
) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
|
||||||
let cipher = Aes256Gcm::new_from_slice(dek)
|
let cipher = Aes256Gcm::new_from_slice(dek)
|
||||||
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
|
||||||
@@ -660,7 +673,15 @@ pub fn encrypt_chunk(
|
|||||||
OsRng.fill_bytes(&mut nonce_bytes);
|
OsRng.fill_bytes(&mut nonce_bytes);
|
||||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||||
|
|
||||||
let aad = build_chunk_aad(node_id, chunk_index);
|
let aad_16;
|
||||||
|
let aad_24;
|
||||||
|
let aad: &[u8] = if format_version >= FORMAT_VERSION_V3 {
|
||||||
|
aad_24 = build_chunk_aad_v3(node_id, chunk_index, generation);
|
||||||
|
&aad_24
|
||||||
|
} else {
|
||||||
|
aad_16 = build_chunk_aad(node_id, chunk_index);
|
||||||
|
&aad_16
|
||||||
|
};
|
||||||
|
|
||||||
let mut buffer = if format_version >= FORMAT_VERSION_V2 {
|
let mut buffer = if format_version >= FORMAT_VERSION_V2 {
|
||||||
if plaintext.is_empty() {
|
if plaintext.is_empty() {
|
||||||
@@ -685,7 +706,7 @@ pub fn encrypt_chunk(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let tag = cipher
|
let tag = cipher
|
||||||
.encrypt_in_place_detached(nonce, &aad, &mut buffer)
|
.encrypt_in_place_detached(nonce, aad, &mut buffer)
|
||||||
.map_err(|e| anyhow::anyhow!("Chunk-Verschlüsselung fehlgeschlagen: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("Chunk-Verschlüsselung fehlgeschlagen: {e}"))?;
|
||||||
|
|
||||||
let mut tag_bytes = [0u8; 16];
|
let mut tag_bytes = [0u8; 16];
|
||||||
@@ -696,6 +717,7 @@ pub fn encrypt_chunk(
|
|||||||
|
|
||||||
/// Entschlüsselt und authentifiziert einen Payload-Chunk mit dem DEK via AES-256-GCM.
|
/// Entschlüsselt und authentifiziert einen Payload-Chunk mit dem DEK via AES-256-GCM.
|
||||||
/// Dekomprimiert LZ4-gepackte Chunks automatisch (in Formatversion >= 2).
|
/// Dekomprimiert LZ4-gepackte Chunks automatisch (in Formatversion >= 2).
|
||||||
|
/// In Formatversion >= 3 wird ein 24-Byte AAD inklusive des Generationszählers geprüft (K-02).
|
||||||
pub fn decrypt_chunk(
|
pub fn decrypt_chunk(
|
||||||
dek: &[u8; 32],
|
dek: &[u8; 32],
|
||||||
node_id: i64,
|
node_id: i64,
|
||||||
@@ -704,17 +726,27 @@ pub fn decrypt_chunk(
|
|||||||
nonce_bytes: &[u8; 12],
|
nonce_bytes: &[u8; 12],
|
||||||
tag_bytes: &[u8; 16],
|
tag_bytes: &[u8; 16],
|
||||||
format_version: u32,
|
format_version: u32,
|
||||||
|
generation: u64,
|
||||||
) -> Result<Vec<u8>> {
|
) -> Result<Vec<u8>> {
|
||||||
let cipher = Aes256Gcm::new_from_slice(dek)
|
let cipher = Aes256Gcm::new_from_slice(dek)
|
||||||
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
|
||||||
|
|
||||||
let nonce = Nonce::from_slice(nonce_bytes);
|
let nonce = Nonce::from_slice(nonce_bytes);
|
||||||
let tag = Tag::from_slice(tag_bytes);
|
let tag = Tag::from_slice(tag_bytes);
|
||||||
let aad = build_chunk_aad(node_id, chunk_index);
|
|
||||||
|
let aad_16;
|
||||||
|
let aad_24;
|
||||||
|
let aad: &[u8] = if format_version >= FORMAT_VERSION_V3 {
|
||||||
|
aad_24 = build_chunk_aad_v3(node_id, chunk_index, generation);
|
||||||
|
&aad_24
|
||||||
|
} else {
|
||||||
|
aad_16 = build_chunk_aad(node_id, chunk_index);
|
||||||
|
&aad_16
|
||||||
|
};
|
||||||
|
|
||||||
let mut buffer = ciphertext.to_vec();
|
let mut buffer = ciphertext.to_vec();
|
||||||
cipher
|
cipher
|
||||||
.decrypt_in_place_detached(nonce, &aad, &mut buffer, tag)
|
.decrypt_in_place_detached(nonce, aad, &mut buffer, tag)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
anyhow::anyhow!(
|
anyhow::anyhow!(
|
||||||
"Chunk-Integritätsprüfung fehlgeschlagen (AEAD Auth-Fehler oder Swap-Angriff)"
|
"Chunk-Integritätsprüfung fehlgeschlagen (AEAD Auth-Fehler oder Swap-Angriff)"
|
||||||
@@ -795,7 +827,7 @@ mod tests {
|
|||||||
let chunk_index = 0u32;
|
let chunk_index = 0u32;
|
||||||
|
|
||||||
let (ciphertext, nonce, tag) =
|
let (ciphertext, nonce, tag) =
|
||||||
encrypt_chunk(&dek, node_id, chunk_index, plaintext, FORMAT_VERSION_V2).unwrap();
|
encrypt_chunk(&dek, node_id, chunk_index, plaintext, FORMAT_VERSION_V2, 0).unwrap();
|
||||||
|
|
||||||
// Reguläre Entschlüsselung (v2)
|
// Reguläre Entschlüsselung (v2)
|
||||||
let decrypted = decrypt_chunk(
|
let decrypted = decrypt_chunk(
|
||||||
@@ -806,6 +838,7 @@ mod tests {
|
|||||||
&nonce,
|
&nonce,
|
||||||
&tag,
|
&tag,
|
||||||
FORMAT_VERSION_V2,
|
FORMAT_VERSION_V2,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(decrypted, plaintext);
|
assert_eq!(decrypted, plaintext);
|
||||||
@@ -819,6 +852,7 @@ mod tests {
|
|||||||
&nonce,
|
&nonce,
|
||||||
&tag,
|
&tag,
|
||||||
FORMAT_VERSION_V2,
|
FORMAT_VERSION_V2,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
assert!(swap_node_err.is_err());
|
assert!(swap_node_err.is_err());
|
||||||
|
|
||||||
@@ -831,6 +865,7 @@ mod tests {
|
|||||||
&nonce,
|
&nonce,
|
||||||
&tag,
|
&tag,
|
||||||
FORMAT_VERSION_V2,
|
FORMAT_VERSION_V2,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
assert!(swap_idx_err.is_err());
|
assert!(swap_idx_err.is_err());
|
||||||
|
|
||||||
@@ -844,11 +879,100 @@ mod tests {
|
|||||||
&tampered_ct,
|
&tampered_ct,
|
||||||
&nonce,
|
&nonce,
|
||||||
&tag,
|
&tag,
|
||||||
FORMAT_VERSION_V2
|
FORMAT_VERSION_V2,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
.is_err());
|
.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_k02_chunk_replay_protection_with_generation_aad() {
|
||||||
|
let dek = generate_dek();
|
||||||
|
let plaintext_v1 = b"Original Chunk Data at Generation 1";
|
||||||
|
let plaintext_v2 = b"Overwritten Chunk Data at Generation 2";
|
||||||
|
let node_id = 42i64;
|
||||||
|
let chunk_index = 0u32;
|
||||||
|
|
||||||
|
// 1. Chunk mit Generation 1 verschlüsseln
|
||||||
|
let (ct1, nonce1, tag1) =
|
||||||
|
encrypt_chunk(&dek, node_id, chunk_index, plaintext_v1, FORMAT_VERSION_V3, 1).unwrap();
|
||||||
|
|
||||||
|
// Verifiziere reguläre Entschlüsselung mit Generation 1
|
||||||
|
let dec1 = decrypt_chunk(
|
||||||
|
&dek,
|
||||||
|
node_id,
|
||||||
|
chunk_index,
|
||||||
|
&ct1,
|
||||||
|
&nonce1,
|
||||||
|
&tag1,
|
||||||
|
FORMAT_VERSION_V3,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(dec1, plaintext_v1);
|
||||||
|
|
||||||
|
// 2. Replay-Schutz: Entschlüsselung mit falscher Generation (z. B. 2) MUSS scheitern!
|
||||||
|
let replay_err = decrypt_chunk(
|
||||||
|
&dek,
|
||||||
|
node_id,
|
||||||
|
chunk_index,
|
||||||
|
&ct1,
|
||||||
|
&nonce1,
|
||||||
|
&tag1,
|
||||||
|
FORMAT_VERSION_V3,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
replay_err.is_err(),
|
||||||
|
"Ciphertext von Gen 1 darf unter Gen 2 AAD nicht entschlüsselt werden"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Chunk überschreiben mit Generation 2
|
||||||
|
let (ct2, nonce2, tag2) =
|
||||||
|
encrypt_chunk(&dek, node_id, chunk_index, plaintext_v2, FORMAT_VERSION_V3, 2).unwrap();
|
||||||
|
let dec2 = decrypt_chunk(
|
||||||
|
&dek,
|
||||||
|
node_id,
|
||||||
|
chunk_index,
|
||||||
|
&ct2,
|
||||||
|
&nonce2,
|
||||||
|
&tag2,
|
||||||
|
FORMAT_VERSION_V3,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(dec2, plaintext_v2);
|
||||||
|
|
||||||
|
// 4. Replay-Angriff: Angreifer spielt ct1 (Gen 1) ein, während System Gen 2 erwartet
|
||||||
|
let attack_res = decrypt_chunk(
|
||||||
|
&dek,
|
||||||
|
node_id,
|
||||||
|
chunk_index,
|
||||||
|
&ct1,
|
||||||
|
&nonce1,
|
||||||
|
&tag1,
|
||||||
|
FORMAT_VERSION_V3,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
assert!(attack_res.is_err(), "Replay von altem Ciphertext muss abgewehrt werden");
|
||||||
|
|
||||||
|
// 5. Abwärtskompatibilität: In V2 wird generation ignoriert
|
||||||
|
let (ct_v2, nonce_v2, tag_v2) =
|
||||||
|
encrypt_chunk(&dek, node_id, chunk_index, plaintext_v1, FORMAT_VERSION_V2, 0).unwrap();
|
||||||
|
let dec_v2 = decrypt_chunk(
|
||||||
|
&dek,
|
||||||
|
node_id,
|
||||||
|
chunk_index,
|
||||||
|
&ct_v2,
|
||||||
|
&nonce_v2,
|
||||||
|
&tag_v2,
|
||||||
|
FORMAT_VERSION_V2,
|
||||||
|
999, // beliebig
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(dec_v2, plaintext_v1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lz4_chunk_compression_efficiency() {
|
fn test_lz4_chunk_compression_efficiency() {
|
||||||
let dek = generate_dek();
|
let dek = generate_dek();
|
||||||
@@ -859,7 +983,7 @@ mod tests {
|
|||||||
let chunk_index = 0u32;
|
let chunk_index = 0u32;
|
||||||
|
|
||||||
let (ciphertext, nonce, tag) =
|
let (ciphertext, nonce, tag) =
|
||||||
encrypt_chunk(&dek, node_id, chunk_index, plaintext, FORMAT_VERSION_V2).unwrap();
|
encrypt_chunk(&dek, node_id, chunk_index, plaintext, FORMAT_VERSION_V2, 0).unwrap();
|
||||||
|
|
||||||
// Der komprimierte Ciphertext muss signifikant kleiner sein als der Klartext
|
// Der komprimierte Ciphertext muss signifikant kleiner sein als der Klartext
|
||||||
assert!(
|
assert!(
|
||||||
@@ -877,6 +1001,7 @@ mod tests {
|
|||||||
&nonce,
|
&nonce,
|
||||||
&tag,
|
&tag,
|
||||||
FORMAT_VERSION_V2,
|
FORMAT_VERSION_V2,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(decrypted, plaintext);
|
assert_eq!(decrypted, plaintext);
|
||||||
@@ -889,11 +1014,11 @@ mod tests {
|
|||||||
let mut random_bytes = vec![0u8; 1000];
|
let mut random_bytes = vec![0u8; 1000];
|
||||||
OsRng.fill_bytes(&mut random_bytes);
|
OsRng.fill_bytes(&mut random_bytes);
|
||||||
|
|
||||||
let (ct, nonce, tag) = encrypt_chunk(&dek, 1, 0, &random_bytes, FORMAT_VERSION_V2).unwrap();
|
let (ct, nonce, tag) = encrypt_chunk(&dek, 1, 0, &random_bytes, FORMAT_VERSION_V2, 0).unwrap();
|
||||||
// Da Kompression keine 64 Bytes spart, wird COMPRESSION_NONE (1 Byte) + Plaintext gespeichert
|
// Da Kompression keine 64 Bytes spart, wird COMPRESSION_NONE (1 Byte) + Plaintext gespeichert
|
||||||
assert_eq!(ct.len(), random_bytes.len() + 1);
|
assert_eq!(ct.len(), random_bytes.len() + 1);
|
||||||
|
|
||||||
let decrypted = decrypt_chunk(&dek, 1, 0, &ct, &nonce, &tag, FORMAT_VERSION_V2).unwrap();
|
let decrypted = decrypt_chunk(&dek, 1, 0, &ct, &nonce, &tag, FORMAT_VERSION_V2, 0).unwrap();
|
||||||
assert_eq!(decrypted, random_bytes);
|
assert_eq!(decrypted, random_bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -906,7 +1031,7 @@ mod tests {
|
|||||||
|
|
||||||
// V1 Format: Reine Verschlüsselung ohne Kompressionspräfix
|
// V1 Format: Reine Verschlüsselung ohne Kompressionspräfix
|
||||||
let (ciphertext, nonce, tag) =
|
let (ciphertext, nonce, tag) =
|
||||||
encrypt_chunk(&dek, node_id, chunk_index, plaintext, FORMAT_VERSION_V1).unwrap();
|
encrypt_chunk(&dek, node_id, chunk_index, plaintext, FORMAT_VERSION_V1, 0).unwrap();
|
||||||
assert_eq!(ciphertext.len(), plaintext.len());
|
assert_eq!(ciphertext.len(), plaintext.len());
|
||||||
|
|
||||||
let decrypted = decrypt_chunk(
|
let decrypted = decrypt_chunk(
|
||||||
@@ -917,6 +1042,7 @@ mod tests {
|
|||||||
&nonce,
|
&nonce,
|
||||||
&tag,
|
&tag,
|
||||||
FORMAT_VERSION_V1,
|
FORMAT_VERSION_V1,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(decrypted, plaintext);
|
assert_eq!(decrypted, plaintext);
|
||||||
@@ -1163,6 +1289,7 @@ mod tests {
|
|||||||
&nonce_bytes,
|
&nonce_bytes,
|
||||||
&tag_bytes,
|
&tag_bytes,
|
||||||
FORMAT_VERSION_V2,
|
FORMAT_VERSION_V2,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
res.is_err(),
|
res.is_err(),
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ pub fn detect_recovery_key_slot(db: &Database, dek: &[u8; 32], version: u32) ->
|
|||||||
&chunk0.nonce,
|
&chunk0.nonce,
|
||||||
&chunk0.tag,
|
&chunk0.tag,
|
||||||
version,
|
version,
|
||||||
|
chunk0.generation,
|
||||||
)
|
)
|
||||||
.is_ok();
|
.is_ok();
|
||||||
if is_dek0 {
|
if is_dek0 {
|
||||||
|
|||||||
+217
-48
@@ -32,6 +32,7 @@ pub struct NodeRecord {
|
|||||||
pub struct ChunkRecord {
|
pub struct ChunkRecord {
|
||||||
pub node_id: i64,
|
pub node_id: i64,
|
||||||
pub chunk_index: u32,
|
pub chunk_index: u32,
|
||||||
|
pub generation: u64,
|
||||||
pub nonce: [u8; 12],
|
pub nonce: [u8; 12],
|
||||||
pub tag: [u8; 16],
|
pub tag: [u8; 16],
|
||||||
pub ciphertext: Vec<u8>,
|
pub ciphertext: Vec<u8>,
|
||||||
@@ -185,7 +186,7 @@ impl ContainerMeta {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
conn: Arc<Mutex<Connection>>,
|
conn: Arc<Mutex<Connection>>,
|
||||||
active_dek: Arc<Mutex<Option<Zeroizing<[u8; 32]>>>>,
|
active_session: Arc<Mutex<Option<(u32, Zeroizing<[u8; 32]>)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_timestamp() -> u64 {
|
fn current_timestamp() -> u64 {
|
||||||
@@ -306,7 +307,7 @@ impl Database {
|
|||||||
|
|
||||||
let db = Self {
|
let db = Self {
|
||||||
conn: Arc::new(Mutex::new(conn)),
|
conn: Arc::new(Mutex::new(conn)),
|
||||||
active_dek: Arc::new(Mutex::new(None)),
|
active_session: Arc::new(Mutex::new(None)),
|
||||||
};
|
};
|
||||||
db.init_pragmas()?;
|
db.init_pragmas()?;
|
||||||
if table_count > 0 {
|
if table_count > 0 {
|
||||||
@@ -321,7 +322,7 @@ impl Database {
|
|||||||
let conn = Connection::open_in_memory()?;
|
let conn = Connection::open_in_memory()?;
|
||||||
let db = Self {
|
let db = Self {
|
||||||
conn: Arc::new(Mutex::new(conn)),
|
conn: Arc::new(Mutex::new(conn)),
|
||||||
active_dek: Arc::new(Mutex::new(None)),
|
active_session: Arc::new(Mutex::new(None)),
|
||||||
};
|
};
|
||||||
db.init_pragmas()?;
|
db.init_pragmas()?;
|
||||||
db.ensure_schema_upgrades()?;
|
db.ensure_schema_upgrades()?;
|
||||||
@@ -332,14 +333,31 @@ impl Database {
|
|||||||
self.conn.lock().unwrap()
|
self.conn.lock().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Setzt den aktiven DEK für automatische Metadaten-Authentifizierung (K-01).
|
/// Erzeugt eine geklonte Instanz mit einer isolierten aktiven Session (Slot & DEK).
|
||||||
|
pub fn with_session(&self, slot_id: u32, dek: Zeroizing<[u8; 32]>) -> Self {
|
||||||
|
Self {
|
||||||
|
conn: self.conn.clone(),
|
||||||
|
active_session: Arc::new(Mutex::new(Some((slot_id, dek)))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setzt den aktiven DEK für automatische Metadaten-Authentifizierung (K-01) auf Slot 0.
|
||||||
pub fn set_active_dek(&self, dek: Zeroizing<[u8; 32]>) {
|
pub fn set_active_dek(&self, dek: Zeroizing<[u8; 32]>) {
|
||||||
*self.active_dek.lock().unwrap() = Some(dek);
|
self.set_active_slot_and_dek(0, dek);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setzt den aktiven Slot und DEK für automatische Metadaten-Authentifizierung (K-01).
|
||||||
|
pub fn set_active_slot_and_dek(&self, slot_id: u32, dek: Zeroizing<[u8; 32]>) {
|
||||||
|
*self.active_session.lock().unwrap() = Some((slot_id, dek));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gibt den aktuellen aktiven DEK zurück, falls gesetzt.
|
/// Gibt den aktuellen aktiven DEK zurück, falls gesetzt.
|
||||||
pub fn active_dek(&self) -> Option<Zeroizing<[u8; 32]>> {
|
pub fn active_dek(&self) -> Option<Zeroizing<[u8; 32]>> {
|
||||||
self.active_dek.lock().unwrap().clone()
|
self.active_session
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.map(|(_, dek)| dek.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Authentifiziert ein Master-Passwort gegen den Container in konstanter Zeit.
|
/// Authentifiziert ein Master-Passwort gegen den Container in konstanter Zeit.
|
||||||
@@ -347,7 +365,7 @@ impl Database {
|
|||||||
let meta = self.read_meta()?;
|
let meta = self.read_meta()?;
|
||||||
let res = meta.authenticate(password);
|
let res = meta.authenticate(password);
|
||||||
if let Some(ref keys) = res {
|
if let Some(ref keys) = res {
|
||||||
self.set_active_dek(keys.dek().clone());
|
self.set_active_slot_and_dek(keys.slot_id(), keys.dek().clone());
|
||||||
if let Some(cid) = keys.carrier_node_id() {
|
if let Some(cid) = keys.carrier_node_id() {
|
||||||
let _ = self.mark_carrier_node_id(cid);
|
let _ = self.mark_carrier_node_id(cid);
|
||||||
}
|
}
|
||||||
@@ -387,6 +405,12 @@ impl Database {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Spalte generation in chunks (Format V3 / K-02)
|
||||||
|
let _ = conn.execute(
|
||||||
|
"ALTER TABLE chunks ADD COLUMN generation INTEGER NOT NULL DEFAULT 0",
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,6 +595,7 @@ impl Database {
|
|||||||
CREATE TABLE IF NOT EXISTS chunks (
|
CREATE TABLE IF NOT EXISTS chunks (
|
||||||
node_id INTEGER NOT NULL,
|
node_id INTEGER NOT NULL,
|
||||||
chunk_index INTEGER NOT NULL,
|
chunk_index INTEGER NOT NULL,
|
||||||
|
generation INTEGER NOT NULL DEFAULT 0,
|
||||||
nonce BLOB NOT NULL,
|
nonce BLOB NOT NULL,
|
||||||
tag BLOB NOT NULL,
|
tag BLOB NOT NULL,
|
||||||
ciphertext BLOB NOT NULL,
|
ciphertext BLOB NOT NULL,
|
||||||
@@ -652,7 +677,7 @@ impl Database {
|
|||||||
let manifest_bytes = serde_json::to_vec(&manifest)?;
|
let manifest_bytes = serde_json::to_vec(&manifest)?;
|
||||||
|
|
||||||
let (inner_ct, inner_nonce, inner_tag) =
|
let (inner_ct, inner_nonce, inner_tag) =
|
||||||
crate::crypto::encrypt_chunk(dek_1, c_id, 0, &manifest_bytes, FORMAT_VERSION)?;
|
crate::crypto::encrypt_chunk(dek_1, c_id, 0, &manifest_bytes, FORMAT_VERSION, 0)?;
|
||||||
let inner_ct_len = inner_ct.len() as u32;
|
let inner_ct_len = inner_ct.len() as u32;
|
||||||
|
|
||||||
let mut outer_plaintext = vec![0u8; CHUNK_SIZE];
|
let mut outer_plaintext = vec![0u8; CHUNK_SIZE];
|
||||||
@@ -668,18 +693,18 @@ impl Database {
|
|||||||
outer_plaintext[32..ct_end].copy_from_slice(&inner_ct);
|
outer_plaintext[32..ct_end].copy_from_slice(&inner_ct);
|
||||||
|
|
||||||
let (outer_ct, outer_nonce, outer_tag) =
|
let (outer_ct, outer_nonce, outer_tag) =
|
||||||
crate::crypto::encrypt_chunk(dek_0, c_id, 0, &outer_plaintext, FORMAT_VERSION)?;
|
crate::crypto::encrypt_chunk(dek_0, c_id, 0, &outer_plaintext, FORMAT_VERSION, 0)?;
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO chunks (node_id, chunk_index, nonce, tag, ciphertext)
|
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
||||||
VALUES (?1, 0, ?2, ?3, ?4)",
|
VALUES (?1, 0, 0, ?2, ?3, ?4)",
|
||||||
params![c_id, outer_nonce.as_slice(), outer_tag.as_slice(), outer_ct],
|
params![c_id, outer_nonce.as_slice(), outer_tag.as_slice(), outer_ct],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Blöcke 1..total_blocks-1 mit DEK_0 vorallokieren
|
// Blöcke 1..total_blocks-1 mit DEK_0 vorallokieren
|
||||||
let mut chunk_stmt = conn.prepare(
|
let mut chunk_stmt = conn.prepare(
|
||||||
"INSERT INTO chunks (node_id, chunk_index, nonce, tag, ciphertext)
|
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
VALUES (?1, ?2, 0, ?3, ?4, ?5)",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut dummy_noise = vec![0u8; CHUNK_SIZE];
|
let mut dummy_noise = vec![0u8; CHUNK_SIZE];
|
||||||
@@ -688,7 +713,7 @@ impl Database {
|
|||||||
conn.execute_batch("BEGIN TRANSACTION;")?;
|
conn.execute_batch("BEGIN TRANSACTION;")?;
|
||||||
for b in 1..total_blocks {
|
for b in 1..total_blocks {
|
||||||
let (ct, nonce, tag) =
|
let (ct, nonce, tag) =
|
||||||
crate::crypto::encrypt_chunk(dek_0, c_id, b, &dummy_noise, FORMAT_VERSION)?;
|
crate::crypto::encrypt_chunk(dek_0, c_id, b, &dummy_noise, FORMAT_VERSION, 0)?;
|
||||||
chunk_stmt.execute(params![c_id, b, nonce.as_slice(), tag.as_slice(), ct,])?;
|
chunk_stmt.execute(params![c_id, b, nonce.as_slice(), tag.as_slice(), ct,])?;
|
||||||
|
|
||||||
if b % 500 == 0 {
|
if b % 500 == 0 {
|
||||||
@@ -696,9 +721,14 @@ impl Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
conn.execute_batch("COMMIT;")?;
|
conn.execute_batch("COMMIT;")?;
|
||||||
|
drop(chunk_stmt);
|
||||||
self.set_active_dek(Zeroizing::new(*dek_0));
|
self.set_active_slot_and_dek(0, Zeroizing::new(*dek_0));
|
||||||
Some(c_id)
|
drop(conn);
|
||||||
|
let _ = self.update_metadata_mac();
|
||||||
|
self.set_active_slot_and_dek(1, Zeroizing::new(*dek_1));
|
||||||
|
let _ = self.update_metadata_mac();
|
||||||
|
self.set_active_slot_and_dek(0, Zeroizing::new(*dek_0));
|
||||||
|
return Ok(Some(c_id));
|
||||||
} else {
|
} else {
|
||||||
// Slot 1 mit CSPRNG-Zufallsdaten gleicher Struktur und Entropie (72 Bytes für Modell A)
|
// Slot 1 mit CSPRNG-Zufallsdaten gleicher Struktur und Entropie (72 Bytes für Modell A)
|
||||||
let (dummy_dek, dummy_nonce, dummy_tag, dummy_salt) = generate_dummy_slot();
|
let (dummy_dek, dummy_nonce, dummy_tag, dummy_salt) = generate_dummy_slot();
|
||||||
@@ -772,6 +802,7 @@ impl Database {
|
|||||||
CREATE TABLE IF NOT EXISTS chunks (
|
CREATE TABLE IF NOT EXISTS chunks (
|
||||||
node_id INTEGER NOT NULL,
|
node_id INTEGER NOT NULL,
|
||||||
chunk_index INTEGER NOT NULL,
|
chunk_index INTEGER NOT NULL,
|
||||||
|
generation INTEGER NOT NULL DEFAULT 0,
|
||||||
nonce BLOB NOT NULL,
|
nonce BLOB NOT NULL,
|
||||||
tag BLOB NOT NULL,
|
tag BLOB NOT NULL,
|
||||||
ciphertext BLOB NOT NULL,
|
ciphertext BLOB NOT NULL,
|
||||||
@@ -1446,7 +1477,7 @@ impl Database {
|
|||||||
pub fn read_chunk(&self, node_id: i64, chunk_index: u32) -> Result<Option<ChunkRecord>> {
|
pub fn read_chunk(&self, node_id: i64, chunk_index: u32) -> Result<Option<ChunkRecord>> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT nonce, tag, ciphertext FROM chunks WHERE node_id = ?1 AND chunk_index = ?2",
|
"SELECT nonce, tag, ciphertext, generation FROM chunks WHERE node_id = ?1 AND chunk_index = ?2",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let record = stmt
|
let record = stmt
|
||||||
@@ -1454,6 +1485,7 @@ impl Database {
|
|||||||
let nonce_vec: Vec<u8> = row.get(0)?;
|
let nonce_vec: Vec<u8> = row.get(0)?;
|
||||||
let tag_vec: Vec<u8> = row.get(1)?;
|
let tag_vec: Vec<u8> = row.get(1)?;
|
||||||
let ciphertext: Vec<u8> = row.get(2)?;
|
let ciphertext: Vec<u8> = row.get(2)?;
|
||||||
|
let generation: i64 = row.get(3).unwrap_or(0);
|
||||||
|
|
||||||
let mut nonce = [0u8; 12];
|
let mut nonce = [0u8; 12];
|
||||||
let mut tag = [0u8; 16];
|
let mut tag = [0u8; 16];
|
||||||
@@ -1467,6 +1499,7 @@ impl Database {
|
|||||||
Ok(ChunkRecord {
|
Ok(ChunkRecord {
|
||||||
node_id,
|
node_id,
|
||||||
chunk_index,
|
chunk_index,
|
||||||
|
generation: generation as u64,
|
||||||
nonce,
|
nonce,
|
||||||
tag,
|
tag,
|
||||||
ciphertext,
|
ciphertext,
|
||||||
@@ -1477,26 +1510,52 @@ impl Database {
|
|||||||
Ok(record)
|
Ok(record)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ermittelt die nächste Generation für einen Chunk (K-02 Chunk-Replay-Schutz).
|
||||||
|
/// Garantiert eine strikt monoton steigende Generation containerweit.
|
||||||
|
pub fn next_chunk_generation(&self, node_id: i64, chunk_index: u32) -> Result<u64> {
|
||||||
|
let conn = self.conn.lock().unwrap();
|
||||||
|
let current_gen: Option<i64> = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT generation FROM chunks WHERE node_id = ?1 AND chunk_index = ?2",
|
||||||
|
params![node_id, chunk_index],
|
||||||
|
|r| r.get(0),
|
||||||
|
)
|
||||||
|
.optional()?;
|
||||||
|
let max_gen: i64 = conn
|
||||||
|
.query_row("SELECT COALESCE(MAX(generation), 0) FROM chunks", [], |r| {
|
||||||
|
r.get(0)
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
let next = match current_gen {
|
||||||
|
Some(g) => (g + 1).max(max_gen + 1),
|
||||||
|
None => max_gen + 1,
|
||||||
|
};
|
||||||
|
Ok(next as u64)
|
||||||
|
}
|
||||||
|
|
||||||
/// Schreibt oder aktualisiert einen verschlüsselten Chunk in der Datenbank.
|
/// Schreibt oder aktualisiert einen verschlüsselten Chunk in der Datenbank.
|
||||||
pub fn write_chunk(
|
pub fn write_chunk(
|
||||||
&self,
|
&self,
|
||||||
node_id: i64,
|
node_id: i64,
|
||||||
chunk_index: u32,
|
chunk_index: u32,
|
||||||
|
generation: u64,
|
||||||
nonce: &[u8; 12],
|
nonce: &[u8; 12],
|
||||||
tag: &[u8; 16],
|
tag: &[u8; 16],
|
||||||
ciphertext: &[u8],
|
ciphertext: &[u8],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO chunks (node_id, chunk_index, nonce, tag, ciphertext)
|
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||||
ON CONFLICT(node_id, chunk_index) DO UPDATE SET
|
ON CONFLICT(node_id, chunk_index) DO UPDATE SET
|
||||||
|
generation = excluded.generation,
|
||||||
nonce = excluded.nonce,
|
nonce = excluded.nonce,
|
||||||
tag = excluded.tag,
|
tag = excluded.tag,
|
||||||
ciphertext = excluded.ciphertext",
|
ciphertext = excluded.ciphertext",
|
||||||
params![
|
params![
|
||||||
node_id,
|
node_id,
|
||||||
chunk_index,
|
chunk_index,
|
||||||
|
generation as i64,
|
||||||
nonce.as_slice(),
|
nonce.as_slice(),
|
||||||
tag.as_slice(),
|
tag.as_slice(),
|
||||||
ciphertext,
|
ciphertext,
|
||||||
@@ -1511,6 +1570,7 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
node_id: i64,
|
node_id: i64,
|
||||||
chunk_index: u32,
|
chunk_index: u32,
|
||||||
|
generation: u64,
|
||||||
nonce: &[u8; 12],
|
nonce: &[u8; 12],
|
||||||
tag: &[u8; 16],
|
tag: &[u8; 16],
|
||||||
ciphertext: &[u8],
|
ciphertext: &[u8],
|
||||||
@@ -1521,15 +1581,17 @@ impl Database {
|
|||||||
let mut conn = self.conn.lock().unwrap();
|
let mut conn = self.conn.lock().unwrap();
|
||||||
let tx = conn.transaction()?;
|
let tx = conn.transaction()?;
|
||||||
tx.execute(
|
tx.execute(
|
||||||
"INSERT INTO chunks (node_id, chunk_index, nonce, tag, ciphertext)
|
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||||
ON CONFLICT(node_id, chunk_index) DO UPDATE SET
|
ON CONFLICT(node_id, chunk_index) DO UPDATE SET
|
||||||
|
generation = excluded.generation,
|
||||||
nonce = excluded.nonce,
|
nonce = excluded.nonce,
|
||||||
tag = excluded.tag,
|
tag = excluded.tag,
|
||||||
ciphertext = excluded.ciphertext",
|
ciphertext = excluded.ciphertext",
|
||||||
params![
|
params![
|
||||||
node_id,
|
node_id,
|
||||||
chunk_index,
|
chunk_index,
|
||||||
|
generation as i64,
|
||||||
nonce.as_slice(),
|
nonce.as_slice(),
|
||||||
tag.as_slice(),
|
tag.as_slice(),
|
||||||
ciphertext,
|
ciphertext,
|
||||||
@@ -1596,18 +1658,54 @@ impl Database {
|
|||||||
|
|
||||||
/// Erzeugt die deterministische kanonische Byterepräsentation aller Knoten für den Metadaten-MAC (K-01).
|
/// Erzeugt die deterministische kanonische Byterepräsentation aller Knoten für den Metadaten-MAC (K-01).
|
||||||
pub fn canonical_nodes_bytes(&self) -> Result<Vec<u8>> {
|
pub fn canonical_nodes_bytes(&self) -> Result<Vec<u8>> {
|
||||||
let conn = self.conn.lock().unwrap();
|
self.canonical_nodes_bytes_for_vault(0)
|
||||||
let mut stmt = conn.prepare(
|
}
|
||||||
"SELECT n.id, n.parent_id, n.name, n.is_dir, n.size, n.created_at, n.modified_at, n.is_carrier,
|
|
||||||
(SELECT COUNT(*) FROM chunks c WHERE c.node_id = n.id) as chunk_count
|
|
||||||
FROM nodes n
|
|
||||||
ORDER BY n.id ASC",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut rows = stmt.query([])?;
|
/// Erzeugt die deterministische kanonische Byterepräsentation für einen spezifischen Vault.
|
||||||
|
pub fn canonical_nodes_bytes_for_vault(&self, vault_id: u32) -> Result<Vec<u8>> {
|
||||||
|
let conn = self.conn.lock().unwrap();
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
|
if vault_id == 1 {
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"WITH RECURSIVE vault1(id) AS (
|
||||||
|
SELECT 2
|
||||||
|
UNION ALL
|
||||||
|
SELECT n.id FROM nodes n JOIN vault1 v ON n.parent_id = v.id
|
||||||
|
)
|
||||||
|
SELECT n.id, n.parent_id, n.name, n.is_dir, n.size, n.created_at, n.modified_at, n.is_carrier,
|
||||||
|
(SELECT COUNT(*) FROM chunks c WHERE c.node_id = n.id) as chunk_count
|
||||||
|
FROM nodes n
|
||||||
|
WHERE n.id IN (SELECT id FROM vault1)
|
||||||
|
ORDER BY n.id ASC",
|
||||||
|
)?;
|
||||||
|
let mut rows = stmt.query([])?;
|
||||||
while let Some(row) = rows.next()? {
|
while let Some(row) = rows.next()? {
|
||||||
|
Self::serialize_node_row(&row, &mut buf)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"WITH RECURSIVE vault0(id) AS (
|
||||||
|
SELECT 1
|
||||||
|
UNION ALL
|
||||||
|
SELECT n.id FROM nodes n JOIN vault0 v ON n.parent_id = v.id
|
||||||
|
)
|
||||||
|
SELECT n.id, n.parent_id, n.name, n.is_dir, n.size, n.created_at, n.modified_at, n.is_carrier,
|
||||||
|
(SELECT COUNT(*) FROM chunks c WHERE c.node_id = n.id) as chunk_count
|
||||||
|
FROM nodes n
|
||||||
|
WHERE n.id = 2 OR n.id IN (SELECT id FROM vault0)
|
||||||
|
ORDER BY n.id ASC",
|
||||||
|
)?;
|
||||||
|
let mut rows = stmt.query([])?;
|
||||||
|
while let Some(row) = rows.next()? {
|
||||||
|
Self::serialize_node_row(&row, &mut buf)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_node_row(row: &rusqlite::Row, buf: &mut Vec<u8>) -> Result<()> {
|
||||||
let id: i64 = row.get(0)?;
|
let id: i64 = row.get(0)?;
|
||||||
let parent_id: Option<i64> = row.get(1)?;
|
let parent_id: Option<i64> = row.get(1)?;
|
||||||
let name: String = row.get(2)?;
|
let name: String = row.get(2)?;
|
||||||
@@ -1638,23 +1736,22 @@ impl Database {
|
|||||||
buf.extend_from_slice(&modified_at.to_le_bytes());
|
buf.extend_from_slice(&modified_at.to_le_bytes());
|
||||||
buf.push(if is_carrier != 0 { 1u8 } else { 0u8 });
|
buf.push(if is_carrier != 0 { 1u8 } else { 0u8 });
|
||||||
buf.extend_from_slice(&chunk_count.to_le_bytes());
|
buf.extend_from_slice(&chunk_count.to_le_bytes());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(buf)
|
/// Aktualisiert den Metadaten-MAC des aktiven Slots bei strukturellen Modifikationen (Format V3 / K-01).
|
||||||
}
|
|
||||||
|
|
||||||
/// Aktualisiert den Metadaten-MAC in Slot 0 bei strukturellen Modifikationen (Format V3 / K-01).
|
|
||||||
pub fn update_metadata_mac(&self) -> Result<()> {
|
pub fn update_metadata_mac(&self) -> Result<()> {
|
||||||
let dek_opt = self.active_dek.lock().unwrap().clone();
|
let session_opt = self.active_session.lock().unwrap().clone();
|
||||||
let Some(dek) = dek_opt else {
|
let Some((slot_id, dek)) = session_opt else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
let version_and_gen: Option<(u32, u64)> = conn
|
let version_and_gen: Option<(u32, u64)> = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT version, metadata_gen FROM meta WHERE slot_id = 0 LIMIT 1",
|
"SELECT version, metadata_gen FROM meta WHERE slot_id = ?1 LIMIT 1",
|
||||||
[],
|
params![slot_id],
|
||||||
|r| Ok((r.get(0)?, r.get(1).unwrap_or(0))),
|
|r| Ok((r.get(0)?, r.get(1).unwrap_or(0))),
|
||||||
)
|
)
|
||||||
.optional()?;
|
.optional()?;
|
||||||
@@ -1670,14 +1767,14 @@ impl Database {
|
|||||||
drop(conn);
|
drop(conn);
|
||||||
|
|
||||||
let next_gen = current_gen + 1;
|
let next_gen = current_gen + 1;
|
||||||
let canonical = self.canonical_nodes_bytes()?;
|
let canonical = self.canonical_nodes_bytes_for_vault(slot_id)?;
|
||||||
let mac_key = derive_metadata_mac_key(&dek);
|
let mac_key = derive_metadata_mac_key(&dek);
|
||||||
let new_mac = compute_metadata_mac(&mac_key, next_gen, &canonical);
|
let new_mac = compute_metadata_mac(&mac_key, next_gen, &canonical);
|
||||||
|
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE meta SET metadata_mac = ?1, metadata_gen = ?2 WHERE slot_id = 0",
|
"UPDATE meta SET metadata_mac = ?1, metadata_gen = ?2 WHERE slot_id = ?3",
|
||||||
params![new_mac.as_slice(), next_gen],
|
params![new_mac.as_slice(), next_gen, slot_id],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1685,17 +1782,28 @@ impl Database {
|
|||||||
|
|
||||||
/// Prüft die Integrität des Metadaten-MAC gegen den gegebenen DEK (Format V3 / K-01).
|
/// Prüft die Integrität des Metadaten-MAC gegen den gegebenen DEK (Format V3 / K-01).
|
||||||
pub fn verify_metadata_mac(&self, dek: &[u8; 32]) -> Result<bool> {
|
pub fn verify_metadata_mac(&self, dek: &[u8; 32]) -> Result<bool> {
|
||||||
|
if self.verify_metadata_mac_for_slot(0, dek)? {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
if self.verify_metadata_mac_for_slot(1, dek)? {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prüft die Integrität des Metadaten-MAC für einen spezifischen Slot (0: Decoy, 1: Hidden).
|
||||||
|
pub fn verify_metadata_mac_for_slot(&self, slot_id: u32, dek: &[u8; 32]) -> Result<bool> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
let meta_row: Option<(u32, Option<Vec<u8>>, u64)> = conn
|
let meta_row: Option<(u32, Option<Vec<u8>>, u64)> = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT version, metadata_mac, metadata_gen FROM meta WHERE slot_id = 0 LIMIT 1",
|
"SELECT version, metadata_mac, metadata_gen FROM meta WHERE slot_id = ?1 LIMIT 1",
|
||||||
[],
|
params![slot_id],
|
||||||
|r| Ok((r.get(0)?, r.get(1).ok(), r.get(2).unwrap_or(0))),
|
|r| Ok((r.get(0)?, r.get(1).ok(), r.get(2).unwrap_or(0))),
|
||||||
)
|
)
|
||||||
.optional()?;
|
.optional()?;
|
||||||
|
|
||||||
let Some((version, mac_opt, gen)) = meta_row else {
|
let Some((version, mac_opt, gen)) = meta_row else {
|
||||||
return Ok(true);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
if version < FORMAT_VERSION_V3 {
|
if version < FORMAT_VERSION_V3 {
|
||||||
@@ -1714,7 +1822,7 @@ impl Database {
|
|||||||
expected_mac.copy_from_slice(&mac_bytes);
|
expected_mac.copy_from_slice(&mac_bytes);
|
||||||
drop(conn);
|
drop(conn);
|
||||||
|
|
||||||
let canonical = self.canonical_nodes_bytes()?;
|
let canonical = self.canonical_nodes_bytes_for_vault(slot_id)?;
|
||||||
let mac_key = derive_metadata_mac_key(dek);
|
let mac_key = derive_metadata_mac_key(dek);
|
||||||
Ok(verify_metadata_mac(&mac_key, gen, &canonical, &expected_mac))
|
Ok(verify_metadata_mac(&mac_key, gen, &canonical, &expected_mac))
|
||||||
}
|
}
|
||||||
@@ -1742,6 +1850,65 @@ impl Database {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// K-02: Alle bestehenden Chunks von alter 16-Byte-AAD auf Format V3 24-Byte-AAD (generation = 0) umverschlüsseln
|
||||||
|
{
|
||||||
|
let mut chunk_stmt = conn.prepare(
|
||||||
|
"SELECT node_id, chunk_index, nonce, tag, ciphertext FROM chunks",
|
||||||
|
)?;
|
||||||
|
let chunk_rows: Vec<(i64, u32, [u8; 12], [u8; 16], Vec<u8>)> = chunk_stmt
|
||||||
|
.query_map([], |row| {
|
||||||
|
let node_id: i64 = row.get(0)?;
|
||||||
|
let chunk_index: u32 = row.get(1)?;
|
||||||
|
let nonce_vec: Vec<u8> = row.get(2)?;
|
||||||
|
let tag_vec: Vec<u8> = row.get(3)?;
|
||||||
|
let ciphertext: Vec<u8> = row.get(4)?;
|
||||||
|
let mut nonce = [0u8; 12];
|
||||||
|
let mut tag = [0u8; 16];
|
||||||
|
if nonce_vec.len() == 12 {
|
||||||
|
nonce.copy_from_slice(&nonce_vec);
|
||||||
|
}
|
||||||
|
if tag_vec.len() == 16 {
|
||||||
|
tag.copy_from_slice(&tag_vec);
|
||||||
|
}
|
||||||
|
Ok((node_id, chunk_index, nonce, tag, ciphertext))
|
||||||
|
})?
|
||||||
|
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||||
|
drop(chunk_stmt);
|
||||||
|
|
||||||
|
for (node_id, chunk_index, nonce, tag, ct) in chunk_rows {
|
||||||
|
if let Ok(plaintext) = crate::crypto::decrypt_chunk(
|
||||||
|
dek,
|
||||||
|
node_id,
|
||||||
|
chunk_index,
|
||||||
|
&ct,
|
||||||
|
&nonce,
|
||||||
|
&tag,
|
||||||
|
version,
|
||||||
|
0,
|
||||||
|
) {
|
||||||
|
if let Ok((new_ct, new_nonce, new_tag)) = crate::crypto::encrypt_chunk(
|
||||||
|
dek,
|
||||||
|
node_id,
|
||||||
|
chunk_index,
|
||||||
|
&plaintext,
|
||||||
|
FORMAT_VERSION_V3,
|
||||||
|
0,
|
||||||
|
) {
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE chunks SET nonce = ?1, tag = ?2, ciphertext = ?3, generation = 0 WHERE node_id = ?4 AND chunk_index = ?5",
|
||||||
|
params![
|
||||||
|
new_nonce.as_slice(),
|
||||||
|
new_tag.as_slice(),
|
||||||
|
new_ct,
|
||||||
|
node_id,
|
||||||
|
chunk_index
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE meta SET version = ?1, metadata_gen = 0 WHERE slot_id = 0",
|
"UPDATE meta SET version = ?1, metadata_gen = 0 WHERE slot_id = 0",
|
||||||
[FORMAT_VERSION_V3],
|
[FORMAT_VERSION_V3],
|
||||||
@@ -1762,6 +1929,7 @@ impl Database {
|
|||||||
conn.query_row("PRAGMA wal_checkpoint(TRUNCATE);", [], |row| {
|
conn.query_row("PRAGMA wal_checkpoint(TRUNCATE);", [], |row| {
|
||||||
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
|
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
|
||||||
})?;
|
})?;
|
||||||
|
let _ = conn.execute_batch("PRAGMA incremental_vacuum;");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2042,7 +2210,7 @@ mod tests {
|
|||||||
let test_cipher = b"ENCRYPTED_DATA_BLOCK";
|
let test_cipher = b"ENCRYPTED_DATA_BLOCK";
|
||||||
let c_nonce = [7u8; 12];
|
let c_nonce = [7u8; 12];
|
||||||
let c_tag = [8u8; 16];
|
let c_tag = [8u8; 16];
|
||||||
db.write_chunk(file.id, 0, &c_nonce, &c_tag, test_cipher)
|
db.write_chunk(file.id, 0, 0, &c_nonce, &c_tag, test_cipher)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let chunk = db
|
let chunk = db
|
||||||
@@ -2089,7 +2257,7 @@ mod tests {
|
|||||||
let c_nonce = [5u8; 12];
|
let c_nonce = [5u8; 12];
|
||||||
let c_tag = [6u8; 16];
|
let c_tag = [6u8; 16];
|
||||||
for i in 0..20 {
|
for i in 0..20 {
|
||||||
db.write_chunk(file.id, i, &c_nonce, &c_tag, &payload)
|
db.write_chunk(file.id, i, 0, &c_nonce, &c_tag, &payload)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
db.checkpoint().unwrap();
|
db.checkpoint().unwrap();
|
||||||
@@ -2135,7 +2303,7 @@ mod tests {
|
|||||||
let sensitive_payload = b"VERY_SENSITIVE_PLAINTEXT_OR_CIPHERTEXT";
|
let sensitive_payload = b"VERY_SENSITIVE_PLAINTEXT_OR_CIPHERTEXT";
|
||||||
let c_nonce = [10u8; 12];
|
let c_nonce = [10u8; 12];
|
||||||
let c_tag = [11u8; 16];
|
let c_tag = [11u8; 16];
|
||||||
db.write_chunk(file.id, 0, &c_nonce, &c_tag, sensitive_payload)
|
db.write_chunk(file.id, 0, 0, &c_nonce, &c_tag, sensitive_payload)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Shredde Chunks
|
// Shredde Chunks
|
||||||
@@ -2322,6 +2490,7 @@ mod tests {
|
|||||||
db.write_chunk_and_update_size(
|
db.write_chunk_and_update_size(
|
||||||
file.id,
|
file.id,
|
||||||
0,
|
0,
|
||||||
|
0,
|
||||||
&chunk_nonce,
|
&chunk_nonce,
|
||||||
&chunk_tag,
|
&chunk_tag,
|
||||||
&ciphertext,
|
&ciphertext,
|
||||||
|
|||||||
+6
-1
@@ -57,6 +57,7 @@ fn calc_vault_node_sha256(
|
|||||||
&record.nonce,
|
&record.nonce,
|
||||||
&record.tag,
|
&record.tag,
|
||||||
version,
|
version,
|
||||||
|
record.generation,
|
||||||
)?;
|
)?;
|
||||||
hasher.update(&decrypted);
|
hasher.update(&decrypted);
|
||||||
}
|
}
|
||||||
@@ -313,12 +314,15 @@ pub fn sync_single_file_to_vault(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let chunk_data = &buffer[..n];
|
let chunk_data = &buffer[..n];
|
||||||
let (ciphertext, nonce, tag) = encrypt_chunk(dek, node_id, chunk_idx, chunk_data, version)?;
|
let gen = db.next_chunk_generation(node_id, chunk_idx)?;
|
||||||
|
let (ciphertext, nonce, tag) =
|
||||||
|
encrypt_chunk(dek, node_id, chunk_idx, chunk_data, version, gen)?;
|
||||||
bytes_written += n as u64;
|
bytes_written += n as u64;
|
||||||
|
|
||||||
db.write_chunk_and_update_size(
|
db.write_chunk_and_update_size(
|
||||||
node_id,
|
node_id,
|
||||||
chunk_idx,
|
chunk_idx,
|
||||||
|
gen,
|
||||||
&nonce,
|
&nonce,
|
||||||
&tag,
|
&tag,
|
||||||
&ciphertext,
|
&ciphertext,
|
||||||
@@ -409,6 +413,7 @@ pub fn sync_single_file_to_host(
|
|||||||
&record.nonce,
|
&record.nonce,
|
||||||
&record.tag,
|
&record.tag,
|
||||||
version,
|
version,
|
||||||
|
record.generation,
|
||||||
)?;
|
)?;
|
||||||
out_file.write_all(&plaintext)?;
|
out_file.write_all(&plaintext)?;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+7
-6
@@ -301,6 +301,7 @@ pub fn verify_container(
|
|||||||
&record.nonce,
|
&record.nonce,
|
||||||
&record.tag,
|
&record.tag,
|
||||||
format_version,
|
format_version,
|
||||||
|
record.generation,
|
||||||
) {
|
) {
|
||||||
Ok(plaintext) => {
|
Ok(plaintext) => {
|
||||||
report.total_bytes_decrypted += plaintext.len() as u64;
|
report.total_bytes_decrypted += plaintext.len() as u64;
|
||||||
@@ -373,12 +374,12 @@ mod tests {
|
|||||||
|
|
||||||
// 2 Chunks schreiben
|
// 2 Chunks schreiben
|
||||||
let chunk0_data = b"Sample JPEG data header and pixels";
|
let chunk0_data = b"Sample JPEG data header and pixels";
|
||||||
let (ct0, n0, t0) = encrypt_chunk(&dek, file.id, 0, chunk0_data, FORMAT_VERSION).unwrap();
|
let (ct0, n0, t0) = encrypt_chunk(&dek, file.id, 0, chunk0_data, FORMAT_VERSION, 0).unwrap();
|
||||||
db.write_chunk(file.id, 0, &n0, &t0, &ct0).unwrap();
|
db.write_chunk(file.id, 0, 0, &n0, &t0, &ct0).unwrap();
|
||||||
|
|
||||||
let chunk1_data = b"Additional payload data bytes";
|
let chunk1_data = b"Additional payload data bytes";
|
||||||
let (ct1, n1, t1) = encrypt_chunk(&dek, file.id, 1, chunk1_data, FORMAT_VERSION).unwrap();
|
let (ct1, n1, t1) = encrypt_chunk(&dek, file.id, 1, chunk1_data, FORMAT_VERSION, 0).unwrap();
|
||||||
db.write_chunk(file.id, 1, &n1, &t1, &ct1).unwrap();
|
db.write_chunk(file.id, 1, 0, &n1, &t1, &ct1).unwrap();
|
||||||
|
|
||||||
db.update_node_size_and_time(
|
db.update_node_size_and_time(
|
||||||
file.id,
|
file.id,
|
||||||
@@ -432,8 +433,8 @@ mod tests {
|
|||||||
|
|
||||||
let file = db.create_node(1, "document.pdf", false).unwrap();
|
let file = db.create_node(1, "document.pdf", false).unwrap();
|
||||||
let chunk_data = b"Vital documents that must not be corrupted";
|
let chunk_data = b"Vital documents that must not be corrupted";
|
||||||
let (ct, n, t) = encrypt_chunk(&dek, file.id, 0, chunk_data, FORMAT_VERSION).unwrap();
|
let (ct, n, t) = encrypt_chunk(&dek, file.id, 0, chunk_data, FORMAT_VERSION, 0).unwrap();
|
||||||
db.write_chunk(file.id, 0, &n, &t, &ct).unwrap();
|
db.write_chunk(file.id, 0, 0, &n, &t, &ct).unwrap();
|
||||||
db.checkpoint().unwrap();
|
db.checkpoint().unwrap();
|
||||||
drop(db);
|
drop(db);
|
||||||
|
|
||||||
|
|||||||
+15
-3
@@ -171,8 +171,12 @@ impl SanctumFile {
|
|||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
if let Some((idx, ref data, true)) = self.cached_chunk {
|
if let Some((idx, ref data, true)) = self.cached_chunk {
|
||||||
|
let gen = self
|
||||||
|
.db
|
||||||
|
.next_chunk_generation(self.node_id, idx)
|
||||||
|
.unwrap_or(0);
|
||||||
let (ciphertext, nonce, tag) =
|
let (ciphertext, nonce, tag) =
|
||||||
encrypt_chunk(&self.dek, self.node_id, idx, data, self.format_version).map_err(
|
encrypt_chunk(&self.dek, self.node_id, idx, data, self.format_version, gen).map_err(
|
||||||
|e| {
|
|e| {
|
||||||
error!("Verschlüsselungsfehler beim Chunk-Flush: {e}");
|
error!("Verschlüsselungsfehler beim Chunk-Flush: {e}");
|
||||||
FsError::GeneralFailure
|
FsError::GeneralFailure
|
||||||
@@ -182,6 +186,7 @@ impl SanctumFile {
|
|||||||
if let Err(e) = self.db.write_chunk_and_update_size(
|
if let Err(e) = self.db.write_chunk_and_update_size(
|
||||||
self.node_id,
|
self.node_id,
|
||||||
idx,
|
idx,
|
||||||
|
gen,
|
||||||
&nonce,
|
&nonce,
|
||||||
&tag,
|
&tag,
|
||||||
&ciphertext,
|
&ciphertext,
|
||||||
@@ -246,6 +251,7 @@ impl SanctumFile {
|
|||||||
&record.nonce,
|
&record.nonce,
|
||||||
&record.tag,
|
&record.tag,
|
||||||
self.format_version,
|
self.format_version,
|
||||||
|
record.generation,
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("AEAD-Entschlüsselungsfehler bei Chunk #{chunk_index}: {e}");
|
error!("AEAD-Entschlüsselungsfehler bei Chunk #{chunk_index}: {e}");
|
||||||
@@ -490,7 +496,7 @@ impl SanctumFs {
|
|||||||
let dek_arc = Arc::new(dek);
|
let dek_arc = Arc::new(dek);
|
||||||
let carrier_dek_arc = carrier_dek.map(Arc::new);
|
let carrier_dek_arc = carrier_dek.map(Arc::new);
|
||||||
|
|
||||||
db.set_active_dek((*dek_arc).clone());
|
let db = db.with_session(vault_id, (*dek_arc).clone());
|
||||||
|
|
||||||
// Im Decoy-Vault (Slot 0): Stelle sicher, dass carrier_node_id stets bekannt ist,
|
// Im Decoy-Vault (Slot 0): Stelle sicher, dass carrier_node_id stets bekannt ist,
|
||||||
// um die Trägerdatei vor versehentlichem Löschen oder Überschreiben zu schützen.
|
// um die Trägerdatei vor versehentlichem Löschen oder Überschreiben zu schützen.
|
||||||
@@ -1018,20 +1024,26 @@ impl DavFileSystem for SanctumFs {
|
|||||||
&record.nonce,
|
&record.nonce,
|
||||||
&record.tag,
|
&record.tag,
|
||||||
self.format_version,
|
self.format_version,
|
||||||
|
record.generation,
|
||||||
)
|
)
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
.map_err(|_| FsError::GeneralFailure)?;
|
||||||
|
|
||||||
|
let gen = self
|
||||||
|
.db
|
||||||
|
.next_chunk_generation(dest_node.id, idx)
|
||||||
|
.map_err(|_| FsError::GeneralFailure)?;
|
||||||
let (new_ct, new_nonce, new_tag) = encrypt_chunk(
|
let (new_ct, new_nonce, new_tag) = encrypt_chunk(
|
||||||
&self.dek,
|
&self.dek,
|
||||||
dest_node.id,
|
dest_node.id,
|
||||||
idx,
|
idx,
|
||||||
&plaintext,
|
&plaintext,
|
||||||
self.format_version,
|
self.format_version,
|
||||||
|
gen,
|
||||||
)
|
)
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
.map_err(|_| FsError::GeneralFailure)?;
|
||||||
|
|
||||||
self.db
|
self.db
|
||||||
.write_chunk(dest_node.id, idx, &new_nonce, &new_tag, &new_ct)
|
.write_chunk(dest_node.id, idx, gen, &new_nonce, &new_tag, &new_ct)
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
.map_err(|_| FsError::GeneralFailure)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -433,8 +433,8 @@ async fn test_sanctum_v1_backward_compatibility() {
|
|||||||
// V1 Chunk mit encrypt_chunk(..., FORMAT_VERSION_V1) erzeugen und direkt in DB schreiben
|
// V1 Chunk mit encrypt_chunk(..., FORMAT_VERSION_V1) erzeugen und direkt in DB schreiben
|
||||||
let v1_plaintext = b"Legacy Sanctum V1 uncompressed data payload.";
|
let v1_plaintext = b"Legacy Sanctum V1 uncompressed data payload.";
|
||||||
let (ct, nonce, tag) =
|
let (ct, nonce, tag) =
|
||||||
encrypt_chunk(&dek, node.id, 0, v1_plaintext, FORMAT_VERSION_V1).expect("encrypt v1");
|
encrypt_chunk(&dek, node.id, 0, v1_plaintext, FORMAT_VERSION_V1, 0).expect("encrypt v1");
|
||||||
db.write_chunk(node.id, 0, &nonce, &tag, &ct)
|
db.write_chunk(node.id, 0, 0, &nonce, &tag, &ct)
|
||||||
.expect("write chunk");
|
.expect("write chunk");
|
||||||
db.update_node_size_and_time(node.id, v1_plaintext.len() as u64, 12345678)
|
db.update_node_size_and_time(node.id, v1_plaintext.len() as u64, 12345678)
|
||||||
.expect("update size");
|
.expect("update size");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use sanctum::crypto::{
|
use sanctum::crypto::{
|
||||||
derive_kek, generate_dek, generate_salt, wrap_dek, wrap_slot0_payload, KdfParams,
|
decrypt_chunk, derive_kek, encrypt_chunk, generate_dek, generate_salt, wrap_dek,
|
||||||
MIN_MEMORY_COST_KIB, MIN_TIME_COST,
|
wrap_slot0_payload, KdfParams, MIN_MEMORY_COST_KIB, MIN_TIME_COST,
|
||||||
};
|
};
|
||||||
use sanctum::storage::Database;
|
use sanctum::storage::Database;
|
||||||
use sanctum::verify::verify_container;
|
use sanctum::verify::verify_container;
|
||||||
@@ -869,3 +869,132 @@ fn test_k01_upgrade_format_v2_to_v3() {
|
|||||||
let _ = std::fs::remove_file(&db_path);
|
let _ = std::fs::remove_file(&db_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_k02_chunk_replay_detected_by_vfs_and_crypto() {
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let db_path = temp_dir.join(format!(
|
||||||
|
"k02_replay_test_{}.sanctum",
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos()
|
||||||
|
));
|
||||||
|
|
||||||
|
let password = "TestPasswordK02!";
|
||||||
|
let salt = generate_salt();
|
||||||
|
let kdf_params = KdfParams {
|
||||||
|
memory_cost: MIN_MEMORY_COST_KIB,
|
||||||
|
time_cost: MIN_TIME_COST,
|
||||||
|
parallelism: 1,
|
||||||
|
};
|
||||||
|
let kek = derive_kek(password, &salt, &kdf_params).unwrap();
|
||||||
|
let dek = generate_dek();
|
||||||
|
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||||
|
|
||||||
|
let db = Database::open(&db_path).unwrap();
|
||||||
|
db.set_active_dek(dek.clone());
|
||||||
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 1. Datei im Format V3 anlegen
|
||||||
|
let file = db.create_node(1, "replay_target.txt", false).unwrap();
|
||||||
|
let initial_data = b"State 1: Initial secret content in chunk 0.";
|
||||||
|
let gen1 = db.next_chunk_generation(file.id, 0).unwrap();
|
||||||
|
let (ct1, nonce1, tag1) = encrypt_chunk(
|
||||||
|
&dek,
|
||||||
|
file.id,
|
||||||
|
0,
|
||||||
|
initial_data,
|
||||||
|
sanctum::crypto::FORMAT_VERSION_V3,
|
||||||
|
gen1,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
db.write_chunk_and_update_size(
|
||||||
|
file.id,
|
||||||
|
0,
|
||||||
|
gen1,
|
||||||
|
&nonce1,
|
||||||
|
&tag1,
|
||||||
|
&ct1,
|
||||||
|
initial_data.len() as u64,
|
||||||
|
1000,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
db.checkpoint().unwrap();
|
||||||
|
|
||||||
|
// Ciphertext-Zeile von Zustand 1 sichern (Nonce, Tag, Ciphertext)
|
||||||
|
let saved_chunk1 = db.read_chunk(file.id, 0).unwrap().unwrap();
|
||||||
|
assert_eq!(saved_chunk1.generation, gen1);
|
||||||
|
|
||||||
|
// 2. Chunk mit neuem Inhalt überschreiben (Zustand 2)
|
||||||
|
let updated_data = b"State 2: Updated overwritten content in chunk 0.";
|
||||||
|
let gen2 = db.next_chunk_generation(file.id, 0).unwrap();
|
||||||
|
assert!(gen2 > gen1, "Generation muss monoton steigen");
|
||||||
|
let (ct2, nonce2, tag2) = encrypt_chunk(
|
||||||
|
&dek,
|
||||||
|
file.id,
|
||||||
|
0,
|
||||||
|
updated_data,
|
||||||
|
sanctum::crypto::FORMAT_VERSION_V3,
|
||||||
|
gen2,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
db.write_chunk_and_update_size(
|
||||||
|
file.id,
|
||||||
|
0,
|
||||||
|
gen2,
|
||||||
|
&nonce2,
|
||||||
|
&tag2,
|
||||||
|
&ct2,
|
||||||
|
updated_data.len() as u64,
|
||||||
|
2000,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
db.checkpoint().unwrap();
|
||||||
|
|
||||||
|
let current_chunk = db.read_chunk(file.id, 0).unwrap().unwrap();
|
||||||
|
assert_eq!(current_chunk.generation, gen2);
|
||||||
|
|
||||||
|
// 3. Replay-Angriff: Angreifer spielt alte Ciphertext-Zeile von Zustand 1 zurück in die SQLite-Tabelle
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE chunks SET nonce = ?1, tag = ?2, ciphertext = ?3 WHERE node_id = ?4 AND chunk_index = 0",
|
||||||
|
rusqlite::params![
|
||||||
|
saved_chunk1.nonce.as_slice(),
|
||||||
|
saved_chunk1.tag.as_slice(),
|
||||||
|
saved_chunk1.ciphertext,
|
||||||
|
file.id,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entschlüsselungsversuch muss fehlschlagen (AEAD Auth-Fehler wegen AAD-Generationsabweichung)
|
||||||
|
let replayed_chunk = db.read_chunk(file.id, 0).unwrap().unwrap();
|
||||||
|
let decrypt_res = decrypt_chunk(
|
||||||
|
&dek,
|
||||||
|
file.id,
|
||||||
|
0,
|
||||||
|
&replayed_chunk.ciphertext,
|
||||||
|
&replayed_chunk.nonce,
|
||||||
|
&replayed_chunk.tag,
|
||||||
|
sanctum::crypto::FORMAT_VERSION_V3,
|
||||||
|
replayed_chunk.generation,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
decrypt_res.is_err(),
|
||||||
|
"K-02: Replay von altem Ciphertext in aktuellem Chunk-Slot muss durch AEAD AAD-Mismatch abgewiesen werden!"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify muss Replay/Manipulierte Chunks erkennen
|
||||||
|
let verify_res = verify_container(&db_path, Some(&dek), true).unwrap();
|
||||||
|
assert!(
|
||||||
|
!verify_res.is_healthy() || verify_res.corrupted_chunks > 0,
|
||||||
|
"Verify muss Replay/Manipulierte Chunks erkennen"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user