jax-common 0.1.11

Core data structures and cryptography for JaxBucket - end-to-end encrypted P2P storage
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
//! Secure key sharing using ECDH + AES Key Wrap
//!
//! This module implements a protocol for sharing bucket encryption keys between peers.
//! It combines Elliptic Curve Diffie-Hellman (ECDH) for key agreement with AES Key Wrap (RFC 3394)
//! for key encryption.
//!
//! # Protocol Overview
//!
//! To share a secret with a peer:
//! 1. **Generate ephemeral keypair**: Create a temporary Ed25519 keypair
//! 2. **Perform ECDH**: Convert keys to X25519 and compute shared secret
//! 3. **Wrap key**: Use AES-KW to encrypt the bucket secret with the shared secret
//! 4. **Package**: Create a `Share` containing the ephemeral public key and wrapped secret
//!
//! The recipient can recover the secret by:
//! 1. **Extract ephemeral key**: Read the ephemeral public key from the Share
//! 2. **Perform ECDH**: Use their private key to compute the same shared secret
//! 3. **Unwrap key**: Use AES-KW to decrypt the bucket secret
//!
//! # Security Properties
//!
//! - **Forward Secrecy**: Ephemeral keys are not stored, so past sessions cannot be decrypted
//! - **Authentication**: The recipient's public key must be known in advance
//! - **Integrity**: AES-KW provides authentication of the wrapped key

use std::convert::TryFrom;

use aes_kw::KekAes256 as Kek;
use serde::{Deserialize, Serialize};

use super::keys::{KeyError, PublicKey, SecretKey, PUBLIC_KEY_SIZE};
use super::secret::{Secret, SecretError, SECRET_SIZE};

/// Size of AES Key Wrap padding/nonce in bytes
pub const KW_NONCE_SIZE: usize = 8;
/// Total size of a Share in bytes
///
/// Layout: ephemeral_pubkey (32) || wrapped_secret (40) = 72 bytes
/// Note: AES-KW adds 8 bytes of padding to the 32-byte secret, resulting in 40 bytes
pub const SECRET_SHARE_SIZE: usize = PUBLIC_KEY_SIZE + SECRET_SIZE + KW_NONCE_SIZE;

/// Errors that can occur during share creation or recovery
#[derive(Debug, thiserror::Error)]
pub enum SecretShareError {
    #[error("share error: {0}")]
    Default(#[from] anyhow::Error),
    #[error("key error: {0}")]
    Key(#[from] KeyError),
    #[error("secret error: {0}")]
    Secret(#[from] SecretError),
}

/// A cryptographic share that securely wraps a secret for a specific recipient
///
/// A `Share` contains an ephemeral public key and an AES-KW wrapped secret.
/// Only the intended recipient (whose public key was used during creation) can recover the secret.
///
/// # Wire Format
///
/// ```text
/// [ ephemeral_pubkey: 32 bytes ][ wrapped_secret: 40 bytes ]
/// ```
///
/// # Examples
///
/// ```ignore
/// // Alice wants to share a bucket secret with Bob
/// let bucket_secret = Secret::generate();
/// let bob_pubkey = bob_secret_key.public();
///
/// // Alice creates a share for Bob
/// let share = Share::new(&bucket_secret, &bob_pubkey)?;
///
/// // Bob can recover the secret using his private key
/// let recovered_secret = share.recover(&bob_secret_key)?;
/// assert_eq!(bucket_secret, recovered_secret);
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct SecretShare(pub(crate) [u8; SECRET_SHARE_SIZE]);

impl Serialize for SecretShare {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_bytes(&self.0)
    }
}

impl<'de> Deserialize<'de> for SecretShare {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        use serde::de::{Error, Visitor};
        use std::fmt;

        struct ShareVisitor;

        impl<'de> Visitor<'de> for ShareVisitor {
            type Value = SecretShare;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("a byte array or sequence of SHARE_SIZE")
            }

            fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
            where
                E: Error,
            {
                if v.len() != SECRET_SHARE_SIZE {
                    return Err(E::invalid_length(
                        v.len(),
                        &format!("expected {} bytes", SECRET_SHARE_SIZE).as_str(),
                    ));
                }
                let mut array = [0u8; SECRET_SHARE_SIZE];
                array.copy_from_slice(v);
                Ok(SecretShare(array))
            }

            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
            where
                A: serde::de::SeqAccess<'de>,
            {
                let mut bytes = Vec::new();
                while let Some(byte) = seq.next_element::<u8>()? {
                    bytes.push(byte);
                }
                if bytes.len() != SECRET_SHARE_SIZE {
                    return Err(A::Error::invalid_length(
                        bytes.len(),
                        &format!("expected {} bytes", SECRET_SHARE_SIZE).as_str(),
                    ));
                }
                let mut array = [0u8; SECRET_SHARE_SIZE];
                array.copy_from_slice(&bytes);
                Ok(SecretShare(array))
            }
        }

        // Try bytes first (for CBOR/bincode), fallback to seq (for JSON)
        deserializer.deserialize_byte_buf(ShareVisitor)
    }
}

