matrix_sdk_crypto/
ciphers.rs

1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use aes::{
16    cipher::{generic_array::GenericArray, IvSizeUser, KeyIvInit, KeySizeUser, StreamCipher},
17    Aes256,
18};
19use ctr::Ctr128BE;
20use hkdf::Hkdf;
21use hmac::{
22    digest::{FixedOutput, MacError},
23    Hmac, Mac as _,
24};
25use pbkdf2::pbkdf2;
26use rand::{thread_rng, RngCore};
27use sha2::{Sha256, Sha512};
28use zeroize::{Zeroize, ZeroizeOnDrop};
29
30// We could use the `keysize()` method Aes256Ctr as KeySize exposes, but it's
31// not const (yet?), same for the IV size.
32pub(crate) const IV_SIZE: usize = 16;
33pub(crate) const KEY_SIZE: usize = 32;
34pub(crate) const SALT_SIZE: usize = 16;
35pub(crate) const MAC_SIZE: usize = 32;
36
37type Aes256Ctr = Ctr128BE<Aes256>;
38
39type Aes256Key = GenericArray<u8, <Aes256Ctr as KeySizeUser>::KeySize>;
40type Aes256Iv = GenericArray<u8, <Aes256Ctr as IvSizeUser>::IvSize>;
41type HmacSha256Key = [u8; KEY_SIZE];
42
43/// An authentication tag for the HMAC-SHA-256 message authentication algorithm.
44#[derive(Debug)]
45pub(crate) struct HmacSha256Mac([u8; MAC_SIZE]);
46
47impl HmacSha256Mac {
48    /// Represent the MAC tag as an array of bytes.
49    pub(crate) fn as_bytes(&self) -> &[u8; MAC_SIZE] {
50        &self.0
51    }
52
53    /// Return the underlying array of bytes of the authentication tag.
54    pub(crate) fn into_bytes(self) -> [u8; MAC_SIZE] {
55        self.0
56    }
57
58    /// Try to create a [`HmacSha256Mac`] from a slice of bytes.
59    ///
60    /// Returns `None` if the length of the byte slice isn't 32 bytes.
61    pub(crate) fn from_slice(bytes: &[u8]) -> Option<Self> {
62        if bytes.len() != MAC_SIZE {
63            None
64        } else {
65            let mut mac = [0u8; MAC_SIZE];
66            mac.copy_from_slice(bytes);
67
68            Some(HmacSha256Mac(mac))
69        }
70    }
71}
72
73/// Keys used for our combination of AES-CTR-256 and HMAC-SHA-256.
74///
75/// ⚠️  This struct provides low-level cryptographic primitives.
76///
77/// This combination is, as of now, used in the following places:
78///
79/// 1. Secret storage[1]
80/// 2. File-based key exports[2]
81///
82/// [1]: https://spec.matrix.org/v1.8/client-server-api/#msecret_storagev1aes-hmac-sha2
83/// [2]: https://spec.matrix.org/v1.8/client-server-api/#key-exports
84#[derive(Zeroize, ZeroizeOnDrop)]
85pub(crate) struct AesHmacSha2Key {
86    aes_key: Box<[u8; KEY_SIZE]>,
87    mac_key: Box<[u8; KEY_SIZE]>,
88}
89
90impl AesHmacSha2Key {
91    /// Create a [`AesHmacSha2Key`] from a passphrase.
92    ///
93    /// The passphrase will be expanded using the algorithm described in the
94    /// "Key export" part of the [spec].
95    ///
96    /// [spec]: https://spec.matrix.org/v1.8/client-server-api/#key-exports
97    const ZERO_SALT: &'static [u8; 32] = &[0u8; 32];
98
99    /// Create a per-secret specific [`AesHmacSha2Key`] from the secret storage
100    /// key.
101    ///
102    /// The secret storage key will be expanded as described in the [spec].
103    ///
104    /// [spec]: https://spec.matrix.org/v1.8/client-server-api/#msecret_storagev1aes-hmac-sha2
105    pub(crate) fn from_secret_storage_key(
106        secret_storage_key: &[u8; KEY_SIZE],
107        secret_name: &str,
108    ) -> Self {
109        let mut expanded_keys = [0u8; KEY_SIZE * 2];
110        let hkdf: Hkdf<Sha256> = Hkdf::new(Some(Self::ZERO_SALT), secret_storage_key);
111
112        hkdf.expand(secret_name.as_bytes(), &mut expanded_keys)
113            .expect("We should be able to expand 64 bytes of output key material.");
114
115        let (aes_key, mac_key) = Self::split_keys(&expanded_keys);
116
117        expanded_keys.zeroize();
118
119        Self { aes_key, mac_key }
120    }
121
122    pub(crate) fn from_passphrase(
123        passphrase: &str,
124        pbkdf_rounds: u32,
125        salt: &[u8; SALT_SIZE],
126    ) -> Self {
127        let mut expanded_keys = [0u8; KEY_SIZE * 2];
128
129        pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), salt, pbkdf_rounds, &mut expanded_keys)
130            .expect(
131                "We should be able to expand a passphrase of any length due to \
132                 HMAC being able to be initialized with any input size",
133            );
134
135        let (aes_key, mac_key) = Self::split_keys(&expanded_keys);
136
137        expanded_keys.zeroize();
138
139        Self { aes_key, mac_key }
140    }
141
142    /// Encrypt the given plaintext and return the ciphertext and the
143    /// initialization vector.
144    ///
145    /// ⚠️  This method is a low-level cryptographic primitive.
146    ///
147    /// This method does not provide authenticity. You *must* call the
148    /// [`AesHmacSha2Key::create_mac_tag()`] method after the encryption step to
149    /// create a authentication tag.
150    pub(crate) fn encrypt(&self, plaintext: Vec<u8>) -> (Vec<u8>, [u8; IV_SIZE]) {
151        let initialization_vector = Self::generate_iv();
152        let ciphertext = self.apply_keystream(plaintext, &initialization_vector);
153
154        (ciphertext, initialization_vector)
155    }
156
157    /// Apply the keystream to the data stream, producing either the plaintext
158    /// or the ciphertext depending on whether the data stream is the ciphertext
159    /// or the plaintext, respectively.
160    ///
161    /// ⚠️  This method is a low-level cryptographic primitive.
162    ///
163    /// If this method is encrypting a plaintext, you *must* ensure that the
164    /// initialization vector is unique across all calls to this method for
165    /// a given key.
166    ///
167    /// This method does not provide authenticity. You *must* call the
168    /// [`AesHmacSha2Key::create_mac_tag()`] method after the encryption step to
169    /// create a authentication tag or the [`AesHmacSha2Key::verify_mac()`]
170    /// method before decrypting.
171    pub(crate) fn apply_keystream(
172        &self,
173        mut plaintext: Vec<u8>,
174        initialization_vector: &[u8; IV_SIZE],
175    ) -> Vec<u8> {
176        let mut cipher =
177            Aes256Ctr::new(self.aes_key(), Aes256Iv::from_slice(initialization_vector));
178        cipher.apply_keystream(&mut plaintext);
179
180        plaintext
181    }
182
183    /// Create an authentication tag for the given ciphertext.
184    ///
185    /// ⚠️  This method is a low-level cryptographic primitive.
186    ///
187    /// This method *must* be called after a call to
188    /// [`AesHmacSha2Key::encrypt()`]. The authentication tag must be
189    /// provided besides the ciphertext for a decryption attempt.
190    pub(crate) fn create_mac_tag(&self, ciphertext: &[u8]) -> HmacSha256Mac {
191        let mut mac = [0u8; 32];
192        let mac_array = GenericArray::from_mut_slice(&mut mac);
193
194        let mut hmac = Hmac::<Sha256>::new_from_slice(self.mac_key())
195            .expect("We should be able to create a new HMAC object from our 32 byte MAC key");
196
197        hmac.update(ciphertext);
198        hmac.finalize_into(mac_array);
199
200        HmacSha256Mac(mac)
201    }
202
203    /// Verify an authentication tag for the given, encrypted, message.
204    ///
205    /// You *must* use this method to compare the authentication tags. This
206    /// method provides a constant-time comparison for the authentication tags.
207    ///
208    /// This method *must* be called before a call to
209    /// [`AesHmacSha2Key::decrypt()`].
210    pub(crate) fn verify_mac(&self, message: &[u8], mac: &[u8; MAC_SIZE]) -> Result<(), MacError> {
211        let mac_array = GenericArray::from_slice(mac);
212
213        let mut hmac = Hmac::<Sha256>::new_from_slice(self.mac_key())
214            .expect("We should be able to create a new HMAC object from our 32 byte MAC key");
215
216        hmac.update(message);
217        hmac.verify(mac_array)
218    }
219
220    /// Decrypt the given ciphertext and return the decrypted plaintext.
221    ///
222    /// The method does not provide authenticity. You *must* call the
223    /// [`AesHmacSha2Key::verify_mac()`] method before the decryption step to
224    /// verify the authentication tag.
225    pub(crate) fn decrypt(
226        &self,
227        ciphertext: Vec<u8>,
228        initialization_vector: &[u8; IV_SIZE],
229    ) -> Vec<u8> {
230        self.apply_keystream(ciphertext, initialization_vector)
231    }
232
233    fn split_keys(
234        expanded_keys: &[u8; KEY_SIZE * 2],
235    ) -> (Box<[u8; KEY_SIZE]>, Box<[u8; KEY_SIZE]>) {
236        let mut aes_key = Box::new([0u8; KEY_SIZE]);
237        let mut mac_key = Box::new([0u8; KEY_SIZE]);
238
239        aes_key.copy_from_slice(&expanded_keys[0..32]);
240        mac_key.copy_from_slice(&expanded_keys[32..64]);
241
242        (aes_key, mac_key)
243    }
244
245    /// Generate a new, random initialization vector.
246    ///
247    /// The initialization vector will be clamped and will be used to encrypt
248    /// the ciphertext.
249    fn generate_iv() -> [u8; IV_SIZE] {
250        let mut rng = thread_rng();
251        let mut iv = [0u8; IV_SIZE];
252
253        rng.fill_bytes(&mut iv);
254
255        Self::clamp_iv(iv)
256    }
257
258    /// The spec tells us to set bit 63 to 0 in some cases for some reason, I'm
259    /// not sure why, but fine:
260    ///     Generate 16 random bytes, set bit 63 to 0 (in order to work around
261    ///     differences in AES-CTR implementations), and use this as the AES
262    ///     initialization vector. This becomes the iv property, encoded using
263    ///     base64[1].
264    ///
265    /// [1]: https://spec.matrix.org/v1.8/client-server-api/#msecret_storagev1aes-hmac-sha2
266    fn clamp_iv(iv: [u8; 16]) -> [u8; IV_SIZE] {
267        let mut iv = u128::from_be_bytes(iv);
268        iv &= !(1 << 63);
269        iv.to_be_bytes()
270    }
271
272    /// Get the encryption key.
273    fn aes_key(&self) -> &Aes256Key {
274        Aes256Key::from_slice(self.aes_key.as_slice())
275    }
276
277    /// Get the authentication key.
278    fn mac_key(&self) -> &HmacSha256Key {
279        &self.mac_key
280    }
281}
282
283#[cfg(test)]
284mod test {
285    use super::*;
286
287    #[test]
288    fn encryption_roundtrip() {
289        let plaintext = "It's a secret to everybody";
290
291        let salt = [0u8; SALT_SIZE];
292        let key = AesHmacSha2Key::from_passphrase("My passphrase", 10, &salt);
293
294        let (ciphertext, iv) = key.encrypt(plaintext.as_bytes().to_vec());
295        let mac = key.create_mac_tag(&ciphertext);
296
297        key.verify_mac(&ciphertext, mac.as_bytes())
298            .expect("The MAC tag should be successfully verified");
299        let decrypted = key.decrypt(ciphertext, &iv);
300
301        assert_eq!(
302            plaintext.as_bytes(),
303            decrypted,
304            "An encryption roundtrip should produce the same plaintext"
305        );
306    }
307
308    #[test]
309    fn mac_decoding() {
310        let invalid_mac = [0u8; 10];
311
312        assert!(
313            HmacSha256Mac::from_slice(&invalid_mac).is_none(),
314            "We should return an error if the MAC is too short"
315        );
316
317        let mac = [0u8; 32];
318
319        HmacSha256Mac::from_slice(&mac)
320            .expect("We should be able to create a MAC from a 32 byte long slice");
321    }
322}