fix(crypto): K-01 — format v3 canonical metadata authentication

This commit is contained in:
2026-09-19 00:26:27 +02:00
parent 151fdba09d
commit ad531d8393
7 changed files with 609 additions and 12 deletions
+111 -1
View File
@@ -7,12 +7,15 @@ use argon2::{Algorithm, Argon2, Params, Version};
use rand::rngs::OsRng;
use rand::RngCore;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
use zeroize::Zeroizing;
pub const MAGIC_BYTES: &[u8; 8] = b"SANCTUM\0";
pub const FORMAT_VERSION_V1: u32 = 1;
pub const FORMAT_VERSION_V2: u32 = 2;
pub const FORMAT_VERSION: u32 = FORMAT_VERSION_V2;
pub const FORMAT_VERSION_V3: u32 = 3;
pub const FORMAT_VERSION: u32 = FORMAT_VERSION_V3;
pub const CHUNK_SIZE: usize = 1024 * 1024; // 1 MB
/// Kompressions-Flags für Chunk-Payloads in Formatversion >= 2
@@ -554,6 +557,81 @@ pub fn mnemonic_to_dek(phrase: &str) -> Result<Zeroizing<[u8; 32]>> {
Ok(dek)
}
/// Berechnet HMAC-SHA256 gemäß RFC 2104.
pub fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
let mut key_block = [0u8; 64];
if key.len() > 64 {
let mut hasher = Sha256::new();
hasher.update(key);
let hash = hasher.finalize();
key_block[..32].copy_from_slice(&hash);
} else {
key_block[..key.len()].copy_from_slice(key);
}
let mut k_ipad = [0u8; 64];
let mut k_opad = [0u8; 64];
for i in 0..64 {
k_ipad[i] = key_block[i] ^ 0x36;
k_opad[i] = key_block[i] ^ 0x5c;
}
let mut inner_hasher = Sha256::new();
inner_hasher.update(&k_ipad);
inner_hasher.update(data);
let inner_hash = inner_hasher.finalize();
let mut outer_hasher = Sha256::new();
outer_hasher.update(&k_opad);
outer_hasher.update(&inner_hash);
let out = outer_hasher.finalize();
let mut result = [0u8; 32];
result.copy_from_slice(&out);
result
}
/// Leitet einen 32-Byte Schlüssel mittels HKDF-SHA256 (RFC 5869) aus IKM und Info-String ab.
pub fn hkdf_sha256(ikm: &[u8], info: &[u8]) -> [u8; 32] {
let salt = [0u8; 32];
let prk = hmac_sha256(&salt, ikm);
let mut expand_input = Vec::with_capacity(info.len() + 1);
expand_input.extend_from_slice(info);
expand_input.push(0x01);
hmac_sha256(&prk, &expand_input)
}
/// Leitet den dedizierten Metadaten-MAC-Schlüssel aus dem DEK ab (Format V3 / K-01).
pub fn derive_metadata_mac_key(dek: &[u8; 32]) -> [u8; 32] {
hkdf_sha256(dek, b"SANCTUM_META_MAC_V3")
}
/// Berechnet den Metadaten-MAC über kanonisch serialisierte Knoten und Generation (Format V3 / K-01).
pub fn compute_metadata_mac(
mac_key: &[u8; 32],
metadata_gen: u64,
canonical_nodes: &[u8],
) -> [u8; 32] {
let mut data = Vec::with_capacity(24 + canonical_nodes.len());
data.extend_from_slice(b"SANCTUM_META_V3\0");
data.extend_from_slice(&metadata_gen.to_le_bytes());
data.extend_from_slice(canonical_nodes);
hmac_sha256(mac_key, &data)
}
/// Prüft in konstanter Zeit, ob der gegebene Metadaten-MAC gültig ist (Format V3 / K-01).
pub fn verify_metadata_mac(
mac_key: &[u8; 32],
metadata_gen: u64,
canonical_nodes: &[u8],
expected_mac: &[u8; 32],
) -> bool {
let computed = compute_metadata_mac(mac_key, metadata_gen, canonical_nodes);
computed.ct_eq(expected_mac).into()
}
/// Erzeugt die 16-Byte Associated Data (AAD) für einen Chunk, um Swap-Angriffe zu verhindern:
/// node_id (8 Bytes Little-Endian) || chunk_index (8 Bytes Little-Endian).
#[inline]
@@ -1097,4 +1175,36 @@ mod tests {
err_msg
);
}
#[test]
fn test_hmac_sha256_rfc4231_test_case_1() {
// RFC 4231 Test Case 1: Key = 20x 0x0b, Data = "Hi There"
let key = [0x0b; 20];
let data = b"Hi There";
let mac = hmac_sha256(&key, data);
let expected_hex = "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7";
assert_eq!(hex::encode(mac), expected_hex);
}
#[test]
fn test_metadata_mac_verification() {
let dek = generate_dek();
let mac_key = derive_metadata_mac_key(&dek);
let canonical_nodes = b"node_canonical_bytes_mock_12345";
let gen = 0u64;
let mac = compute_metadata_mac(&mac_key, gen, canonical_nodes);
assert!(verify_metadata_mac(&mac_key, gen, canonical_nodes, &mac));
// Manipulierte Generation -> ungültig
assert!(!verify_metadata_mac(&mac_key, gen + 1, canonical_nodes, &mac));
// Manipulierte Knoten-Bytes -> ungültig
assert!(!verify_metadata_mac(&mac_key, gen, b"tampered_nodes", &mac));
// Falscher Schlüssel -> ungültig
let wrong_dek = generate_dek();
let wrong_key = derive_metadata_mac_key(&wrong_dek);
assert!(!verify_metadata_mac(&wrong_key, gen, canonical_nodes, &mac));
}
}