matrix_sdk_crypto/file_encryption/
key_export.rs1use 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#[derive(Error, Debug)]
36pub enum KeyExportError {
37 #[error("Invalid or missing key export headers.")]
39 InvalidHeaders,
40 #[error("The key export has been encrypted with an unsupported version.")]
42 UnsupportedVersion,
43 #[error("The MAC of the encrypted payload is invalid.")]
45 InvalidMac,
46 #[error(transparent)]
48 InvalidUtf8(#[from] std::string::FromUtf8Error),
49 #[error(transparent)]
51 Json(#[from] SerdeError),
52 #[error(transparent)]
54 Decode(#[from] vodozemac::Base64DecodeError),
55 #[error(transparent)]
57 Io(#[from] std::io::Error),
58}
59
60pub 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
104pub 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}