1pub use claw_core::types::RECIPIENT_ENVELOPE_ALGORITHM;
3
4use claw_core::types::{Capsule, CapsuleRecipient};
5use rand::RngCore;
6use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret};
7
8use crate::encrypt;
9use crate::CryptoError;
10
11#[derive(Debug, Clone)]
13pub struct RecipientPublicKey {
14 pub recipient_id: String,
16 pub key_id: String,
18 pub public_key: [u8; 32],
20}
21
22pub fn recipient_public_key(secret_key: &[u8; 32]) -> [u8; 32] {
24 let secret = StaticSecret::from(*secret_key);
25 PublicKey::from(&secret).to_bytes()
26}
27
28pub fn random_content_key() -> [u8; 32] {
30 let mut key = [0u8; 32];
31 rand::thread_rng().fill_bytes(&mut key);
32 key
33}
34
35pub fn wrap_content_key_for_recipients(
37 content_key: &[u8; 32],
38 recipients: &[RecipientPublicKey],
39) -> Result<Vec<CapsuleRecipient>, CryptoError> {
40 recipients
41 .iter()
42 .map(|recipient| {
43 let ephemeral = EphemeralSecret::random_from_rng(rand::thread_rng());
44 let ephemeral_public = PublicKey::from(&ephemeral);
45 let recipient_public = PublicKey::from(recipient.public_key);
46 let shared = ephemeral.diffie_hellman(&recipient_public);
47 let wrapping_key = derive_wrapping_key(shared.as_bytes());
48 let encrypted_content_key = encrypt::encrypt(&wrapping_key, content_key)?;
49
50 Ok(CapsuleRecipient {
51 recipient_id: recipient.recipient_id.clone(),
52 key_id: recipient.key_id.clone(),
53 algorithm: RECIPIENT_ENVELOPE_ALGORITHM.to_string(),
54 ephemeral_public_key: ephemeral_public.to_bytes().to_vec(),
55 encrypted_content_key,
56 })
57 })
58 .collect()
59}
60
61pub fn unwrap_content_key(
63 recipient_secret_key: &[u8; 32],
64 envelope: &CapsuleRecipient,
65) -> Result<[u8; 32], CryptoError> {
66 if envelope.algorithm != RECIPIENT_ENVELOPE_ALGORITHM {
67 return Err(CryptoError::DecryptionFailed(format!(
68 "unsupported recipient envelope algorithm: {}",
69 envelope.algorithm
70 )));
71 }
72 let ephemeral_public: [u8; 32] = envelope
73 .ephemeral_public_key
74 .as_slice()
75 .try_into()
76 .map_err(|_| CryptoError::DecryptionFailed("invalid ephemeral public key".into()))?;
77 let secret = StaticSecret::from(*recipient_secret_key);
78 let shared = secret.diffie_hellman(&PublicKey::from(ephemeral_public));
79 let wrapping_key = derive_wrapping_key(shared.as_bytes());
80 let content_key = encrypt::decrypt(&wrapping_key, &envelope.encrypted_content_key)?;
81 content_key
82 .as_slice()
83 .try_into()
84 .map_err(|_| CryptoError::DecryptionFailed("invalid content key length".into()))
85}
86
87pub fn decrypt_capsule_private_for_recipient(
89 capsule: &Capsule,
90 recipient_id: &str,
91 recipient_secret_key: &[u8; 32],
92) -> Result<Vec<u8>, CryptoError> {
93 let encrypted_private = capsule.encrypted_private.as_deref().ok_or_else(|| {
94 CryptoError::DecryptionFailed("capsule has no encrypted private fields".into())
95 })?;
96 let envelope = capsule
97 .recipients
98 .iter()
99 .find(|recipient| recipient.recipient_id.eq_ignore_ascii_case(recipient_id))
100 .ok_or_else(|| CryptoError::DecryptionFailed("recipient envelope not found".into()))?;
101 let content_key = unwrap_content_key(recipient_secret_key, envelope)?;
102 encrypt::decrypt(&content_key, encrypted_private)
103}
104
105fn derive_wrapping_key(shared_secret: &[u8; 32]) -> [u8; 32] {
106 blake3::derive_key(
107 "claw-vcs recipient envelope content key wrap v1",
108 shared_secret,
109 )
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn wraps_and_unwraps_content_key() {
118 let recipient_secret = [9u8; 32];
119 let recipient_public = recipient_public_key(&recipient_secret);
120 let content_key = [42u8; 32];
121
122 let envelopes = wrap_content_key_for_recipients(
123 &content_key,
124 &[RecipientPublicKey {
125 recipient_id: "security".to_string(),
126 key_id: "security-key".to_string(),
127 public_key: recipient_public,
128 }],
129 )
130 .unwrap();
131
132 let unwrapped = unwrap_content_key(&recipient_secret, &envelopes[0]).unwrap();
133 assert_eq!(unwrapped, content_key);
134 }
135}