common/crypto/
share.rs

1//! Secure key sharing using ECDH + AES Key Wrap
2//!
3//! This module implements a protocol for sharing bucket encryption keys between peers.
4//! It combines Elliptic Curve Diffie-Hellman (ECDH) for key agreement with AES Key Wrap (RFC 3394)
5//! for key encryption.
6//!
7//! # Protocol Overview
8//!
9//! To share a secret with a peer:
10//! 1. **Generate ephemeral keypair**: Create a temporary Ed25519 keypair
11//! 2. **Perform ECDH**: Convert keys to X25519 and compute shared secret
12//! 3. **Wrap key**: Use AES-KW to encrypt the bucket secret with the shared secret
13//! 4. **Package**: Create a `Share` containing the ephemeral public key and wrapped secret
14//!
15//! The recipient can recover the secret by:
16//! 1. **Extract ephemeral key**: Read the ephemeral public key from the Share
17//! 2. **Perform ECDH**: Use their private key to compute the same shared secret
18//! 3. **Unwrap key**: Use AES-KW to decrypt the bucket secret
19//!
20//! # Security Properties
21//!
22//! - **Forward Secrecy**: Ephemeral keys are not stored, so past sessions cannot be decrypted
23//! - **Authentication**: The recipient's public key must be known in advance
24//! - **Integrity**: AES-KW provides authentication of the wrapped key
25
26use std::convert::TryFrom;
27
28use aes_kw::KekAes256 as Kek;
29use serde::{Deserialize, Serialize};
30
31use super::keys::{KeyError, PublicKey, SecretKey, PUBLIC_KEY_SIZE};
32use super::secret::{Secret, SecretError, SECRET_SIZE};
33
34/// Size of AES Key Wrap padding/nonce in bytes
35pub const KW_NONCE_SIZE: usize = 8;
36/// Total size of a Share in bytes
37///
38/// Layout: ephemeral_pubkey (32) || wrapped_secret (40) = 72 bytes
39/// Note: AES-KW adds 8 bytes of padding to the 32-byte secret, resulting in 40 bytes
40pub const SHARE_SIZE: usize = PUBLIC_KEY_SIZE + SECRET_SIZE + KW_NONCE_SIZE;
41
42/// Errors that can occur during share creation or recovery
43#[derive(Debug, thiserror::Error)]
44pub enum ShareError {
45    #[error("share error: {0}")]
46    Default(#[from] anyhow::Error),
47    #[error("key error: {0}")]
48    Key(#[from] KeyError),
49    #[error("secret error: {0}")]
50    Secret(#[from] SecretError),
51}
52
53/// A cryptographic share that securely wraps a secret for a specific recipient
54///
55/// A `Share` contains an ephemeral public key and an AES-KW wrapped secret.
56/// Only the intended recipient (whose public key was used during creation) can recover the secret.
57///
58/// # Wire Format
59///
60/// ```text
61/// [ ephemeral_pubkey: 32 bytes ][ wrapped_secret: 40 bytes ]
62/// ```
63///
64/// # Examples
65///
66/// ```ignore
67/// // Alice wants to share a bucket secret with Bob
68/// let bucket_secret = Secret::generate();
69/// let bob_pubkey = bob_secret_key.public();
70///
71/// // Alice creates a share for Bob
72/// let share = Share::new(&bucket_secret, &bob_pubkey)?;
73///
74/// // Bob can recover the secret using his private key
75/// let recovered_secret = share.recover(&bob_secret_key)?;
76/// assert_eq!(bucket_secret, recovered_secret);
77/// ```
78#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
79pub struct Share(pub(crate) [u8; SHARE_SIZE]);
80
81impl Serialize for Share {
82    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
83    where
84        S: serde::Serializer,
85    {
86        serializer.serialize_bytes(&self.0)
87    }
88}
89
90impl<'de> Deserialize<'de> for Share {
91    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
92    where
93        D: serde::Deserializer<'de>,
94    {
95        use serde::de::{Error, Visitor};
96        use std::fmt;
97
98        struct ShareVisitor;
99
100        impl<'de> Visitor<'de> for ShareVisitor {
101            type Value = Share;
102
103            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
104                formatter.write_str("a byte array or sequence of SHARE_SIZE")
105            }
106
107            fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
108            where
109                E: Error,
110            {
111                if v.len() != SHARE_SIZE {
112                    return Err(E::invalid_length(
113                        v.len(),
114                        &format!("expected {} bytes", SHARE_SIZE).as_str(),
115                    ));
116                }
117                let mut array = [0u8; SHARE_SIZE];
118                array.copy_from_slice(v);
119                Ok(Share(array))
120            }
121
122            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
123            where
124                A: serde::de::SeqAccess<'de>,
125            {
126                let mut bytes = Vec::new();
127                while let Some(byte) = seq.next_element::<u8>()? {
128                    bytes.push(byte);
129                }
130                if bytes.len() != SHARE_SIZE {
131                    return Err(A::Error::invalid_length(
132                        bytes.len(),
133                        &format!("expected {} bytes", SHARE_SIZE).as_str(),
134                    ));
135                }
136                let mut array = [0u8; SHARE_SIZE];
137                array.copy_from_slice(&bytes);
138                Ok(Share(array))
139            }
140        }
141
142        // Try bytes first (for CBOR/bincode), fallback to seq (for JSON)
143        deserializer.deserialize_byte_buf(ShareVisitor)
144    }
145}
146
147impl Default for Share {
148    fn default() -> Self {
149        Share([0; SHARE_SIZE])
150    }
151}
152
153impl From<[u8; SHARE_SIZE]> for Share {
154    fn from(bytes: [u8; SHARE_SIZE]) -> Self {
155        Share(bytes)
156    }
157}
158
159impl From<Share> for [u8; SHARE_SIZE] {
160    fn from(share: Share) -> Self {
161        share.0
162    }
163}
164
165impl TryFrom<&[u8]> for Share {
166    type Error = ShareError;
167    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
168        if bytes.len() != SHARE_SIZE {
169            return Err(anyhow::anyhow!(
170                "invalid share size, expected {}, got {}",
171                SHARE_SIZE,
172                bytes.len()
173            )
174            .into());
175        }
176        let mut share = Share::default();
177        share.0.copy_from_slice(bytes);
178        Ok(share)
179    }
180}
181
182impl Share {
183    /// Parse a share from a hexadecimal string
184    ///
185    /// Accepts both plain hex and "0x"-prefixed hex strings.
186    pub fn from_hex(hex: &str) -> Result<Self, ShareError> {
187        let hex = hex.strip_prefix("0x").unwrap_or(hex);
188        let mut buff = [0; SHARE_SIZE];
189        hex::decode_to_slice(hex, &mut buff).map_err(|_| anyhow::anyhow!("hex decode error"))?;
190        Ok(Share::from(buff))
191    }
192
193    /// Convert share to hexadecimal string
194    #[allow(clippy::wrong_self_convention)]
195    pub fn to_hex(&self) -> String {
196        hex::encode(self.0)
197    }
198
199    /// Create a new share that wraps a secret for a specific recipient
200    ///
201    /// This uses ECDH + AES Key Wrap to securely share the secret:
202    /// 1. Generates an ephemeral Ed25519 keypair
203    /// 2. Converts both keys to X25519 for ECDH
204    /// 3. Performs ECDH to derive a shared secret
205    /// 4. Uses AES-KW to wrap the secret with the shared secret
206    /// 5. Returns a Share containing [ephemeral_pubkey || wrapped_secret]
207    ///
208    /// # Arguments
209    ///
210    /// * `secret` - The secret to share (e.g., a bucket encryption key)
211    /// * `recipient` - The public key of the intended recipient
212    ///
213    /// # Errors
214    ///
215    /// Returns an error if key conversion or encryption fails.
216    pub fn new(secret: &Secret, recipient: &PublicKey) -> Result<Self, ShareError> {
217        // Generate ephemeral Ed25519 keypair
218        let ephemeral_private = SecretKey::generate();
219        let ephemeral_public = ephemeral_private.public();
220
221        // Convert both keys to X25519 for ECDH
222        let ephemeral_x25519_private = ephemeral_private.to_x25519();
223        let recipient_x25519_public = recipient.to_x25519()?;
224
225        // Perform ECDH to get shared secret
226        let shared_secret = ephemeral_x25519_private.diffie_hellman(&recipient_x25519_public);
227
228        // Use shared secret as KEK for AES-KW
229        // copy the bytes to a fixed array
230        let mut shared_secret_bytes = [0; SECRET_SIZE];
231        shared_secret_bytes.copy_from_slice(shared_secret.as_bytes());
232        let kek = Kek::from(shared_secret_bytes);
233        let wrapped = kek
234            .wrap_vec(secret.bytes())
235            .map_err(|_| anyhow::anyhow!("AES-KW wrap error"))?;
236
237        // Build share: ephemeral_public_key || wrapped_secret
238        let mut share = Share::default();
239        let ephemeral_bytes = ephemeral_public.to_bytes();
240
241        // sanity check we're getting `SHARE_SIZE` bytes here
242        if ephemeral_bytes.len() + wrapped.len() != SHARE_SIZE {
243            return Err(anyhow::anyhow!("expected share size is incorrect").into());
244        };
245
246        // Copy the bytes in
247        share.0[..PUBLIC_KEY_SIZE].copy_from_slice(&ephemeral_bytes);
248        share.0[PUBLIC_KEY_SIZE..PUBLIC_KEY_SIZE + wrapped.len()].copy_from_slice(&wrapped);
249
250        Ok(share)
251    }
252
253    /// Recover the wrapped secret using the recipient's private key
254    ///
255    /// This reverses the wrapping process:
256    /// 1. Extracts the ephemeral public key from the Share
257    /// 2. Converts keys to X25519 for ECDH
258    /// 3. Performs ECDH to derive the same shared secret
259    /// 4. Uses AES-KW to unwrap the secret
260    ///
261    /// # Arguments
262    ///
263    /// * `recipient_secret` - The recipient's private key (must match the public key used in `new`)
264    ///
265    /// # Errors
266    ///
267    /// Returns an error if:
268    /// - Key conversion fails
269    /// - AES-KW unwrapping fails (wrong key or corrupted data)
270    /// - Unwrapped secret has incorrect size
271    ///
272    /// # Security Note
273    ///
274    /// If this function returns an error, it means either the Share was created for a different
275    /// recipient, the data was corrupted, or an attacker tampered with it.
276    pub fn recover(&self, recipient_secret: &SecretKey) -> Result<Secret, ShareError> {
277        // Extract the ephemeral public key
278        let ephemeral_public_bytes = &self.0[..PUBLIC_KEY_SIZE];
279        let ephemeral_public = PublicKey::try_from(ephemeral_public_bytes)?;
280
281        // Convert keys to X25519 for ECDH
282        let recipient_x25519_private = recipient_secret.to_x25519();
283        let ephemeral_x25519_public = ephemeral_public.to_x25519()?;
284
285        // Perform ECDH to get same shared secret
286        let shared_secret = recipient_x25519_private.diffie_hellman(&ephemeral_x25519_public);
287
288        // Use shared secret as KEK for AES-KW unwrapping
289        let shared_secret_bytes = *shared_secret.as_bytes();
290        let kek = Kek::from(shared_secret_bytes);
291        let wrapped_data = &self.0[PUBLIC_KEY_SIZE..];
292
293        // Find the actual length of wrapped data (AES-KW adds padding)
294        let unwrapped = kek
295            .unwrap_vec(wrapped_data)
296            .map_err(|_| anyhow::anyhow!("AES-KW unwrap error"))?;
297
298        if unwrapped.len() != SECRET_SIZE {
299            return Err(anyhow::anyhow!("unwrapped secret has wrong size").into());
300        }
301
302        let mut secret_bytes = [0; SECRET_SIZE];
303        secret_bytes.copy_from_slice(&unwrapped);
304        Ok(Secret::from(secret_bytes))
305    }
306
307    /// Get a reference to the raw share bytes
308    pub fn bytes(&self) -> &[u8] {
309        &self.0
310    }
311}
312
313#[cfg(test)]
314mod test {
315    use super::*;
316
317    #[test]
318    fn test_share_secret() {
319        let secret = Secret::from_slice(&[42u8; SECRET_SIZE]).unwrap();
320        let private_key = SecretKey::generate();
321        let public_key = private_key.public();
322        let share = Share::new(&secret, &public_key).unwrap();
323        let recovered_secret = share.recover(&private_key).unwrap();
324        assert_eq!(secret, recovered_secret);
325    }
326
327    #[test]
328    fn test_share_different_keys() {
329        let secret = Secret::generate();
330        let alice_private = SecretKey::generate();
331        let alice_public = alice_private.public();
332        let bob_private = SecretKey::generate();
333        // Alice creates a share for Bob
334        let share = Share::new(&secret, &alice_public).unwrap();
335        // Alice can recover the secret
336        let recovered_by_alice = share.recover(&alice_private).unwrap();
337        assert_eq!(secret, recovered_by_alice);
338        // Bob cannot recover the secret (should fail)
339        let result = share.recover(&bob_private);
340        assert!(result.is_err());
341    }
342
343    #[test]
344    fn test_share_hex_roundtrip() {
345        let secret = Secret::generate();
346        let private_key = SecretKey::generate();
347        let public_key = private_key.public();
348        let share = Share::new(&secret, &public_key).unwrap();
349        let hex = share.to_hex();
350        let recovered_share = Share::from_hex(&hex).unwrap();
351        assert_eq!(share, recovered_share);
352        let recovered_secret = recovered_share.recover(&private_key).unwrap();
353        assert_eq!(secret, recovered_secret);
354    }
355
356    #[test]
357    fn test_share_serde_json_roundtrip() {
358        let secret = Secret::generate();
359        let private_key = SecretKey::generate();
360        let public_key = private_key.public();
361        let share = Share::new(&secret, &public_key).unwrap();
362
363        // Serialize to JSON
364        let json = serde_json::to_string(&share).unwrap();
365
366        // Deserialize from JSON
367        let recovered_share: Share = serde_json::from_str(&json).unwrap();
368
369        // Verify the share is identical
370        assert_eq!(share, recovered_share);
371
372        // Verify we can still recover the original secret
373        let recovered_secret = recovered_share.recover(&private_key).unwrap();
374        assert_eq!(secret, recovered_secret);
375    }
376
377    #[test]
378    fn test_share_serde_bincode_roundtrip() {
379        let secret = Secret::generate();
380        let private_key = SecretKey::generate();
381        let public_key = private_key.public();
382        let share = Share::new(&secret, &public_key).unwrap();
383
384        // Serialize to binary
385        let binary = bincode::serialize(&share).unwrap();
386
387        // Deserialize from binary
388        let recovered_share: Share = bincode::deserialize(&binary).unwrap();
389
390        // Verify the share is identical
391        assert_eq!(share, recovered_share);
392
393        // Verify we can still recover the original secret
394        let recovered_secret = recovered_share.recover(&private_key).unwrap();
395        assert_eq!(secret, recovered_secret);
396    }
397
398    #[test]
399    fn test_share_deserialize_invalid_length() {
400        // Test with too short data
401        let short_data = vec![0u8; SHARE_SIZE - 1];
402        let result: Result<Share, _> =
403            bincode::deserialize(&bincode::serialize(&short_data).unwrap());
404        assert!(result.is_err());
405
406        // Test with too long data
407        let long_data = vec![0u8; SHARE_SIZE + 1];
408        let result: Result<Share, _> =
409            bincode::deserialize(&bincode::serialize(&long_data).unwrap());
410        assert!(result.is_err());
411    }
412
413    #[test]
414    fn test_share_deserialize_exact_size() {
415        // Test that exact size data can be deserialized
416        let exact_data = vec![0u8; SHARE_SIZE];
417        let serialized = bincode::serialize(&exact_data).unwrap();
418        let result: Result<Share, _> = bincode::deserialize(&serialized);
419        assert!(result.is_ok());
420
421        let share = result.unwrap();
422        assert_eq!(share.0, [0u8; SHARE_SIZE]);
423    }
424
425    #[test]
426    fn test_share_serde_multiple_formats() {
427        let secret = Secret::generate();
428        let private_key = SecretKey::generate();
429        let public_key = private_key.public();
430        let original_share = Share::new(&secret, &public_key).unwrap();
431
432        // Test JSON roundtrip
433        let json = serde_json::to_string(&original_share).unwrap();
434        let json_share: Share = serde_json::from_str(&json).unwrap();
435        assert_eq!(original_share, json_share);
436
437        // Test Bincode roundtrip
438        let binary = bincode::serialize(&original_share).unwrap();
439        let binary_share: Share = bincode::deserialize(&binary).unwrap();
440        assert_eq!(original_share, binary_share);
441
442        // Ensure all formats produce the same result
443        assert_eq!(json_share, binary_share);
444
445        // Verify all can recover the same secret
446        let secret1 = json_share.recover(&private_key).unwrap();
447        let secret2 = binary_share.recover(&private_key).unwrap();
448        assert_eq!(secret, secret1);
449        assert_eq!(secret, secret2);
450        assert_eq!(secret1, secret2);
451    }
452}