Skip to main content

matrix_sdk_crypto_ffi/
backup_recovery_key.rs

1use std::{collections::HashMap, iter, ops::DerefMut, sync::Arc};
2
3use hmac::Hmac;
4use matrix_sdk_crypto::{
5    backups::DecryptionError,
6    store::{types::BackupDecryptionKey, CryptoStoreError as InnerStoreError},
7};
8use pbkdf2::pbkdf2;
9use rand::{distr::Alphanumeric, rng, RngExt};
10use sha2::Sha512;
11use thiserror::Error;
12use zeroize::Zeroize;
13
14/// The private part of the backup key, the one used for recovery.
15#[derive(uniffi::Object)]
16pub struct BackupRecoveryKey {
17    pub(crate) inner: BackupDecryptionKey,
18    pub(crate) passphrase_info: Option<PassphraseInfo>,
19}
20
21/// Error type for the decryption of backed up room keys.
22#[derive(Debug, Error, uniffi::Error)]
23#[uniffi(flat_error)]
24pub enum PkDecryptionError {
25    /// An internal libolm error happened during decryption.
26    #[error("Error decryption a PkMessage {0}")]
27    Olm(#[from] DecryptionError),
28}
29
30/// Error type for the decoding and storing of the backup key.
31#[derive(Debug, Error, uniffi::Error)]
32#[uniffi(flat_error)]
33pub enum DecodeError {
34    /// An error happened while decoding the recovery key.
35    #[error(transparent)]
36    Decode(#[from] matrix_sdk_crypto::backups::DecodeError),
37    /// An error happened in the storage layer while trying to save the
38    /// decoded recovery key.
39    #[error(transparent)]
40    CryptoStore(#[from] InnerStoreError),
41}
42
43/// Struct containing info about the way the backup key got derived from a
44/// passphrase.
45#[derive(Debug, Clone, uniffi::Record)]
46pub struct PassphraseInfo {
47    /// The salt that was used during key derivation.
48    pub private_key_salt: String,
49    /// The number of PBKDF rounds that were used for key derivation.
50    pub private_key_iterations: i32,
51}
52
53/// The public part of the backup key.
54#[derive(uniffi::Record)]
55pub struct MegolmV1BackupKey {
56    /// The actual base64 encoded public key.
57    pub public_key: String,
58    /// Signatures that have signed our backup key.
59    pub signatures: HashMap<String, HashMap<String, String>>,
60    /// The passphrase info, if the key was derived from one.
61    pub passphrase_info: Option<PassphraseInfo>,
62    /// Get the full name of the backup algorithm this backup key supports.
63    pub backup_algorithm: String,
64}
65
66impl BackupRecoveryKey {
67    const KEY_SIZE: usize = 32;
68    const SALT_SIZE: usize = 32;
69    const PBKDF_ROUNDS: i32 = 500_000;
70}
71
72#[matrix_sdk_ffi_macros::export]
73impl BackupRecoveryKey {
74    /// Create a new random [`BackupRecoveryKey`].
75    #[allow(clippy::new_without_default)]
76    #[uniffi::constructor]
77    pub fn new() -> Arc<Self> {
78        Arc::new(Self { inner: BackupDecryptionKey::new(), passphrase_info: None })
79    }
80
81    /// Try to create a [`BackupRecoveryKey`] from a base 64 encoded string.
82    #[uniffi::constructor]
83    pub fn from_base64(key: String) -> Result<Arc<Self>, DecodeError> {
84        Ok(Arc::new(Self { inner: BackupDecryptionKey::from_base64(&key)?, passphrase_info: None }))
85    }
86
87    /// Try to create a [`BackupRecoveryKey`] from a base 58 encoded string.
88    #[uniffi::constructor]
89    pub fn from_base58(key: String) -> Result<Arc<Self>, DecodeError> {
90        Ok(Arc::new(Self { inner: BackupDecryptionKey::from_base58(&key)?, passphrase_info: None }))
91    }
92
93    /// Create a new [`BackupRecoveryKey`] from the given passphrase.
94    #[uniffi::constructor]
95    pub fn new_from_passphrase(passphrase: String) -> Arc<Self> {
96        let mut rng = rng();
97        let salt: String = iter::repeat(())
98            .map(|()| rng.sample(Alphanumeric))
99            .map(char::from)
100            .take(Self::SALT_SIZE)
101            .collect();
102
103        Self::from_passphrase(passphrase, salt, Self::PBKDF_ROUNDS)
104    }
105
106    /// Restore a [`BackupRecoveryKey`] from the given passphrase.
107    #[uniffi::constructor]
108    pub fn from_passphrase(passphrase: String, salt: String, rounds: i32) -> Arc<Self> {
109        let mut key = Box::new([0u8; Self::KEY_SIZE]);
110        let rounds = rounds as u32;
111
112        pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), salt.as_bytes(), rounds, key.deref_mut())
113            .expect(
114                "We should be able to expand a passphrase of any length due to \
115                 HMAC being able to be initialized with any input size",
116            );
117
118        let backup_decryption_key = BackupDecryptionKey::from_bytes(&key);
119
120        key.zeroize();
121
122        Arc::new(Self {
123            inner: backup_decryption_key,
124            passphrase_info: Some(PassphraseInfo {
125                private_key_salt: salt,
126                private_key_iterations: rounds as i32,
127            }),
128        })
129    }
130
131    /// Convert the recovery key to a base 58 encoded string.
132    pub fn to_base58(&self) -> String {
133        self.inner.to_base58()
134    }
135
136    /// Convert the recovery key to a base 64 encoded string.
137    pub fn to_base64(&self) -> String {
138        self.inner.to_base64()
139    }
140
141    /// Get the public part of the backup key.
142    pub fn megolm_v1_public_key(&self) -> MegolmV1BackupKey {
143        let public_key = self.inner.megolm_v1_public_key();
144
145        let signatures: HashMap<String, HashMap<String, String>> = public_key
146            .signatures()
147            .into_iter()
148            .map(|(k, v)| {
149                (
150                    k.to_string(),
151                    v.into_iter()
152                        .map(|(k, v)| {
153                            (
154                                k.to_string(),
155                                match v {
156                                    Ok(s) => s.to_base64(),
157                                    Err(s) => s.source,
158                                },
159                            )
160                        })
161                        .collect(),
162                )
163            })
164            .collect();
165
166        MegolmV1BackupKey {
167            public_key: public_key.to_base64(),
168            signatures,
169            passphrase_info: self.passphrase_info.clone(),
170            backup_algorithm: public_key.backup_algorithm().to_owned(),
171        }
172    }
173
174    /// Try to decrypt a message that was encrypted using the public part of the
175    /// backup key.
176    pub fn decrypt_v1(
177        &self,
178        ephemeral_key: String,
179        mac: String,
180        ciphertext: String,
181    ) -> Result<String, PkDecryptionError> {
182        self.inner.decrypt_v1(&ephemeral_key, &mac, &ciphertext).map_err(|e| e.into())
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use ruma::api::client::backup::KeyBackupData;
189    use serde_json::json;
190
191    use super::BackupRecoveryKey;
192
193    #[test]
194    fn test_decrypt_key() {
195        let recovery_key = BackupRecoveryKey::from_base64(
196            "Ha9cklU/9NqFo9WKdVfGzmqUL/9wlkdxfEitbSIPVXw".to_owned(),
197        )
198        .unwrap();
199
200        let data = json!({
201            "first_message_index": 0,
202            "forwarded_count": 0,
203            "is_verified": false,
204            "session_data": {
205                "ephemeral": "HlLi76oV6wxHz3PCqE/bxJi6yF1HnYz5Dq3T+d/KpRw",
206                "ciphertext": "MuM8E3Yc6TSAvhVGb77rQ++jE6p9dRepx63/3YPD2wACKAppkZHeFrnTH6wJ/HSyrmzo\
207                               7HfwqVl6tKNpfooSTHqUf6x1LHz+h4B/Id5ITO1WYt16AaI40LOnZqTkJZCfSPuE2oxa\
208                               lwEHnCS3biWybutcnrBFPR3LMtaeHvvkb+k3ny9l5ZpsU9G7vCm3XoeYkWfLekWXvDhb\
209                               qWrylXD0+CNUuaQJ/S527TzLd4XKctqVjjO/cCH7q+9utt9WJAfK8LGaWT/mZ3AeWjf5\
210                               kiqOpKKf5Cn4n5SSil5p/pvGYmjnURvZSEeQIzHgvunIBEPtzK/MYEPOXe/P5achNGlC\
211                               x+5N19Ftyp9TFaTFlTWCTi0mpD7ePfCNISrwpozAz9HZc0OhA8+1aSc7rhYFIeAYXFU3\
212                               26NuFIFHI5pvpSxjzPQlOA+mavIKmiRAtjlLw11IVKTxgrdT4N8lXeMr4ndCSmvIkAzF\
213                               Mo1uZA4fzjiAdQJE4/2WeXFNNpvdfoYmX8Zl9CAYjpSO5HvpwkAbk4/iLEH3hDfCVUwD\
214                               fMh05PdGLnxeRpiEFWSMSsJNp+OWAA+5JsF41BoRGrxoXXT+VKqlUDONd+O296Psu8Q+\
215                               d8/S618",
216                "mac": "GtMrurhDTwo"
217            }
218        });
219
220        let key_backup_data: KeyBackupData = serde_json::from_value(data).unwrap();
221        let ephemeral = key_backup_data.session_data.ephemeral.encode();
222        let ciphertext = key_backup_data.session_data.ciphertext.encode();
223        let mac = key_backup_data.session_data.mac.encode();
224
225        let _ = recovery_key
226            .decrypt_v1(ephemeral, mac, ciphertext)
227            .expect("The backed up key should be decrypted successfully");
228    }
229}