impl Default for SecretShare {
    fn default() -> Self {
        SecretShare([0; SECRET_SHARE_SIZE])
    }
}

impl From<[u8; SECRET_SHARE_SIZE]> for SecretShare {
    fn from(bytes: [u8; SECRET_SHARE_SIZE]) -> Self {
        SecretShare(bytes)
    }
}

impl From<SecretShare> for [u8; SECRET_SHARE_SIZE] {
    fn from(share: SecretShare) -> Self {
        share.0
    }
}

impl TryFrom<&[u8]> for SecretShare {
    type Error = SecretShareError;
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
        if bytes.len() != SECRET_SHARE_SIZE {
            return Err(anyhow::anyhow!(
                "invalid share size, expected {}, got {}",
                SECRET_SHARE_SIZE,
                bytes.len()
            )
            .into());
        }
        let mut share = SecretShare::default();
        share.0.copy_from_slice(bytes);
        Ok(share)
    }
}

impl SecretShare {
    /// Parse a share from a hexadecimal string
    ///
    /// Accepts both plain hex and "0x"-prefixed hex strings.
    pub fn from_hex(hex: &str) -> Result<Self, SecretShareError> {
        let hex = hex.strip_prefix("0x").unwrap_or(hex);
        let mut buff = [0; SECRET_SHARE_SIZE];
        hex::decode_to_slice(hex, &mut buff).map_err(|_| anyhow::anyhow!("hex decode error"))?;
        Ok(SecretShare::from(buff))
    }

    /// Convert share to hexadecimal string
    #[allow(clippy::wrong_self_convention)]
    pub fn to_hex(&self) -> String {
        hex::encode(self.0)
    }

    /// Create a new share that wraps a secret for a specific recipient
    ///
    /// This uses ECDH + AES Key Wrap to securely share the secret:
    /// 1. Generates an ephemeral Ed25519 keypair
    /// 2. Converts both keys to X25519 for ECDH
    /// 3. Performs ECDH to derive a shared secret
    /// 4. Uses AES-KW to wrap the secret with the shared secret
    /// 5. Returns a Share containing [ephemeral_pubkey || wrapped_secret]
    ///
    /// # Arguments
    ///
    /// * `secret` - The secret to share (e.g., a bucket encryption key)
    /// * `recipient` - The public key of the intended recipient
    ///
    /// # Errors
    ///
    /// Returns an error if key conversion or encryption fails.
    pub fn new(secret: &Secret, recipient: &PublicKey) -> Result<Self, SecretShareError> {
        // Generate ephemeral Ed25519 keypair
        let ephemeral_private = SecretKey::generate();
        let ephemeral_public = ephemeral_private.public();

        // Convert both keys to X25519 for ECDH
        let ephemeral_x25519_private = ephemeral_private.to_x25519();
        let recipient_x25519_public = recipient.to_x25519()?;

        // Perform ECDH to get shared secret
        let shared_secret = ephemeral_x25519_private.diffie_hellman(&recipient_x25519_public);

        // Use shared secret as KEK for AES-KW
        // copy the bytes to a fixed array
        let mut shared_secret_bytes = [0; SECRET_SIZE];
        shared_secret_bytes.copy_from_slice(shared_secret.as_bytes());
        let kek = Kek::from(shared_secret_bytes);
        let wrapped = kek
            .wrap_vec(secret.bytes())
            .map_err(|_| anyhow::anyhow!("AES-KW wrap error"))?;

        // Build share: ephemeral_public_key || wrapped_secret
        let mut share = SecretShare::default();
        let ephemeral_bytes = ephemeral_public.to_bytes();

        // sanity check we're getting `SHARE_SIZE` bytes here
        if ephemeral_bytes.len() + wrapped.len() != SECRET_SHARE_SIZE {
            return Err(anyhow::anyhow!("expected share size is incorrect").into());
        };

        // Copy the bytes in
        share.0[..PUBLIC_KEY_SIZE].copy_from_slice(&ephemeral_bytes);
        share.0[PUBLIC_KEY_SIZE..PUBLIC_KEY_SIZE + wrapped.len()].copy_from_slice(&wrapped);

        Ok(share)
    }

