chacha12_blake3/
chacha12_blake3.rs

1#![no_std]
2#![doc = include_str!("README.md")]
3
4use chacha20::{
5    ChaCha12,
6    cipher::{KeyIvInit, StreamCipher},
7};
8use constant_time_eq::constant_time_eq_32;
9
10#[cfg(feature = "zeroize")]
11use zeroize::{Zeroize, ZeroizeOnDrop};
12
13#[cfg(feature = "alloc")]
14extern crate alloc;
15#[cfg(feature = "alloc")]
16use alloc::vec::Vec;
17
18pub const KEY_SIZE: usize = 32;
19pub const NONCE_SIZE: usize = 32;
20pub const TAG_SIZE: usize = 32;
21
22const ENCRYPTION_KDF_CONTEXT: &str = "ChaCha12-BLAKE3 encryption key";
23const AUTHENTICATION_KDF_CONTEXT: &str = "ChaCha12-BLAKE3 authentication key";
24
25#[derive(Clone, Copy, Debug)]
26pub struct Error {}
27
28#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
29pub struct ChaCha12Blake3 {
30    key: [u8; 32],
31    authentication_key: [u8; 32],
32}
33
34impl ChaCha12Blake3 {
35    pub fn new(key: [u8; 32]) -> Self {
36        let authentication_key: [u8; 32] = blake3::derive_key(AUTHENTICATION_KDF_CONTEXT, &key);
37        return ChaCha12Blake3 {
38            key,
39            authentication_key,
40        };
41    }
42
43    #[cfg(feature = "alloc")]
44    pub fn encrypt(&self, nonce: &[u8; 32], plaintext: &[u8], aad: &[u8]) -> Vec<u8> {
45        let mut ciphertext = alloc::vec![0u8; plaintext.len() + TAG_SIZE];
46        ciphertext[..plaintext.len()].copy_from_slice(&plaintext);
47
48        let tag = self.encrypt_in_place_detached(nonce, &mut ciphertext[..plaintext.len()], aad);
49        ciphertext[plaintext.len()..].copy_from_slice(&tag);
50
51        return ciphertext;
52    }
53
54    #[cfg(feature = "alloc")]
55    pub fn decrypt(&self, nonce: &[u8; 32], ciphertext: &[u8], aad: &[u8]) -> Result<Vec<u8>, Error> {
56        if ciphertext.len() < TAG_SIZE {
57            return Err(Error {});
58        }
59
60        let mut plaintext = alloc::vec![0u8; ciphertext.len() - TAG_SIZE];
61        plaintext.copy_from_slice(&ciphertext[..ciphertext.len() - TAG_SIZE]);
62
63        self.decrypt_in_place_detached(
64            nonce,
65            &mut plaintext,
66            &ciphertext[ciphertext.len() - TAG_SIZE..].try_into().unwrap(),
67            aad,
68        )?;
69
70        return Ok(plaintext);
71    }
72
73    pub fn encrypt_in_place_detached(&self, nonce: &[u8; 32], plaintext: &mut [u8], aad: &[u8]) -> [u8; 32] {
74        // encryptionKey = blake3::derive_key(context="...", ikm=key || nonce)
75        let mut encryption_key: [u8; 32] = blake3::Hasher::new_derive_key(ENCRYPTION_KDF_CONTEXT)
76            .update(&self.key)
77            .update(nonce)
78            .finalize()
79            .into();
80
81        // while the ChaCha12-BLAKE3 specification uses ChaCha12 with a 64-bit counter and a 64-bit nonce,
82        // like the original ChaCha20 by Daniel J. Bernstein, our current implementation unfortunately
83        // uses a 32-bit counter and a 96-bit nonce as specified in RFC 8439/
84        // That's why for now we use a 96-bit nonce where the first 4 bytes (32 bits) are fixed to 0
85        let mut chacha_ietf_nonce = [0u8; 12];
86        chacha_ietf_nonce[4..].copy_from_slice(&nonce[..8]);
87
88        // The current implementation uses a ChaCha12 with a 96-bit nonce and 32-bit counter,
89        // so we are limited to encrypt (64 * 2^32) - 64 bytes of data with a single (key, nonce) pair,
90        // which is around 256 GiB.
91        // This is unlikely to be an issue because the API requires the entire message to be in
92        // memory.
93        assert!(
94            plaintext.len() < ((64 * (1 << 32)) - 64),
95            "ChaCha12-BLAKE3 currently can't encrypt more than 256 GiB of data with a single (key, nonce) pair"
96        );
97
98        ChaCha12::new(&encryption_key.into(), &chacha_ietf_nonce.into()).apply_keystream(plaintext);
99
100        let mut mac_hasher = blake3::Hasher::new_keyed(&self.authentication_key);
101        mac_hasher.update(nonce);
102        mac_hasher.update(aad);
103        mac_hasher.update(&(aad.len() as u64).to_le_bytes());
104        mac_hasher.update(&plaintext);
105        mac_hasher.update(&(plaintext.len() as u64).to_le_bytes());
106        let tag = mac_hasher.finalize();
107
108        #[cfg(feature = "zeroize")]
109        encryption_key.zeroize();
110
111        return tag.into();
112    }
113
114    pub fn decrypt_in_place_detached(
115        &self,
116        nonce: &[u8; 32],
117        ciphertext: &mut [u8],
118        tag: &[u8; 32],
119        aad: &[u8],
120    ) -> Result<(), Error> {
121        let mut mac_hasher = blake3::Hasher::new_keyed(&self.authentication_key);
122        mac_hasher.update(nonce);
123        mac_hasher.update(aad);
124        mac_hasher.update(&(aad.len() as u64).to_le_bytes());
125        mac_hasher.update(&ciphertext);
126        mac_hasher.update(&(ciphertext.len() as u64).to_le_bytes());
127        let mac = mac_hasher.finalize();
128
129        if !constant_time_eq_32(mac.as_bytes(), tag) {
130            return Err(Error {});
131        }
132
133        // encryptionKey = blake3::derive_key(context="...", ikm=key || nonce)
134        let mut encryption_key: [u8; 32] = blake3::Hasher::new_derive_key(ENCRYPTION_KDF_CONTEXT)
135            .update(&self.key)
136            .update(nonce)
137            .finalize()
138            .into();
139
140        // see `encrypt_in_place_detached` for why we currently use only the last 64 bits of a 96-bit nonce
141        // for ChaCha12
142        let mut chacha_ietf_nonce = [0u8; 12];
143        chacha_ietf_nonce[4..].copy_from_slice(&nonce[..8]);
144
145        ChaCha12::new(&encryption_key.into(), &chacha_ietf_nonce.into()).apply_keystream(ciphertext);
146
147        #[cfg(feature = "zeroize")]
148        encryption_key.zeroize();
149
150        return Ok(());
151    }
152}