Skip to main content

matrix_sdk_crypto/file_encryption/
key_export.rs

1// Copyright 2020 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 std::io::{Cursor, Read, Seek, SeekFrom};
16
17use byteorder::{BigEndian, ReadBytesExt};
18use rand::{Rng, rng};
19use serde_json::Error as SerdeError;
20use thiserror::Error;
21use vodozemac::{base64_decode, base64_encode};
22use zeroize::Zeroize;
23
24use crate::{
25    ciphers::{AesHmacSha2Key, IV_SIZE, MAC_SIZE, SALT_SIZE},
26    olm::ExportedRoomKey,
27};
28
29const VERSION: u8 = 1;
30
31const HEADER: &str = "-----BEGIN MEGOLM SESSION DATA-----";
32const FOOTER: &str = "-----END MEGOLM SESSION DATA-----";
33
34/// Error representing a failure during key export or import.
35#[derive(Error, Debug)]
36pub enum KeyExportError {
37    /// The key export doesn't contain valid headers.
38    #[error("Invalid or missing key export headers.")]
39    InvalidHeaders,
40    /// The key export has been encrypted with an unsupported version.
41    #[error("The key export has been encrypted with an unsupported version.")]
42    UnsupportedVersion,
43    /// The MAC of the encrypted payload is invalid.
44    #[error("The MAC of the encrypted payload is invalid.")]
45    InvalidMac,
46    /// The decrypted key export isn't valid UTF-8.
47    #[error(transparent)]
48    InvalidUtf8(#[from] std::string::FromUtf8Error),
49    /// The decrypted key export doesn't contain valid JSON.
50    #[error(transparent)]
51    Json(#[from] SerdeError),
52    /// The key export string isn't valid base64.
53    #[error(transparent)]
54    Decode(#[from] vodozemac::Base64DecodeError),
55    /// The key export doesn't all the required fields.
56    #[error(transparent)]
57    Io(#[from] std::io::Error),
58}
59
60/// Try to decrypt a reader into a list of exported room keys.
61///
62/// # Arguments
63///
64/// - `passphrase` - The passphrase that was used to encrypt the exported keys.
65///
66/// # Examples
67///
68/// ```no_run
69/// # use std::io::Cursor;
70/// # use matrix_sdk_crypto::{OlmMachine, decrypt_room_key_export};
71/// # use ruma::{device_id, user_id};
72/// # let alice = user_id!("@alice:example.org");
73/// # async {
74/// # let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
75/// # let export = Cursor::new("".to_owned());
76/// let exported_keys = decrypt_room_key_export(export, "1234").unwrap();
77/// machine.store().import_room_keys(exported_keys, None, |_, _| {}).await.unwrap();
78/// # };
79/// ```
80pub fn decrypt_room_key_export(
81    mut input: impl Read,
82    passphrase: &str,
83) -> Result<Vec<ExportedRoomKey>, KeyExportError> {
84    let mut x: String = String::new();
85
86    input.read_to_string(&mut x)?;
87
88    if !(x.trim_start().starts_with(HEADER) && x.trim_end().ends_with(FOOTER)) {
89        return Err(KeyExportError::InvalidHeaders);
90    }
91
92    let payload: String =
93        x.lines().filter(|l| !(l.starts_with(HEADER) || l.starts_with(FOOTER))).collect();
94
95    let mut decrypted = decrypt_helper(&payload, passphrase)?;
96
97    let ret = serde_json::from_str(&decrypted);
98
99    decrypted.zeroize();
100
101    Ok(ret?)
102}
103
104/// Encrypt the list of exported room keys using the given passphrase.
105///
106/// # Arguments
107///
108/// - `keys` - A list of sessions that should be encrypted.
109/// - `passphrase` - The passphrase that will be used to encrypt the exported
110///   room keys.
111///
112/// - `rounds` - The number of rounds that should be used for the key derivation
113///   when the passphrase gets turned into an AES key. More rounds are
114///   increasingly computationally intensive and as such help against
115///   brute-force attacks. Should be at least `10_000`, while values in the
116///   `100_000` ranges should be preferred.
117///
118/// # Panics
119///
120/// This method will panic if it can't get enough randomness from the OS to
121/// encrypt the exported keys securely.
122///
123/// # Examples
124///
125/// ```no_run
126/// # use matrix_sdk_crypto::{OlmMachine, encrypt_room_key_export};
127/// # use ruma::{device_id, user_id, room_id};
128/// # let alice = user_id!("@alice:example.org");
129/// # async {
130/// # let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
131/// let room_id = room_id!("!test:localhost");
132/// let exported_keys = machine.store().export_room_keys(|s| s.room_id() == room_id).await.unwrap();
133/// let encrypted_export = encrypt_room_key_export(&exported_keys, "1234", 1);
134/// # };
135/// ```
136pub fn encrypt_room_key_export(
137    keys: &[ExportedRoomKey],
138    passphrase: &str,
139    rounds: u32,
140) -> Result<String, SerdeError> {
141    let mut plaintext = serde_json::to_string(keys)?.into_bytes();
142    let ciphertext = encrypt_helper(&plaintext, passphrase, rounds);
143
144    plaintext.zeroize();
145
146    Ok([HEADER.to_owned(), ciphertext, FOOTER.to_owned()].join("\n"))
147}
148
149fn encrypt_helper(plaintext: &[u8], passphrase: &str, rounds: u32) -> String {
150    let mut salt = [0u8; SALT_SIZE];
151    let mut rng = rng();
152
153    rng.fill_bytes(&mut salt);
154
155    let key = AesHmacSha2Key::from_passphrase(passphrase, rounds, &salt);
156    let (ciphertext, initialization_vector) = key.encrypt(plaintext.to_owned());
157
158    let mut payload = [
159        VERSION.to_be_bytes().as_slice(),
160        &salt,
161        &initialization_vector,
162        rounds.to_be_bytes().as_slice(),
163        &ciphertext,
164    ]
165    .concat();
166
167    let mac = key.create_mac_tag(&payload);
168    payload.extend(mac.as_bytes());
169
170    base64_encode(payload)
171}
172
173fn decrypt_helper(ciphertext: &str, passphrase: &str) -> Result<String, KeyExportError> {
174    let decoded = base64_decode(ciphertext)?;
175
176    let mut decoded = Cursor::new(decoded);
177
178    let mut salt = [0u8; SALT_SIZE];
179    let mut iv = [0u8; IV_SIZE];
180    let mut mac = [0u8; MAC_SIZE];
181
182    let version = decoded.read_u8()?;
183    decoded.read_exact(&mut salt)?;
184    decoded.read_exact(&mut iv)?;
185
186    let rounds = decoded.read_u32::<BigEndian>()?;
187    let ciphertext_start = decoded.position() as usize;
188
189    decoded.seek(SeekFrom::End(-32))?;
190    let ciphertext_end = decoded.position() as usize;
191
192    decoded.read_exact(&mut mac)?;
193
194    let mut decoded = decoded.into_inner();
195
196    if version != VERSION {
197        return Err(KeyExportError::UnsupportedVersion);
198    }
199
200    let key = AesHmacSha2Key::from_passphrase(passphrase, rounds, &salt);
201    key.verify_mac(&decoded[0..ciphertext_end], &mac).map_err(|_| KeyExportError::InvalidMac)?;
202
203    let ciphertext = &mut decoded[ciphertext_start..ciphertext_end];
204    let plaintext = key.decrypt(ciphertext.to_owned(), &iv);
205    let ret = String::from_utf8(plaintext);
206
207    Ok(ret?)
208}
209
210#[cfg(all(test, not(target_family = "wasm")))]
211mod proptests {
212    use proptest::prelude::*;
213
214    use super::{decrypt_helper, encrypt_helper};
215
216    proptest! {
217        #[test]
218        fn proptest_encrypt_cycle(plaintext in prop::string::string_regex(".*").unwrap()) {
219            let plaintext_bytes = plaintext.clone().into_bytes();
220
221            let ciphertext = encrypt_helper(&plaintext_bytes, "test", 1);
222            let decrypted = decrypt_helper(&ciphertext, "test").unwrap();
223
224            prop_assert!(plaintext == decrypted);
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use std::{
232        collections::{BTreeMap, BTreeSet},
233        io::Cursor,
234    };
235
236    use matrix_sdk_test::async_test;
237    use ruma::{room_id, user_id};
238
239    use super::{
240        base64_decode, decrypt_helper, decrypt_room_key_export, encrypt_helper,
241        encrypt_room_key_export,
242    };
243    use crate::{
244        RoomKeyImportResult, error::OlmResult,
245        machine::test_helpers::get_prepared_machine_test_helper,
246    };
247
248    const PASSPHRASE: &str = "1234";
249
250    const TEST_EXPORT: &str = "\
251        -----BEGIN MEGOLM SESSION DATA-----\n\
252        Af7mGhlzQ+eGvHu93u0YXd3D/+vYMs3E7gQqOhuCtkvGAAAAASH7pEdWvFyAP1JUisAcpEo\n\
253        Xke2Q7Kr9hVl/SCc6jXBNeJCZcrUbUV4D/tRQIl3E9L4fOk928YI1J+3z96qiH0uE7hpsCI\n\
254        CkHKwjPU+0XTzFdIk1X8H7sZ+MD/2Sg/q3y8rtUjz7uEj4GUTnb+9SCOTVmJsRfqgUpM1CU\n\
255        bDLytHf1JkohY4tWEgpsCc67xdzgodjr12qYrfg/zNm3LGpxlrffJknw4rk5QFTj4kMbqbD\n\
256        ZZgDTni+HxRTDGge2J620lMOiznvXX+H09Rwruqx5aJvvaaKd86jWRpiO2oSFqHn4u5ONl9\n\
257        41uzm62Sj0eIm6ZbA9NQs87jQw4LxsejhZVL+NdjIg80zVSBTWhTdo0DTnbFSNP4ReOiz0U\n\
258        XosOF8A5T8Vdx2nvA0GXltfcHKVKQYh/LJAkNQ7P9UYL4ae/5TtQZkhB1KxCLTRWqADCl53\n\
259        uBMGpG53EMgY6G6K2DEIOkcv7sdXQF5WpemiSWZqJRWj+cjfs9BpCTbkp/rszWFl2TniWpR\n\
260        RqIbT2jORlN4rTvdtF0F4z1pqP4qWyR3sLNTkXm9CFRzWADNG0RDZKxbCoo6RPvtaCTfaHo\n\
261        SwfvzBS6CjfAG+FOugpV48o7+XetaUUPZ6/tZSPhCdeV8eP9q5r0QwWeXFogzoNzWt4HYx9\n\
262        MdXxzD+f0mtg5gzehrrEEARwI2bCvPpHxlt/Na9oW/GBpkjwR1LSKgg4CtpRyWngPjdEKpZ\n\
263        GYW19pdjg0qdXNk/eqZsQTsNWVo6A\n\
264        -----END MEGOLM SESSION DATA-----\
265    ";
266
267    fn export_without_headers() -> String {
268        TEST_EXPORT.lines().filter(|l| !l.starts_with("-----")).collect()
269    }
270
271    #[test]
272    fn test_decode() {
273        let export = export_without_headers();
274        base64_decode(export).unwrap();
275    }
276
277    #[test]
278    fn test_encrypt_decrypt() {
279        let data = "It's a secret to everybody";
280        let bytes = data.to_owned().into_bytes();
281
282        let encrypted = encrypt_helper(&bytes, PASSPHRASE, 10);
283        let decrypted = decrypt_helper(&encrypted, PASSPHRASE).unwrap();
284
285        assert_eq!(data, decrypted);
286    }
287
288    #[async_test]
289    async fn test_session_encrypt() {
290        let user_id = user_id!("@alice:localhost");
291        let (machine, _) = get_prepared_machine_test_helper(user_id, false).await;
292        let room_id = room_id!("!test:localhost");
293
294        machine.create_outbound_group_session_with_defaults_test_helper(room_id).await.unwrap();
295        let export = machine.store().export_room_keys(|s| s.room_id() == room_id).await.unwrap();
296
297        assert!(!export.is_empty());
298
299        let encrypted = encrypt_room_key_export(&export, "1234", 1).unwrap();
300        let decrypted = decrypt_room_key_export(Cursor::new(encrypted), "1234").unwrap();
301
302        for (exported, decrypted) in export.iter().zip(decrypted.iter()) {
303            assert_eq!(exported.session_key.to_base64(), decrypted.session_key.to_base64());
304        }
305
306        assert_eq!(
307            machine.store().import_exported_room_keys(decrypted, |_, _| {}).await.unwrap(),
308            RoomKeyImportResult::new(0, 1, BTreeMap::new())
309        );
310    }
311
312    #[async_test]
313    async fn test_importing_better_session() -> OlmResult<()> {
314        let user_id = user_id!("@alice:localhost");
315
316        let (machine, _) = get_prepared_machine_test_helper(user_id, false).await;
317        let room_id = room_id!("!test:localhost");
318        let session = machine.create_inbound_session_test_helper(room_id).await?;
319
320        let export = vec![session.export_at_index(10).await];
321
322        let keys = RoomKeyImportResult::new(
323            1,
324            1,
325            BTreeMap::from([(
326                session.room_id().to_owned(),
327                BTreeMap::from([(
328                    session.sender_key().to_base64(),
329                    BTreeSet::from([session.session_id().to_owned()]),
330                )]),
331            )]),
332        );
333
334        assert_eq!(machine.store().import_exported_room_keys(export, |_, _| {}).await?, keys);
335
336        let export = vec![session.export_at_index(10).await];
337        assert_eq!(
338            machine.store().import_exported_room_keys(export, |_, _| {}).await?,
339            RoomKeyImportResult::new(0, 1, BTreeMap::new())
340        );
341
342        let better_export = vec![session.export().await];
343
344        assert_eq!(
345            machine.store().import_exported_room_keys(better_export, |_, _| {}).await?,
346            keys
347        );
348
349        let another_session = machine.create_inbound_session_test_helper(room_id).await?;
350        let export = vec![another_session.export_at_index(10).await];
351
352        let keys = RoomKeyImportResult::new(
353            1,
354            1,
355            BTreeMap::from([(
356                another_session.room_id().to_owned(),
357                BTreeMap::from([(
358                    another_session.sender_key().to_base64(),
359                    BTreeSet::from([another_session.session_id().to_owned()]),
360                )]),
361            )]),
362        );
363
364        assert_eq!(machine.store().import_exported_room_keys(export, |_, _| {}).await?, keys);
365
366        Ok(())
367    }
368
369    #[test]
370    fn test_real_decrypt() {
371        let reader = Cursor::new(TEST_EXPORT);
372        let imported =
373            decrypt_room_key_export(reader, PASSPHRASE).expect("Can't decrypt key export");
374        assert!(!imported.is_empty())
375    }
376}