fix(crypto): K-01 — format v3 canonical metadata authentication
This commit is contained in:
+111
-1
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user