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.tag,
|
||||
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
|
||||
@@ -149,6 +150,7 @@ pub fn read_carrier_block(
|
||||
&inner_nonce,
|
||||
&inner_tag,
|
||||
format_version,
|
||||
chunk_rec.generation,
|
||||
)?;
|
||||
|
||||
Ok(inner_plaintext)
|
||||
@@ -167,6 +169,8 @@ pub fn write_carrier_block(
|
||||
plaintext: &[u8],
|
||||
format_version: u32,
|
||||
) -> Result<()> {
|
||||
let gen = 0u64;
|
||||
|
||||
// 1. Innere Schicht verschlüsseln (mit dek_inner = DEK_1)
|
||||
let (inner_ct, inner_nonce, inner_tag) = encrypt_chunk(
|
||||
dek_inner,
|
||||
@@ -174,6 +178,7 @@ pub fn write_carrier_block(
|
||||
block_idx,
|
||||
plaintext,
|
||||
format_version,
|
||||
gen,
|
||||
)?;
|
||||
|
||||
let inner_ct_len = inner_ct.len() as u32;
|
||||
@@ -208,12 +213,14 @@ pub fn write_carrier_block(
|
||||
block_idx,
|
||||
&outer_plaintext,
|
||||
format_version,
|
||||
gen,
|
||||
)?;
|
||||
|
||||
// 4. In SQLite schreiben (in-place Überschreiben des bestehenden Chunks)
|
||||
db.write_chunk(
|
||||
carrier_node_id,
|
||||
block_idx,
|
||||
gen,
|
||||
&outer_nonce,
|
||||
&outer_tag,
|
||||
&outer_ct,
|
||||
@@ -234,17 +241,20 @@ pub fn shred_carrier_block(
|
||||
let mut noise = vec![0u8; CHUNK_SIZE];
|
||||
OsRng.fill_bytes(&mut noise);
|
||||
|
||||
let gen = 0u64;
|
||||
let (outer_ct, outer_nonce, outer_tag) = encrypt_chunk(
|
||||
dek_outer,
|
||||
carrier_node_id,
|
||||
block_idx,
|
||||
&noise,
|
||||
format_version,
|
||||
gen,
|
||||
)?;
|
||||
|
||||
db.write_chunk(
|
||||
carrier_node_id,
|
||||
block_idx,
|
||||
gen,
|
||||
&outer_nonce,
|
||||
&outer_tag,
|
||||
&outer_ct,
|
||||
|
||||
+137
-10
@@ -642,9 +642,21 @@ pub fn build_chunk_aad(node_id: i64, chunk_index: u32) -> [u8; 16] {
|
||||
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.
|
||||
/// In Formatversion >= 2 wird der Chunk vor der Verschlüsselung transparent mit LZ4 komprimiert,
|
||||
/// 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.
|
||||
pub fn encrypt_chunk(
|
||||
dek: &[u8; 32],
|
||||
@@ -652,6 +664,7 @@ pub fn encrypt_chunk(
|
||||
chunk_index: u32,
|
||||
plaintext: &[u8],
|
||||
format_version: u32,
|
||||
generation: u64,
|
||||
) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
|
||||
let cipher = Aes256Gcm::new_from_slice(dek)
|
||||
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
|
||||
@@ -660,7 +673,15 @@ pub fn encrypt_chunk(
|
||||
OsRng.fill_bytes(&mut 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 {
|
||||
if plaintext.is_empty() {
|
||||
@@ -685,7 +706,7 @@ pub fn encrypt_chunk(
|
||||
};
|
||||
|
||||
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}"))?;
|
||||
|
||||
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.
|
||||
/// 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(
|
||||
dek: &[u8; 32],
|
||||
node_id: i64,
|
||||
@@ -704,17 +726,27 @@ pub fn decrypt_chunk(
|
||||
nonce_bytes: &[u8; 12],
|
||||
tag_bytes: &[u8; 16],
|
||||
format_version: u32,
|
||||
generation: u64,
|
||||
) -> 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 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();
|
||||
cipher
|
||||
.decrypt_in_place_detached(nonce, &aad, &mut buffer, tag)
|
||||
.decrypt_in_place_detached(nonce, aad, &mut buffer, tag)
|
||||
.map_err(|_| {
|
||||
anyhow::anyhow!(
|
||||
"Chunk-Integritätsprüfung fehlgeschlagen (AEAD Auth-Fehler oder Swap-Angriff)"
|
||||
@@ -795,7 +827,7 @@ mod tests {
|
||||
let chunk_index = 0u32;
|
||||
|
||||
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)
|
||||
let decrypted = decrypt_chunk(
|
||||
@@ -806,6 +838,7 @@ mod tests {
|
||||
&nonce,
|
||||
&tag,
|
||||
FORMAT_VERSION_V2,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(decrypted, plaintext);
|
||||
@@ -819,6 +852,7 @@ mod tests {
|
||||
&nonce,
|
||||
&tag,
|
||||
FORMAT_VERSION_V2,
|
||||
0,
|
||||
);
|
||||
assert!(swap_node_err.is_err());
|
||||
|
||||
@@ -831,6 +865,7 @@ mod tests {
|
||||
&nonce,
|
||||
&tag,
|
||||
FORMAT_VERSION_V2,
|
||||
0,
|
||||
);
|
||||
assert!(swap_idx_err.is_err());
|
||||
|
||||
@@ -844,11 +879,100 @@ mod tests {
|
||||
&tampered_ct,
|
||||
&nonce,
|
||||
&tag,
|
||||
FORMAT_VERSION_V2
|
||||
FORMAT_VERSION_V2,
|
||||
0,
|
||||
)
|
||||
.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]
|
||||
fn test_lz4_chunk_compression_efficiency() {
|
||||
let dek = generate_dek();
|
||||
@@ -859,7 +983,7 @@ mod tests {
|
||||
let chunk_index = 0u32;
|
||||
|
||||
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
|
||||
assert!(
|
||||
@@ -877,6 +1001,7 @@ mod tests {
|
||||
&nonce,
|
||||
&tag,
|
||||
FORMAT_VERSION_V2,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(decrypted, plaintext);
|
||||
@@ -889,11 +1014,11 @@ mod tests {
|
||||
let mut random_bytes = vec![0u8; 1000];
|
||||
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
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -906,7 +1031,7 @@ mod tests {
|
||||
|
||||
// V1 Format: Reine Verschlüsselung ohne Kompressionspräfix
|
||||
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());
|
||||
|
||||
let decrypted = decrypt_chunk(
|
||||
@@ -917,6 +1042,7 @@ mod tests {
|
||||
&nonce,
|
||||
&tag,
|
||||
FORMAT_VERSION_V1,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(decrypted, plaintext);
|
||||
@@ -1163,6 +1289,7 @@ mod tests {
|
||||
&nonce_bytes,
|
||||
&tag_bytes,
|
||||
FORMAT_VERSION_V2,
|
||||
0,
|
||||
);
|
||||
assert!(
|
||||
res.is_err(),
|
||||
|
||||
@@ -297,6 +297,7 @@ pub fn detect_recovery_key_slot(db: &Database, dek: &[u8; 32], version: u32) ->
|
||||
&chunk0.nonce,
|
||||
&chunk0.tag,
|
||||
version,
|
||||
chunk0.generation,
|
||||
)
|
||||
.is_ok();
|
||||
if is_dek0 {
|
||||
|
||||
+244
-75
@@ -32,6 +32,7 @@ pub struct NodeRecord {
|
||||
pub struct ChunkRecord {
|
||||
pub node_id: i64,
|
||||
pub chunk_index: u32,
|
||||
pub generation: u64,
|
||||
pub nonce: [u8; 12],
|
||||
pub tag: [u8; 16],
|
||||
pub ciphertext: Vec<u8>,
|
||||
@@ -185,7 +186,7 @@ impl ContainerMeta {
|
||||
#[derive(Clone)]
|
||||
pub struct Database {
|
||||
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 {
|
||||
@@ -306,7 +307,7 @@ impl Database {
|
||||
|
||||
let db = Self {
|
||||
conn: Arc::new(Mutex::new(conn)),
|
||||
active_dek: Arc::new(Mutex::new(None)),
|
||||
active_session: Arc::new(Mutex::new(None)),
|
||||
};
|
||||
db.init_pragmas()?;
|
||||
if table_count > 0 {
|
||||
@@ -321,7 +322,7 @@ impl Database {
|
||||
let conn = Connection::open_in_memory()?;
|
||||
let db = Self {
|
||||
conn: Arc::new(Mutex::new(conn)),
|
||||
active_dek: Arc::new(Mutex::new(None)),
|
||||
active_session: Arc::new(Mutex::new(None)),
|
||||
};
|
||||
db.init_pragmas()?;
|
||||
db.ensure_schema_upgrades()?;
|
||||
@@ -332,14 +333,31 @@ impl Database {
|
||||
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]>) {
|
||||
*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.
|
||||
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.
|
||||
@@ -347,7 +365,7 @@ impl Database {
|
||||
let meta = self.read_meta()?;
|
||||
let res = meta.authenticate(password);
|
||||
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() {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -571,6 +595,7 @@ impl Database {
|
||||
CREATE TABLE IF NOT EXISTS chunks (
|
||||
node_id INTEGER NOT NULL,
|
||||
chunk_index INTEGER NOT NULL,
|
||||
generation INTEGER NOT NULL DEFAULT 0,
|
||||
nonce BLOB NOT NULL,
|
||||
tag BLOB NOT NULL,
|
||||
ciphertext BLOB NOT NULL,
|
||||
@@ -652,7 +677,7 @@ impl Database {
|
||||
let manifest_bytes = serde_json::to_vec(&manifest)?;
|
||||
|
||||
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 mut outer_plaintext = vec![0u8; CHUNK_SIZE];
|
||||
@@ -668,18 +693,18 @@ impl Database {
|
||||
outer_plaintext[32..ct_end].copy_from_slice(&inner_ct);
|
||||
|
||||
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(
|
||||
"INSERT INTO chunks (node_id, chunk_index, nonce, tag, ciphertext)
|
||||
VALUES (?1, 0, ?2, ?3, ?4)",
|
||||
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
||||
VALUES (?1, 0, 0, ?2, ?3, ?4)",
|
||||
params![c_id, outer_nonce.as_slice(), outer_tag.as_slice(), outer_ct],
|
||||
)?;
|
||||
|
||||
// Blöcke 1..total_blocks-1 mit DEK_0 vorallokieren
|
||||
let mut chunk_stmt = conn.prepare(
|
||||
"INSERT INTO chunks (node_id, chunk_index, nonce, tag, ciphertext)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
||||
VALUES (?1, ?2, 0, ?3, ?4, ?5)",
|
||||
)?;
|
||||
|
||||
let mut dummy_noise = vec![0u8; CHUNK_SIZE];
|
||||
@@ -688,7 +713,7 @@ impl Database {
|
||||
conn.execute_batch("BEGIN TRANSACTION;")?;
|
||||
for b in 1..total_blocks {
|
||||
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,])?;
|
||||
|
||||
if b % 500 == 0 {
|
||||
@@ -696,9 +721,14 @@ impl Database {
|
||||
}
|
||||
}
|
||||
conn.execute_batch("COMMIT;")?;
|
||||
|
||||
self.set_active_dek(Zeroizing::new(*dek_0));
|
||||
Some(c_id)
|
||||
drop(chunk_stmt);
|
||||
self.set_active_slot_and_dek(0, Zeroizing::new(*dek_0));
|
||||
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 {
|
||||
// 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();
|
||||
@@ -772,6 +802,7 @@ impl Database {
|
||||
CREATE TABLE IF NOT EXISTS chunks (
|
||||
node_id INTEGER NOT NULL,
|
||||
chunk_index INTEGER NOT NULL,
|
||||
generation INTEGER NOT NULL DEFAULT 0,
|
||||
nonce BLOB NOT NULL,
|
||||
tag 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>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
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
|
||||
@@ -1454,6 +1485,7 @@ impl Database {
|
||||
let nonce_vec: Vec<u8> = row.get(0)?;
|
||||
let tag_vec: Vec<u8> = row.get(1)?;
|
||||
let ciphertext: Vec<u8> = row.get(2)?;
|
||||
let generation: i64 = row.get(3).unwrap_or(0);
|
||||
|
||||
let mut nonce = [0u8; 12];
|
||||
let mut tag = [0u8; 16];
|
||||
@@ -1467,6 +1499,7 @@ impl Database {
|
||||
Ok(ChunkRecord {
|
||||
node_id,
|
||||
chunk_index,
|
||||
generation: generation as u64,
|
||||
nonce,
|
||||
tag,
|
||||
ciphertext,
|
||||
@@ -1477,26 +1510,52 @@ impl Database {
|
||||
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.
|
||||
pub fn write_chunk(
|
||||
&self,
|
||||
node_id: i64,
|
||||
chunk_index: u32,
|
||||
generation: u64,
|
||||
nonce: &[u8; 12],
|
||||
tag: &[u8; 16],
|
||||
ciphertext: &[u8],
|
||||
) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO chunks (node_id, chunk_index, nonce, tag, ciphertext)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
ON CONFLICT(node_id, chunk_index) DO UPDATE SET
|
||||
generation = excluded.generation,
|
||||
nonce = excluded.nonce,
|
||||
tag = excluded.tag,
|
||||
ciphertext = excluded.ciphertext",
|
||||
params![
|
||||
node_id,
|
||||
chunk_index,
|
||||
generation as i64,
|
||||
nonce.as_slice(),
|
||||
tag.as_slice(),
|
||||
ciphertext,
|
||||
@@ -1511,6 +1570,7 @@ impl Database {
|
||||
&self,
|
||||
node_id: i64,
|
||||
chunk_index: u32,
|
||||
generation: u64,
|
||||
nonce: &[u8; 12],
|
||||
tag: &[u8; 16],
|
||||
ciphertext: &[u8],
|
||||
@@ -1521,15 +1581,17 @@ impl Database {
|
||||
let mut conn = self.conn.lock().unwrap();
|
||||
let tx = conn.transaction()?;
|
||||
tx.execute(
|
||||
"INSERT INTO chunks (node_id, chunk_index, nonce, tag, ciphertext)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
ON CONFLICT(node_id, chunk_index) DO UPDATE SET
|
||||
generation = excluded.generation,
|
||||
nonce = excluded.nonce,
|
||||
tag = excluded.tag,
|
||||
ciphertext = excluded.ciphertext",
|
||||
params![
|
||||
node_id,
|
||||
chunk_index,
|
||||
generation as i64,
|
||||
nonce.as_slice(),
|
||||
tag.as_slice(),
|
||||
ciphertext,
|
||||
@@ -1596,65 +1658,100 @@ impl Database {
|
||||
|
||||
/// Erzeugt die deterministische kanonische Byterepräsentation aller Knoten für den Metadaten-MAC (K-01).
|
||||
pub fn canonical_nodes_bytes(&self) -> Result<Vec<u8>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
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",
|
||||
)?;
|
||||
self.canonical_nodes_bytes_for_vault(0)
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
let id: i64 = row.get(0)?;
|
||||
let parent_id: Option<i64> = row.get(1)?;
|
||||
let name: String = row.get(2)?;
|
||||
let is_dir: i64 = row.get(3)?;
|
||||
let size: i64 = row.get(4)?;
|
||||
let created_at: i64 = row.get(5)?;
|
||||
let modified_at: i64 = row.get(6)?;
|
||||
let is_carrier: i64 = row.get(7)?;
|
||||
let chunk_count: i64 = row.get(8)?;
|
||||
|
||||
buf.extend_from_slice(&id.to_le_bytes());
|
||||
match parent_id {
|
||||
Some(pid) => {
|
||||
buf.push(1u8);
|
||||
buf.extend_from_slice(&pid.to_le_bytes());
|
||||
}
|
||||
None => {
|
||||
buf.push(0u8);
|
||||
buf.extend_from_slice(&0i64.to_le_bytes());
|
||||
}
|
||||
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()? {
|
||||
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)?;
|
||||
}
|
||||
let name_bytes = name.as_bytes();
|
||||
buf.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
|
||||
buf.extend_from_slice(name_bytes);
|
||||
buf.push(if is_dir != 0 { 1u8 } else { 0u8 });
|
||||
buf.extend_from_slice(&size.to_le_bytes());
|
||||
buf.extend_from_slice(&created_at.to_le_bytes());
|
||||
buf.extend_from_slice(&modified_at.to_le_bytes());
|
||||
buf.push(if is_carrier != 0 { 1u8 } else { 0u8 });
|
||||
buf.extend_from_slice(&chunk_count.to_le_bytes());
|
||||
}
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Aktualisiert den Metadaten-MAC in Slot 0 bei strukturellen Modifikationen (Format V3 / K-01).
|
||||
fn serialize_node_row(row: &rusqlite::Row, buf: &mut Vec<u8>) -> Result<()> {
|
||||
let id: i64 = row.get(0)?;
|
||||
let parent_id: Option<i64> = row.get(1)?;
|
||||
let name: String = row.get(2)?;
|
||||
let is_dir: i64 = row.get(3)?;
|
||||
let size: i64 = row.get(4)?;
|
||||
let created_at: i64 = row.get(5)?;
|
||||
let modified_at: i64 = row.get(6)?;
|
||||
let is_carrier: i64 = row.get(7)?;
|
||||
let chunk_count: i64 = row.get(8)?;
|
||||
|
||||
buf.extend_from_slice(&id.to_le_bytes());
|
||||
match parent_id {
|
||||
Some(pid) => {
|
||||
buf.push(1u8);
|
||||
buf.extend_from_slice(&pid.to_le_bytes());
|
||||
}
|
||||
None => {
|
||||
buf.push(0u8);
|
||||
buf.extend_from_slice(&0i64.to_le_bytes());
|
||||
}
|
||||
}
|
||||
let name_bytes = name.as_bytes();
|
||||
buf.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
|
||||
buf.extend_from_slice(name_bytes);
|
||||
buf.push(if is_dir != 0 { 1u8 } else { 0u8 });
|
||||
buf.extend_from_slice(&size.to_le_bytes());
|
||||
buf.extend_from_slice(&created_at.to_le_bytes());
|
||||
buf.extend_from_slice(&modified_at.to_le_bytes());
|
||||
buf.push(if is_carrier != 0 { 1u8 } else { 0u8 });
|
||||
buf.extend_from_slice(&chunk_count.to_le_bytes());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Aktualisiert den Metadaten-MAC des aktiven Slots bei strukturellen Modifikationen (Format V3 / K-01).
|
||||
pub fn update_metadata_mac(&self) -> Result<()> {
|
||||
let dek_opt = self.active_dek.lock().unwrap().clone();
|
||||
let Some(dek) = dek_opt else {
|
||||
let session_opt = self.active_session.lock().unwrap().clone();
|
||||
let Some((slot_id, dek)) = session_opt else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let version_and_gen: Option<(u32, u64)> = conn
|
||||
.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))),
|
||||
)
|
||||
.optional()?;
|
||||
@@ -1670,14 +1767,14 @@ impl Database {
|
||||
drop(conn);
|
||||
|
||||
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 new_mac = compute_metadata_mac(&mac_key, next_gen, &canonical);
|
||||
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE meta SET metadata_mac = ?1, metadata_gen = ?2 WHERE slot_id = 0",
|
||||
params![new_mac.as_slice(), next_gen],
|
||||
"UPDATE meta SET metadata_mac = ?1, metadata_gen = ?2 WHERE slot_id = ?3",
|
||||
params![new_mac.as_slice(), next_gen, slot_id],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
@@ -1685,17 +1782,28 @@ impl Database {
|
||||
|
||||
/// 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> {
|
||||
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 meta_row: Option<(u32, Option<Vec<u8>>, u64)> = conn
|
||||
.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))),
|
||||
)
|
||||
.optional()?;
|
||||
|
||||
let Some((version, mac_opt, gen)) = meta_row else {
|
||||
return Ok(true);
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
if version < FORMAT_VERSION_V3 {
|
||||
@@ -1714,7 +1822,7 @@ impl Database {
|
||||
expected_mac.copy_from_slice(&mac_bytes);
|
||||
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);
|
||||
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(
|
||||
"UPDATE meta SET version = ?1, metadata_gen = 0 WHERE slot_id = 0",
|
||||
[FORMAT_VERSION_V3],
|
||||
@@ -1762,6 +1929,7 @@ impl Database {
|
||||
conn.query_row("PRAGMA wal_checkpoint(TRUNCATE);", [], |row| {
|
||||
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
|
||||
})?;
|
||||
let _ = conn.execute_batch("PRAGMA incremental_vacuum;");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2042,7 +2210,7 @@ mod tests {
|
||||
let test_cipher = b"ENCRYPTED_DATA_BLOCK";
|
||||
let c_nonce = [7u8; 12];
|
||||
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();
|
||||
|
||||
let chunk = db
|
||||
@@ -2089,7 +2257,7 @@ mod tests {
|
||||
let c_nonce = [5u8; 12];
|
||||
let c_tag = [6u8; 16];
|
||||
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();
|
||||
}
|
||||
db.checkpoint().unwrap();
|
||||
@@ -2135,7 +2303,7 @@ mod tests {
|
||||
let sensitive_payload = b"VERY_SENSITIVE_PLAINTEXT_OR_CIPHERTEXT";
|
||||
let c_nonce = [10u8; 12];
|
||||
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();
|
||||
|
||||
// Shredde Chunks
|
||||
@@ -2322,6 +2490,7 @@ mod tests {
|
||||
db.write_chunk_and_update_size(
|
||||
file.id,
|
||||
0,
|
||||
0,
|
||||
&chunk_nonce,
|
||||
&chunk_tag,
|
||||
&ciphertext,
|
||||
|
||||
+6
-1
@@ -57,6 +57,7 @@ fn calc_vault_node_sha256(
|
||||
&record.nonce,
|
||||
&record.tag,
|
||||
version,
|
||||
record.generation,
|
||||
)?;
|
||||
hasher.update(&decrypted);
|
||||
}
|
||||
@@ -313,12 +314,15 @@ pub fn sync_single_file_to_vault(
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
db.write_chunk_and_update_size(
|
||||
node_id,
|
||||
chunk_idx,
|
||||
gen,
|
||||
&nonce,
|
||||
&tag,
|
||||
&ciphertext,
|
||||
@@ -409,6 +413,7 @@ pub fn sync_single_file_to_host(
|
||||
&record.nonce,
|
||||
&record.tag,
|
||||
version,
|
||||
record.generation,
|
||||
)?;
|
||||
out_file.write_all(&plaintext)?;
|
||||
} else {
|
||||
|
||||
+7
-6
@@ -301,6 +301,7 @@ pub fn verify_container(
|
||||
&record.nonce,
|
||||
&record.tag,
|
||||
format_version,
|
||||
record.generation,
|
||||
) {
|
||||
Ok(plaintext) => {
|
||||
report.total_bytes_decrypted += plaintext.len() as u64;
|
||||
@@ -373,12 +374,12 @@ mod tests {
|
||||
|
||||
// 2 Chunks schreiben
|
||||
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();
|
||||
db.write_chunk(file.id, 0, &n0, &t0, &ct0).unwrap();
|
||||
let (ct0, n0, t0) = encrypt_chunk(&dek, file.id, 0, chunk0_data, FORMAT_VERSION, 0).unwrap();
|
||||
db.write_chunk(file.id, 0, 0, &n0, &t0, &ct0).unwrap();
|
||||
|
||||
let chunk1_data = b"Additional payload data bytes";
|
||||
let (ct1, n1, t1) = encrypt_chunk(&dek, file.id, 1, chunk1_data, FORMAT_VERSION).unwrap();
|
||||
db.write_chunk(file.id, 1, &n1, &t1, &ct1).unwrap();
|
||||
let (ct1, n1, t1) = encrypt_chunk(&dek, file.id, 1, chunk1_data, FORMAT_VERSION, 0).unwrap();
|
||||
db.write_chunk(file.id, 1, 0, &n1, &t1, &ct1).unwrap();
|
||||
|
||||
db.update_node_size_and_time(
|
||||
file.id,
|
||||
@@ -432,8 +433,8 @@ mod tests {
|
||||
|
||||
let file = db.create_node(1, "document.pdf", false).unwrap();
|
||||
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();
|
||||
db.write_chunk(file.id, 0, &n, &t, &ct).unwrap();
|
||||
let (ct, n, t) = encrypt_chunk(&dek, file.id, 0, chunk_data, FORMAT_VERSION, 0).unwrap();
|
||||
db.write_chunk(file.id, 0, 0, &n, &t, &ct).unwrap();
|
||||
db.checkpoint().unwrap();
|
||||
drop(db);
|
||||
|
||||
|
||||
+15
-3
@@ -171,8 +171,12 @@ impl SanctumFile {
|
||||
.unwrap_or(0);
|
||||
|
||||
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) =
|
||||
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| {
|
||||
error!("Verschlüsselungsfehler beim Chunk-Flush: {e}");
|
||||
FsError::GeneralFailure
|
||||
@@ -182,6 +186,7 @@ impl SanctumFile {
|
||||
if let Err(e) = self.db.write_chunk_and_update_size(
|
||||
self.node_id,
|
||||
idx,
|
||||
gen,
|
||||
&nonce,
|
||||
&tag,
|
||||
&ciphertext,
|
||||
@@ -246,6 +251,7 @@ impl SanctumFile {
|
||||
&record.nonce,
|
||||
&record.tag,
|
||||
self.format_version,
|
||||
record.generation,
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!("AEAD-Entschlüsselungsfehler bei Chunk #{chunk_index}: {e}");
|
||||
@@ -490,7 +496,7 @@ impl SanctumFs {
|
||||
let dek_arc = Arc::new(dek);
|
||||
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,
|
||||
// um die Trägerdatei vor versehentlichem Löschen oder Überschreiben zu schützen.
|
||||
@@ -1018,20 +1024,26 @@ impl DavFileSystem for SanctumFs {
|
||||
&record.nonce,
|
||||
&record.tag,
|
||||
self.format_version,
|
||||
record.generation,
|
||||
)
|
||||
.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(
|
||||
&self.dek,
|
||||
dest_node.id,
|
||||
idx,
|
||||
&plaintext,
|
||||
self.format_version,
|
||||
gen,
|
||||
)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user