    /// Recover the wrapped secret using the recipient's private key
    ///
    /// This reverses the wrapping process:
    /// 1. Extracts the ephemeral public key from the Share
    /// 2. Converts keys to X25519 for ECDH
    /// 3. Performs ECDH to derive the same shared secret
    /// 4. Uses AES-KW to unwrap the secret
    ///
    /// # Arguments
    ///
    /// * `recipient_secret` - The recipient's private key (must match the public key used in `new`)
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Key conversion fails
    /// - AES-KW unwrapping fails (wrong key or corrupted data)
    /// - Unwrapped secret has incorrect size
    ///
    /// # Security Note
    ///
    /// If this function returns an error, it means either the Share was created for a different
    /// recipient, the data was corrupted, or an attacker tampered with it.
    pub fn recover(&self, recipient_secret: &SecretKey) -> Result<Secret, SecretShareError> {
        // Extract the ephemeral public key
        let ephemeral_public_bytes = &self.0[..PUBLIC_KEY_SIZE];
        let ephemeral_public = PublicKey::try_from(ephemeral_public_bytes)?;

        // Convert keys to X25519 for ECDH
        let recipient_x25519_private = recipient_secret.to_x25519();
        let ephemeral_x25519_public = ephemeral_public.to_x25519()?;

        // Perform ECDH to get same shared secret
        let shared_secret = recipient_x25519_private.diffie_hellman(&ephemeral_x25519_public);

        // Use shared secret as KEK for AES-KW unwrapping
        let shared_secret_bytes = *shared_secret.as_bytes();
        let kek = Kek::from(shared_secret_bytes);
        let wrapped_data = &self.0[PUBLIC_KEY_SIZE..];

        // Find the actual length of wrapped data (AES-KW adds padding)
        let unwrapped = kek
            .unwrap_vec(wrapped_data)
            .map_err(|_| anyhow::anyhow!("AES-KW unwrap error"))?;

        if unwrapped.len() != SECRET_SIZE {
            return Err(anyhow::anyhow!("unwrapped secret has wrong size").into());
        }

        let mut secret_bytes = [0; SECRET_SIZE];
        secret_bytes.copy_from_slice(&unwrapped);
        Ok(Secret::from(secret_bytes))
    }

