matrix_sdk_crypto/backups/keys/
decryption.rs

1// Copyright 2021 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::{
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/// Error type for the decoding of a [`BackupDecryptionKey`].
36#[derive(Debug, Error)]
37pub enum DecodeError {
38    /// The decoded recovery key has an invalid prefix.
39    #[error("The decoded recovery key has an invalid prefix: expected {0:?}, got {1:?}")]
40    Prefix([u8; 2], [u8; 2]),
41    /// The parity byte of the recovery key didn't match.
42    #[error("The parity byte of the recovery key doesn't match: expected {0:?}, got {1:?}")]
43    Parity(u8, u8),
44    /// The recovery key has an invalid length.
45    #[error("The decoded recovery key has a invalid length: expected {0}, got {1}")]
46    Length(usize, usize),
47    /// The recovery key isn't valid base58.
48    #[error(transparent)]
49    Base58(#[from] bs58::decode::Error),
50    /// The  recovery key isn't valid base64.
51    #[error(transparent)]
52    Base64(#[from] vodozemac::Base64DecodeError),
53    /// The recovery key is too short, we couldn't read enough data.
54    #[error(transparent)]
55    Io(#[from] std::io::Error),
56    /// The recovery key, a Curve25519 public key, couldn't be decoded.
57    #[error(transparent)]
58    PublicKey(#[from] vodozemac::KeyError),
59}
60
61/// Error type describing the failure cases the Pk decryption step can have.
62#[derive(Debug, Error)]
63pub enum DecryptionError {
64    /// The message failed to decrypt.
65    #[error("The MAC of the ciphertext didn't pass validation {0}")]
66    Encryption(#[from] vodozemac::pk_encryption::Error),
67    /// The message failed to be decoded.
68    #[error("The message could not been decoded: {0}")]
69    Decoding(#[from] vodozemac::pk_encryption::MessageDecodeError),
70    /// The decrypted message should contain a backed up room key, but the
71    /// plaintext isn't valid JSON.
72    #[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    /// Create a new decryption key from the given byte array.
112    ///
113    /// **Warning**: You need to make sure that the byte array contains correct
114    /// random data, either by using a random number generator or by using an
115    /// exported version of a previously created [`BackupDecryptionKey`].
116    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    /// Get the decryption key as a raw byte representation.
128    pub fn as_bytes(&self) -> &[u8; Self::KEY_SIZE] {
129        &self.inner
130    }
131
132    /// Try to create a [`BackupDecryptionKey`] from a base64 export.
133    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    /// Try to create a [`BackupDecryptionKey`] from a base58 export.
147    pub fn from_base58(value: &str) -> Result<Self, DecodeError> {
148        // Remove any whitespace we might have
149        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    /// Export the `[`BackupDecryptionKey`] as a base58 encoded string.
177    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    /// Extract the megolm.v1 public key from this [`BackupDecryptionKey`].
196    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    /// Get the [`RoomKeyBackupInfo`] for this [`BackupDecryptionKey`].
202    ///
203    /// The [`RoomKeyBackupInfo`] can be uploaded to the homeserver to activate
204    /// a new backup version.
205    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    /// Try to decrypt the given ciphertext using this [`BackupDecryptionKey`].
213    ///
214    /// This will use the [`m.megolm_backup.v1.curve25519-aes-sha2`] algorithm
215    /// to decrypt the given ciphertext.
216    ///
217    /// [`m.megolm_backup.v1.curve25519-aes-sha2`]:
218    /// https://spec.matrix.org/unstable/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
219    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    /// Try to decrypt the given [`EncryptedSessionData`] using this
234    /// [`BackupDecryptionKey`].
235    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    /// Check if the given public key from the [`RoomKeyBackupInfo`] matches to
257    /// this [`BackupDecryptionKey`].
258    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}