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::{BackupDecryptionKey, CryptoStoreError as InnerStoreError},
7};
8use pbkdf2::pbkdf2;
9use rand::{distributions::Alphanumeric, thread_rng, Rng};
10use sha2::Sha512;
11use thiserror::Error;
12use zeroize::Zeroize;
13
14#[derive(uniffi::Object)]
16pub struct BackupRecoveryKey {
17 pub(crate) inner: BackupDecryptionKey,
18 pub(crate) passphrase_info: Option<PassphraseInfo>,
19}
20
21#[derive(Debug, Error, uniffi::Error)]
23#[uniffi(flat_error)]
24pub enum PkDecryptionError {
25 #[error("Error decryption a PkMessage {0}")]
27 Olm(#[from] DecryptionError),
28}
29
30#[derive(Debug, Error, uniffi::Error)]
32#[uniffi(flat_error)]
33pub enum DecodeError {
34 #[error(transparent)]
36 Decode(#[from] matrix_sdk_crypto::backups::DecodeError),
37 #[error(transparent)]
40 CryptoStore(#[from] InnerStoreError),
41}
42
43#[derive(Debug, Clone, uniffi::Record)]
46pub struct PassphraseInfo {
47 pub private_key_salt: String,
49 pub private_key_iterations: i32,
51}
52
53#[derive(uniffi::Record)]
55pub struct MegolmV1BackupKey {
56 pub public_key: String,
58 pub signatures: HashMap<String, HashMap<String, String>>,
60 pub passphrase_info: Option<PassphraseInfo>,
62 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 #[allow(clippy::new_without_default)]
76 #[uniffi::constructor]
77 pub fn new() -> Arc<Self> {
78 Arc::new(Self {
79 inner: BackupDecryptionKey::new()
80 .expect("Can't gather enough randomness to create a recovery key"),
81 passphrase_info: None,
82 })
83 }
84
85 #[uniffi::constructor]
87 pub fn from_base64(key: String) -> Result<Arc<Self>, DecodeError> {
88 Ok(Arc::new(Self { inner: BackupDecryptionKey::from_base64(&key)?, passphrase_info: None }))
89 }
90
91 #[uniffi::constructor]
93 pub fn from_base58(key: String) -> Result<Arc<Self>, DecodeError> {
94 Ok(Arc::new(Self { inner: BackupDecryptionKey::from_base58(&key)?, passphrase_info: None }))
95 }
96
97 #[uniffi::constructor]
99 pub fn new_from_passphrase(passphrase: String) -> Arc<Self> {
100 let mut rng = thread_rng();
101 let salt: String = iter::repeat(())
102 .map(|()| rng.sample(Alphanumeric))
103 .map(char::from)
104 .take(Self::SALT_SIZE)
105 .collect();
106
107 Self::from_passphrase(passphrase, salt, Self::PBKDF_ROUNDS)
108 }
109
110 #[uniffi::constructor]
112 pub fn from_passphrase(passphrase: String, salt: String, rounds: i32) -> Arc<Self> {
113 let mut key = Box::new([0u8; Self::KEY_SIZE]);
114 let rounds = rounds as u32;
115
116 pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), salt.as_bytes(), rounds, key.deref_mut())
117 .expect(
118 "We should be able to expand a passphrase of any length due to \
119 HMAC being able to be initialized with any input size",
120 );
121
122 let backup_decryption_key = BackupDecryptionKey::from_bytes(&key);
123
124 key.zeroize();
125
126 Arc::new(Self {
127 inner: backup_decryption_key,
128 passphrase_info: Some(PassphraseInfo {
129 private_key_salt: salt,
130 private_key_iterations: rounds as i32,
131 }),
132 })
133 }
134
135 pub fn to_base58(&self) -> String {
137 self.inner.to_base58()
138 }
139
140 pub fn to_base64(&self) -> String {
142 self.inner.to_base64()
143 }
144
145 pub fn megolm_v1_public_key(&self) -> MegolmV1BackupKey {
147 let public_key = self.inner.megolm_v1_public_key();
148
149 let signatures: HashMap<String, HashMap<String, String>> = public_key
150 .signatures()
151 .into_iter()
152 .map(|(k, v)| {
153 (
154 k.to_string(),
155 v.into_iter()
156 .map(|(k, v)| {
157 (
158 k.to_string(),
159 match v {
160 Ok(s) => s.to_base64(),
161 Err(s) => s.source,
162 },
163 )
164 })
165 .collect(),
166 )
167 })
168 .collect();
169
170 MegolmV1BackupKey {
171 public_key: public_key.to_base64(),
172 signatures,
173 passphrase_info: self.passphrase_info.clone(),
174 backup_algorithm: public_key.backup_algorithm().to_owned(),
175 }
176 }
177
178 pub fn decrypt_v1(
181 &self,
182 ephemeral_key: String,
183 mac: String,
184 ciphertext: String,
185 ) -> Result<String, PkDecryptionError> {
186 self.inner.decrypt_v1(&ephemeral_key, &mac, &ciphertext).map_err(|e| e.into())
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use ruma::api::client::backup::KeyBackupData;
193 use serde_json::json;
194
195 use super::BackupRecoveryKey;
196
197 #[test]
198 fn test_decrypt_key() {
199 let recovery_key = BackupRecoveryKey::from_base64(
200 "Ha9cklU/9NqFo9WKdVfGzmqUL/9wlkdxfEitbSIPVXw".to_owned(),
201 )
202 .unwrap();
203
204 let data = json!({
205 "first_message_index": 0,
206 "forwarded_count": 0,
207 "is_verified": false,
208 "session_data": {
209 "ephemeral": "HlLi76oV6wxHz3PCqE/bxJi6yF1HnYz5Dq3T+d/KpRw",
210 "ciphertext": "MuM8E3Yc6TSAvhVGb77rQ++jE6p9dRepx63/3YPD2wACKAppkZHeFrnTH6wJ/HSyrmzo\
211 7HfwqVl6tKNpfooSTHqUf6x1LHz+h4B/Id5ITO1WYt16AaI40LOnZqTkJZCfSPuE2oxa\
212 lwEHnCS3biWybutcnrBFPR3LMtaeHvvkb+k3ny9l5ZpsU9G7vCm3XoeYkWfLekWXvDhb\
213 qWrylXD0+CNUuaQJ/S527TzLd4XKctqVjjO/cCH7q+9utt9WJAfK8LGaWT/mZ3AeWjf5\
214 kiqOpKKf5Cn4n5SSil5p/pvGYmjnURvZSEeQIzHgvunIBEPtzK/MYEPOXe/P5achNGlC\
215 x+5N19Ftyp9TFaTFlTWCTi0mpD7ePfCNISrwpozAz9HZc0OhA8+1aSc7rhYFIeAYXFU3\
216 26NuFIFHI5pvpSxjzPQlOA+mavIKmiRAtjlLw11IVKTxgrdT4N8lXeMr4ndCSmvIkAzF\
217 Mo1uZA4fzjiAdQJE4/2WeXFNNpvdfoYmX8Zl9CAYjpSO5HvpwkAbk4/iLEH3hDfCVUwD\
218 fMh05PdGLnxeRpiEFWSMSsJNp+OWAA+5JsF41BoRGrxoXXT+VKqlUDONd+O296Psu8Q+\
219 d8/S618",
220 "mac": "GtMrurhDTwo"
221 }
222 });
223
224 let key_backup_data: KeyBackupData = serde_json::from_value(data).unwrap();
225 let ephemeral = key_backup_data.session_data.ephemeral.encode();
226 let ciphertext = key_backup_data.session_data.ciphertext.encode();
227 let mac = key_backup_data.session_data.mac.encode();
228
229 let _ = recovery_key
230 .decrypt_v1(ephemeral, mac, ciphertext)
231 .expect("The backed up key should be decrypted successfully");
232 }
233}