use alloy::primitives::keccak256;
use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;
use super::{
error::CryptoError,
hpke::{self, HpkePrivateKey, HpkePublicKey},
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecureEnvelope {
pub enc: String,
pub ciphertext: String,
pub policy_client: String,
pub chain_id: u64,
pub recipient_pubkey: String,
}
impl SecureEnvelope {
pub fn seal(
plaintext: &[u8],
policy_client: &str,
chain_id: u64,
recipient_hpke_pk: &HpkePublicKey,
recipient_ed25519_pubkey: &[u8],
) -> Result<Self, CryptoError> {
let aad = compute_aad(policy_client, chain_id)?;
let (enc_bytes, ct_bytes) = hpke::encrypt(recipient_hpke_pk, plaintext, aad.as_slice())?;
Ok(Self {
enc: hex::encode(&enc_bytes),
ciphertext: hex::encode(&ct_bytes),
policy_client: policy_client.to_string(),
chain_id,
recipient_pubkey: hex::encode(recipient_ed25519_pubkey),
})
}
pub fn open(&self, recipient_hpke_sk: &HpkePrivateKey) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
let aad = compute_aad(&self.policy_client, self.chain_id)?;
let enc_bytes =
hex::decode(&self.enc).map_err(|e| CryptoError::Deserialization(format!("invalid enc hex: {e}")))?;
let ct_bytes = hex::decode(&self.ciphertext)
.map_err(|e| CryptoError::Deserialization(format!("invalid ciphertext hex: {e}")))?;
hpke::decrypt(recipient_hpke_sk, &enc_bytes, &ct_bytes, aad.as_slice())
}
}
pub fn compute_aad(policy_client: &str, chain_id: u64) -> Result<Vec<u8>, CryptoError> {
let client_hex = policy_client.strip_prefix("0x").unwrap_or(policy_client);
let policy_client_bytes =
hex::decode(client_hex).map_err(|e| CryptoError::Serialization(format!("invalid policy_client hex: {e}")))?;
let mut packed = Vec::with_capacity(policy_client_bytes.len() + 8);
packed.extend_from_slice(&policy_client_bytes);
packed.extend_from_slice(&chain_id.to_be_bytes());
let hash = keccak256(&packed);
Ok(hash.to_vec())
}
pub fn derive_hpke_keypair_from_ecdsa(ecdsa_key: &[u8; 32]) -> Result<(HpkePrivateKey, HpkePublicKey), CryptoError> {
let ed25519_sk = super::ed25519::derive_ed25519_from_ecdsa(ecdsa_key)?;
let x25519_sk_bytes = super::ed25519::ed25519_to_x25519_private(&ed25519_sk)?;
let x25519_pk_bytes = super::ed25519::ed25519_to_x25519_public(&ed25519_sk)?;
let hpke_sk = HpkePrivateKey::from_bytes(&x25519_sk_bytes)?;
let hpke_pk = HpkePublicKey::from_bytes(&x25519_pk_bytes)?;
Ok((hpke_sk, hpke_pk))
}
#[cfg(feature = "database")]
pub type DecryptedEnvelope = (uuid::Uuid, Zeroizing<Vec<u8>>);
#[cfg(feature = "database")]
pub fn decrypt_envelopes(
ecdsa_key: &[u8; 32],
refs: &[crate::database::EncryptedDataRefRecord],
) -> Result<Vec<DecryptedEnvelope>, CryptoError> {
let (hpke_sk, _) = derive_hpke_keypair_from_ecdsa(ecdsa_key)?;
let mut results = Vec::with_capacity(refs.len());
for record in refs {
let envelope: SecureEnvelope = serde_json::from_slice(&record.envelope).map_err(|e| {
CryptoError::Deserialization(format!("failed to deserialize envelope for ref {}: {e}", record.id))
})?;
let plaintext = envelope.open(&hpke_sk)?;
results.push((record.id, plaintext));
}
Ok(results)
}
pub fn derive_hpke_keypair_from_raw(raw_key: &[u8; 32]) -> Result<(HpkePrivateKey, HpkePublicKey), CryptoError> {
derive_hpke_keypair_from_ecdsa(raw_key)
}
#[cfg(feature = "database")]
pub fn decrypt_envelopes_with_key(
hpke_sk: &HpkePrivateKey,
refs: &[crate::database::EncryptedDataRefRecord],
) -> Result<serde_json::Value, CryptoError> {
if refs.is_empty() {
return Ok(serde_json::Value::Object(serde_json::Map::new()));
}
let mut merged = serde_json::Map::new();
for (i, r) in refs.iter().enumerate() {
let envelope: SecureEnvelope = serde_json::from_slice(&r.envelope)
.map_err(|e| CryptoError::HpkeDecrypt(format!("ref {i}: failed to deserialize envelope: {e}")))?;
let plaintext = envelope
.open(hpke_sk)
.map_err(|e| CryptoError::HpkeDecrypt(format!("ref {i}: HPKE decryption failed: {e}")))?;
let key = format!("ref_{}", i);
if let Ok(json_val) = serde_json::from_slice::<serde_json::Value>(&plaintext) {
merged.insert(key, json_val);
} else {
let text = String::from_utf8_lossy(&plaintext).to_string();
merged.insert(key, serde_json::Value::String(text));
}
}
Ok(serde_json::Value::Object(merged))
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_POLICY_CLIENT: &str = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const TEST_CHAIN_ID: u64 = 31337;
fn test_keypair() -> (HpkePrivateKey, HpkePublicKey) {
hpke::generate_keypair()
}
#[test]
fn seal_open_roundtrip() {
let (sk, pk) = test_keypair();
let ed25519_pubkey = [0xABu8; 32];
let plaintext = b"confidential policy input data";
let envelope = SecureEnvelope::seal(plaintext, TEST_POLICY_CLIENT, TEST_CHAIN_ID, &pk, &ed25519_pubkey)
.expect("seal failed");
let recovered = envelope.open(&sk).expect("open failed");
assert_eq!(&*recovered, plaintext);
}
#[test]
fn open_with_wrong_key_fails() {
let (_sk, pk) = test_keypair();
let (wrong_sk, _wrong_pk) = test_keypair();
let ed25519_pubkey = [0xBBu8; 32];
let plaintext = b"secret";
let envelope = SecureEnvelope::seal(plaintext, TEST_POLICY_CLIENT, TEST_CHAIN_ID, &pk, &ed25519_pubkey)
.expect("seal failed");
let result = envelope.open(&wrong_sk);
assert!(result.is_err(), "open with wrong key should fail");
}
#[test]
fn tampered_ciphertext_fails() {
let (sk, pk) = test_keypair();
let ed25519_pubkey = [0xCCu8; 32];
let plaintext = b"integrity check";
let mut envelope = SecureEnvelope::seal(plaintext, TEST_POLICY_CLIENT, TEST_CHAIN_ID, &pk, &ed25519_pubkey)
.expect("seal failed");
let mut ct_bytes = hex::decode(&envelope.ciphertext).expect("hex decode");
if let Some(b) = ct_bytes.first_mut() {
*b ^= 0xFF;
}
envelope.ciphertext = hex::encode(&ct_bytes);
let result = envelope.open(&sk);
assert!(result.is_err(), "tampered ciphertext should fail");
}
#[test]
fn tampered_policy_client_fails() {
let (sk, pk) = test_keypair();
let ed25519_pubkey = [0xDDu8; 32];
let plaintext = b"aad binding test";
let mut envelope = SecureEnvelope::seal(plaintext, TEST_POLICY_CLIENT, TEST_CHAIN_ID, &pk, &ed25519_pubkey)
.expect("seal failed");
envelope.policy_client = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string();
let result = envelope.open(&sk);
assert!(result.is_err(), "open with tampered policy_client should fail");
}
#[test]
fn tampered_chain_id_fails() {
let (sk, pk) = test_keypair();
let ed25519_pubkey = [0xEEu8; 32];
let plaintext = b"chain binding test";
let mut envelope = SecureEnvelope::seal(plaintext, TEST_POLICY_CLIENT, TEST_CHAIN_ID, &pk, &ed25519_pubkey)
.expect("seal failed");
envelope.chain_id = 99999;
let result = envelope.open(&sk);
assert!(result.is_err(), "open with tampered chain_id should fail");
}
#[test]
fn serde_json_roundtrip() {
let (_sk, pk) = test_keypair();
let ed25519_pubkey = [0x11u8; 32];
let plaintext = b"json serialization test";
let envelope = SecureEnvelope::seal(plaintext, TEST_POLICY_CLIENT, TEST_CHAIN_ID, &pk, &ed25519_pubkey)
.expect("seal failed");
let json = serde_json::to_string(&envelope).expect("serialize failed");
let deserialized: SecureEnvelope = serde_json::from_str(&json).expect("deserialize failed");
assert_eq!(envelope.enc, deserialized.enc);
assert_eq!(envelope.ciphertext, deserialized.ciphertext);
assert_eq!(envelope.policy_client, deserialized.policy_client);
assert_eq!(envelope.chain_id, deserialized.chain_id);
assert_eq!(envelope.recipient_pubkey, deserialized.recipient_pubkey);
}
}