    /// Get a reference to the raw share bytes
    pub fn bytes(&self) -> &[u8] {
        &self.0
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_share_secret() {
        let secret = Secret::from_slice(&[42u8; SECRET_SIZE]).unwrap();
        let private_key = SecretKey::generate();
        let public_key = private_key.public();
        let share = SecretShare::new(&secret, &public_key).unwrap();
        let recovered_secret = share.recover(&private_key).unwrap();
        assert_eq!(secret, recovered_secret);
    }

    #[test]
    fn test_share_different_keys() {
        let secret = Secret::generate();
        let alice_private = SecretKey::generate();
        let alice_public = alice_private.public();
        let bob_private = SecretKey::generate();
        // Alice creates a share for Bob
        let share = SecretShare::new(&secret, &alice_public).unwrap();
        // Alice can recover the secret
        let recovered_by_alice = share.recover(&alice_private).unwrap();
        assert_eq!(secret, recovered_by_alice);
        // Bob cannot recover the secret (should fail)
        let result = share.recover(&bob_private);
        assert!(result.is_err());
    }

    #[test]
    fn test_share_hex_roundtrip() {
        let secret = Secret::generate();
        let private_key = SecretKey::generate();
        let public_key = private_key.public();
        let share = SecretShare::new(&secret, &public_key).unwrap();
        let hex = share.to_hex();
        let recovered_share = SecretShare::from_hex(&hex).unwrap();
        assert_eq!(share, recovered_share);
        let recovered_secret = recovered_share.recover(&private_key).unwrap();
        assert_eq!(secret, recovered_secret);
    }

    #[test]
    fn test_share_serde_json_roundtrip() {
        let secret = Secret::generate();
        let private_key = SecretKey::generate();
        let public_key = private_key.public();
        let share = SecretShare::new(&secret, &public_key).unwrap();

        // Serialize to JSON
        let json = serde_json::to_string(&share).unwrap();

        // Deserialize from JSON
        let recovered_share: SecretShare = serde_json::from_str(&json).unwrap();

        // Verify the share is identical
        assert_eq!(share, recovered_share);

        // Verify we can still recover the original secret
        let recovered_secret = recovered_share.recover(&private_key).unwrap();
        assert_eq!(secret, recovered_secret);
    }

    #[test]
    fn test_share_serde_bincode_roundtrip() {
        let secret = Secret::generate();
        let private_key = SecretKey::generate();
        let public_key = private_key.public();
        let share = SecretShare::new(&secret, &public_key).unwrap();

        // Serialize to binary
        let binary = bincode::serialize(&share).unwrap();

        // Deserialize from binary
        let recovered_share: SecretShare = bincode::deserialize(&binary).unwrap();

        // Verify the share is identical
        assert_eq!(share, recovered_share);

        // Verify we can still recover the original secret
        let recovered_secret = recovered_share.recover(&private_key).unwrap();
        assert_eq!(secret, recovered_secret);
    }

    #[test]
    fn test_share_deserialize_invalid_length() {
        // Test with too short data
        let short_data = vec![0u8; SECRET_SHARE_SIZE - 1];
        let result: Result<SecretShare, _> =
            bincode::deserialize(&bincode::serialize(&short_data).unwrap());
        assert!(result.is_err());

        // Test with too long data
        let long_data = vec![0u8; SECRET_SHARE_SIZE + 1];
        let result: Result<SecretShare, _> =
            bincode::deserialize(&bincode::serialize(&long_data).unwrap());
        assert!(result.is_err());
    }

    #[test]
    fn test_share_deserialize_exact_size() {
        // Test that exact size data can be deserialized
        let exact_data = vec![0u8; SECRET_SHARE_SIZE];
        let serialized = bincode::serialize(&exact_data).unwrap();
        let result: Result<SecretShare, _> = bincode::deserialize(&serialized);
        assert!(result.is_ok());

        let share = result.unwrap();
        assert_eq!(share.0, [0u8; SECRET_SHARE_SIZE]);
    }

    #[test]
    fn test_share_serde_multiple_formats() {
        let secret = Secret::generate();
        let private_key = SecretKey::generate();
        let public_key = private_key.public();
        let original_share = SecretShare::new(&secret, &public_key).unwrap();

        // Test JSON roundtrip
        let json = serde_json::to_string(&original_share).unwrap();
        let json_share: SecretShare = serde_json::from_str(&json).unwrap();
        assert_eq!(original_share, json_share);

        // Test Bincode roundtrip
        let binary = bincode::serialize(&original_share).unwrap();
        let binary_share: SecretShare = bincode::deserialize(&binary).unwrap();
        assert_eq!(original_share, binary_share);

        // Ensure all formats produce the same result
        assert_eq!(json_share, binary_share);

        // Verify all can recover the same secret
        let secret1 = json_share.recover(&private_key).unwrap();
        let secret2 = binary_share.recover(&private_key).unwrap();
        assert_eq!(secret, secret1);
        assert_eq!(secret, secret2);
        assert_eq!(secret1, secret2);
    }
}