use std::path::Path;
use aes::Aes256;
use aes::cipher::{KeyIvInit, StreamCipher};
use blake3;
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce, aead::Aead};
use ctr::Ctr128BE;
use hmac::{Hmac, Mac};
use secp256k1::{Secp256k1, ecdh::SharedSecret};
use sha2::Sha512;
use crate::error::CarbonadoError;
#[cfg(feature = "pqc")]
pub use bitcoinpqc::{
self, Algorithm, KeyPair, PqcError as BitcoinPqcError, PublicKey, SecretKey, Signature,
};
pub use secp256k1::{PublicKey as SecpPublicKey, SecretKey as SecpSecretKey};
const LABEL_PREFIX: &[u8] = b"carbonado-v2/";
pub const SLH1_MAGIC: &[u8; 4] = b"SLH1";
pub const SLH1_SIGNATURE_LEN: usize = 7856;
pub const SLH1_SIDECAR_LEN: usize = 4 + SLH1_SIGNATURE_LEN;
type HmacSha512 = Hmac<Sha512>;
#[inline]
pub(crate) fn ct_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut result = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
result |= x ^ y;
}
result == 0
}
pub fn derive_subkey(master: &[u8], label: &str) -> Result<[u8; 64], CarbonadoError> {
if master.is_empty() {
return Err(CarbonadoError::InvalidKeyLength);
}
let mut mac =
HmacSha512::new_from_slice(master).map_err(|_| CarbonadoError::InvalidKeyLength)?;
mac.update(LABEL_PREFIX);
mac.update(label.as_bytes());
let result = mac.finalize().into_bytes();
let mut out = [0u8; 64];
out.copy_from_slice(&result[..64]);
Ok(out)
}
pub fn carbonado_verification_key(format: u8) -> [u8; 32] {
blake3::derive_key("carbonado-v2/verification", &[format])
}
#[deprecated(since = "0.7.0", note = "renamed to carbonado_verification_key")]
pub fn carbonado_bao_key(format: u8) -> [u8; 32] {
carbonado_verification_key(format)
}
pub fn symmetric_encrypt_with_nonce(
master_key: &[u8],
nonce: [u8; 16],
input: &[u8],
) -> Result<Vec<u8>, CarbonadoError> {
if master_key.len() < 32 {
return Err(CarbonadoError::InvalidKeyLength);
}
let enc_material = derive_subkey(master_key, "aes-ctr")?;
let mac_key = derive_subkey(master_key, "etm-hmac")?;
let aes_key: [u8; 32] = enc_material[..32].try_into().map_err(|_| {
CarbonadoError::InternalStateError("derive_subkey must yield 64 bytes".to_string())
})?;
let mut cipher = Ctr128BE::<Aes256>::new(&aes_key.into(), &nonce.into());
let mut ct = input.to_vec();
cipher.apply_keystream(&mut ct);
let mut mac =
HmacSha512::new_from_slice(&mac_key).map_err(|_| CarbonadoError::InvalidKeyLength)?;
mac.update(b"carbonado-v2-etm");
mac.update(&nonce);
mac.update(&ct);
let tag = mac.finalize().into_bytes();
let mut out = Vec::with_capacity(64 + ct.len());
out.extend_from_slice(&tag);
out.extend_from_slice(&ct);
Ok(out)
}
pub fn symmetric_encrypt(master_key: &[u8], input: &[u8]) -> Result<Vec<u8>, CarbonadoError> {
let mut nonce = [0u8; 16];
getrandom::getrandom(&mut nonce).map_err(|_| CarbonadoError::RandomnessError)?;
let inner = symmetric_encrypt_with_nonce(master_key, nonce, input)?;
let mut out = Vec::with_capacity(16 + inner.len());
out.extend_from_slice(&nonce);
out.extend_from_slice(&inner);
Ok(out)
}
pub fn symmetric_decrypt_with_nonce(
master_key: &[u8],
nonce: [u8; 16],
input: &[u8], ) -> Result<Vec<u8>, CarbonadoError> {
if input.len() < 64 {
return Err(CarbonadoError::InvalidCiphertextLength);
}
if master_key.len() < 32 {
return Err(CarbonadoError::InvalidKeyLength);
}
let tag = &input[0..64];
let ct = &input[64..];
let enc_material = derive_subkey(master_key, "aes-ctr")?;
let mac_key = derive_subkey(master_key, "etm-hmac")?;
let aes_key: [u8; 32] = enc_material[..32].try_into().map_err(|_| {
CarbonadoError::InternalStateError("derive_subkey must yield 64 bytes".to_string())
})?;
let mut mac =
HmacSha512::new_from_slice(&mac_key).map_err(|_| CarbonadoError::InvalidKeyLength)?;
mac.update(b"carbonado-v2-etm");
mac.update(&nonce);
mac.update(ct);
mac.verify_slice(tag)
.map_err(|_| CarbonadoError::AuthenticationFailed)?;
let mut cipher = Ctr128BE::<Aes256>::new(&aes_key.into(), &nonce.into());
let mut pt = ct.to_vec();
cipher.apply_keystream(&mut pt);
Ok(pt)
}
pub fn symmetric_decrypt(master_key: &[u8], input: &[u8]) -> Result<Vec<u8>, CarbonadoError> {
if input.len() < 80 {
return Err(CarbonadoError::InvalidCiphertextLength);
}
let nonce: [u8; 16] = input[0..16]
.try_into()
.map_err(|_| CarbonadoError::InvalidCiphertextLength)?;
let rest = &input[16..];
symmetric_decrypt_with_nonce(master_key, nonce, rest)
}
#[cfg(feature = "pqc")]
pub fn slh_dsa_generate_keypair(entropy: &[u8]) -> Result<KeyPair, CarbonadoError> {
if entropy.len() < 128 {
return Err(CarbonadoError::InvalidKeyLength);
}
bitcoinpqc::generate_keypair(Algorithm::SLH_DSA_SHA2_128S, entropy)
.map_err(|e| CarbonadoError::PqcError(e.to_string()))
}
#[cfg(feature = "pqc")]
pub fn slh_dsa_sign(secret_key: &SecretKey, message: &[u8]) -> Result<Signature, CarbonadoError> {
if secret_key.algorithm != Algorithm::SLH_DSA_SHA2_128S {
return Err(CarbonadoError::PqcError(
"wrong algorithm for SLH-DSA".into(),
));
}
bitcoinpqc::sign(secret_key, message).map_err(|e| CarbonadoError::PqcError(e.to_string()))
}
#[cfg(feature = "pqc")]
pub fn slh_dsa_verify(
public_key: &PublicKey,
message: &[u8],
signature: &Signature,
) -> Result<bool, CarbonadoError> {
if public_key.algorithm != Algorithm::SLH_DSA_SHA2_128S {
return Err(CarbonadoError::PqcError(
"wrong algorithm for SLH-DSA".into(),
));
}
match bitcoinpqc::verify(public_key, message, signature) {
Ok(()) => Ok(true),
Err(bitcoinpqc::PqcError::BadSignature) => Ok(false),
Err(e) => Err(CarbonadoError::PqcError(e.to_string())),
}
}
pub fn ecc_aead_encrypt(
recipient_pub: &SecpPublicKey,
input: &[u8],
) -> Result<Vec<u8>, CarbonadoError> {
let mut secret_bytes = [0u8; 32];
getrandom::getrandom(&mut secret_bytes).map_err(|_| CarbonadoError::RandomnessError)?;
let eph_secret =
SecpSecretKey::from_slice(&secret_bytes).map_err(|_| CarbonadoError::InvalidKeyLength)?;
let eph_pub = SecpPublicKey::from_secret_key(&Secp256k1::new(), &eph_secret);
let shared = SharedSecret::new(recipient_pub, &eph_secret);
let key_material = derive_subkey(shared.secret_bytes().as_ref(), "ecc-chacha-poly")?;
let chacha_key: [u8; 32] = key_material[..32].try_into().map_err(|_| {
CarbonadoError::InternalStateError("derive_subkey must yield 64 bytes".to_string())
})?;
let cipher =
<ChaCha20Poly1305 as chacha20poly1305::aead::KeyInit>::new(Key::from_slice(&chacha_key));
let mut nonce_bytes = [0u8; 12];
getrandom::getrandom(&mut nonce_bytes).map_err(|_| CarbonadoError::RandomnessError)?;
let nonce = Nonce::from_slice(&nonce_bytes);
let ct = cipher
.encrypt(nonce, input)
.map_err(|_| CarbonadoError::AuthenticationFailed)?;
let mut out = Vec::with_capacity(33 + 12 + ct.len());
out.extend_from_slice(&eph_pub.serialize()); out.extend_from_slice(&nonce_bytes);
out.extend_from_slice(&ct);
Ok(out)
}
pub fn ecc_aead_decrypt(
recipient_secret: &SecpSecretKey,
blob: &[u8],
) -> Result<Vec<u8>, CarbonadoError> {
if blob.len() < 33 + 12 {
return Err(CarbonadoError::InvalidCiphertextLength);
}
let eph_pub = SecpPublicKey::from_slice(&blob[0..33])
.map_err(|_| CarbonadoError::InvalidCiphertextLength)?;
let nonce_bytes = &blob[33..45];
let ct = &blob[45..];
let shared = SharedSecret::new(&eph_pub, recipient_secret);
let key_material = derive_subkey(shared.secret_bytes().as_ref(), "ecc-chacha-poly")?;
let chacha_key: [u8; 32] = key_material[..32].try_into().map_err(|_| {
CarbonadoError::InternalStateError("derive_subkey must yield 64 bytes".to_string())
})?;
let cipher =
<ChaCha20Poly1305 as chacha20poly1305::aead::KeyInit>::new(Key::from_slice(&chacha_key));
let nonce = Nonce::from_slice(nonce_bytes);
let pt = cipher
.decrypt(nonce, ct)
.map_err(|_| CarbonadoError::AuthenticationFailed)?;
Ok(pt)
}
pub fn hybrid_encrypt_with_nonce(
master_key: &[u8],
nonce: [u8; 16],
recipient_pub: &SecpPublicKey,
input: &[u8],
) -> Result<Vec<u8>, CarbonadoError> {
let inner = ecc_aead_encrypt(recipient_pub, input)?;
symmetric_encrypt_with_nonce(master_key, nonce, &inner)
}
pub fn hybrid_decrypt_with_nonce(
master_key: &[u8],
nonce: [u8; 16],
recipient_secret: &SecpSecretKey,
input: &[u8], ) -> Result<Vec<u8>, CarbonadoError> {
let outer_pt = symmetric_decrypt_with_nonce(master_key, nonce, input)?;
ecc_aead_decrypt(recipient_secret, &outer_pt)
}
pub fn hybrid_encrypt(
master_key: &[u8],
recipient_pub: &SecpPublicKey,
input: &[u8],
) -> Result<Vec<u8>, CarbonadoError> {
let mut nonce = [0u8; 16];
getrandom::getrandom(&mut nonce).map_err(|_| CarbonadoError::RandomnessError)?;
let inner = hybrid_encrypt_with_nonce(master_key, nonce, recipient_pub, input)?;
let mut out = Vec::with_capacity(16 + inner.len());
out.extend_from_slice(&nonce);
out.extend_from_slice(&inner);
Ok(out)
}
pub fn hybrid_decrypt(
master_key: &[u8],
recipient_secret: &SecpSecretKey,
input: &[u8], ) -> Result<Vec<u8>, CarbonadoError> {
if input.len() < 80 {
return Err(CarbonadoError::InvalidCiphertextLength);
}
let nonce: [u8; 16] = input[0..16]
.try_into()
.map_err(|_| CarbonadoError::InvalidCiphertextLength)?;
let rest = &input[16..];
hybrid_decrypt_with_nonce(master_key, nonce, recipient_secret, rest)
}
#[cfg(feature = "pqc")]
pub fn slh_sign(seed: &[u8], message: &[u8]) -> Result<Vec<u8>, CarbonadoError> {
if seed.len() < 32 {
return Err(CarbonadoError::InvalidKeyLength);
}
let stretched = derive_subkey(seed, "slh-dsa-seed")?;
let stretched2 = derive_subkey(seed, "slh-dsa-seed-2")?;
let mut entropy = [0u8; 128];
entropy[..64].copy_from_slice(&stretched);
entropy[64..].copy_from_slice(&stretched2);
let keypair = slh_dsa_generate_keypair(&entropy)?;
let sig = slh_dsa_sign(&keypair.secret_key, message)?;
Ok(sig.bytes)
}
#[cfg(feature = "pqc")]
pub fn slh_verify(pk: &[u8], message: &[u8], sig: &[u8]) -> Result<bool, CarbonadoError> {
if pk.len() != 32 {
return Err(CarbonadoError::InvalidKeyLength);
}
let public_key = PublicKey {
algorithm: Algorithm::SLH_DSA_SHA2_128S,
bytes: pk.to_vec(),
};
let signature = Signature {
algorithm: Algorithm::SLH_DSA_SHA2_128S,
bytes: sig.to_vec(),
};
slh_dsa_verify(&public_key, message, &signature)
}
pub fn compute_header_mac(
master_key: &[u8],
header_data: &[u8],
) -> Result<[u8; 64], CarbonadoError> {
if master_key.len() < 32 {
return Err(CarbonadoError::InvalidKeyLength);
}
let header_key = derive_subkey(master_key, "header-auth")?;
let mut mac =
HmacSha512::new_from_slice(&header_key).map_err(|_| CarbonadoError::InvalidKeyLength)?;
mac.update(header_data);
let result = mac.finalize().into_bytes();
let mut out = [0u8; 64];
out.copy_from_slice(&result);
Ok(out)
}
pub fn write_slh_sidecar(path: impl AsRef<Path>, signature: &[u8]) -> Result<(), CarbonadoError> {
if signature.len() != SLH1_SIGNATURE_LEN {
return Err(CarbonadoError::OutboardVerificationFailed(format!(
"SLH-DSA signature must be {SLH1_SIGNATURE_LEN} bytes, got {}",
signature.len()
)));
}
let mut sidecar = Vec::with_capacity(SLH1_SIDECAR_LEN);
sidecar.extend_from_slice(SLH1_MAGIC);
sidecar.extend_from_slice(signature);
std::fs::write(path, &sidecar).map_err(CarbonadoError::StdIoError)
}
pub fn read_slh_sidecar(path: impl AsRef<Path>) -> Result<Vec<u8>, CarbonadoError> {
let bytes = std::fs::read(path).map_err(CarbonadoError::StdIoError)?;
if bytes.len() != SLH1_SIDECAR_LEN {
return Err(CarbonadoError::OutboardVerificationFailed(format!(
"SLH-DSA sidecar must be {SLH1_SIDECAR_LEN} bytes, got {}",
bytes.len()
)));
}
if &bytes[..4] != SLH1_MAGIC {
return Err(CarbonadoError::InvalidMagicNumber(
String::from_utf8_lossy(&bytes[..4.min(bytes.len())]).into_owned(),
));
}
Ok(bytes[4..].to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
fn random_key(len: usize) -> Vec<u8> {
let mut k = vec![0u8; len];
getrandom::getrandom(&mut k).unwrap();
k
}
#[test]
fn encrypt_decrypt_roundtrip() {
let key = random_key(32);
let plaintext = b"Hello, this is a test of the new symmetric crypto stack in Carbonado.";
let ciphertext = symmetric_encrypt(&key, plaintext).unwrap();
let decrypted = symmetric_decrypt(&key, &ciphertext).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn encrypt_decrypt_empty_plaintext() {
let key = random_key(32);
let plaintext = b"";
let ciphertext = symmetric_encrypt(&key, plaintext).unwrap();
let decrypted = symmetric_decrypt(&key, &ciphertext).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn wrong_key_fails_auth() {
let key1 = random_key(32);
let key2 = random_key(32);
let plaintext = b"secret data";
let ciphertext = symmetric_encrypt(&key1, plaintext).unwrap();
let result = symmetric_decrypt(&key2, &ciphertext);
assert!(matches!(result, Err(CarbonadoError::AuthenticationFailed)));
}
#[test]
fn tampered_tag_fails_auth() {
let key = random_key(32);
let plaintext = b"important message";
let mut ciphertext = symmetric_encrypt(&key, plaintext).unwrap();
ciphertext[20] ^= 0xFF;
let result = symmetric_decrypt(&key, &ciphertext);
assert!(matches!(result, Err(CarbonadoError::AuthenticationFailed)));
}
#[test]
fn tampered_ciphertext_fails_auth() {
let key = random_key(32);
let plaintext = b"another test payload that is long enough";
let mut ciphertext = symmetric_encrypt(&key, plaintext).unwrap();
let ct_start = 16 + 64;
if ciphertext.len() > ct_start {
ciphertext[ct_start] ^= 0xFF;
}
let result = symmetric_decrypt(&key, &ciphertext);
assert!(matches!(result, Err(CarbonadoError::AuthenticationFailed)));
}
#[test]
fn derive_subkey_different_labels_produce_different_keys() {
let master = random_key(32);
let k1 = derive_subkey(&master, "aes-ctr").unwrap();
let k2 = derive_subkey(&master, "etm-hmac").unwrap();
assert_ne!(k1, k2);
}
#[test]
fn derive_subkey_empty_master_fails() {
let result = derive_subkey(&[], "test-label");
assert!(matches!(result, Err(CarbonadoError::InvalidKeyLength)));
}
#[test]
fn encrypt_rejects_short_master_key() {
let short_key = random_key(16);
let result = symmetric_encrypt(&short_key, b"data");
assert!(matches!(result, Err(CarbonadoError::InvalidKeyLength)));
}
#[test]
fn decrypt_rejects_short_input() {
let key = random_key(32);
let short_input = vec![0u8; 50]; let result = symmetric_decrypt(&key, &short_input);
assert!(matches!(
result,
Err(CarbonadoError::InvalidCiphertextLength)
));
}
#[test]
fn symmetric_hot_paths_derive_subkeys_without_panic_and_produce_specific_errors_on_bad() {
let master = random_key(32);
let ct = symmetric_encrypt_with_nonce(&master, [0u8; 16], b"test data for hot path")
.expect("encrypt ok for valid");
let pt = symmetric_decrypt_with_nonce(&master, [0u8; 16], &ct).expect("decrypt ok");
assert_eq!(pt, b"test data for hot path");
let short = random_key(16);
let res = symmetric_encrypt(&short, b"data");
assert!(matches!(res, Err(CarbonadoError::InvalidKeyLength)));
}
#[test]
fn header_mac_and_public_paths_reject_short_master_keys() {
let short = random_key(16); let payload_nonce = [0u8; 16];
let hash = [0u8; 32];
let fmt = crate::constants::Format::from(0u8); let res =
crate::file::Header::new(&short, payload_nonce, &hash, [0u8; 32], fmt, 0, 0, 0, None);
assert!(
matches!(res, Err(CarbonadoError::InvalidKeyLength)),
"Header::new (public header_mac path) with short master must err InvalidKeyLength, got {:?}",
res
);
let auth_dummy = b"dummy";
let res2 = compute_header_mac(&short, auth_dummy);
assert!(
matches!(res2, Err(CarbonadoError::InvalidKeyLength)),
"compute_header_mac short must specific error"
);
}
#[cfg(feature = "pqc")]
#[test]
fn slh_dsa_generate_and_sign_verify_roundtrip() {
let mut entropy = [0u8; 128];
getrandom::getrandom(&mut entropy).unwrap();
let keypair =
slh_dsa_generate_keypair(&entropy).expect("keygen should succeed with 128 bytes");
assert_eq!(keypair.public_key.bytes.len(), 32);
assert_eq!(keypair.secret_key.bytes.len(), 64);
assert_eq!(keypair.public_key.algorithm, Algorithm::SLH_DSA_SHA2_128S);
let message = b"important manifest or checkpoint hash goes here";
let sig = slh_dsa_sign(&keypair.secret_key, message).expect("signing must succeed");
assert_eq!(sig.bytes.len(), 7856); assert_eq!(sig.algorithm, Algorithm::SLH_DSA_SHA2_128S);
let valid = slh_dsa_verify(&keypair.public_key, message, &sig)
.expect("verify call should not error");
assert!(valid, "fresh signature must verify");
let mut bad_msg = message.to_vec();
bad_msg[0] ^= 0x01;
let still_valid = slh_dsa_verify(&keypair.public_key, &bad_msg, &sig)
.expect("verify should succeed or return false");
assert!(!still_valid, "tampered message must fail verification");
}
#[cfg(feature = "pqc")]
#[test]
fn slh_sign_convenience_produces_verifiable_signature() {
let seed = random_key(32);
let message = b"sidecar over bao root hash";
let _sig_bytes = slh_sign(&seed, message).expect("slh_sign convenience must work");
let mut entropy = [0u8; 128];
getrandom::getrandom(&mut entropy).unwrap();
let kp = slh_dsa_generate_keypair(&entropy).unwrap();
let sig2 = slh_dsa_sign(&kp.secret_key, message).unwrap();
let ok = slh_dsa_verify(&kp.public_key, message, &sig2).unwrap();
assert!(ok);
}
#[cfg(feature = "pqc")]
#[test]
fn slh_dsa_rejects_short_entropy() {
let short_entropy = [0u8; 64];
let result = slh_dsa_generate_keypair(&short_entropy);
assert!(matches!(result, Err(CarbonadoError::InvalidKeyLength)));
}
#[test]
fn large_payload_roundtrip_1mb() {
let key = random_key(32);
let mut plaintext = vec![0u8; 1024 * 1024]; for (i, byte) in plaintext.iter_mut().enumerate() {
*byte = (i % 251) as u8;
}
let ciphertext = symmetric_encrypt(&key, &plaintext).unwrap();
let decrypted = symmetric_decrypt(&key, &ciphertext).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn internal_nonce_format_nonce_is_protected() {
let key = random_key(32);
let plaintext = b"data that should not decrypt if nonce is flipped";
let mut ciphertext = symmetric_encrypt(&key, plaintext).unwrap();
ciphertext[5] ^= 0xFF;
let result = symmetric_decrypt(&key, &ciphertext);
assert!(matches!(result, Err(CarbonadoError::AuthenticationFailed)));
}
#[test]
fn explicit_nonce_and_header_mac_path() {
let master = random_key(32);
let mut nonce = [0u8; 16];
getrandom::getrandom(&mut nonce).unwrap();
let data = b"payload that goes through the explicit nonce + header auth path";
let ct = symmetric_encrypt_with_nonce(&master, nonce, data).unwrap();
let mut auth_data = Vec::new();
auth_data.extend_from_slice(crate::constants::MAGICNO);
auth_data.extend_from_slice(&nonce);
auth_data.extend_from_slice(&[0u8; 32]); auth_data.extend_from_slice(&[0u8; 32]); auth_data.push(0x0F); auth_data.extend_from_slice(&0u32.to_le_bytes()); auth_data.extend_from_slice(&0u32.to_le_bytes()); auth_data.extend_from_slice(&0u32.to_le_bytes()); auth_data.extend_from_slice(&[0u8; 8]);
let header_mac = compute_header_mac(&master, &auth_data).unwrap();
let pt = symmetric_decrypt_with_nonce(&master, nonce, &ct).unwrap();
assert_eq!(pt, data);
let wrong_master = random_key(32);
let bad_mac = compute_header_mac(&wrong_master, &auth_data).unwrap();
assert_ne!(bad_mac, header_mac);
}
#[test]
fn same_key_different_nonces_produce_different_ciphertexts() {
let key = random_key(32);
let plaintext = b"identical plaintext under two different nonces";
let ct1 = symmetric_encrypt(&key, plaintext).unwrap();
let ct2 = symmetric_encrypt(&key, plaintext).unwrap();
assert_ne!(ct1, ct2);
}
#[test]
fn truncated_ciphertext_is_rejected() {
let key = random_key(32);
let plaintext = b"some data that will be truncated after encryption";
let mut ct = symmetric_encrypt(&key, plaintext).unwrap();
ct.truncate(40);
let result = symmetric_decrypt(&key, &ct);
assert!(matches!(
result,
Err(CarbonadoError::InvalidCiphertextLength)
));
}
use proptest::prelude::*;
proptest! {
#[test]
fn prop_encrypt_decrypt_roundtrip(
key in prop::collection::vec(any::<u8>(), 32..64),
data in prop::collection::vec(any::<u8>(), 0..4096)
) {
let ct = symmetric_encrypt(&key, &data).unwrap();
let pt = symmetric_decrypt(&key, &ct).unwrap();
prop_assert_eq!(pt, data);
}
#[test]
fn prop_tampered_data_fails_auth(
key in prop::collection::vec(any::<u8>(), 32..64),
data in prop::collection::vec(any::<u8>(), 1..2048),
tamper_pos in 0..2048usize
) {
let mut ct = symmetric_encrypt(&key, &data).unwrap();
if tamper_pos < ct.len() {
ct[tamper_pos] ^= 0xFF;
let result = symmetric_decrypt(&key, &ct);
prop_assert!(matches!(result, Err(CarbonadoError::AuthenticationFailed)));
}
}
}
fn random_secp_keypair() -> (SecpSecretKey, SecpPublicKey) {
let mut secret_bytes = [0u8; 32];
getrandom::getrandom(&mut secret_bytes).unwrap();
let secret = SecpSecretKey::from_slice(&secret_bytes).unwrap();
let public = SecpPublicKey::from_secret_key(&Secp256k1::new(), &secret);
(secret, public)
}
#[test]
fn hybrid_roundtrip_basic() {
let master = random_key(32);
let (recipient_secret, recipient_pub) = random_secp_keypair();
let plaintext = b"hybrid test: inner chacha+secp wrapped by outer aes+hmac";
let ct = hybrid_encrypt(&master, &recipient_pub, plaintext).unwrap();
let pt = hybrid_decrypt(&master, &recipient_secret, &ct).unwrap();
assert_eq!(pt, plaintext);
}
#[test]
fn hybrid_roundtrip_empty() {
let master = random_key(32);
let (recipient_secret, recipient_pub) = random_secp_keypair();
let plaintext = b"";
let ct = hybrid_encrypt(&master, &recipient_pub, plaintext).unwrap();
let pt = hybrid_decrypt(&master, &recipient_secret, &ct).unwrap();
assert_eq!(pt, plaintext);
}
#[test]
fn hybrid_wrong_master_fails_outer() {
let master1 = random_key(32);
let master2 = random_key(32);
let (good_secret, pubk) = random_secp_keypair();
let plaintext = b"secret via hybrid";
let ct = hybrid_encrypt(&master1, &pubk, plaintext).unwrap();
let result = hybrid_decrypt(&master2, &good_secret, &ct);
assert!(matches!(result, Err(CarbonadoError::AuthenticationFailed)));
}
#[test]
fn hybrid_wrong_recipient_secret_fails_inner() {
let master = random_key(32);
let (_secret_good, pubk) = random_secp_keypair();
let (secret_bad, _pub_bad) = random_secp_keypair();
let plaintext = b"only correct recipient priv can open inner";
let ct = hybrid_encrypt(&master, &pubk, plaintext).unwrap();
let result = hybrid_decrypt(&master, &secret_bad, &ct);
assert!(matches!(result, Err(CarbonadoError::AuthenticationFailed)));
}
#[test]
fn hybrid_tamper_outer_tag_fails() {
let master = random_key(32);
let (_s, pubk) = random_secp_keypair();
let plaintext = b"tamper the outer HMAC tag";
let mut ct = hybrid_encrypt(&master, &pubk, plaintext).unwrap();
if ct.len() > 16 + 10 {
ct[16 + 5] ^= 0xFF;
}
let result = hybrid_decrypt(&master, &_s, &ct);
assert!(matches!(result, Err(CarbonadoError::AuthenticationFailed)));
}
#[test]
fn hybrid_tamper_inner_blob_fails_inner_after_outer() {
let master = random_key(32);
let (secret, pubk) = random_secp_keypair();
let plaintext = b"tamper inside the ecc aead portion";
let mut ct = hybrid_encrypt(&master, &pubk, plaintext).unwrap();
let tamper_start = 16 + 64 + 5;
if ct.len() > tamper_start {
ct[tamper_start] ^= 0xFF;
}
let result = hybrid_decrypt(&master, &secret, &ct);
assert!(matches!(result, Err(CarbonadoError::AuthenticationFailed)));
}
#[test]
fn hybrid_with_nonce_roundtrip() {
let master = random_key(32);
let (secret, pubk) = random_secp_keypair();
let plaintext = b"explicit nonce hybrid path";
let mut nonce = [0u8; 16];
getrandom::getrandom(&mut nonce).unwrap();
let ct = hybrid_encrypt_with_nonce(&master, nonce, &pubk, plaintext).unwrap();
let pt = hybrid_decrypt_with_nonce(&master, nonce, &secret, &ct).unwrap();
assert_eq!(pt, plaintext);
}
#[test]
fn ecc_aead_standalone_roundtrip() {
let (secret, pubk) = random_secp_keypair();
let data = b"direct ecc+chacha test data, not wrapped";
let blob = ecc_aead_encrypt(&pubk, data).unwrap();
let recovered = ecc_aead_decrypt(&secret, &blob).unwrap();
assert_eq!(recovered, data);
}
#[test]
fn hybrid_large_payload() {
let master = random_key(32);
let (secret, pubk) = random_secp_keypair();
let mut data = vec![0u8; 64 * 1024]; for (i, b) in data.iter_mut().enumerate() {
*b = (i % 251) as u8;
}
let ct = hybrid_encrypt(&master, &pubk, &data).unwrap();
let pt = hybrid_decrypt(&master, &secret, &ct).unwrap();
assert_eq!(pt, data);
}
#[test]
fn carbonado_verification_key_matches_blake3_derive_key() {
for format in [0u8, 4, 14, 15] {
let expected = blake3::derive_key("carbonado-v2/verification", &[format]);
assert_eq!(carbonado_verification_key(format), expected);
}
}
#[test]
fn header_mac_uses_auth_data_only_no_extra_domain_prefix() {
let master = random_key(32);
let mut auth_data = Vec::new();
auth_data.extend_from_slice(crate::constants::MAGICNO);
auth_data.extend_from_slice(&[0xAAu8; 16]); auth_data.extend_from_slice(&[0xBBu8; 32]); auth_data.extend_from_slice(&[0u8; 32]); auth_data.push(0x0E);
auth_data.extend_from_slice(&0u32.to_le_bytes());
auth_data.extend_from_slice(&1024u32.to_le_bytes());
auth_data.extend_from_slice(&0u32.to_le_bytes());
auth_data.extend_from_slice(&[0u8; 8]);
let mac = compute_header_mac(&master, &auth_data).unwrap();
let header_key = derive_subkey(&master, "header-auth").unwrap();
let mut expected =
HmacSha512::new_from_slice(&header_key).expect("header-auth subkey length");
expected.update(&auth_data);
let expected_bytes = expected.finalize().into_bytes();
assert_eq!(mac.as_slice(), expected_bytes.as_slice());
}
#[test]
fn slh_sidecar_roundtrip_and_validation() {
let dir = std::env::temp_dir().join(format!("carbonado-slh1-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("test.slh");
let sig = vec![0xCDu8; SLH1_SIGNATURE_LEN];
write_slh_sidecar(&path, &sig).unwrap();
let read = read_slh_sidecar(&path).unwrap();
assert_eq!(read, sig);
let on_disk = std::fs::read(&path).unwrap();
assert_eq!(on_disk.len(), SLH1_SIDECAR_LEN);
assert_eq!(&on_disk[..4], SLH1_MAGIC);
let bad_magic_path = dir.join("bad-magic.slh");
std::fs::write(&bad_magic_path, vec![0u8; SLH1_SIDECAR_LEN]).unwrap();
let err = read_slh_sidecar(&bad_magic_path).unwrap_err();
assert!(matches!(err, CarbonadoError::InvalidMagicNumber(_)));
let short_path = dir.join("short.slh");
std::fs::write(&short_path, b"SLH1").unwrap();
let err2 = write_slh_sidecar(&short_path, b"short").unwrap_err();
assert!(matches!(
err2,
CarbonadoError::OutboardVerificationFailed(_)
));
let _ = std::fs::remove_dir_all(&dir);
}
}