Skip to main content

claw_crypto/
recipient.rs

1/// Algorithm identifier stored on recipient envelopes.
2pub 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/// Public recipient key metadata used to wrap capsule private-field keys.
12#[derive(Debug, Clone)]
13pub struct RecipientPublicKey {
14    /// Stable recipient identity used by policy and CLI lookups.
15    pub recipient_id: String,
16    /// Key identifier for this recipient key.
17    pub key_id: String,
18    /// X25519 public key bytes.
19    pub public_key: [u8; 32],
20}
21
22/// Derives the X25519 public key for a recipient secret key.
23pub 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
28/// Generates a random 256-bit content encryption key.
29pub fn random_content_key() -> [u8; 32] {
30    let mut key = [0u8; 32];
31    rand::thread_rng().fill_bytes(&mut key);
32    key
33}
34
35/// Wraps a content key for each recipient using ephemeral X25519 envelopes.
36pub 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
61/// Decrypts a recipient envelope into the original content key.
62pub 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
87/// Decrypts capsule private fields for a matching recipient identity and secret key.
88pub 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}