use aes_gcm::aead::array::Array;
use aes_gcm::aead::{Aead, Payload};
use aes_gcm::{Aes256Gcm, KeyInit};
use hkdf::Hkdf;
use sha2::Sha512;
use zeroize::Zeroize;
use crate::CryptoError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Suite {
#[default]
Hybrid,
HybridMatched,
PureCnsa2,
}
pub const SEAL_CONTEXT_V1: &str = "metamorphic/seal/v1";
pub const TAG_KEM_PURE_CNSA2: u8 = 0x10;
pub const TAG_KEM_MATCHED_CAT3: u8 = 0x13;
pub const TAG_KEM_MATCHED_CAT5: u8 = 0x14;
pub const GCM_NONCE_LEN: usize = 12;
pub const GCM_TAG_LEN: usize = 16;
pub const AES256_KEY_LEN: usize = 32;
pub(crate) fn domain_bytes(tag: u8, context_label: &str) -> Vec<u8> {
let mut v = Vec::with_capacity(1 + context_label.len());
v.push(tag);
v.extend_from_slice(context_label.as_bytes());
v
}
pub(crate) fn derive_aes256_key(
ikm: &[u8],
info: &[u8],
) -> Result<[u8; AES256_KEY_LEN], CryptoError> {
let hk = Hkdf::<Sha512>::new(None, ikm);
let mut okm = [0u8; AES256_KEY_LEN];
hk.expand(info, &mut okm)
.map_err(|_| CryptoError::Hybrid("HKDF-SHA512 expand failed".into()))?;
Ok(okm)
}
pub(crate) fn aes256gcm_seal(
key: &[u8; AES256_KEY_LEN],
nonce: &[u8; GCM_NONCE_LEN],
aad: &[u8],
plaintext: &[u8],
) -> Result<Vec<u8>, CryptoError> {
let cipher = Aes256Gcm::new(&Array(*key));
let nonce_arr: Array<u8, _> = Array(*nonce);
cipher
.encrypt(
&nonce_arr,
Payload {
msg: plaintext,
aad,
},
)
.map_err(|_| CryptoError::Hybrid("AES-256-GCM encrypt failed".into()))
}
pub(crate) fn aes256gcm_open(
key: &[u8; AES256_KEY_LEN],
nonce: &[u8; GCM_NONCE_LEN],
aad: &[u8],
ciphertext: &[u8],
) -> Result<Vec<u8>, CryptoError> {
let cipher = Aes256Gcm::new(&Array(*key));
let nonce_arr: Array<u8, _> = Array(*nonce);
cipher
.decrypt(
&nonce_arr,
Payload {
msg: ciphertext,
aad,
},
)
.map_err(|_| CryptoError::Decryption)
}
pub(crate) fn envelope_seal(
ikm: &[u8],
tag: u8,
context_label: &str,
nonce: &[u8; GCM_NONCE_LEN],
plaintext: &[u8],
) -> Result<Vec<u8>, CryptoError> {
let domain = domain_bytes(tag, context_label);
let mut key = derive_aes256_key(ikm, &domain)?;
let out = aes256gcm_seal(&key, nonce, &domain, plaintext);
key.zeroize();
out
}
pub(crate) fn envelope_open(
ikm: &[u8],
tag: u8,
context_label: &str,
nonce: &[u8; GCM_NONCE_LEN],
ciphertext: &[u8],
) -> Result<Vec<u8>, CryptoError> {
let domain = domain_bytes(tag, context_label);
let mut key = derive_aes256_key(ikm, &domain)?;
let out = aes256gcm_open(&key, nonce, &domain, ciphertext);
key.zeroize();
out
}
#[cfg(test)]
mod tests {
use super::*;
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn unhex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
#[test]
fn aes256gcm_nist_kat() {
let key = [0u8; 32];
let nonce = [0u8; 12];
let out = aes256gcm_seal(&key, &nonce, &[], &[]).unwrap();
assert_eq!(hex(&out), "530f8afbc74536b9a963b4f1c4cb738b");
let out16 = aes256gcm_seal(&key, &nonce, &[], &[0u8; 16]).unwrap();
assert_eq!(
hex(&out16),
"cea7403d4d606b6e074ec5d3baf39d18d0d1c8a799996bf0265b98b5d48ab919"
);
assert_eq!(aes256gcm_open(&key, &nonce, &[], &out).unwrap(), b"");
assert_eq!(
aes256gcm_open(&key, &nonce, &[], &out16).unwrap(),
[0u8; 16]
);
}
#[test]
fn hkdf_sha512_kat() {
use hkdf::Hkdf;
use sha2::Sha512;
let ikm = [0x0bu8; 22];
let salt: Vec<u8> = (0u8..=0x0c).collect();
let info = unhex("f0f1f2f3f4f5f6f7f8f9");
let hk = Hkdf::<Sha512>::new(Some(&salt), &ikm);
let mut okm = [0u8; 42];
hk.expand(&info, &mut okm).unwrap();
assert_eq!(
hex(&okm),
"832390086cda71fb47625bb5ceb168e4c8e26a1a16ed34d9fc7fe92c1481579338da362cb8d9f925d7cb"
);
}
#[test]
fn hkdf_sha512_deterministic_and_domain_separated() {
let ikm = [7u8; 32];
let k1 =
derive_aes256_key(&ikm, &domain_bytes(TAG_KEM_PURE_CNSA2, SEAL_CONTEXT_V1)).unwrap();
let k2 =
derive_aes256_key(&ikm, &domain_bytes(TAG_KEM_PURE_CNSA2, SEAL_CONTEXT_V1)).unwrap();
assert_eq!(k1, k2);
let k3 =
derive_aes256_key(&ikm, &domain_bytes(TAG_KEM_MATCHED_CAT5, SEAL_CONTEXT_V1)).unwrap();
assert_ne!(k1, k3);
let k4 =
derive_aes256_key(&ikm, &domain_bytes(TAG_KEM_PURE_CNSA2, "mosslet/seal/v1")).unwrap();
assert_ne!(k1, k4);
}
#[test]
fn aes256gcm_roundtrip_and_aad_binding() {
let key = [9u8; 32];
let nonce = [3u8; 12];
let aad = b"metamorphic/seal/v1";
let ct = aes256gcm_seal(&key, &nonce, aad, b"hello cnsa").unwrap();
assert_eq!(ct.len(), "hello cnsa".len() + GCM_TAG_LEN);
assert_eq!(
aes256gcm_open(&key, &nonce, aad, &ct).unwrap(),
b"hello cnsa"
);
assert!(aes256gcm_open(&key, &nonce, b"other", &ct).is_err());
let mut bad = ct.clone();
bad[0] ^= 0xFF;
assert!(aes256gcm_open(&key, &nonce, aad, &bad).is_err());
}
#[test]
fn envelope_roundtrip() {
let ikm = [1u8; 64];
let nonce = [5u8; 12];
let ct = envelope_seal(
&ikm,
TAG_KEM_PURE_CNSA2,
SEAL_CONTEXT_V1,
&nonce,
b"key material",
)
.unwrap();
let pt = envelope_open(&ikm, TAG_KEM_PURE_CNSA2, SEAL_CONTEXT_V1, &nonce, &ct).unwrap();
assert_eq!(pt, b"key material");
let other = [2u8; 64];
assert!(envelope_open(&other, TAG_KEM_PURE_CNSA2, SEAL_CONTEXT_V1, &nonce, &ct).is_err());
}
}