Skip to main content

bitwarden_send/
rotate.rs

1#[cfg(feature = "wasm")]
2use bitwarden_core::key_management::KeySlotIds;
3#[cfg(feature = "wasm")]
4use bitwarden_crypto::{CompositeEncryptable, KeyStore, SymmetricCryptoKey};
5#[cfg(feature = "wasm")]
6use bitwarden_encoding::B64;
7
8#[cfg(feature = "wasm")]
9use crate::{Send, SendView, send_client::SendClient, send_client::SendEncryptError};
10
11/// Re-wraps a send's per-item key under `new_key`; the send's content stays under its own
12/// unchanged key.
13#[cfg(feature = "wasm")]
14fn encrypt_send_for_rotation(
15    key_store: &KeyStore<KeySlotIds>,
16    send_view: SendView,
17    new_key: B64,
18) -> Result<Send, SendEncryptError> {
19    let new_key = SymmetricCryptoKey::try_from(new_key)?;
20
21    let mut ctx = key_store.context();
22
23    // `Local` slot, not the view's natural `User` slot — passed explicitly to `encrypt_composite`.
24    let new_key_id = ctx.add_local_symmetric_key(new_key);
25
26    Ok(send_view.encrypt_composite(&mut ctx, new_key_id)?)
27}
28
29#[cfg(feature = "wasm")]
30#[bitwarden_ffi::wasm_export]
31impl SendClient {
32    /// Encrypt a send with the provided key. This should only be used when rotating encryption
33    /// keys in the Web client.
34    ///
35    /// Until Typescript based key-rotation is removed after completing the rollout of sdk-based
36    /// key-rotation, this method must be provided the new symmetric key in base64 format. See
37    /// PM-23084
38    // `async` mirrors `encrypt_cipher_for_rotation`; this rotation does no async work itself.
39    #[allow(clippy::unused_async)]
40    pub async fn encrypt_send_for_rotation(
41        &self,
42        send_view: SendView,
43        new_key: B64,
44    ) -> Result<Send, SendEncryptError> {
45        let key_store = self.client.internal.get_key_store();
46
47        encrypt_send_for_rotation(key_store, send_view, new_key)
48    }
49}
50
51#[cfg(test)]
52#[cfg(feature = "wasm")]
53mod tests {
54    use bitwarden_core::key_management::create_test_crypto_with_user_key;
55    use bitwarden_crypto::{CryptoError, SymmetricCryptoKey, SymmetricKeyAlgorithm};
56
57    use super::*;
58    use crate::{AuthType, SendTextView, SendType};
59
60    fn test_send_view() -> SendView {
61        SendView {
62            id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
63            access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
64            name: "Test".to_string(),
65            notes: None,
66            key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
67            new_password: None,
68            has_password: false,
69            r#type: SendType::Text,
70            file: None,
71            text: Some(SendTextView {
72                text: Some("This is a test".to_owned()),
73                hidden: false,
74            }),
75            data: None,
76            max_access_count: None,
77            access_count: 0,
78            disabled: false,
79            hide_email: false,
80            revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
81            deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
82            expiration_date: None,
83            emails: Vec::new(),
84            auth_type: AuthType::None,
85        }
86    }
87
88    #[test]
89    fn test_encrypt_send_for_rotation() {
90        let user_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
91        let store = create_test_crypto_with_user_key(user_key);
92
93        let new_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
94        let new_key_b64 = new_key.to_base64();
95
96        let send_view = test_send_view();
97
98        let rotated = encrypt_send_for_rotation(&store, send_view.clone(), new_key_b64).unwrap();
99
100        // Round-trips under the new key.
101        let new_store = create_test_crypto_with_user_key(new_key);
102        let decrypted: SendView = new_store.decrypt(&rotated).unwrap();
103        assert_eq!(decrypted.name, send_view.name);
104        assert_eq!(decrypted.text, send_view.text);
105
106        // Fails under the old key, since the send key was re-wrapped.
107        assert!(matches!(
108            store.decrypt::<_, Send, SendView>(&rotated).err(),
109            Some(CryptoError::Decrypt)
110        ));
111    }
112}