use crate::escrow::blob::EscrowBlob;
use crate::escrow::metadata::EscrowMetadata;
#[derive(Debug, thiserror::Error)]
pub enum EscrowError {
#[error("encapsulation failure: {0}")]
Encapsulate(String),
#[error("AEAD encryption failure: {0}")]
AeadEncrypt(String),
#[error("decapsulation failure: {0}")]
Decapsulate(String),
#[error("threshold not met: have {have}, need {need}")]
ThresholdNotMet {
have: usize,
need: u32,
},
#[error("invalid blob: {0}")]
InvalidBlob(String),
}
#[derive(Debug, Clone)]
pub struct QuorumPublicKey {
pub quorum_id: String,
pub algorithm: String,
pub bytes: Vec<u8>,
pub custodian_count: u32,
pub threshold: u32,
}
pub trait Encapsulator {
fn encapsulate(&self, recipient: &QuorumPublicKey) -> Result<(Vec<u8>, Vec<u8>), EscrowError>;
}
pub trait Aead {
fn encrypt(
&self,
shared_secret: &[u8],
plaintext: &[u8],
aad: &[u8],
) -> Result<(Vec<u8>, Vec<u8>), EscrowError>;
fn decrypt(
&self,
shared_secret: &[u8],
ciphertext: &[u8],
nonce: &[u8],
aad: &[u8],
) -> Result<Vec<u8>, EscrowError>;
}
pub struct EscrowService;
impl EscrowService {
pub fn new() -> Self {
Self
}
pub fn escrow(
&self,
plaintext_key: &[u8],
recipient: &QuorumPublicKey,
escrowed_by: &str,
key_id: &str,
key_type: &str,
encapsulator: &dyn Encapsulator,
aead: &dyn Aead,
) -> Result<EscrowBlob, EscrowError> {
let metadata = EscrowMetadata::new(
escrowed_by,
key_id,
key_type,
recipient.custodian_count,
recipient.threshold,
);
let aad = metadata_string(&metadata);
let (encapsulated_key, shared_secret) = encapsulator.encapsulate(recipient)?;
let (ciphertext, nonce) = aead.encrypt(&shared_secret, plaintext_key, aad.as_bytes())?;
Ok(EscrowBlob {
recipient_quorum_id: recipient.quorum_id.clone(),
encapsulated_key,
ciphertext,
nonce,
aad: aad.into_bytes(),
metadata,
})
}
pub fn recover(
&self,
blob: &EscrowBlob,
shared_secret: &[u8],
aead: &dyn Aead,
) -> Result<Vec<u8>, EscrowError> {
aead.decrypt(
shared_secret,
&blob.ciphertext,
&blob.nonce,
&blob.aad,
)
}
}
impl Default for EscrowService {
fn default() -> Self {
Self::new()
}
}
fn metadata_string(m: &EscrowMetadata) -> String {
serde_json::to_string(m).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
struct MockEncapsulator;
impl Encapsulator for MockEncapsulator {
fn encapsulate(&self, recipient: &QuorumPublicKey) -> Result<(Vec<u8>, Vec<u8>), EscrowError> {
Ok((recipient.bytes.clone(), vec![0u8; 32]))
}
}
struct MockAead;
impl Aead for MockAead {
fn encrypt(
&self,
shared_secret: &[u8],
plaintext: &[u8],
_aad: &[u8],
) -> Result<(Vec<u8>, Vec<u8>), EscrowError> {
let mut ct = vec![0u8; plaintext.len()];
for (i, b) in plaintext.iter().enumerate() {
ct[i] = b ^ shared_secret[i % shared_secret.len()];
}
Ok((ct, vec![0u8; 12]))
}
fn decrypt(
&self,
shared_secret: &[u8],
ciphertext: &[u8],
_nonce: &[u8],
_aad: &[u8],
) -> Result<Vec<u8>, EscrowError> {
let mut pt = vec![0u8; ciphertext.len()];
for (i, b) in ciphertext.iter().enumerate() {
pt[i] = b ^ shared_secret[i % shared_secret.len()];
}
Ok(pt)
}
}
fn sample_quorum() -> QuorumPublicKey {
QuorumPublicKey {
quorum_id: "test-quorum".into(),
algorithm: "mock-threshold-kem".into(),
bytes: vec![1u8; 32],
custodian_count: 3,
threshold: 2,
}
}
#[test]
fn escrow_then_recover_round_trips() {
let service = EscrowService::new();
let quorum = sample_quorum();
let plaintext = b"this is a very secret key";
let blob = service
.escrow(
plaintext,
&quorum,
"alice",
"key-1",
"Ed25519",
&MockEncapsulator,
&MockAead,
)
.unwrap();
let shared_secret = vec![0u8; 32];
let recovered = service.recover(&blob, &shared_secret, &MockAead).unwrap();
assert_eq!(recovered.as_slice(), plaintext);
assert_eq!(blob.metadata.threshold, 2);
assert_eq!(blob.metadata.custodian_count, 3);
}
#[test]
fn blob_has_fingerprint() {
let service = EscrowService::new();
let blob = service
.escrow(
b"test",
&sample_quorum(),
"alice",
"key-1",
"Ed25519",
&MockEncapsulator,
&MockAead,
)
.unwrap();
let fp = blob.fingerprint();
assert_eq!(fp.len(), 32);
}
}