atagen

matrix-rust-sdk

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk.git
crates/matrix-sdk-crypto/src/ciphers.rsRust322 lines10.8 KB
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
15
use aes::{
16
    Aes256,
17
    cipher::{IvSizeUser, KeyIvInit, KeySizeUser, StreamCipher, generic_array::GenericArray},
18
};
19
use ctr::Ctr128BE;
20
use hkdf::Hkdf;
21
use hmac::{
22
    Hmac, Mac as _,
23
    digest::{FixedOutput, MacError},
24
};
25
use pbkdf2::pbkdf2;
26
use rand::{Rng, rng};
27
use sha2::{Sha256, Sha512};
28
use 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.
32
pub(crate) const IV_SIZE: usize = 16;
33
pub(crate) const KEY_SIZE: usize = 32;
34
pub(crate) const SALT_SIZE: usize = 16;
35
pub(crate) const MAC_SIZE: usize = 32;
36
37
type Aes256Ctr = Ctr128BE<Aes256>;
38
39
type Aes256Key = GenericArray<u8, <Aes256Ctr as KeySizeUser>::KeySize>;
40
type Aes256Iv = GenericArray<u8, <Aes256Ctr as IvSizeUser>::IvSize>;
41
type HmacSha256Key = [u8; KEY_SIZE];
42
43
/// An authentication tag for the HMAC-SHA-256 message authentication algorithm.
44
#[derive(Debug)]
45
pub(crate) struct HmacSha256Mac([u8; MAC_SIZE]);
46
47
impl 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)]
85
pub(crate) struct AesHmacSha2Key {
86
    aes_key: Box<[u8; KEY_SIZE]>,
87
    mac_key: Box<[u8; KEY_SIZE]>,
88
}
89
90
impl 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 = 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)]
284
mod 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
}