crypto_box/
lib.rs

1#![no_std]
2#![cfg_attr(docsrs, feature(doc_auto_cfg))]
3#![doc = include_str!("../README.md")]
4#![doc(
5    html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg",
6    html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg"
7)]
8#![warn(missing_docs, rust_2018_idioms)]
9
10//! ## Usage
11//!
12#![cfg_attr(all(feature = "getrandom", feature = "std"), doc = "```")]
13#![cfg_attr(not(all(feature = "getrandom", feature = "std")), doc = "```ignore")]
14//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
15//! use crypto_box::{
16//!     aead::{Aead, AeadCore, OsRng},
17//!     SalsaBox, PublicKey, SecretKey
18//! };
19//!
20//! //
21//! // Encryption
22//! //
23//!
24//! // Generate a random secret key.
25//! // NOTE: The secret key bytes can be accessed by calling `secret_key.as_bytes()`
26//! let alice_secret_key = SecretKey::generate(&mut OsRng);
27//!
28//! // Get the public key for the secret key we just generated
29//! let alice_public_key_bytes = alice_secret_key.public_key().as_bytes().clone();
30//!
31//! // Obtain your recipient's public key.
32//! let bob_public_key = PublicKey::from([
33//!    0xe8, 0x98, 0xc, 0x86, 0xe0, 0x32, 0xf1, 0xeb,
34//!    0x29, 0x75, 0x5, 0x2e, 0x8d, 0x65, 0xbd, 0xdd,
35//!    0x15, 0xc3, 0xb5, 0x96, 0x41, 0x17, 0x4e, 0xc9,
36//!    0x67, 0x8a, 0x53, 0x78, 0x9d, 0x92, 0xc7, 0x54,
37//! ]);
38//!
39//! // Create a `SalsaBox` by performing Diffie-Hellman key agreement between
40//! // the two keys.
41//! let alice_box = SalsaBox::new(&bob_public_key, &alice_secret_key);
42//!
43//! // Get a random nonce to encrypt the message under
44//! let nonce = SalsaBox::generate_nonce(&mut OsRng);
45//!
46//! // Message to encrypt
47//! let plaintext = b"Top secret message we're encrypting";
48//!
49//! // Encrypt the message using the box
50//! let ciphertext = alice_box.encrypt(&nonce, &plaintext[..])?;
51//!
52//! //
53//! // Decryption
54//! //
55//!
56//! // Either side can encrypt or decrypt messages under the Diffie-Hellman key
57//! // they agree upon. The example below shows Bob's side.
58//! let bob_secret_key = SecretKey::from([
59//!     0xb5, 0x81, 0xfb, 0x5a, 0xe1, 0x82, 0xa1, 0x6f,
60//!     0x60, 0x3f, 0x39, 0x27, 0xd, 0x4e, 0x3b, 0x95,
61//!     0xbc, 0x0, 0x83, 0x10, 0xb7, 0x27, 0xa1, 0x1d,
62//!     0xd4, 0xe7, 0x84, 0xa0, 0x4, 0x4d, 0x46, 0x1b
63//! ]);
64//!
65//! // Deserialize Alice's public key from bytes
66//! let alice_public_key = PublicKey::from(alice_public_key_bytes);
67//!
68//! // Bob can compute the same `SalsaBox` as Alice by performing the
69//! // key agreement operation.
70//! let bob_box = SalsaBox::new(&alice_public_key, &bob_secret_key);
71//!
72//! // Decrypt the message, using the same randomly generated nonce
73//! let decrypted_plaintext = bob_box.decrypt(&nonce, &ciphertext[..])?;
74//!
75//! assert_eq!(&plaintext[..], &decrypted_plaintext[..]);
76//! # Ok(())
77//! # }
78//! ```
79//!
80//! ## Choosing [`ChaChaBox`] vs [`SalsaBox`]
81//!
82//! The `crypto_box` construction was originally specified using [`SalsaBox`].
83//!
84//! However, the newer [`ChaChaBox`] construction is also available, which
85//! provides better security and performance.
86//!
87//! To use it, enable the `chacha20` feature.
88//!
89#![cfg_attr(
90    all(feature = "chacha20", feature = "getrandom", feature = "std"),
91    doc = "```"
92)]
93#![cfg_attr(
94    not(all(feature = "chacha20", feature = "getrandom", feature = "std")),
95    doc = "```ignore"
96)]
97//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
98//! use crypto_box::{
99//!     aead::{Aead, AeadCore, Payload, OsRng},
100//!     ChaChaBox, PublicKey, SecretKey
101//! };
102//!
103//! let alice_secret_key = SecretKey::generate(&mut OsRng);
104//! let alice_public_key_bytes = alice_secret_key.public_key().as_bytes().clone();
105//! let bob_public_key = PublicKey::from([
106//!    0xe8, 0x98, 0xc, 0x86, 0xe0, 0x32, 0xf1, 0xeb,
107//!    0x29, 0x75, 0x5, 0x2e, 0x8d, 0x65, 0xbd, 0xdd,
108//!    0x15, 0xc3, 0xb5, 0x96, 0x41, 0x17, 0x4e, 0xc9,
109//!    0x67, 0x8a, 0x53, 0x78, 0x9d, 0x92, 0xc7, 0x54,
110//! ]);
111//! let alice_box = ChaChaBox::new(&bob_public_key, &alice_secret_key);
112//! let nonce = ChaChaBox::generate_nonce(&mut OsRng);
113//!
114//! // Message to encrypt
115//! let plaintext = b"Top secret message we're encrypting".as_ref();
116//!
117//! // Encrypt the message using the box
118//! let ciphertext = alice_box.encrypt(&nonce, plaintext).unwrap();
119//!
120//! //
121//! // Decryption
122//! //
123//!
124//! let bob_secret_key = SecretKey::from([
125//!     0xb5, 0x81, 0xfb, 0x5a, 0xe1, 0x82, 0xa1, 0x6f,
126//!     0x60, 0x3f, 0x39, 0x27, 0xd, 0x4e, 0x3b, 0x95,
127//!     0xbc, 0x0, 0x83, 0x10, 0xb7, 0x27, 0xa1, 0x1d,
128//!     0xd4, 0xe7, 0x84, 0xa0, 0x4, 0x4d, 0x46, 0x1b
129//! ]);
130//! let alice_public_key = PublicKey::from(alice_public_key_bytes);
131//! let bob_box = ChaChaBox::new(&alice_public_key, &bob_secret_key);
132//!
133//! // Decrypt the message, using the same randomly generated nonce
134//! let decrypted_plaintext = bob_box.decrypt(&nonce, ciphertext.as_slice()).unwrap();
135//!
136//! assert_eq!(&plaintext[..], &decrypted_plaintext[..]);
137//! # Ok(())
138//! # }
139//! ```
140//!
141//! ## In-place Usage (eliminates `alloc` requirement)
142//!
143//! This crate has an optional `alloc` feature which can be disabled in e.g.
144//! microcontroller environments that don't have a heap.
145//!
146//! The [`AeadInPlace::encrypt_in_place`] and [`AeadInPlace::decrypt_in_place`]
147//! methods accept any type that impls the [`aead::Buffer`] trait which
148//! contains the plaintext for encryption or ciphertext for decryption.
149//!
150//! Note that if you enable the `heapless` feature of this crate,
151//! you will receive an impl of `aead::Buffer` for [`heapless::Vec`]
152//! (re-exported from the `aead` crate as `aead::heapless::Vec`),
153//! which can then be passed as the `buffer` parameter to the in-place encrypt
154//! and decrypt methods.
155//!
156//! A `heapless` usage example can be found in the documentation for the
157//! `xsalsa20poly1305` crate:
158//!
159//! <https://docs.rs/xsalsa20poly1305/latest/xsalsa20poly1305/#in-place-usage-eliminates-alloc-requirement>
160//!
161//! [NaCl]: https://nacl.cr.yp.to/
162//! [`crypto_box`]: https://nacl.cr.yp.to/box.html
163//! [X25519]: https://cr.yp.to/ecdh.html
164//! [XSalsa20Poly1305]: https://nacl.cr.yp.to/secretbox.html
165//! [ECIES]: https://en.wikipedia.org/wiki/Integrated_Encryption_Scheme
166//! [`heapless::Vec`]: https://docs.rs/heapless/latest/heapless/struct.Vec.html
167
168#[cfg(feature = "seal")]
169extern crate alloc;
170
171mod public_key;
172mod secret_key;
173
174pub use crate::{public_key::PublicKey, secret_key::SecretKey};
175pub use aead;
176pub use crypto_secretbox::Nonce;
177
178use aead::{
179    consts::{U0, U16, U24, U32, U8},
180    generic_array::GenericArray,
181    AeadCore, AeadInPlace, Buffer, Error, KeyInit,
182};
183use crypto_secretbox::{
184    cipher::{IvSizeUser, KeyIvInit, KeySizeUser, StreamCipher},
185    Kdf, SecretBox,
186};
187use zeroize::Zeroizing;
188
189#[cfg(feature = "chacha20")]
190use chacha20::ChaCha20Legacy as ChaCha20;
191
192#[cfg(feature = "salsa20")]
193use salsa20::Salsa20;
194
195/// Size of a `crypto_box` public or secret key in bytes.
196pub const KEY_SIZE: usize = 32;
197
198/// Poly1305 tag.
199///
200/// Implemented as an alias for [`GenericArray`].
201pub type Tag = GenericArray<u8, U16>;
202
203/// Size of a Poly1305 tag in bytes.
204#[cfg(feature = "seal")]
205const TAG_SIZE: usize = 16;
206
207#[cfg(feature = "seal")]
208/// Extra bytes for the ciphertext of a `crypto_box_seal` compared to the plaintext
209pub const SEALBYTES: usize = KEY_SIZE + TAG_SIZE;
210
211/// [`CryptoBox`] instantiated with the ChaCha20 stream cipher.
212#[cfg(feature = "chacha20")]
213pub type ChaChaBox = CryptoBox<ChaCha20>;
214
215/// [`CryptoBox`] instantiated with with the Salsa20 stream cipher.
216#[cfg(feature = "salsa20")]
217pub type SalsaBox = CryptoBox<Salsa20>;
218
219/// Public-key encryption scheme based on the [X25519] Elliptic Curve
220/// Diffie-Hellman function and the [crypto_secretbox] authenticated encryption
221/// cipher.
222///
223/// This type impls the [`aead::Aead`] trait, and otherwise functions as a
224/// symmetric Authenticated Encryption with Associated Data (AEAD) cipher
225/// once instantiated.
226///
227/// Note that additional associated data (AAD) is not supported and encryption
228/// operations will return [`aead::Error`] if it is provided as an argument.
229///
230/// [X25519]: https://cr.yp.to/ecdh.html
231/// [crypto_secretbox]: https://github.com/RustCrypto/nacl-compat/tree/master/crypto_secretbox
232#[derive(Clone)]
233pub struct CryptoBox<C> {
234    secretbox: SecretBox<C>,
235}
236
237impl<C> CryptoBox<C> {
238    /// Create a new [`CryptoBox`], performing X25519 Diffie-Hellman to derive
239    /// a shared secret from the provided public and secret keys.
240    pub fn new(public_key: &PublicKey, secret_key: &SecretKey) -> Self
241    where
242        C: Kdf,
243    {
244        let shared_secret = Zeroizing::new(secret_key.scalar * public_key.0);
245
246        // Use HChaCha20 to create a uniformly random key from the shared secret
247        let key = Zeroizing::new(C::kdf(
248            GenericArray::from_slice(&shared_secret.0),
249            &GenericArray::default(),
250        ));
251
252        Self {
253            secretbox: SecretBox::<C>::new(&*key),
254        }
255    }
256}
257
258impl<C> AeadCore for CryptoBox<C> {
259    type NonceSize = U24;
260    type TagSize = U16;
261    type CiphertextOverhead = U0;
262}
263
264impl<C> AeadInPlace for CryptoBox<C>
265where
266    C: Kdf + KeyIvInit + KeySizeUser<KeySize = U32> + IvSizeUser<IvSize = U8> + StreamCipher,
267{
268    fn encrypt_in_place(
269        &self,
270        nonce: &GenericArray<u8, Self::NonceSize>,
271        associated_data: &[u8],
272        buffer: &mut dyn Buffer,
273    ) -> Result<(), Error> {
274        self.secretbox
275            .encrypt_in_place(nonce, associated_data, buffer)
276    }
277
278    fn encrypt_in_place_detached(
279        &self,
280        nonce: &GenericArray<u8, Self::NonceSize>,
281        associated_data: &[u8],
282        buffer: &mut [u8],
283    ) -> Result<Tag, Error> {
284        self.secretbox
285            .encrypt_in_place_detached(nonce, associated_data, buffer)
286    }
287
288    fn decrypt_in_place(
289        &self,
290        nonce: &GenericArray<u8, Self::NonceSize>,
291        associated_data: &[u8],
292        buffer: &mut dyn Buffer,
293    ) -> Result<(), Error> {
294        self.secretbox
295            .decrypt_in_place(nonce, associated_data, buffer)
296    }
297
298    fn decrypt_in_place_detached(
299        &self,
300        nonce: &GenericArray<u8, Self::NonceSize>,
301        associated_data: &[u8],
302        buffer: &mut [u8],
303        tag: &Tag,
304    ) -> Result<(), Error> {
305        self.secretbox
306            .decrypt_in_place_detached(nonce, associated_data, buffer, tag)
307    }
308}
309
310#[cfg(feature = "seal")]
311fn get_seal_nonce(ephemeral_pk: &PublicKey, recipient_pk: &PublicKey) -> Nonce {
312    use blake2::{Blake2b, Digest};
313    let mut hasher = Blake2b::<U24>::new();
314    hasher.update(ephemeral_pk.as_bytes());
315    hasher.update(recipient_pk.as_bytes());
316    hasher.finalize()
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[cfg(feature = "serde")]
324    #[test]
325    fn test_public_key_serialization() {
326        use aead::rand_core::RngCore;
327
328        // Random PK bytes
329        let mut public_key_bytes = [0; 32];
330        let mut rng = rand::thread_rng();
331        rng.fill_bytes(&mut public_key_bytes);
332
333        // Create public key
334        let public_key = PublicKey::from(public_key_bytes);
335
336        // Round-trip serialize with bincode
337        let serialized = bincode::serialize(&public_key).unwrap();
338        let deserialized: PublicKey = bincode::deserialize(&serialized).unwrap();
339        assert_eq!(deserialized, public_key,);
340
341        // Round-trip serialize with rmp (msgpack)
342        let serialized = rmp_serde::to_vec_named(&public_key).unwrap();
343        let deserialized: PublicKey = rmp_serde::from_slice(&serialized).unwrap();
344        assert_eq!(deserialized, public_key,);
345    }
346
347    #[cfg(feature = "serde")]
348    #[test]
349    fn test_secret_key_serialization() {
350        use aead::rand_core::RngCore;
351
352        // Random SK bytes
353        let mut secret_key_bytes = [0; 32];
354        let mut rng = rand::thread_rng();
355        rng.fill_bytes(&mut secret_key_bytes);
356
357        // Create secret key
358        let secret_key = SecretKey::from(secret_key_bytes);
359
360        // Round-trip serialize with bincode
361        let serialized = bincode::serialize(&secret_key).unwrap();
362        let deserialized: SecretKey = bincode::deserialize(&serialized).unwrap();
363        assert_eq!(deserialized.to_bytes(), secret_key.to_bytes());
364
365        // Round-trip serialize with rmp (msgpack)
366        let serialized = rmp_serde::to_vec_named(&secret_key).unwrap();
367        let deserialized: SecretKey = rmp_serde::from_slice(&serialized).unwrap();
368        assert_eq!(deserialized.to_bytes(), secret_key.to_bytes());
369    }
370
371    #[test]
372    fn test_public_key_from_slice() {
373        let array = [0; 40];
374
375        // Returns None for empty array
376        assert!(PublicKey::from_slice(&[]).is_err());
377
378        // Returns None for length <32
379        for i in 1..=31 {
380            assert!(PublicKey::from_slice(&array[..i]).is_err());
381        }
382
383        // Succeeds for length 32
384        assert!(PublicKey::from_slice(&array[..32]).is_ok());
385
386        // Returns None for length >32
387        for i in 33..=40 {
388            assert!(PublicKey::from_slice(&array[..i]).is_err());
389        }
390    }
391}