1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
use std::io::{Cursor, Read, Seek, SeekFrom};
use aes::{
cipher::{generic_array::GenericArray, KeyIvInit, StreamCipher},
Aes256,
};
use byteorder::{BigEndian, ReadBytesExt};
use hmac::{Hmac, Mac};
use pbkdf2::pbkdf2;
use rand::{thread_rng, RngCore};
use serde_json::Error as SerdeError;
use sha2::{Sha256, Sha512};
use thiserror::Error;
use zeroize::Zeroize;
use crate::{
olm::ExportedRoomKey,
utilities::{decode, encode, DecodeError},
};
type Aes256Ctr = ctr::Ctr128BE<Aes256>;
const SALT_SIZE: usize = 16;
const IV_SIZE: usize = 16;
const MAC_SIZE: usize = 32;
const KEY_SIZE: usize = 32;
const VERSION: u8 = 1;
const HEADER: &str = "-----BEGIN MEGOLM SESSION DATA-----";
const FOOTER: &str = "-----END MEGOLM SESSION DATA-----";
#[derive(Error, Debug)]
pub enum KeyExportError {
#[error("Invalid or missing key export headers.")]
InvalidHeaders,
#[error("The key export has been encrypted with an unsupported version.")]
UnsupportedVersion,
#[error("The MAC of the encrypted payload is invalid.")]
InvalidMac,
#[error(transparent)]
InvalidUtf8(#[from] std::string::FromUtf8Error),
#[error(transparent)]
Json(#[from] SerdeError),
#[error(transparent)]
Decode(#[from] DecodeError),
#[error(transparent)]
Io(#[from] std::io::Error),
}
pub fn decrypt_key_export(
mut input: impl Read,
passphrase: &str,
) -> Result<Vec<ExportedRoomKey>, KeyExportError> {
let mut x: String = String::new();
input.read_to_string(&mut x)?;
if !(x.trim_start().starts_with(HEADER) && x.trim_end().ends_with(FOOTER)) {
return Err(KeyExportError::InvalidHeaders);
}
let payload: String =
x.lines().filter(|l| !(l.starts_with(HEADER) || l.starts_with(FOOTER))).collect();
let mut decrypted = decrypt_helper(&payload, passphrase)?;
let ret = serde_json::from_str(&decrypted);
decrypted.zeroize();
Ok(ret?)
}
pub fn encrypt_key_export(
keys: &[ExportedRoomKey],
passphrase: &str,
rounds: u32,
) -> Result<String, SerdeError> {
let mut plaintext = serde_json::to_string(keys)?.into_bytes();
let ciphertext = encrypt_helper(&mut plaintext, passphrase, rounds);
plaintext.zeroize();
Ok([HEADER.to_owned(), ciphertext, FOOTER.to_owned()].join("\n"))
}
fn encrypt_helper(plaintext: &mut [u8], passphrase: &str, rounds: u32) -> String {
let mut salt = [0u8; SALT_SIZE];
let mut iv = [0u8; IV_SIZE];
let mut derived_keys = [0u8; KEY_SIZE * 2];
let mut rng = thread_rng();
rng.fill_bytes(&mut salt);
rng.fill_bytes(&mut iv);
let mut iv = u128::from_be_bytes(iv);
iv &= !(1 << 63);
let iv = iv.to_be_bytes();
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), &salt, rounds, &mut derived_keys);
let (key, hmac_key) = derived_keys.split_at(KEY_SIZE);
let key_array = GenericArray::from_slice(key);
let mut aes = Aes256Ctr::new(key_array, &iv.into());
aes.apply_keystream(plaintext);
let mut payload: Vec<u8> = vec![];
payload.extend(&VERSION.to_be_bytes());
payload.extend(&salt);
payload.extend(&iv);
payload.extend(&rounds.to_be_bytes());
payload.extend_from_slice(plaintext);
let mut hmac = Hmac::<Sha256>::new_from_slice(hmac_key).expect("Can't create HMAC object");
hmac.update(&payload);
let mac = hmac.finalize();
payload.extend(mac.into_bytes());
derived_keys.zeroize();
encode(payload)
}
fn decrypt_helper(ciphertext: &str, passphrase: &str) -> Result<String, KeyExportError> {
let decoded = decode(ciphertext)?;
let mut decoded = Cursor::new(decoded);
let mut salt = [0u8; SALT_SIZE];
let mut iv = [0u8; IV_SIZE];
let mut mac = [0u8; MAC_SIZE];
let mut derived_keys = [0u8; KEY_SIZE * 2];
let version = decoded.read_u8()?;
decoded.read_exact(&mut salt)?;
decoded.read_exact(&mut iv)?;
let rounds = decoded.read_u32::<BigEndian>()?;
let ciphertext_start = decoded.position() as usize;
decoded.seek(SeekFrom::End(-32))?;
let ciphertext_end = decoded.position() as usize;
decoded.read_exact(&mut mac)?;
let mut decoded = decoded.into_inner();
if version != VERSION {
return Err(KeyExportError::UnsupportedVersion);
}
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), &salt, rounds, &mut derived_keys);
let (key, hmac_key) = derived_keys.split_at(KEY_SIZE);
let mut hmac = Hmac::<Sha256>::new_from_slice(hmac_key).expect("Can't create an HMAC object");
hmac.update(&decoded[0..ciphertext_end]);
hmac.verify_slice(&mac).map_err(|_| KeyExportError::InvalidMac)?;
let key_array = GenericArray::from_slice(key);
let ciphertext = &mut decoded[ciphertext_start..ciphertext_end];
let mut aes = Aes256Ctr::new(key_array, &iv.into());
aes.apply_keystream(ciphertext);
let ret = String::from_utf8(ciphertext.to_owned());
derived_keys.zeroize();
ciphertext.zeroize();
Ok(ret?)
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod proptests {
use proptest::prelude::*;
use super::{decrypt_helper, encrypt_helper};
proptest! {
#[test]
fn proptest_encrypt_cycle(plaintext in prop::string::string_regex(".*").unwrap()) {
let mut plaintext_bytes = plaintext.clone().into_bytes();
let ciphertext = encrypt_helper(&mut plaintext_bytes, "test", 1);
let decrypted = decrypt_helper(&ciphertext, "test").unwrap();
prop_assert!(plaintext == decrypted);
}
}
}
#[cfg(test)]
mod tests {
use std::{
collections::{BTreeMap, BTreeSet},
io::Cursor,
};
use indoc::indoc;
use matrix_sdk_test::async_test;
use ruma::room_id;
use super::{decode, decrypt_helper, decrypt_key_export, encrypt_helper, encrypt_key_export};
use crate::{error::OlmResult, machine::tests::get_prepared_machine, RoomKeyImportResult};
const PASSPHRASE: &str = "1234";
const TEST_EXPORT: &str = indoc! {"
-----BEGIN MEGOLM SESSION DATA-----
Af7mGhlzQ+eGvHu93u0YXd3D/+vYMs3E7gQqOhuCtkvGAAAAASH7pEdWvFyAP1JUisAcpEo
Xke2Q7Kr9hVl/SCc6jXBNeJCZcrUbUV4D/tRQIl3E9L4fOk928YI1J+3z96qiH0uE7hpsCI
CkHKwjPU+0XTzFdIk1X8H7sZ+MD/2Sg/q3y8rtUjz7uEj4GUTnb+9SCOTVmJsRfqgUpM1CU
bDLytHf1JkohY4tWEgpsCc67xdzgodjr12qYrfg/zNm3LGpxlrffJknw4rk5QFTj4kMbqbD
ZZgDTni+HxRTDGge2J620lMOiznvXX+H09Rwruqx5aJvvaaKd86jWRpiO2oSFqHn4u5ONl9
41uzm62Sj0eIm6ZbA9NQs87jQw4LxsejhZVL+NdjIg80zVSBTWhTdo0DTnbFSNP4ReOiz0U
XosOF8A5T8Vdx2nvA0GXltfcHKVKQYh/LJAkNQ7P9UYL4ae/5TtQZkhB1KxCLTRWqADCl53
uBMGpG53EMgY6G6K2DEIOkcv7sdXQF5WpemiSWZqJRWj+cjfs9BpCTbkp/rszWFl2TniWpR
RqIbT2jORlN4rTvdtF0F4z1pqP4qWyR3sLNTkXm9CFRzWADNG0RDZKxbCoo6RPvtaCTfaHo
SwfvzBS6CjfAG+FOugpV48o7+XetaUUPZ6/tZSPhCdeV8eP9q5r0QwWeXFogzoNzWt4HYx9
MdXxzD+f0mtg5gzehrrEEARwI2bCvPpHxlt/Na9oW/GBpkjwR1LSKgg4CtpRyWngPjdEKpZ
GYW19pdjg0qdXNk/eqZsQTsNWVo6A
-----END MEGOLM SESSION DATA-----
"};
fn export_without_headers() -> String {
TEST_EXPORT.lines().filter(|l| !l.starts_with("-----")).collect()
}
#[test]
fn test_decode() {
let export = export_without_headers();
assert!(decode(export).is_ok());
}
#[test]
fn test_encrypt_decrypt() {
let data = "It's a secret to everybody";
let mut bytes = data.to_owned().into_bytes();
let encrypted = encrypt_helper(&mut bytes, PASSPHRASE, 10);
let decrypted = decrypt_helper(&encrypted, PASSPHRASE).unwrap();
assert_eq!(data, decrypted);
}
#[async_test]
async fn test_session_encrypt() {
let (machine, _) = get_prepared_machine().await;
let room_id = room_id!("!test:localhost");
machine.create_outbound_group_session_with_defaults(room_id).await.unwrap();
let export = machine.export_keys(|s| s.room_id() == room_id).await.unwrap();
assert!(!export.is_empty());
let encrypted = encrypt_key_export(&export, "1234", 1).unwrap();
let decrypted = decrypt_key_export(Cursor::new(encrypted), "1234").unwrap();
for (exported, decrypted) in export.iter().zip(decrypted.iter()) {
assert_eq!(exported.session_key.to_base64(), decrypted.session_key.to_base64());
}
assert_eq!(
machine.import_keys(decrypted, false, |_, _| {}).await.unwrap(),
RoomKeyImportResult::new(0, 1, BTreeMap::new())
);
}
#[async_test]
async fn test_importing_better_session() -> OlmResult<()> {
let (machine, _) = get_prepared_machine().await;
let room_id = room_id!("!test:localhost");
let session = machine.create_inbound_session(room_id).await?;
let export = vec![session.export_at_index(10).await];
let keys = RoomKeyImportResult::new(
1,
1,
BTreeMap::from([(
session.room_id().to_owned(),
BTreeMap::from([(
session.sender_key().to_owned(),
BTreeSet::from([session.session_id().to_owned()]),
)]),
)]),
);
assert_eq!(machine.import_keys(export, false, |_, _| {}).await?, keys,);
let export = vec![session.export_at_index(10).await];
assert_eq!(
machine.import_keys(export, false, |_, _| {}).await?,
RoomKeyImportResult::new(0, 1, BTreeMap::new())
);
let better_export = vec![session.export().await];
assert_eq!(machine.import_keys(better_export, false, |_, _| {}).await?, keys,);
let another_session = machine.create_inbound_session(room_id).await?;
let export = vec![another_session.export_at_index(10).await];
let keys = RoomKeyImportResult::new(
1,
1,
BTreeMap::from([(
another_session.room_id().to_owned(),
BTreeMap::from([(
another_session.sender_key().to_owned(),
BTreeSet::from([another_session.session_id().to_owned()]),
)]),
)]),
);
assert_eq!(machine.import_keys(export, false, |_, _| {}).await?, keys,);
Ok(())
}
#[test]
fn test_real_decrypt() {
let reader = Cursor::new(TEST_EXPORT);
let imported = decrypt_key_export(reader, PASSPHRASE).expect("Can't decrypt key export");
assert!(!imported.is_empty())
}
}