matrix_sdk_crypto/
secret_storage.rs

1// Copyright 2023 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
15//! Helpers for implementing the Secrets Storage mechanism from the Matrix
16//! [spec].
17//!
18//! [spec]: https://spec.matrix.org/v1.8/client-server-api/#storage
19
20use std::fmt;
21
22pub use hmac::digest::MacError;
23use hmac::Hmac;
24use pbkdf2::pbkdf2;
25use rand::{
26    distributions::{Alphanumeric, DistString},
27    thread_rng, RngCore,
28};
29use ruma::{
30    events::{
31        secret::request::SecretName,
32        secret_storage::{
33            key::{
34                PassPhrase, SecretStorageEncryptionAlgorithm, SecretStorageKeyEventContent,
35                SecretStorageV1AesHmacSha2Properties,
36            },
37            secret::SecretEncryptedData,
38        },
39        GlobalAccountDataEventContent, GlobalAccountDataEventType,
40    },
41    serde::Base64,
42    UInt,
43};
44use serde::de::Error;
45use sha2::Sha512;
46use subtle::ConstantTimeEq;
47use thiserror::Error;
48use zeroize::{Zeroize, ZeroizeOnDrop};
49
50use crate::ciphers::{AesHmacSha2Key, HmacSha256Mac, IV_SIZE, KEY_SIZE, MAC_SIZE};
51
52/// Error type for the decoding of a [`SecretStorageKey`].
53///
54/// The [`SecretStorageKey`] can be restored from a Base58 encoded string or
55/// from a string containing a passphrase.
56///
57/// This error type is used to report errors when trying to restore from either
58/// of those strings.
59#[derive(Debug, Error)]
60pub enum DecodeError {
61    /// The decoded secret storage key has an invalid prefix.
62    #[error("The decoded secret storage key has an invalid prefix: expected {0:?}, got {1:?}")]
63    Prefix([u8; 2], [u8; 2]),
64    /// The parity byte of the secret storage key didn't match.
65    #[error("The parity byte of the secret storage key doesn't match: expected {0:?}, got {1:?}")]
66    Parity(u8, u8),
67    /// The secret storage key isn't valid Base58.
68    #[error(transparent)]
69    Base58(#[from] bs58::decode::Error),
70    /// The secret storage key isn't valid Base64.
71    #[error(transparent)]
72    Base64(#[from] vodozemac::Base64DecodeError),
73    /// The secret storage key is too short, we couldn't read enough data.
74    #[error("The Base58 decoded key has an invalid length, expected {0}, got {1}")]
75    KeyLength(usize, usize),
76    /// The typed in secret storage was incorrect, the MAC check failed.
77    #[error("The MAC check for the secret storage key failed")]
78    Mac(#[from] MacError),
79    /// The MAC of the secret storage key for the MAC check has an incorrect
80    /// length.
81    #[error(
82        "The MAC of for the secret storage MAC check has an incorrect length, \
83         expected: {0}, got: {1}"
84    )]
85    MacLength(usize, usize),
86    /// The IV of the secret storage key for the MAC check has an incorrect
87    /// length.
88    #[error(
89        "The IV of for the secret storage key MAC check has an incorrect length, \
90         expected: {0}, got: {1}"
91    )]
92    IvLength(usize, usize),
93    /// The secret storage key is using an unsupported secret encryption
94    /// algorithm. Currently only the [`m.secret_storage.v1.aes-hmac-sha2`]
95    /// algorithm is supported.
96    ///
97    /// [`m.secret_storage.v1.aes-hmac-sha2`]: https://spec.matrix.org/v1.8/client-server-api/#msecret_storagev1aes-hmac-sha2
98    #[error("The secret storage key is using an unsupported secret encryption algorithm: {0}")]
99    UnsupportedAlgorithm(String),
100    /// The passphrase-based secret storage key has an excessively high KDF
101    /// iteration count.
102    #[error(
103        "The passphrase-based secret storage key has an excessively high KDF iteration count: {0}"
104    )]
105    KdfIterationCount(UInt),
106}
107
108/// A secret storage key which can be used to store encrypted data in the user's
109/// account data as defined in the [spec].
110///
111/// The secret storage key can be initialized from a passphrase or from a
112/// base58-encoded string.
113///
114/// To bootstrap a new [`SecretStorageKey`], use the [`SecretStorageKey::new()`]
115/// or [`SecretStorageKey::new_from_passphrase()`] method.
116///
117/// After a new [`SecretStorageKey`] has been created, the info about the key
118/// needs to be uploaded to the homeserver as a global account data event. The
119/// event and event type for this can be retrieved using the
120/// [`SecretStorageKey::event_content()`] and [`SecretStorageKey::event_type()`]
121/// methods, respectively.
122///
123/// # Examples
124/// ```no_run
125/// use matrix_sdk_crypto::secret_storage::SecretStorageKey;
126///
127/// // Create a new secret storage key.
128/// let key =
129///     SecretStorageKey::new_from_passphrase("It's a secret to everybody");
130/// // Retrieve the content.
131/// let content = key.event_content();
132/// // Now upload the content to the server and mark the new key as the default one.
133///
134/// // If we want to restore the secret key, we'll need to retrieve the previously uploaded global
135/// // account data event.
136/// let restored_key = SecretStorageKey::from_account_data(
137///     "It's a secret to everybody",
138///     content.to_owned()
139/// );
140/// ```
141///
142/// [spec]: https://spec.matrix.org/v1.8/client-server-api/#secret-storage
143#[derive(Zeroize, ZeroizeOnDrop)]
144pub struct SecretStorageKey {
145    /// Information about the secret storage key.
146    ///
147    /// This is uploaded to the homeserver in a global account data event.
148    #[zeroize(skip)]
149    storage_key_info: SecretStorageKeyEventContent,
150    /// The private key material.
151    secret_key: Box<[u8; 32]>,
152}
153
154#[cfg(not(tarpaulin_include))]
155impl fmt::Debug for SecretStorageKey {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        f.debug_struct("SecretStorageKey")
158            .field("storage_key_info", &self.storage_key_info)
159            .finish_non_exhaustive()
160    }
161}
162
163/// Encrypted data for the AES-CTR/HMAC-SHA-256 secret storage algorithm.
164#[derive(Clone, Debug)]
165pub struct AesHmacSha2EncryptedData {
166    /// The initialization vector that was used to encrypt the ciphertext.
167    pub iv: [u8; IV_SIZE],
168    /// The ciphertext of the message.
169    pub ciphertext: Base64,
170    /// The message authentication code ensuring that the message was not
171    /// forged.
172    pub mac: [u8; MAC_SIZE],
173}
174
175impl TryFrom<SecretEncryptedData> for AesHmacSha2EncryptedData {
176    type Error = serde_json::Error;
177
178    fn try_from(value: SecretEncryptedData) -> Result<Self, Self::Error> {
179        match value {
180            SecretEncryptedData::AesHmacSha2EncryptedData { iv, ciphertext, mac } => {
181                let iv_length = iv.as_bytes().len();
182                let mac_length = mac.as_bytes().len();
183
184                if iv_length != IV_SIZE {
185                    Err(serde_json::Error::custom(format!(
186                        "Invalid initialization vector length, expected length {IV_SIZE}, got: {iv_length}",
187                    )))
188                } else if mac_length != MAC_SIZE {
189                    Err(serde_json::Error::custom(format!(
190                        "Invalid message authentication tag length, expected length {MAC_SIZE}, got: {mac_length}",
191                    )))
192                } else {
193                    let mut mac_array = [0u8; MAC_SIZE];
194                    let mut iv_array = [0u8; IV_SIZE];
195
196                    mac_array.copy_from_slice(mac.as_bytes());
197                    iv_array.copy_from_slice(iv.as_bytes());
198
199                    Ok(Self { iv: iv_array, ciphertext, mac: mac_array })
200                }
201            }
202            _ => Err(serde_json::Error::custom("Unsupported secret storage algorithm")),
203        }
204    }
205}
206
207impl From<AesHmacSha2EncryptedData> for SecretEncryptedData {
208    fn from(value: AesHmacSha2EncryptedData) -> Self {
209        SecretEncryptedData::AesHmacSha2EncryptedData {
210            iv: Base64::new(value.iv.to_vec()),
211            ciphertext: value.ciphertext,
212            mac: Base64::new(value.mac.to_vec()),
213        }
214    }
215}
216
217impl SecretStorageKey {
218    const ZERO_MESSAGE: &'static [u8; 32] = &[0u8; 32];
219    const PREFIX: [u8; 2] = [0x8b, 0x01];
220    const PREFIX_PARITY: u8 = Self::PREFIX[0] ^ Self::PREFIX[1];
221    const DEFAULT_KEY_ID_LEN: usize = 32;
222    #[cfg(not(test))]
223    const DEFAULT_PBKDF_ITERATIONS: u32 = 500_000;
224    #[cfg(test)]
225    const DEFAULT_PBKDF_ITERATIONS: u32 = 10;
226
227    // 35 bytes in total: a 2-byte prefix, 32 bytes for the key material and one
228    // parity byte
229    const DECODED_BASE58_KEY_LEN: usize = 2 + 32 + 1;
230
231    /// Calculate a parity byte for the base58-encoded variant of the
232    /// [`SecretStorageKey`]. Described in the [spec].
233    ///
234    /// [spec]: https://spec.matrix.org/v1.8/client-server-api/#key-representation
235    fn parity_byte(bytes: &[u8]) -> u8 {
236        bytes.iter().fold(Self::PREFIX_PARITY, |acc, x| acc ^ x)
237    }
238
239    /// Check that the [`SecretStorageKey`] is the one described in the given
240    /// [`SecretEncryptionAlgorithm`].
241    ///
242    /// This is done by encrypting a message containing zero bytes and comparing
243    /// the MAC of this encrypted message to the MAC given in the
244    /// [`SecretEncryptionAlgorithm`]. The exact steps are described in the
245    /// [spec].
246    ///
247    /// This check needs to be done every time we restore a [`SecretStorageKey`]
248    /// from a passphrase or from the base58-encoded variant of it.
249    ///
250    /// [spec]: https://spec.matrix.org/v1.8/client-server-api/#msecret_storagev1aes-hmac-sha2
251    fn check_zero_message(&self) -> Result<(), DecodeError> {
252        match &self.storage_key_info.algorithm {
253            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(properties) => {
254                let (Some(iv), Some(mac)) = (&properties.iv, &properties.mac) else {
255                    // The IV and/or MAC are missing from the account data
256                    // content. As the [spec] says, we have to assume that the
257                    // key is valid.
258                    //
259                    // [spec]: https://spec.matrix.org/unstable/client-server-api/#msecret_storagev1aes-hmac-sha2
260                    return Ok(());
261                };
262
263                let iv = iv.as_bytes();
264                let iv_length = iv.len();
265
266                if iv_length != IV_SIZE {
267                    return Err(DecodeError::IvLength(IV_SIZE, iv_length));
268                }
269
270                let mut iv_array = [0u8; 16];
271                iv_array.copy_from_slice(iv);
272
273                // I'm not particularly convinced that this couldn't have been done simpler.
274                // Why do we need to reproduce the ciphertext?
275                // Couldn't we just generate the MAC tag
276                // using the `ZERO_MESSAGE`?
277                //
278                // If someone is reading this and is designing a new secret encryption
279                // algorithm, please consider the above suggestion.
280                let key = AesHmacSha2Key::from_secret_storage_key(&self.secret_key, "");
281                let ciphertext = key.apply_keystream(Self::ZERO_MESSAGE.to_vec(), &iv_array);
282                let expected_mac = HmacSha256Mac::from_slice(mac.as_bytes())
283                    .ok_or_else(|| DecodeError::MacLength(MAC_SIZE, mac.as_bytes().len()))?;
284
285                key.verify_mac(&ciphertext, expected_mac.as_bytes())?;
286
287                Ok(())
288            }
289            custom => Err(DecodeError::UnsupportedAlgorithm(custom.algorithm().to_owned())),
290        }
291    }
292
293    fn create_event_content(key_id: String, key: &[u8; KEY_SIZE]) -> SecretStorageKeyEventContent {
294        let key = AesHmacSha2Key::from_secret_storage_key(key, "");
295
296        let (ciphertext, iv) = key.encrypt(Self::ZERO_MESSAGE.to_vec());
297        let iv = Base64::new(iv.to_vec());
298        let mac = Base64::new(key.create_mac_tag(&ciphertext).as_bytes().to_vec());
299
300        SecretStorageKeyEventContent::new(
301            key_id,
302            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
303                SecretStorageV1AesHmacSha2Properties::new(Some(iv), Some(mac)),
304            ),
305        )
306    }
307
308    /// Create a new random [`SecretStorageKey`].
309    pub fn new() -> Self {
310        let mut key = Box::new([0u8; KEY_SIZE]);
311        let mut rng = thread_rng();
312        rng.fill_bytes(key.as_mut_slice());
313
314        let key_id = Alphanumeric.sample_string(&mut rng, Self::DEFAULT_KEY_ID_LEN);
315
316        Self::from_bytes(key_id, key)
317    }
318
319    /// Create a new passphrase-based [`SecretStorageKey`].
320    ///
321    /// The passphrase will be expanded into a 32-byte key using the `m.pbkdf2`
322    /// algorithm described in the [spec].
323    ///
324    /// [spec]: https://spec.matrix.org/v1.8/client-server-api/#deriving-keys-from-passphrases
325    pub fn new_from_passphrase(passphrase: &str) -> Self {
326        let mut key = Box::new([0u8; 32]);
327        let mut rng = thread_rng();
328        let salt = Alphanumeric.sample_string(&mut rng, Self::DEFAULT_KEY_ID_LEN);
329
330        pbkdf2::<Hmac<Sha512>>(
331            passphrase.as_bytes(),
332            salt.as_bytes(),
333            Self::DEFAULT_PBKDF_ITERATIONS,
334            key.as_mut_slice(),
335        )
336        .expect(
337            "We should be able to expand a passphrase of any length due to \
338             HMAC being able to be initialized with any input size",
339        );
340
341        let key_id = Alphanumeric.sample_string(&mut rng, Self::DEFAULT_KEY_ID_LEN);
342        let mut key = Self::from_bytes(key_id, key);
343
344        key.storage_key_info.passphrase =
345            Some(PassPhrase::new(salt, Self::DEFAULT_PBKDF_ITERATIONS.into()));
346
347        key
348    }
349
350    pub(crate) fn from_bytes(key_id: String, key: Box<[u8; KEY_SIZE]>) -> Self {
351        let storage_key_info = Self::create_event_content(key_id, &key);
352
353        Self { storage_key_info, secret_key: key }
354    }
355
356    /// Restore a [`SecretStorageKey`] from the given input and the description
357    /// of the key.
358    ///
359    /// The [`SecretStorageKeyEventContent`] will contain the description of the
360    /// [`SecretStorageKey`]. The constructor will check if the provided input
361    /// string matches to the description.
362    ///
363    /// The input can be a passphrase or a Base58 export of the
364    /// [`SecretStorageKey`].
365    pub fn from_account_data(
366        input: &str,
367        content: SecretStorageKeyEventContent,
368    ) -> Result<Self, DecodeError> {
369        let key = if let Some(passphrase_info) = &content.passphrase {
370            // If the content defines a passphrase, first try treating the input
371            // as a passphrase.
372            match Self::from_passphrase(input, &content, passphrase_info) {
373                Ok(key) => key,
374                // Let us fallback to Base58 now. If that fails as well, return the original,
375                // passphrase-based error.
376                Err(e) => Self::from_base58(input, &content).map_err(|_| e)?,
377            }
378        } else {
379            // No passphrase info, so it must be base58-encoded.
380            Self::from_base58(input, &content)?
381        };
382
383        Ok(key)
384    }
385
386    fn from_passphrase(
387        passphrase: &str,
388        key_info: &SecretStorageKeyEventContent,
389        passphrase_info: &PassPhrase,
390    ) -> Result<Self, DecodeError> {
391        let mut key = Box::new([0u8; 32]);
392        pbkdf2::<Hmac<Sha512>>(
393            passphrase.as_bytes(),
394            passphrase_info.salt.as_bytes(),
395            passphrase_info
396                .iterations
397                .try_into()
398                .map_err(|_| DecodeError::KdfIterationCount(passphrase_info.iterations))?,
399            key.as_mut_slice(),
400        )
401        .expect(
402            "We should be able to expand a passphrase of any length due to \
403             HMAC being able to be initialized with any input size",
404        );
405
406        let key = Self { storage_key_info: key_info.to_owned(), secret_key: key };
407        key.check_zero_message()?;
408
409        Ok(key)
410    }
411
412    // Parse a secret storage key represented as a base58-encoded string.
413    //
414    // This method reverses the process in the [`SecretStorageKey::to_base58()`]
415    // method.
416    fn parse_base58_key(value: &str) -> Result<Box<[u8; 32]>, DecodeError> {
417        // The spec tells us to remove any whitespace:
418        // > When decoding a raw key, the process should be reversed, with the exception
419        // > that whitespace is insignificant in the user’s input.
420        //
421        // Spec link: https://spec.matrix.org/unstable/client-server-api/#key-representation
422        let value: String = value.chars().filter(|c| !c.is_whitespace()).collect();
423
424        let mut decoded = bs58::decode(value).with_alphabet(bs58::Alphabet::BITCOIN).into_vec()?;
425
426        let mut prefix = [0u8; 2];
427        let mut key = Box::new([0u8; 32]);
428
429        let decoded_len = decoded.len();
430
431        if decoded_len != Self::DECODED_BASE58_KEY_LEN {
432            Err(DecodeError::KeyLength(Self::DECODED_BASE58_KEY_LEN, decoded_len))
433        } else {
434            prefix.copy_from_slice(&decoded[0..2]);
435            key.copy_from_slice(&decoded[2..34]);
436            let expected_parity = decoded[34];
437
438            decoded.zeroize();
439
440            let parity = Self::parity_byte(key.as_ref());
441
442            let unexpected_choice = prefix.ct_ne(&Self::PREFIX);
443            let unexpected_parity = expected_parity.ct_ne(&parity);
444
445            if unexpected_choice.into() {
446                Err(DecodeError::Prefix(Self::PREFIX, prefix))
447            } else if unexpected_parity.into() {
448                Err(DecodeError::Parity(expected_parity, parity))
449            } else {
450                Ok(key)
451            }
452        }
453    }
454
455    /// Try to create a [`SecretStorageKey`] from a Base58 export.
456    fn from_base58(
457        value: &str,
458        key_info: &SecretStorageKeyEventContent,
459    ) -> Result<Self, DecodeError> {
460        let secret_key = Self::parse_base58_key(value)?;
461        let key = Self { storage_key_info: key_info.to_owned(), secret_key };
462        key.check_zero_message()?;
463
464        Ok(key)
465    }
466
467    /// Export the [`SecretStorageKey`] as a base58-encoded string as defined in
468    /// the [spec].
469    ///
470    /// *Note*: This returns a copy of the private key material of the
471    /// [`SecretStorageKey`] as a string. The caller needs to ensure that this
472    /// string is zeroized.
473    ///
474    /// [spec]: https://spec.matrix.org/v1.8/client-server-api/#key-representation
475    pub fn to_base58(&self) -> String {
476        const DISPLAY_CHUNK_SIZE: usize = 4;
477
478        let mut bytes = Box::new([0u8; Self::DECODED_BASE58_KEY_LEN]);
479
480        // The key is prepended by the two prefix bytes, 0x8b and 0x01.
481        bytes[0..2].copy_from_slice(Self::PREFIX.as_slice());
482        bytes[2..34].copy_from_slice(self.secret_key.as_slice());
483
484        // All the bytes in the string above, including the two header bytes, are XORed
485        // together to form a parity byte. This parity byte is appended to the byte
486        // string.
487        bytes[34] = Self::parity_byte(self.secret_key.as_slice());
488
489        // The byte string is encoded using Base58, using the same mapping as is used
490        // for Bitcoin addresses.
491        let base_58 =
492            bs58::encode(bytes.as_slice()).with_alphabet(bs58::Alphabet::BITCOIN).into_string();
493
494        bytes.zeroize();
495
496        // The string is formatted into groups of four characters separated by spaces.
497        let ret = base_58
498            .chars()
499            .collect::<Vec<char>>()
500            .chunks(DISPLAY_CHUNK_SIZE)
501            .map(|c| c.iter().collect::<String>())
502            .collect::<Vec<_>>()
503            .join(" ");
504
505        ret
506    }
507
508    /// Encrypt a given secret string as a Secrets Storage secret with the
509    /// given secret name.
510    ///
511    /// # Examples
512    ///
513    /// ```
514    /// use matrix_sdk_crypto::secret_storage::SecretStorageKey;
515    /// use ruma::events::secret::request::SecretName;
516    ///
517    /// let key = SecretStorageKey::new();
518    /// let secret = "It's a secret to everybody";
519    /// let secret_name = SecretName::from("my-secret");
520    ///
521    /// let encrypted_data = key.encrypt(secret.as_bytes().to_vec(), &secret_name);
522    ///
523    /// let decrypted = key.decrypt(&encrypted_data, &secret_name)?;
524    ///
525    /// assert_eq!(secret.as_bytes(), decrypted);
526    /// # anyhow::Ok(())
527    /// ```
528    pub fn encrypt(
529        &self,
530        plaintext: Vec<u8>,
531        secret_name: &SecretName,
532    ) -> AesHmacSha2EncryptedData {
533        let key = AesHmacSha2Key::from_secret_storage_key(&self.secret_key, secret_name.as_str());
534
535        let (ciphertext, iv) = key.encrypt(plaintext);
536        let mac = key.create_mac_tag(&ciphertext).into_bytes();
537        let ciphertext = Base64::new(ciphertext);
538
539        AesHmacSha2EncryptedData { iv, ciphertext, mac }
540    }
541
542    /// Decrypt the given [`AesHmacSha2EncryptedData`] containing a secret with
543    /// the given secret name.
544    pub fn decrypt(
545        &self,
546        data: &AesHmacSha2EncryptedData,
547        secret_name: &SecretName,
548    ) -> Result<Vec<u8>, MacError> {
549        let key = AesHmacSha2Key::from_secret_storage_key(&self.secret_key, secret_name.as_str());
550        let ciphertext = data.ciphertext.to_owned().into_inner();
551
552        key.verify_mac(&ciphertext, &data.mac)?;
553
554        let plaintext = key.decrypt(ciphertext, &data.iv);
555
556        Ok(plaintext)
557    }
558
559    /// The info about the [`SecretStorageKey`] formatted as a
560    /// [`SecretStorageKeyEventContent`].
561    ///
562    /// The [`SecretStorageKeyEventContent`] contains information about the
563    /// secret storage key. This information can be used to determine whether
564    /// the secret the user has entered is a valid secret for unlocking the
565    /// Secrets Storage (i.e. a valid [`SecretStorageKey`]).
566    pub fn event_content(&self) -> &SecretStorageKeyEventContent {
567        &self.storage_key_info
568    }
569
570    /// The unique ID of this [`SecretStorageKey`].
571    pub fn key_id(&self) -> &str {
572        &self.storage_key_info.key_id
573    }
574
575    /// The event type of this [`SecretStorageKey`].
576    ///
577    /// Can be used when uploading the key info as a
578    /// [`SecretStorageKeyEventContent`] to the homeserver.
579    ///
580    /// The type is equal to the concatenation of the string
581    /// `"m.secret_storage.key."` and the key ID from the
582    /// [`SecretStorageKey::key_id()`] method.
583    pub fn event_type(&self) -> GlobalAccountDataEventType {
584        self.event_content().event_type()
585    }
586}
587
588impl Default for SecretStorageKey {
589    fn default() -> Self {
590        Self::new()
591    }
592}
593
594#[cfg(test)]
595mod test {
596    use assert_matches::assert_matches;
597    use assert_matches2::assert_let;
598    use ruma::events::EventContentFromType;
599    use serde_json::{json, value::to_raw_value};
600
601    use super::*;
602
603    const SECRET_STORAGE_KEY: &[u8; 32] = &[0u8; 32];
604
605    #[test]
606    fn encrypting() {
607        let secret = "It's a secret to everybody";
608        let secret_name = SecretName::from("secret_message");
609
610        let key = SecretStorageKey::from_bytes(
611            "key_id".to_owned(),
612            Box::new(SECRET_STORAGE_KEY.to_owned()),
613        );
614
615        let encrypted = key.encrypt(secret.as_bytes().to_vec(), &secret_name);
616        let decrypted = key
617            .decrypt(&encrypted, &secret_name)
618            .expect("We should be able to decrypt the message we just encrypted");
619
620        assert_eq!(
621            secret.as_bytes(),
622            decrypted,
623            "Encryption roundtrip should result in the same plaintext"
624        );
625    }
626
627    #[test]
628    fn from_passphrase_roundtrip() {
629        let passphrase = "It's a secret to everybody";
630        let secret = "Foobar";
631        let secret_name = SecretName::from("secret_message");
632
633        let key = SecretStorageKey::new_from_passphrase("It's a secret to everybody");
634
635        let encrypted = key.encrypt(secret.as_bytes().to_vec(), &secret_name);
636        let content = to_raw_value(key.event_content())
637            .expect("We should be able to serialize the secret storage key event content");
638
639        let content =
640            SecretStorageKeyEventContent::from_parts(&key.event_type().to_string(), &content)
641                .expect(
642                "We should be able to parse our, just serialized, secret storage key event content",
643            );
644
645        let key = SecretStorageKey::from_account_data(passphrase, content)
646            .expect("We should be able to restore our secret storage key");
647
648        let decrypted = key.decrypt(&encrypted, &secret_name).expect(
649            "We should be able to decrypt the message using the restored secret storage key",
650        );
651
652        assert_eq!(
653            secret.as_bytes(),
654            decrypted,
655            "The encryption roundtrip should produce the same plaintext"
656        );
657    }
658
659    #[test]
660    fn from_base58_roundtrip() {
661        let secret = "Foobar";
662        let secret_name = SecretName::from("secret_message");
663
664        let key = SecretStorageKey::new();
665
666        let encrypted = key.encrypt(secret.as_bytes().to_vec(), &secret_name);
667        let content = to_raw_value(key.event_content())
668            .expect("We should be able to serialize the secret storage key event content");
669
670        let content =
671            SecretStorageKeyEventContent::from_parts(&key.event_type().to_string(), &content)
672                .expect(
673                "We should be able to parse our, just serialized, secret storage key event content",
674            );
675
676        let base58_key = key.to_base58();
677
678        let key = SecretStorageKey::from_account_data(&base58_key, content)
679            .expect("We should be able to restore our secret storage key");
680
681        let decrypted = key.decrypt(&encrypted, &secret_name).expect(
682            "We should be able to decrypt the message using the restored secret storage key",
683        );
684
685        assert_eq!(
686            secret.as_bytes(),
687            decrypted,
688            "The encryption roundtrip should produce the same plaintext"
689        );
690    }
691
692    #[test]
693    fn from_account_data_and_passphrase() {
694        let json = to_raw_value(&json!({
695            "algorithm":"m.secret_storage.v1.aes-hmac-sha2",
696            "iv":"gH2iNpiETFhApvW6/FFEJQ",
697            "mac":"9Lw12m5SKDipNghdQXKjgpfdj1/K7HFI2brO+UWAGoM",
698            "passphrase":{
699                "algorithm":"m.pbkdf2",
700                "salt":"IuLnH7S85YtZmkkBJKwNUKxWF42g9O1H",
701                "iterations":10
702            }
703        }))
704        .unwrap();
705
706        let content = SecretStorageKeyEventContent::from_parts(
707            "m.secret_storage.key.DZkbKc0RtKSq0z8V61w6KBmJCK6OCiIu",
708            &json,
709        )
710        .expect("We should be able to deserialize our static secret storage key");
711
712        SecretStorageKey::from_account_data("It's a secret to everybody", content)
713            .expect("We should be able to restore the secret storage key");
714    }
715
716    #[test]
717    fn from_account_data_and_base58() {
718        let base58_key = "EsTj 3yST y93F SLpB jJsz eAXc 2XzA ygD3 w69H fGaN TKBj jXEd";
719        let key_id = "bmur2d9ypPUH1msSwCxQOJkuKRmJI55e";
720
721        let json = to_raw_value(&json!({
722            "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
723            "iv": "xv5b6/p3ExEw++wTyfSHEg==",
724            "mac": "ujBBbXahnTAMkmPUX2/0+VTfUh63pGyVRuBcDMgmJC8="
725        }))
726        .unwrap();
727
728        let content = SecretStorageKeyEventContent::from_parts(
729            &format!("m.secret_storage.key.{key_id}"),
730            &json,
731        )
732        .expect("We should be able to deserialize our static secret storage key");
733
734        let key = SecretStorageKey::from_account_data(base58_key, content)
735            .expect("We should be able to restore the secret storage key");
736
737        assert_eq!(key_id, key.key_id(), "The key should correctly remember the key ID");
738    }
739
740    #[test]
741    fn invalid_key() {
742        let key = SecretStorageKey::new_from_passphrase("It's a secret to everybody");
743
744        let content = to_raw_value(key.event_content())
745            .expect("We should be able to serialize the secret storage key event content");
746
747        let content =
748            SecretStorageKeyEventContent::from_parts(&key.event_type().to_string(), &content)
749                .expect(
750                "We should be able to parse our, just serialized, secret storage key event content",
751            );
752
753        assert_matches!(
754            SecretStorageKey::from_account_data("It's a secret to nobody", content.to_owned()),
755            Err(DecodeError::Mac(_)),
756            "Using the wrong passphrase should throw a MAC error"
757        );
758
759        let key = SecretStorageKey::new();
760        let base58_key = key.to_base58();
761
762        assert_matches!(
763            SecretStorageKey::from_account_data(&base58_key, content),
764            Err(DecodeError::Mac(_)),
765            "Using the wrong base58 key should throw a MAC error"
766        );
767    }
768
769    /// The `iv` and `mac` properties within the `m.secret_storage.key.*`
770    /// content are optional, and the spec says we must assume the
771    /// passphrase is correct in that case.
772    #[test]
773    fn accepts_any_passphrase_if_mac_and_iv_are_missing() {
774        let mut content = SecretStorageKeyEventContent::new(
775            "my_new_key_id".to_owned(),
776            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
777                SecretStorageV1AesHmacSha2Properties::new(None, None),
778            ),
779        );
780        content.passphrase =
781            Some(PassPhrase::new("salty goodness".to_owned(), UInt::new_saturating(100)));
782
783        SecretStorageKey::from_account_data("It's a secret to nobody", content)
784            .expect("Should accept any passphrase");
785    }
786
787    #[test]
788    fn base58_parsing() {
789        const DECODED_KEY: [u8; 32] = [
790            159, 189, 70, 187, 52, 81, 113, 198, 246, 2, 44, 154, 37, 213, 104, 27, 165, 78, 236,
791            106, 108, 73, 83, 243, 173, 192, 185, 110, 157, 145, 173, 163,
792        ];
793
794        let key = "EsT           pRvZTnjck8    YrhRAtw XLS84Nr2r9S9LGAWDaExVAPBvLRK   ";
795        let parsed_key = SecretStorageKey::parse_base58_key(key)
796            .expect("Whitespace in the Base58 encoded key should not matter");
797
798        assert_eq!(
799            parsed_key.as_slice(),
800            DECODED_KEY,
801            "Decoding the key should produce the correct bytes"
802        );
803
804        let key = "EsTpRvZTnjck8YrhRAtwXLS84Nr2r9S9LGAWDaExVAPBvLRk";
805        assert_matches!(
806            SecretStorageKey::parse_base58_key(key),
807            Err(DecodeError::Parity(..)),
808            "We should detect an invalid parity byte"
809        );
810
811        let key = "AATpRvZTnjck8YrhRAtwXLS84Nr2r9S9LGAWDaExVAPBvLRk";
812        assert_matches!(
813            SecretStorageKey::parse_base58_key(key),
814            Err(DecodeError::Prefix(..)),
815            "We should detect an invalid prefix"
816        );
817
818        let key = "AATpRvZTnjck8YrhRAtwXLS84Nr2r9S9";
819        assert_matches!(
820            SecretStorageKey::parse_base58_key(key),
821            Err(DecodeError::KeyLength(..)),
822            "We should detect if the key isn't of the correct length"
823        );
824
825        let key = "AATpRvZTnjck8YrhRAtwXLS84Nr0OIl";
826        assert_matches!(
827            SecretStorageKey::parse_base58_key(key),
828            Err(DecodeError::Base58(..)),
829            "We should detect if the key isn't Base58"
830        );
831    }
832
833    #[test]
834    fn encrypted_data_decoding() {
835        let json = json!({
836              "iv": "bdfCwu+ECYgZ/jWTkGrQ/A==",
837              "ciphertext": "lCRSSA1lChONEXj/8RyogsgAa8ouQwYDnLr4XBCheRikrZykLRzPCx3doCE=",
838              "mac": "NXeV1dZaOe2JLvQ6Hh6tFto7AgFFdaQnY0l9pruwdtE="
839        });
840
841        let content: SecretEncryptedData = serde_json::from_value(json)
842            .expect("We should be able to deserialize our static JSON content");
843
844        let encrypted_data: AesHmacSha2EncryptedData = content.try_into()
845            .expect("We should be able to convert a valid SecretEncryptedData to a AesHmacSha2EncryptedData struct");
846
847        assert_eq!(
848            encrypted_data.mac,
849            [
850                53, 119, 149, 213, 214, 90, 57, 237, 137, 46, 244, 58, 30, 30, 173, 22, 218, 59, 2,
851                1, 69, 117, 164, 39, 99, 73, 125, 166, 187, 176, 118, 209
852            ]
853        );
854        assert_eq!(
855            encrypted_data.iv,
856            [109, 215, 194, 194, 239, 132, 9, 136, 25, 254, 53, 147, 144, 106, 208, 252]
857        );
858
859        let secret_encrypted_data: SecretEncryptedData = encrypted_data.to_owned().into();
860
861        assert_let!(
862            SecretEncryptedData::AesHmacSha2EncryptedData { iv, ciphertext, mac } =
863                secret_encrypted_data
864        );
865        assert_eq!(mac.as_bytes(), encrypted_data.mac.as_slice());
866        assert_eq!(iv.as_bytes(), encrypted_data.iv.as_slice());
867        assert_eq!(ciphertext, encrypted_data.ciphertext);
868
869        let invalid_mac_json = json!({
870              "iv": "bdfCwu+ECYgZ/jWTkGrQ/A==",
871              "ciphertext": "lCRSSA1lChONEXj/8RyogsgAa8ouQwYDnLr4XBCheRikrZykLRzPCx3doCE=",
872              "mac": "NXeV1dZaOe2JLvQ6Hh6tFtgFFdaQnY0l9pruwdtE"
873        });
874
875        let content: SecretEncryptedData = serde_json::from_value(invalid_mac_json)
876            .expect("We should be able to deserialize our static JSON content");
877
878        let encrypted_data: Result<AesHmacSha2EncryptedData, _> = content.try_into();
879        encrypted_data.expect_err(
880            "We should be able to detect if a SecretEncryptedData content has an invalid MAC",
881        );
882
883        let invalid_iv_json = json!({
884              "iv": "bdfCwu+gZ/jWTkGrQ/A",
885              "ciphertext": "lCRSSA1lChONEXj/8RyogsgAa8ouQwYDnLr4XBCheRikrZykLRzPCx3doCE=",
886              "mac": "NXeV1dZaOe2JLvQ6Hh6tFto7AgFFdaQnY0l9pruwdtE="
887        });
888
889        let content: SecretEncryptedData = serde_json::from_value(invalid_iv_json)
890            .expect("We should be able to deserialize our static JSON content");
891
892        let encrypted_data: Result<AesHmacSha2EncryptedData, _> = content.try_into();
893        encrypted_data.expect_err(
894            "We should be able to detect if a SecretEncryptedData content has an invalid IV",
895        );
896    }
897
898    #[test]
899    fn invalid_key_info() {
900        let base58_key = "EsTj 3yST y93F SLpB jJsz eAXc 2XzA ygD3 w69H fGaN TKBj jXEd";
901
902        let content = SecretStorageKeyEventContent::new(
903            "bmur2d9ypPUH1msSwCxQOJkuKRmJI55e".to_owned(),
904            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
905                SecretStorageV1AesHmacSha2Properties::new(
906                    Some(Base64::new(vec![0u8; 14])),
907                    Some(Base64::new(vec![0u8; 32])),
908                ),
909            ),
910        );
911
912        assert_matches!(
913            SecretStorageKey::from_account_data(base58_key, content),
914            Err(DecodeError::IvLength(..)),
915            "We should correctly detect an invalid IV"
916        );
917
918        let content = SecretStorageKeyEventContent::new(
919            "bmur2d9ypPUH1msSwCxQOJkuKRmJI55e".to_owned(),
920            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
921                SecretStorageV1AesHmacSha2Properties::new(
922                    Some(Base64::new(vec![0u8; 16])),
923                    Some(Base64::new(vec![0u8; 10])),
924                ),
925            ),
926        );
927
928        assert_matches!(
929            SecretStorageKey::from_account_data(base58_key, content),
930            Err(DecodeError::MacLength(..)),
931            "We should correctly detect an invalid MAC"
932        );
933
934        let json = to_raw_value(&json!({
935            "algorithm": "m.secret_storage.custom",
936            "iv": "xv5b6/p3ExEw++wTyfSHEg==",
937            "mac": "ujBBbXahnTAMkmPUX2/0+VTfUh63pGyVRuBcDMgmJC8="
938        }))
939        .unwrap();
940
941        let content = SecretStorageKeyEventContent::from_parts(
942            "m.secret_storage.key.bmur2d9ypPUH1msSwCxQOJkuKRmJI55e",
943            &json,
944        )
945        .expect("We should be able to deserialize our static secret storage key");
946
947        assert_matches!(
948            SecretStorageKey::from_account_data(base58_key, content),
949            Err(DecodeError::UnsupportedAlgorithm(..)),
950            "We should correctly detect a unsupported algorithm"
951        );
952    }
953}