matrix_sdk_crypto/file_encryption/
key_export.rs1use std::io::{Cursor, Read, Seek, SeekFrom};
16
17use byteorder::{BigEndian, ReadBytesExt};
18use rand::{thread_rng, RngCore};
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(
138 keys: &[ExportedRoomKey],
139 passphrase: &str,
140 rounds: u32,
141) -> Result<String, SerdeError> {
142 let mut plaintext = serde_json::to_string(keys)?.into_bytes();
143 let ciphertext = encrypt_helper(&plaintext, passphrase, rounds);
144
145 plaintext.zeroize();
146
147 Ok([HEADER.to_owned(), ciphertext, FOOTER.to_owned()].join("\n"))
148}
149
150fn encrypt_helper(plaintext: &[u8], passphrase: &str, rounds: u32) -> String {
151 let mut salt = [0u8; SALT_SIZE];
152 let mut rng = thread_rng();
153
154 rng.fill_bytes(&mut salt);
155
156 let key = AesHmacSha2Key::from_passphrase(passphrase, rounds, &salt);
157 let (ciphertext, initialization_vector) = key.encrypt(plaintext.to_owned());
158
159 let mut payload = [
160 VERSION.to_be_bytes().as_slice(),
161 &salt,
162 &initialization_vector,
163 rounds.to_be_bytes().as_slice(),
164 &ciphertext,
165 ]
166 .concat();
167
168 let mac = key.create_mac_tag(&payload);
169 payload.extend(mac.as_bytes());
170
171 base64_encode(payload)
172}
173
174fn decrypt_helper(ciphertext: &str, passphrase: &str) -> Result<String, KeyExportError> {
175 let decoded = base64_decode(ciphertext)?;
176
177 let mut decoded = Cursor::new(decoded);
178
179 let mut salt = [0u8; SALT_SIZE];
180 let mut iv = [0u8; IV_SIZE];
181 let mut mac = [0u8; MAC_SIZE];
182
183 let version = decoded.read_u8()?;
184 decoded.read_exact(&mut salt)?;
185 decoded.read_exact(&mut iv)?;
186
187 let rounds = decoded.read_u32::<BigEndian>()?;
188 let ciphertext_start = decoded.position() as usize;
189
190 decoded.seek(SeekFrom::End(-32))?;
191 let ciphertext_end = decoded.position() as usize;
192
193 decoded.read_exact(&mut mac)?;
194
195 let mut decoded = decoded.into_inner();
196
197 if version != VERSION {
198 return Err(KeyExportError::UnsupportedVersion);
199 }
200
201 let key = AesHmacSha2Key::from_passphrase(passphrase, rounds, &salt);
202 key.verify_mac(&decoded[0..ciphertext_end], &mac).map_err(|_| KeyExportError::InvalidMac)?;
203
204 let ciphertext = &mut decoded[ciphertext_start..ciphertext_end];
205 let plaintext = key.decrypt(ciphertext.to_owned(), &iv);
206 let ret = String::from_utf8(plaintext);
207
208 Ok(ret?)
209}
210
211#[cfg(all(test, not(target_arch = "wasm32")))]
212mod proptests {
213 use proptest::prelude::*;
214
215 use super::{decrypt_helper, encrypt_helper};
216
217 proptest! {
218 #[test]
219 fn proptest_encrypt_cycle(plaintext in prop::string::string_regex(".*").unwrap()) {
220 let plaintext_bytes = plaintext.clone().into_bytes();
221
222 let ciphertext = encrypt_helper(&plaintext_bytes, "test", 1);
223 let decrypted = decrypt_helper(&ciphertext, "test").unwrap();
224
225 prop_assert!(plaintext == decrypted);
226 }
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use std::{
233 collections::{BTreeMap, BTreeSet},
234 io::Cursor,
235 };
236
237 use indoc::indoc;
238 use matrix_sdk_test::async_test;
239 use ruma::{room_id, user_id};
240
241 use super::{
242 base64_decode, decrypt_helper, decrypt_room_key_export, encrypt_helper,
243 encrypt_room_key_export,
244 };
245 use crate::{
246 error::OlmResult, machine::test_helpers::get_prepared_machine_test_helper,
247 RoomKeyImportResult,
248 };
249
250 const PASSPHRASE: &str = "1234";
251
252 const TEST_EXPORT: &str = indoc! {"
253 -----BEGIN MEGOLM SESSION DATA-----
254 Af7mGhlzQ+eGvHu93u0YXd3D/+vYMs3E7gQqOhuCtkvGAAAAASH7pEdWvFyAP1JUisAcpEo
255 Xke2Q7Kr9hVl/SCc6jXBNeJCZcrUbUV4D/tRQIl3E9L4fOk928YI1J+3z96qiH0uE7hpsCI
256 CkHKwjPU+0XTzFdIk1X8H7sZ+MD/2Sg/q3y8rtUjz7uEj4GUTnb+9SCOTVmJsRfqgUpM1CU
257 bDLytHf1JkohY4tWEgpsCc67xdzgodjr12qYrfg/zNm3LGpxlrffJknw4rk5QFTj4kMbqbD
258 ZZgDTni+HxRTDGge2J620lMOiznvXX+H09Rwruqx5aJvvaaKd86jWRpiO2oSFqHn4u5ONl9
259 41uzm62Sj0eIm6ZbA9NQs87jQw4LxsejhZVL+NdjIg80zVSBTWhTdo0DTnbFSNP4ReOiz0U
260 XosOF8A5T8Vdx2nvA0GXltfcHKVKQYh/LJAkNQ7P9UYL4ae/5TtQZkhB1KxCLTRWqADCl53
261 uBMGpG53EMgY6G6K2DEIOkcv7sdXQF5WpemiSWZqJRWj+cjfs9BpCTbkp/rszWFl2TniWpR
262 RqIbT2jORlN4rTvdtF0F4z1pqP4qWyR3sLNTkXm9CFRzWADNG0RDZKxbCoo6RPvtaCTfaHo
263 SwfvzBS6CjfAG+FOugpV48o7+XetaUUPZ6/tZSPhCdeV8eP9q5r0QwWeXFogzoNzWt4HYx9
264 MdXxzD+f0mtg5gzehrrEEARwI2bCvPpHxlt/Na9oW/GBpkjwR1LSKgg4CtpRyWngPjdEKpZ
265 GYW19pdjg0qdXNk/eqZsQTsNWVo6A
266 -----END MEGOLM SESSION DATA-----
267 "};
268
269 fn export_without_headers() -> String {
270 TEST_EXPORT.lines().filter(|l| !l.starts_with("-----")).collect()
271 }
272
273 #[test]
274 fn test_decode() {
275 let export = export_without_headers();
276 base64_decode(export).unwrap();
277 }
278
279 #[test]
280 fn test_encrypt_decrypt() {
281 let data = "It's a secret to everybody";
282 let bytes = data.to_owned().into_bytes();
283
284 let encrypted = encrypt_helper(&bytes, PASSPHRASE, 10);
285 let decrypted = decrypt_helper(&encrypted, PASSPHRASE).unwrap();
286
287 assert_eq!(data, decrypted);
288 }
289
290 #[async_test]
291 async fn test_session_encrypt() {
292 let user_id = user_id!("@alice:localhost");
293 let (machine, _) = get_prepared_machine_test_helper(user_id, false).await;
294 let room_id = room_id!("!test:localhost");
295
296 machine.create_outbound_group_session_with_defaults_test_helper(room_id).await.unwrap();
297 let export = machine.store().export_room_keys(|s| s.room_id() == room_id).await.unwrap();
298
299 assert!(!export.is_empty());
300
301 let encrypted = encrypt_room_key_export(&export, "1234", 1).unwrap();
302 let decrypted = decrypt_room_key_export(Cursor::new(encrypted), "1234").unwrap();
303
304 for (exported, decrypted) in export.iter().zip(decrypted.iter()) {
305 assert_eq!(exported.session_key.to_base64(), decrypted.session_key.to_base64());
306 }
307
308 assert_eq!(
309 machine.store().import_exported_room_keys(decrypted, |_, _| {}).await.unwrap(),
310 RoomKeyImportResult::new(0, 1, BTreeMap::new())
311 );
312 }
313
314 #[async_test]
315 async fn test_importing_better_session() -> OlmResult<()> {
316 let user_id = user_id!("@alice:localhost");
317
318 let (machine, _) = get_prepared_machine_test_helper(user_id, false).await;
319 let room_id = room_id!("!test:localhost");
320 let session = machine.create_inbound_session_test_helper(room_id).await?;
321
322 let export = vec![session.export_at_index(10).await];
323
324 let keys = RoomKeyImportResult::new(
325 1,
326 1,
327 BTreeMap::from([(
328 session.room_id().to_owned(),
329 BTreeMap::from([(
330 session.sender_key().to_base64(),
331 BTreeSet::from([session.session_id().to_owned()]),
332 )]),
333 )]),
334 );
335
336 assert_eq!(machine.store().import_exported_room_keys(export, |_, _| {}).await?, keys);
337
338 let export = vec![session.export_at_index(10).await];
339 assert_eq!(
340 machine.store().import_exported_room_keys(export, |_, _| {}).await?,
341 RoomKeyImportResult::new(0, 1, BTreeMap::new())
342 );
343
344 let better_export = vec![session.export().await];
345
346 assert_eq!(
347 machine.store().import_exported_room_keys(better_export, |_, _| {}).await?,
348 keys
349 );
350
351 let another_session = machine.create_inbound_session_test_helper(room_id).await?;
352 let export = vec![another_session.export_at_index(10).await];
353
354 let keys = RoomKeyImportResult::new(
355 1,
356 1,
357 BTreeMap::from([(
358 another_session.room_id().to_owned(),
359 BTreeMap::from([(
360 another_session.sender_key().to_base64(),
361 BTreeSet::from([another_session.session_id().to_owned()]),
362 )]),
363 )]),
364 );
365
366 assert_eq!(machine.store().import_exported_room_keys(export, |_, _| {}).await?, keys);
367
368 Ok(())
369 }
370
371 #[test]
372 fn test_real_decrypt() {
373 let reader = Cursor::new(TEST_EXPORT);
374 let imported =
375 decrypt_room_key_export(reader, PASSPHRASE).expect("Can't decrypt key export");
376 assert!(!imported.is_empty())
377 }
378}