Skip to main content

bitwarden_core/key_management/
crypto_client.rs

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