Skip to main content

bitwarden_user_crypto_management/key_rotation/
partial_rotateable_keyset.rs

1use std::str::FromStr;
2
3use bitwarden_api_api::models::{
4    OtherDeviceKeysUpdateRequestModel, PasskeyKeyDataResponseModel,
5    TrustedDeviceKeyDataResponseModel, WebAuthnLoginRotateKeyRequestModel,
6};
7#[cfg(test)]
8use bitwarden_core::key_management::PrivateKeySlotId;
9use bitwarden_core::{
10    key_management::{KeySlotIds, SymmetricKeySlotId},
11    require,
12};
13use bitwarden_crypto::{
14    Decryptable, EncString, KeyStoreContext, PrimitiveEncryptable, PublicKey, UnsignedSharedKey,
15};
16
17use crate::key_rotation::KeyRotationDataParseError;
18
19/// A version of a rotateable keyset, missing the upstream-key-encrypted-private-key.
20/// This can only be used to re-share the downstream-key (in this case the user-key) with the
21/// key-set.
22pub(super) struct PartialRotateableKeyset {
23    pub(super) id: uuid::Uuid,
24    pub(super) encrypted_public_key: EncString,
25    pub(super) encrypted_user_key: UnsignedSharedKey,
26}
27
28impl From<PartialRotateableKeyset> for OtherDeviceKeysUpdateRequestModel {
29    fn from(val: PartialRotateableKeyset) -> Self {
30        OtherDeviceKeysUpdateRequestModel {
31            device_id: val.id,
32            encrypted_public_key: val.encrypted_public_key.to_string(),
33            encrypted_user_key: val.encrypted_user_key.to_string(),
34        }
35    }
36}
37
38impl From<PartialRotateableKeyset> for WebAuthnLoginRotateKeyRequestModel {
39    fn from(val: PartialRotateableKeyset) -> Self {
40        WebAuthnLoginRotateKeyRequestModel {
41            id: val.id,
42            encrypted_public_key: val.encrypted_public_key.to_string(),
43            encrypted_user_key: val.encrypted_user_key.to_string(),
44        }
45    }
46}
47
48impl TryFrom<TrustedDeviceKeyDataResponseModel> for PartialRotateableKeyset {
49    type Error = KeyRotationDataParseError;
50
51    fn try_from(d: TrustedDeviceKeyDataResponseModel) -> Result<Self, Self::Error> {
52        Ok(Self {
53            id: require!(d.id),
54            encrypted_public_key: EncString::from_str(&require!(d.encrypted_public_key))?,
55            encrypted_user_key: UnsignedSharedKey::from_str(&require!(d.encrypted_user_key))?,
56        })
57    }
58}
59
60impl TryFrom<PasskeyKeyDataResponseModel> for PartialRotateableKeyset {
61    type Error = KeyRotationDataParseError;
62
63    fn try_from(p: PasskeyKeyDataResponseModel) -> Result<Self, Self::Error> {
64        Ok(Self {
65            id: require!(p.id),
66            encrypted_public_key: EncString::from_str(&require!(p.encrypted_public_key))?,
67            encrypted_user_key: UnsignedSharedKey::from_str(&require!(p.encrypted_user_key))?,
68        })
69    }
70}
71
72impl PartialRotateableKeyset {
73    /// Makes a new `PartialRotateableKeyset` by re-encrypting the user-key. Specifically,
74    /// the user-key-encrypted-public-key is re-encrypted for the new user-key, and the
75    /// public-key-encrypted-user-key is re-created for the new user-key.
76    #[bitwarden_logging::instrument(fields(current_user_key_id = ?current_user_key_id, new_user_key_id = ?new_user_key_id))]
77    pub(super) fn rotate_userkey(
78        &self,
79        current_user_key_id: SymmetricKeySlotId,
80        new_user_key_id: SymmetricKeySlotId,
81        ctx: &mut KeyStoreContext<KeySlotIds>,
82    ) -> Result<PartialRotateableKeyset, ()> {
83        let pubkey_bytes: Vec<u8> = self
84            .encrypted_public_key
85            .decrypt(ctx, current_user_key_id)
86            .map_err(|_| ())?;
87        let pubkey_der =
88            bitwarden_crypto::Bytes::<bitwarden_crypto::SpkiPublicKeyDerContentFormat>::from(
89                pubkey_bytes,
90            );
91        let pubkey = PublicKey::from_der(&pubkey_der).map_err(|_| ())?;
92        let reencrypted_user_key =
93            UnsignedSharedKey::encapsulate(new_user_key_id, &pubkey, ctx).map_err(|_| ())?;
94        let reencrypted_public_key = pubkey_der.encrypt(ctx, new_user_key_id).map_err(|_| ())?;
95        Ok(PartialRotateableKeyset {
96            id: self.id,
97            encrypted_public_key: reencrypted_public_key,
98            encrypted_user_key: reencrypted_user_key,
99        })
100    }
101
102    /// Makes a test `PartialRotateableKeyset` for the given downstream key.
103    /// The private key is stored on the context since it is no present on the partial keyset.
104    #[cfg(test)]
105    pub(crate) fn make_test_keyset(
106        downstream_key_id: SymmetricKeySlotId,
107        ctx: &mut KeyStoreContext<KeySlotIds>,
108    ) -> (Self, PrivateKeySlotId) {
109        use bitwarden_crypto::PublicKeyEncryptionAlgorithm;
110
111        let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
112        let pubkey_der = ctx.get_public_key(private_key).unwrap().to_der().unwrap();
113        let encrypted_public_key = pubkey_der.encrypt(ctx, downstream_key_id).unwrap();
114        let encrypted_user_key = UnsignedSharedKey::encapsulate(
115            downstream_key_id,
116            &ctx.get_public_key(private_key).unwrap(),
117            ctx,
118        )
119        .unwrap();
120        (
121            PartialRotateableKeyset {
122                id: uuid::Uuid::new_v4(),
123                encrypted_public_key,
124                encrypted_user_key,
125            },
126            private_key,
127        )
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use bitwarden_core::key_management::KeySlotIds;
134    use bitwarden_crypto::{Bytes, KeyStore, SpkiPublicKeyDerContentFormat};
135
136    use super::*;
137
138    #[test]
139    fn test_keyset_reencrypt() {
140        let store: KeyStore<KeySlotIds> = KeyStore::default();
141        let mut ctx = store.context_mut();
142
143        // Generate two symmetric keys, old and new
144        let key_id_1 = ctx.generate_symmetric_key();
145        let key_id_2 = ctx.generate_symmetric_key();
146
147        // Generate an asymmetric key pair and encapsulate a symmetric key
148        let (test_keyset, private_key) =
149            PartialRotateableKeyset::make_test_keyset(key_id_1, &mut ctx);
150        let pubkey_der = {
151            let decrypted_pubkey_bytes: Vec<u8> = test_keyset
152                .encrypted_public_key
153                .decrypt(&mut ctx, key_id_1)
154                .expect("decryption should succeed");
155            Bytes::<SpkiPublicKeyDerContentFormat>::from(decrypted_pubkey_bytes)
156        };
157
158        // Re-encrypt the keyset with the new key
159        let reencrypted = test_keyset
160            .rotate_userkey(key_id_1, key_id_2, &mut ctx)
161            .expect("reencrypt should succeed");
162
163        // Check that the re-encrypted user key can be decapsulated and is the same symmetric key
164        let decapsulated = {
165            let decapsulated = reencrypted
166                .encrypted_user_key
167                .decapsulate(private_key, &mut ctx)
168                .expect("decapsulation should succeed");
169            #[expect(deprecated)]
170            ctx.dangerous_get_symmetric_key(decapsulated)
171                .expect("key should exist")
172                .to_owned()
173        };
174
175        let key_2 = {
176            #[expect(deprecated)]
177            ctx.dangerous_get_symmetric_key(key_id_2)
178                .expect("key should exist")
179                .to_owned()
180        };
181        assert_eq!(decapsulated, key_2);
182
183        // Check that the re-encyrpted public-key can be decrypted with the new key
184        let decrypted_pubkey_bytes: Vec<u8> = reencrypted
185            .encrypted_public_key
186            .decrypt(&mut ctx, key_id_2)
187            .expect("decryption should succeed");
188        assert_eq!(decrypted_pubkey_bytes, pubkey_der.as_ref());
189    }
190}