matrix_sdk_crypto/backups/keys/
decryption.rs1use std::{
16 io::{Cursor, Read},
17 ops::DerefMut,
18};
19
20use ruma::api::client::backup::EncryptedSessionData;
21use thiserror::Error;
22use vodozemac::{
23 pk_encryption::{Message, PkDecryption},
24 Curve25519PublicKey, Curve25519SecretKey,
25};
26use zeroize::{Zeroize, Zeroizing};
27
28use super::MegolmV1BackupKey;
29use crate::{
30 olm::BackedUpRoomKey,
31 store::BackupDecryptionKey,
32 types::{MegolmV1AuthData, RoomKeyBackupInfo},
33};
34
35#[derive(Debug, Error)]
37pub enum DecodeError {
38 #[error("The decoded recovery key has an invalid prefix: expected {0:?}, got {1:?}")]
40 Prefix([u8; 2], [u8; 2]),
41 #[error("The parity byte of the recovery key doesn't match: expected {0:?}, got {1:?}")]
43 Parity(u8, u8),
44 #[error("The decoded recovery key has a invalid length: expected {0}, got {1}")]
46 Length(usize, usize),
47 #[error(transparent)]
49 Base58(#[from] bs58::decode::Error),
50 #[error(transparent)]
52 Base64(#[from] vodozemac::Base64DecodeError),
53 #[error(transparent)]
55 Io(#[from] std::io::Error),
56 #[error(transparent)]
58 PublicKey(#[from] vodozemac::KeyError),
59}
60
61#[derive(Debug, Error)]
63pub enum DecryptionError {
64 #[error("The MAC of the ciphertext didn't pass validation {0}")]
66 Encryption(#[from] vodozemac::pk_encryption::Error),
67 #[error("The message could not been decoded: {0}")]
69 Decoding(#[from] vodozemac::pk_encryption::MessageDecodeError),
70 #[error("The decrypted message isn't valid JSON: {0}")]
73 Json(#[from] serde_json::error::Error),
74}
75
76impl TryFrom<String> for BackupDecryptionKey {
77 type Error = DecodeError;
78
79 fn try_from(value: String) -> Result<Self, Self::Error> {
80 Self::from_base58(&value)
81 }
82}
83
84impl std::fmt::Display for BackupDecryptionKey {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 let string = Zeroizing::new(self.to_base58());
87
88 let string = Zeroizing::new(
89 string
90 .chars()
91 .collect::<Vec<char>>()
92 .chunks(Self::DISPLAY_CHUNK_SIZE)
93 .map(|c| c.iter().collect::<String>())
94 .collect::<Vec<_>>()
95 .join(" "),
96 );
97
98 write!(f, "{}", string.as_str())
99 }
100}
101
102impl BackupDecryptionKey {
103 const PREFIX: [u8; 2] = [0x8b, 0x01];
104 const PREFIX_PARITY: u8 = Self::PREFIX[0] ^ Self::PREFIX[1];
105 const DISPLAY_CHUNK_SIZE: usize = 4;
106
107 fn parity_byte(bytes: &[u8]) -> u8 {
108 bytes.iter().fold(Self::PREFIX_PARITY, |acc, x| acc ^ x)
109 }
110
111 pub fn from_bytes(key: &[u8; Self::KEY_SIZE]) -> Self {
117 let mut inner = Box::new([0u8; Self::KEY_SIZE]);
118 inner.copy_from_slice(key);
119
120 Self::from_boxed_bytes(inner)
121 }
122
123 fn from_boxed_bytes(key: Box<[u8; Self::KEY_SIZE]>) -> Self {
124 Self { inner: key }
125 }
126
127 pub fn as_bytes(&self) -> &[u8; Self::KEY_SIZE] {
129 &self.inner
130 }
131
132 pub fn from_base64(key: &str) -> Result<Self, DecodeError> {
134 let decoded = Zeroizing::new(vodozemac::base64_decode(key)?);
135
136 if decoded.len() != Self::KEY_SIZE {
137 Err(DecodeError::Length(Self::KEY_SIZE, decoded.len()))
138 } else {
139 let mut key = Box::new([0u8; Self::KEY_SIZE]);
140 key.copy_from_slice(&decoded);
141
142 Ok(Self::from_boxed_bytes(key))
143 }
144 }
145
146 pub fn from_base58(value: &str) -> Result<Self, DecodeError> {
148 let value: String = value.chars().filter(|c| !c.is_whitespace()).collect();
150
151 let decoded = bs58::decode(value).with_alphabet(bs58::Alphabet::BITCOIN).into_vec()?;
152 let mut decoded = Cursor::new(decoded);
153
154 let mut prefix = [0u8; 2];
155 let mut key = Box::new([0u8; Self::KEY_SIZE]);
156 let mut expected_parity = [0u8; 1];
157
158 decoded.read_exact(&mut prefix)?;
159 decoded.read_exact(key.deref_mut())?;
160 decoded.read_exact(&mut expected_parity)?;
161
162 let expected_parity = expected_parity[0];
163 let parity = Self::parity_byte(key.as_ref());
164
165 let _ = Zeroizing::new(decoded.into_inner());
166
167 if prefix != Self::PREFIX {
168 Err(DecodeError::Prefix(Self::PREFIX, prefix))
169 } else if expected_parity != parity {
170 Err(DecodeError::Parity(expected_parity, parity))
171 } else {
172 Ok(Self::from_boxed_bytes(key))
173 }
174 }
175
176 pub fn to_base58(&self) -> String {
178 let bytes = Zeroizing::new(
179 [
180 Self::PREFIX.as_ref(),
181 self.inner.as_ref(),
182 [Self::parity_byte(self.inner.as_ref())].as_ref(),
183 ]
184 .concat(),
185 );
186
187 bs58::encode(bytes.as_slice()).with_alphabet(bs58::Alphabet::BITCOIN).into_string()
188 }
189
190 fn get_pk_decryption(&self) -> PkDecryption {
191 let secret_key = Curve25519SecretKey::from_slice(self.inner.as_ref());
192 PkDecryption::from_key(secret_key)
193 }
194
195 pub fn megolm_v1_public_key(&self) -> MegolmV1BackupKey {
197 let pk = self.get_pk_decryption();
198 MegolmV1BackupKey::new(pk.public_key(), None)
199 }
200
201 pub fn to_backup_info(&self) -> RoomKeyBackupInfo {
206 let pk = self.get_pk_decryption();
207 let auth_data = MegolmV1AuthData::new(pk.public_key(), Default::default());
208
209 RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(auth_data)
210 }
211
212 pub fn decrypt_v1(
220 &self,
221 ephemeral_key: &str,
222 mac: &str,
223 ciphertext: &str,
224 ) -> Result<String, DecryptionError> {
225 let message = Message::from_base64(ciphertext, mac, ephemeral_key)?;
226 let pk = self.get_pk_decryption();
227
228 let decrypted = pk.decrypt(&message)?;
229
230 Ok(String::from_utf8_lossy(&decrypted).to_string())
231 }
232
233 pub fn decrypt_session_data(
236 &self,
237 session_data: EncryptedSessionData,
238 ) -> Result<BackedUpRoomKey, DecryptionError> {
239 let message = Message {
240 ciphertext: session_data.ciphertext.into_inner(),
241 mac: session_data.mac.into_inner(),
242 ephemeral_key: Curve25519PublicKey::from_slice(session_data.ephemeral.as_bytes())
243 .map_err(vodozemac::pk_encryption::MessageDecodeError::from)?,
244 };
245
246 let pk = self.get_pk_decryption();
247
248 let mut decrypted = pk.decrypt(&message)?;
249 let result = serde_json::from_slice(&decrypted);
250
251 decrypted.zeroize();
252
253 Ok(result?)
254 }
255
256 pub fn backup_key_matches(&self, info: &RoomKeyBackupInfo) -> bool {
259 match info {
260 RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(info) => {
261 let pk = self.get_pk_decryption();
262 let public_key = pk.public_key();
263
264 info.public_key == public_key
265 }
266 RoomKeyBackupInfo::Other { .. } => false,
267 }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use matrix_sdk_test::async_test;
274 use ruma::api::client::backup::KeyBackupData;
275 use serde_json::json;
276
277 use super::{BackupDecryptionKey, DecodeError};
278 use crate::olm::{BackedUpRoomKey, ExportedRoomKey, InboundGroupSession};
279
280 const TEST_KEY: [u8; 32] = [
281 0x77, 0x07, 0x6D, 0x0A, 0x73, 0x18, 0xA5, 0x7D, 0x3C, 0x16, 0xC1, 0x72, 0x51, 0xB2, 0x66,
282 0x45, 0xDF, 0x4C, 0x2F, 0x87, 0xEB, 0xC0, 0x99, 0x2A, 0xB1, 0x77, 0xFB, 0xA5, 0x1D, 0xB9,
283 0x2C, 0x2A,
284 ];
285
286 fn room_key() -> ExportedRoomKey {
287 let json = json!({
288 "algorithm": "m.megolm.v1.aes-sha2",
289 "sender_key": "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA",
290 "session_id": "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU",
291 "room_id": "!DovneieKSTkdHKpIXy:morpheus.localhost",
292 "session_key": "AQAAAABvWMNZjKFtebYIePKieQguozuoLgzeY6wKcyJjLJcJtQgy1dPqTBD12U+XrYLrRHn\
293 lKmxoozlhFqJl456+9hlHCL+yq+6ScFuBHtJepnY1l2bdLb4T0JMDkNsNErkiLiLnD6yp3J\
294 DSjIhkdHxmup/huygrmroq6/L5TaThEoqvW4DPIuO14btKudsS34FF82pwjKS4p6Mlch+0e\
295 fHAblQV",
296 "sender_claimed_keys":{},
297 "forwarding_curve25519_key_chain":[]
298 });
299
300 serde_json::from_value(json)
301 .expect("We should be able to deserialize our backed up room key")
302 }
303
304 #[test]
305 fn base64_decoding() -> Result<(), DecodeError> {
306 let key = BackupDecryptionKey::new().expect("Can't create a new recovery key");
307
308 let base64 = key.to_base64();
309 let decoded_key = BackupDecryptionKey::from_base64(&base64)?;
310 assert_eq!(key.inner, decoded_key.inner, "The decode key doesn't match the original");
311
312 BackupDecryptionKey::from_base64("i").expect_err("The recovery key is too short");
313
314 Ok(())
315 }
316
317 #[test]
318 fn base58_decoding() -> Result<(), DecodeError> {
319 let key = BackupDecryptionKey::new().expect("Can't create a new recovery key");
320
321 let base64 = key.to_base58();
322 let decoded_key = BackupDecryptionKey::from_base58(&base64)?;
323 assert_eq!(key.inner, decoded_key.inner, "The decode key doesn't match the original");
324
325 let test_key =
326 BackupDecryptionKey::from_base58("EsTcLW2KPGiFwKEA3As5g5c4BXwkqeeJZJV8Q9fugUMNUE4d")?;
327 assert_eq!(
328 test_key.as_bytes(),
329 &TEST_KEY,
330 "The decoded recovery key doesn't match the test key"
331 );
332
333 let test_key = BackupDecryptionKey::from_base58(
334 "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
335 )?;
336 assert_eq!(
337 test_key.as_bytes(),
338 &TEST_KEY,
339 "The decoded recovery key doesn't match the test key"
340 );
341
342 BackupDecryptionKey::from_base58(
343 "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4e",
344 )
345 .expect_err("Can't create a recovery key if the parity byte is invalid");
346
347 Ok(())
348 }
349
350 #[test]
351 fn test_decrypt_key() {
352 let decryption_key =
353 BackupDecryptionKey::from_base64("Ha9cklU/9NqFo9WKdVfGzmqUL/9wlkdxfEitbSIPVXw")
354 .unwrap();
355
356 let data = json!({
357 "first_message_index": 0,
358 "forwarded_count": 0,
359 "is_verified": false,
360 "session_data": {
361 "ephemeral": "HlLi76oV6wxHz3PCqE/bxJi6yF1HnYz5Dq3T+d/KpRw",
362 "ciphertext": "MuM8E3Yc6TSAvhVGb77rQ++jE6p9dRepx63/3YPD2wACKAppkZHeFrnTH6wJ/HSyrmzo\
363 7HfwqVl6tKNpfooSTHqUf6x1LHz+h4B/Id5ITO1WYt16AaI40LOnZqTkJZCfSPuE2oxa\
364 lwEHnCS3biWybutcnrBFPR3LMtaeHvvkb+k3ny9l5ZpsU9G7vCm3XoeYkWfLekWXvDhb\
365 qWrylXD0+CNUuaQJ/S527TzLd4XKctqVjjO/cCH7q+9utt9WJAfK8LGaWT/mZ3AeWjf5\
366 kiqOpKKf5Cn4n5SSil5p/pvGYmjnURvZSEeQIzHgvunIBEPtzK/MYEPOXe/P5achNGlC\
367 x+5N19Ftyp9TFaTFlTWCTi0mpD7ePfCNISrwpozAz9HZc0OhA8+1aSc7rhYFIeAYXFU3\
368 26NuFIFHI5pvpSxjzPQlOA+mavIKmiRAtjlLw11IVKTxgrdT4N8lXeMr4ndCSmvIkAzF\
369 Mo1uZA4fzjiAdQJE4/2WeXFNNpvdfoYmX8Zl9CAYjpSO5HvpwkAbk4/iLEH3hDfCVUwD\
370 fMh05PdGLnxeRpiEFWSMSsJNp+OWAA+5JsF41BoRGrxoXXT+VKqlUDONd+O296Psu8Q+\
371 d8/S618",
372 "mac": "GtMrurhDTwo"
373 }
374 });
375
376 let key_backup_data: KeyBackupData = serde_json::from_value(data).unwrap();
377 let ephemeral = key_backup_data.session_data.ephemeral.encode();
378 let ciphertext = key_backup_data.session_data.ciphertext.encode();
379 let mac = key_backup_data.session_data.mac.encode();
380
381 let decrypted = decryption_key
382 .decrypt_v1(&ephemeral, &mac, &ciphertext)
383 .expect("The backed up key should be decrypted successfully");
384
385 let _: BackedUpRoomKey = serde_json::from_str(&decrypted)
386 .expect("The decrypted payload should contain valid JSON");
387
388 let _ = decryption_key
389 .decrypt_session_data(key_backup_data.session_data)
390 .expect("The backed up key should be decrypted successfully");
391 }
392
393 #[async_test]
394 async fn test_encryption_cycle() {
395 let session = InboundGroupSession::from_export(&room_key()).unwrap();
396
397 let decryption_key = BackupDecryptionKey::new().unwrap();
398 let encryption_key = decryption_key.megolm_v1_public_key();
399
400 let encrypted = encryption_key.encrypt(session).await;
401
402 let _ = decryption_key
403 .decrypt_session_data(encrypted.session_data)
404 .expect("We should be able to decrypt a just encrypted room key");
405 }
406
407 #[test]
408 fn key_matches() {
409 let decryption_key = BackupDecryptionKey::new().unwrap();
410
411 let key_info = decryption_key.to_backup_info();
412
413 assert!(
414 decryption_key.backup_key_matches(&key_info),
415 "The backup info should match the decryption key"
416 );
417 }
418}