Skip to main content

bitwarden_core/key_management/
crypto_client.rs

1#[cfg(feature = "wasm")]
2use bitwarden_crypto::safe::{PasswordProtectedKeyEnvelope, PasswordProtectedKeyEnvelopeNamespace};
3use bitwarden_crypto::{CryptoError, Decryptable, Kdf, RotateableKeySet, SymmetricKeyAlgorithm};
4#[cfg(feature = "internal")]
5use bitwarden_crypto::{EncString, UnsignedSharedKey};
6use bitwarden_encoding::B64;
7#[cfg(feature = "wasm")]
8use wasm_bindgen::prelude::*;
9
10use super::crypto::{
11    DeriveKeyConnectorError, DeriveKeyConnectorRequest, EnrollAdminPasswordResetError,
12    MakeJitMasterPasswordRegistrationResponse, MakeKeyConnectorRegistrationResponse,
13    MakeKeyPairResponse, VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse,
14    derive_key_connector, make_key_pair, make_user_jit_master_password_registration,
15    make_user_key_connector_registration, verify_asymmetric_keys,
16};
17use crate::key_management::V2UpgradeToken;
18#[cfg(feature = "internal")]
19use crate::key_management::{
20    SymmetricKeyId,
21    crypto::{
22        DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, UpdatePasswordResponse,
23        derive_pin_key, derive_pin_user_key, enroll_admin_password_reset, get_user_encryption_key,
24        initialize_org_crypto, initialize_user_crypto, make_prf_user_key_set,
25    },
26};
27#[expect(deprecated)]
28use crate::{
29    Client,
30    client::encryption_settings::EncryptionSettingsError,
31    error::{NotAuthenticatedError, StatefulCryptoError},
32    key_management::crypto::{
33        CryptoClientError, EnrollPinResponse, MakeKeysError, MakeTdeRegistrationResponse,
34        UpdateKdfResponse, UserCryptoV2KeysResponse, enroll_pin, get_v2_rotated_account_keys,
35        make_update_kdf, make_update_password, make_user_tde_registration,
36        make_v2_keys_for_v1_user,
37    },
38};
39
40/// A client for the crypto operations.
41#[cfg_attr(feature = "wasm", wasm_bindgen)]
42pub struct CryptoClient {
43    pub(crate) client: crate::Client,
44}
45
46#[cfg_attr(feature = "wasm", wasm_bindgen)]
47impl CryptoClient {
48    /// Initialization method for the user crypto. Needs to be called before any other crypto
49    /// operations.
50    pub async fn initialize_user_crypto(
51        &self,
52        req: InitUserCryptoRequest,
53    ) -> Result<(), EncryptionSettingsError> {
54        initialize_user_crypto(&self.client, req).await
55    }
56
57    /// Initialization method for the organization crypto. Needs to be called after
58    /// `initialize_user_crypto` but before any other crypto operations.
59    pub async fn initialize_org_crypto(
60        &self,
61        req: InitOrgCryptoRequest,
62    ) -> Result<(), EncryptionSettingsError> {
63        initialize_org_crypto(&self.client, req).await
64    }
65
66    /// Generates a new key pair and encrypts the private key with the provided user key.
67    /// Crypto initialization not required.
68    pub fn make_key_pair(&self, user_key: B64) -> Result<MakeKeyPairResponse, CryptoError> {
69        make_key_pair(user_key)
70    }
71
72    /// Verifies a user's asymmetric keys by decrypting the private key with the provided user
73    /// key. Returns if the private key is decryptable and if it is a valid matching key.
74    /// Crypto initialization not required.
75    pub fn verify_asymmetric_keys(
76        &self,
77        request: VerifyAsymmetricKeysRequest,
78    ) -> Result<VerifyAsymmetricKeysResponse, CryptoError> {
79        verify_asymmetric_keys(request)
80    }
81
82    /// Makes a new signing key pair and signs the public key for the user
83    pub fn make_keys_for_user_crypto_v2(
84        &self,
85    ) -> Result<UserCryptoV2KeysResponse, StatefulCryptoError> {
86        #[expect(deprecated)]
87        make_v2_keys_for_v1_user(&self.client)
88    }
89
90    /// Creates a rotated set of account keys for the current state
91    pub fn get_v2_rotated_account_keys(
92        &self,
93    ) -> Result<UserCryptoV2KeysResponse, StatefulCryptoError> {
94        #[expect(deprecated)]
95        get_v2_rotated_account_keys(&self.client)
96    }
97
98    /// Create the data necessary to update the user's kdf settings. The user's encryption key is
99    /// re-encrypted for the password under the new kdf settings. This returns the re-encrypted
100    /// user key and the new password hash but does not update sdk state.
101    pub fn make_update_kdf(
102        &self,
103        password: String,
104        kdf: Kdf,
105    ) -> Result<UpdateKdfResponse, CryptoClientError> {
106        make_update_kdf(&self.client, &password, &kdf)
107    }
108
109    /// Protects the current user key with the provided PIN. The result can be stored and later
110    /// used to initialize another client instance by using the PIN and the PIN key with
111    /// `initialize_user_crypto`.
112    pub fn enroll_pin(&self, pin: String) -> Result<EnrollPinResponse, CryptoClientError> {
113        enroll_pin(&self.client, pin)
114    }
115
116    /// Protects the current user key with the provided PIN. The result can be stored and later
117    /// used to initialize another client instance by using the PIN and the PIN key with
118    /// `initialize_user_crypto`. The provided pin is encrypted with the user key.
119    pub fn enroll_pin_with_encrypted_pin(
120        &self,
121        // Note: This will be replaced by `EncString` with https://bitwarden.atlassian.net/browse/PM-24775
122        encrypted_pin: String,
123    ) -> Result<EnrollPinResponse, CryptoClientError> {
124        let encrypted_pin: EncString = encrypted_pin.parse()?;
125        let pin = encrypted_pin.decrypt(
126            &mut self.client.internal.get_key_store().context_mut(),
127            SymmetricKeyId::User,
128        )?;
129        enroll_pin(&self.client, pin)
130    }
131
132    /// Decrypts a `PasswordProtectedKeyEnvelope`, returning the user key, if successful.
133    /// This is a stop-gap solution, until initialization of the SDK is used.
134    #[cfg(any(feature = "wasm", test))]
135    pub fn unseal_password_protected_key_envelope(
136        &self,
137        pin: String,
138        envelope: PasswordProtectedKeyEnvelope,
139    ) -> Result<Vec<u8>, CryptoClientError> {
140        let mut ctx = self.client.internal.get_key_store().context_mut();
141        let key_slot = envelope.unseal(
142            pin.as_str(),
143            PasswordProtectedKeyEnvelopeNamespace::PinUnlock,
144            &mut ctx,
145        )?;
146        #[allow(deprecated)]
147        let key = ctx.dangerous_get_symmetric_key(key_slot)?;
148        Ok(key.to_encoded().to_vec())
149    }
150}
151
152impl CryptoClient {
153    /// Get the uses's decrypted encryption key. Note: It's very important
154    /// to keep this key safe, as it can be used to decrypt all of the user's data
155    pub async fn get_user_encryption_key(&self) -> Result<B64, CryptoClientError> {
156        get_user_encryption_key(&self.client).await
157    }
158
159    /// Create the data necessary to update the user's password. The user's encryption key is
160    /// re-encrypted with the new password. This returns the new encrypted user key and the new
161    /// password hash but does not update sdk state.
162    pub fn make_update_password(
163        &self,
164        new_password: String,
165    ) -> Result<UpdatePasswordResponse, CryptoClientError> {
166        make_update_password(&self.client, new_password)
167    }
168
169    /// Generates a PIN protected user key from the provided PIN. The result can be stored and later
170    /// used to initialize another client instance by using the PIN and the PIN key with
171    /// `initialize_user_crypto`.
172    pub fn derive_pin_key(&self, pin: String) -> Result<DerivePinKeyResponse, CryptoClientError> {
173        derive_pin_key(&self.client, pin)
174    }
175
176    /// Derives the pin protected user key from encrypted pin. Used when pin requires master
177    /// password on first unlock.
178    pub fn derive_pin_user_key(
179        &self,
180        encrypted_pin: EncString,
181    ) -> Result<EncString, CryptoClientError> {
182        derive_pin_user_key(&self.client, encrypted_pin)
183    }
184
185    /// Creates a new rotateable key set for the current user key protected
186    /// by a key derived from the given PRF.
187    pub fn make_prf_user_key_set(&self, prf: B64) -> Result<RotateableKeySet, CryptoClientError> {
188        make_prf_user_key_set(&self.client, prf)
189    }
190
191    /// Prepares the account for being enrolled in the admin password reset feature. This encrypts
192    /// the users [UserKey][bitwarden_crypto::UserKey] with the organization's public key.
193    pub fn enroll_admin_password_reset(
194        &self,
195        public_key: B64,
196    ) -> Result<UnsignedSharedKey, EnrollAdminPasswordResetError> {
197        enroll_admin_password_reset(&self.client, public_key)
198    }
199
200    /// Derive the master key for migrating to the key connector
201    pub fn derive_key_connector(
202        &self,
203        request: DeriveKeyConnectorRequest,
204    ) -> Result<B64, DeriveKeyConnectorError> {
205        derive_key_connector(request)
206    }
207
208    /// Creates a new V2 account cryptographic state for TDE registration.
209    /// This generates fresh cryptographic keys (private key, signing key, signed public key,
210    /// and security state) wrapped with a new user key.
211    pub fn make_user_tde_registration(
212        &self,
213        org_public_key: B64,
214    ) -> Result<MakeTdeRegistrationResponse, MakeKeysError> {
215        make_user_tde_registration(&self.client, org_public_key)
216    }
217
218    /// Creates a new V2 account cryptographic state for Key Connector registration.
219    /// This generates fresh cryptographic keys (private key, signing key, signed public key,
220    /// and security state) wrapped with a new user key.
221    pub fn make_user_key_connector_registration(
222        &self,
223    ) -> Result<MakeKeyConnectorRegistrationResponse, MakeKeysError> {
224        make_user_key_connector_registration(&self.client)
225    }
226
227    /// Creates a new V2 account cryptographic state for SSO JIT master password registration.
228    /// This generates fresh cryptographic keys (private key, signing key, signed public key,
229    /// and security state) wrapped with a new user key.
230    pub fn make_user_jit_master_password_registration(
231        &self,
232        master_password: String,
233        salt: String,
234        org_public_key: B64,
235    ) -> Result<MakeJitMasterPasswordRegistrationResponse, MakeKeysError> {
236        make_user_jit_master_password_registration(
237            &self.client,
238            master_password,
239            salt,
240            org_public_key,
241        )
242    }
243
244    /// Gets the upgraded V2 user key using an upgrade token.
245    /// If the current key is already V2, returns it directly.
246    /// If the current key is V1 and a token is provided, extracts the V2 key.
247    pub fn get_upgraded_user_key(
248        &self,
249        upgrade_token: Option<V2UpgradeToken>,
250    ) -> Result<B64, CryptoClientError> {
251        let mut ctx = self.client.internal.get_key_store().context_mut();
252
253        let algorithm = ctx
254            .get_symmetric_key_algorithm(SymmetricKeyId::User)
255            .map_err(|_| CryptoClientError::NotAuthenticated(NotAuthenticatedError))?;
256
257        match (algorithm, upgrade_token) {
258            // Already V2, return current key
259            (SymmetricKeyAlgorithm::XChaCha20Poly1305, _) => {
260                #[allow(deprecated)]
261                let current_key = ctx
262                    .dangerous_get_symmetric_key(SymmetricKeyId::User)
263                    .map_err(|_| CryptoClientError::NotAuthenticated(NotAuthenticatedError))?;
264                Ok(current_key.clone().to_base64())
265            }
266            // V1 with token, extract V2
267            (SymmetricKeyAlgorithm::Aes256CbcHmac, Some(token)) => {
268                let v2_key_id = token
269                    .unwrap_v2(SymmetricKeyId::User, &mut ctx)
270                    .map_err(|_| CryptoClientError::InvalidUpgradeToken)?;
271                #[allow(deprecated)]
272                let v2_key = ctx
273                    .dangerous_get_symmetric_key(v2_key_id)
274                    .map_err(|_| CryptoClientError::InvalidUpgradeToken)?;
275                Ok(v2_key.clone().to_base64())
276            }
277            // V1 without token, error
278            (SymmetricKeyAlgorithm::Aes256CbcHmac, None) => {
279                Err(CryptoClientError::UpgradeTokenRequired)
280            }
281        }
282    }
283}
284
285impl Client {
286    /// Access to crypto functionality.
287    pub fn crypto(&self) -> CryptoClient {
288        CryptoClient {
289            client: self.clone(),
290        }
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use bitwarden_crypto::{BitwardenLegacyKeyBytes, KeyStore, SymmetricCryptoKey};
297
298    use super::*;
299    use crate::{
300        client::test_accounts::{test_bitwarden_com_account, test_bitwarden_com_account_v2},
301        key_management::{KeyIds, V2UpgradeToken},
302    };
303
304    #[tokio::test]
305    async fn test_enroll_pin_envelope() {
306        // Initialize a test client with user crypto
307        let client = Client::init_test_account(test_bitwarden_com_account()).await;
308        let user_key_initial =
309            SymmetricCryptoKey::try_from(client.crypto().get_user_encryption_key().await.unwrap())
310                .unwrap();
311
312        // Enroll with a PIN, then re-enroll
313        let pin = "1234";
314        let enroll_response = client.crypto().enroll_pin(pin.to_string()).unwrap();
315        let re_enroll_response = client
316            .crypto()
317            .enroll_pin_with_encrypted_pin(enroll_response.user_key_encrypted_pin.to_string())
318            .unwrap();
319
320        let secret = BitwardenLegacyKeyBytes::from(
321            client
322                .crypto()
323                .unseal_password_protected_key_envelope(
324                    pin.to_string(),
325                    re_enroll_response.pin_protected_user_key_envelope,
326                )
327                .unwrap(),
328        );
329        let user_key_final = SymmetricCryptoKey::try_from(&secret).expect("valid user key");
330        assert_eq!(user_key_initial, user_key_final);
331    }
332
333    #[test]
334    fn test_get_upgraded_user_key_not_authenticated() {
335        let client = Client::new(None);
336        let result = client.crypto().get_upgraded_user_key(None);
337        assert!(matches!(
338            result,
339            Err(CryptoClientError::NotAuthenticated(_))
340        ));
341    }
342
343    #[tokio::test]
344    async fn test_get_upgraded_user_key_v1_no_token_returns_error() {
345        let client = Client::init_test_account(test_bitwarden_com_account()).await;
346        let result = client.crypto().get_upgraded_user_key(None);
347        assert!(matches!(
348            result,
349            Err(CryptoClientError::UpgradeTokenRequired)
350        ));
351    }
352
353    #[tokio::test]
354    async fn test_get_upgraded_user_key_v1_with_token_returns_v2_key() {
355        let client = Client::init_test_account(test_bitwarden_com_account()).await;
356
357        // Add a fresh V2 key to the client's keystore and build a token linking it to the V1 key
358        let (token, expected_v2_b64) = {
359            let mut ctx = client.internal.get_key_store().context_mut();
360            let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
361            #[allow(deprecated)]
362            let v2_key = ctx.dangerous_get_symmetric_key(v2_key_id).unwrap().clone();
363            let token = V2UpgradeToken::create(SymmetricKeyId::User, v2_key_id, &ctx).unwrap();
364            (token, v2_key.to_base64())
365        };
366
367        let result = client.crypto().get_upgraded_user_key(Some(token)).unwrap();
368        assert_eq!(result, expected_v2_b64);
369    }
370
371    #[tokio::test]
372    async fn test_get_upgraded_user_key_v1_invalid_token_returns_error() {
373        let client = Client::init_test_account(test_bitwarden_com_account()).await;
374
375        // Token built with a different V1 key — unwrapping with the client's V1 key will fail
376        let mismatched_token = {
377            let key_store = KeyStore::<KeyIds>::default();
378            let mut ctx = key_store.context_mut();
379            let wrong_v1_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
380            let v2_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
381            V2UpgradeToken::create(wrong_v1_id, v2_id, &ctx).unwrap()
382        };
383
384        let result = client
385            .crypto()
386            .get_upgraded_user_key(Some(mismatched_token));
387        assert!(matches!(
388            result,
389            Err(CryptoClientError::InvalidUpgradeToken)
390        ));
391    }
392
393    #[tokio::test]
394    async fn test_get_upgraded_user_key_already_v2_no_token_returns_v2_key() {
395        let client = Client::init_test_account(test_bitwarden_com_account_v2()).await;
396
397        let result = client.crypto().get_upgraded_user_key(None).unwrap();
398        let result_key = SymmetricCryptoKey::try_from(result).unwrap();
399        assert!(
400            matches!(result_key, SymmetricCryptoKey::XChaCha20Poly1305Key(_)),
401            "V2 user should receive a V2 key"
402        );
403    }
404
405    #[tokio::test]
406    async fn test_get_upgraded_user_key_already_v2_with_token_ignored() {
407        let client = Client::init_test_account(test_bitwarden_com_account_v2()).await;
408
409        // Build a structurally valid token with unrelated keys; it must be ignored for V2 users.
410        let dummy_token = {
411            let key_store = KeyStore::<KeyIds>::default();
412            let mut ctx = key_store.context_mut();
413            let v1_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
414            let v2_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
415            V2UpgradeToken::create(v1_id, v2_id, &ctx).unwrap()
416        };
417
418        let result_with_token = client
419            .crypto()
420            .get_upgraded_user_key(Some(dummy_token))
421            .unwrap();
422        let result_no_token = client.crypto().get_upgraded_user_key(None).unwrap();
423        assert_eq!(
424            result_with_token, result_no_token,
425            "Token must be ignored for a V2 user"
426        );
427    }
428}