Skip to content
Merged
Show file tree
Hide file tree
Changes from 185 commits
Commits
Show all changes
187 commits
Select commit Hold shift + click to select a range
bc5900b
Implement cose content format
quexten May 9, 2025
2e905c4
Cargo fmt
quexten May 9, 2025
b41f5ef
Fix docs
quexten May 9, 2025
eb70dfc
Fix formatting
quexten May 9, 2025
1e2e95c
Merge main
quexten May 9, 2025
68be6e6
Fix formatting
quexten May 9, 2025
d6a18a4
Fix comment
quexten May 9, 2025
6e9e526
Cleanup
quexten May 21, 2025
50d8f70
Switch from pass by ref to pass by value for enum
quexten May 21, 2025
e5bd251
Add CompositeEncryptable trait
quexten May 21, 2025
b92010b
Cleanup
quexten May 21, 2025
9635098
Merge branch 'main' into km/cose-content-format
quexten May 21, 2025
b8056c2
Fix typo
quexten May 21, 2025
9440825
Typed encryptable
quexten May 21, 2025
f8cb804
Apply cargo fmt
quexten May 22, 2025
e6939e6
Fix build
quexten May 22, 2025
2a202c1
Remove unused imports
quexten May 22, 2025
ba9e2c3
Rename to primitiveencryptablewithcontenttype
quexten May 22, 2025
a42b1e7
Set correct content type for pkcs8
quexten May 22, 2025
5447cbb
Add cose keywrap
quexten May 22, 2025
66c345f
Fix keywrap
quexten May 22, 2025
9b2f6c9
Run cargo fmt
quexten May 22, 2025
88b96b7
Fix encryptable
quexten May 22, 2025
7e69b7b
Run cargo fmt
quexten May 22, 2025
b7d2bc8
Cleanup
quexten May 29, 2025
acb6d12
Rename to PrimitiveEncryptableWithoutContentFormat
quexten May 29, 2025
723ec63
Rename to PrimitiveEncryptable
quexten May 29, 2025
36d8de9
Merge branch 'main' into km/cose-content-format
quexten May 29, 2025
33da07b
Add documentation for the encryptable traits
quexten May 29, 2025
2861a9a
Merge branch 'km/cose-content-format' of github.com:bitwarden/sdk-intโ€ฆ
quexten May 29, 2025
1681df9
Fix clippy errors
quexten Jun 2, 2025
0cd85fa
Fix docs
quexten Jun 2, 2025
f91f0b8
Cargo fmt
quexten Jun 2, 2025
34ee00e
Fix docs
quexten Jun 2, 2025
485b6d8
Fix docs
quexten Jun 2, 2025
f122ff0
Fix docs
quexten Jun 2, 2025
f22531c
Update crates/bitwarden-crypto/src/keys/utils.rs
quexten Jun 4, 2025
ba10631
Update crates/bitwarden-crypto/src/keys/utils.rs
quexten Jun 4, 2025
69d8c57
Update crates/bitwarden-crypto/src/keys/utils.rs
quexten Jun 4, 2025
8833146
Merge branch 'main' into km/cose-content-format
quexten Jun 4, 2025
47ced5a
Merge branch 'main' into km/cose-content-format
quexten Jun 6, 2025
42dccb0
Add docs
quexten Jun 6, 2025
9d0ea55
Merge branch 'km/cose-content-format' of github.com:bitwarden/sdk-intโ€ฆ
quexten Jun 6, 2025
a3ed9d6
Merge branch 'main' into km/cose-content-format
quexten Jun 16, 2025
9eceb32
Cargo fmt
quexten Jun 16, 2025
f30c3ce
Apply fixes
quexten Jun 16, 2025
5f4dc3a
Cleanup
quexten Jun 16, 2025
7b17ca3
Cleanup
quexten Jun 16, 2025
38c8945
Cleanup
quexten Jun 16, 2025
b5dd862
Apply more fixes
quexten Jun 16, 2025
47c7764
Apply fixes
quexten Jun 16, 2025
b009c81
Apply fixes
quexten Jun 16, 2025
c9f6111
Update test vector to include content type
quexten Jun 17, 2025
b9b0f6e
Add bitwarden legacy content type
quexten Jun 17, 2025
67dd5e9
Move content format to separate file
quexten Jun 17, 2025
7635cd0
Remove unused import
quexten Jun 17, 2025
ec14e51
Fix missing parsing for content type
quexten Jun 17, 2025
75be6db
Merge branch 'main' into km/cose-content-format
quexten Jun 17, 2025
6d8bb8f
Fix doc error
quexten Jun 17, 2025
38b8958
Typed byte arrays
quexten Jun 19, 2025
9f334fb
Fix readme
quexten Jun 19, 2025
02cc7b3
Clippy cleanup
quexten Jun 19, 2025
f145eb3
Fix clippy errors
quexten Jun 19, 2025
99d909c
Fix clippy errors
quexten Jun 19, 2025
d47e8c0
Switch bitwarden symmetric crypto key bytes to serialized bytes generic
quexten Jun 19, 2025
1920a49
Rename to bytes
quexten Jun 19, 2025
625b830
Cargo fmt
quexten Jun 19, 2025
24f1431
Simplify encoded symmetric key
quexten Jun 19, 2025
9eb4ff8
Merge branch 'main' into km/cose-content-format
quexten Jun 19, 2025
916a46e
Remove unwrap in example
quexten Jun 19, 2025
8722077
Merge branch 'km/cose-content-format' of github.com:bitwarden/sdk-intโ€ฆ
quexten Jun 19, 2025
670c6a7
Cleanup
quexten Jun 19, 2025
6da4a0b
Fix docs
quexten Jun 19, 2025
4e5510b
Merge branch 'main' into km/cose-content-format
quexten Jun 23, 2025
b00f48b
Make content format trait sealed and add type aliases
quexten Jun 24, 2025
bf9f8c4
Merge branch 'km/cose-content-format' of github.com:bitwarden/sdk-intโ€ฆ
quexten Jun 24, 2025
235f3bc
Apply cargo fmt
quexten Jun 24, 2025
1953ba3
Apply fixes
quexten Jun 24, 2025
41bb1ad
Replace non type aliased Bytes references with type aliases
quexten Jun 24, 2025
c41e513
Apply clippy fixes
quexten Jun 24, 2025
90d2295
Update crates/bitwarden-crypto/src/enc_string/symmetric.rs
quexten Jun 25, 2025
de8f957
Update crates/bitwarden-crypto/src/keys/signed_public_key.rs
quexten Jun 25, 2025
f6ad513
Update crates/bitwarden-crypto/src/enc_string/symmetric.rs
quexten Jun 25, 2025
3c2984b
Update crates/bitwarden-crypto/src/fingerprint.rs
quexten Jun 25, 2025
bdc90b3
Update crates/bitwarden-crypto/src/signing/signed_object.rs
quexten Jun 25, 2025
80dde40
Update crates/bitwarden-crypto/src/signing/signed_object.rs
quexten Jun 25, 2025
1f30896
Update crates/bitwarden-crypto/README.md
quexten Jun 25, 2025
1817ae0
Update crates/bitwarden-core/src/key_management/crypto.rs
quexten Jun 25, 2025
416dbf5
Update crates/bitwarden-core/src/client/encryption_settings.rs
quexten Jun 25, 2025
aaad503
Fix build and cleanup
quexten Jun 25, 2025
2e21906
Remove to_vec from VerifyingKey usages
quexten Jun 25, 2025
dbdee15
Undo take
quexten Jun 25, 2025
375fd0d
Unapply allow missing docs
quexten Jun 25, 2025
fd22ded
Clean up KeyEncryptable or pin key
quexten Jun 25, 2025
7ee0278
Cleanup
quexten Jun 25, 2025
9b3549a
Apply cleanup
quexten Jun 25, 2025
a5abaae
Undo changes to crypto init
quexten Jun 25, 2025
81138ea
Apply allow private interfaces to content format
quexten Jun 25, 2025
4445aec
Cleanup
quexten Jun 25, 2025
25d1907
Typesafe base64 handling
quexten Jun 25, 2025
0d0f59b
Cleanup
quexten Jun 25, 2025
e3ee279
Cleanup cose sign1 types
quexten Jun 25, 2025
bf7bc82
Revert "Typesafe base64 handling"
quexten Jun 26, 2025
1da0173
Revert "Cleanup"
quexten Jun 26, 2025
87f87a6
Cleanup
quexten Jun 26, 2025
15ffca9
Clippy fix
quexten Jun 26, 2025
144620f
Move use under internal flag
quexten Jun 26, 2025
53aeeee
Merge branch 'main' into km/cose-content-format
quexten Jun 26, 2025
81b5a15
Update crates/bitwarden-core/src/client/encryption_settings.rs
quexten Jun 26, 2025
9427634
Update crates/bitwarden-core/src/client/encryption_settings.rs
quexten Jun 26, 2025
d1f8029
Move cose content format trait higher
quexten Jun 26, 2025
7f52fb0
Add docs
quexten Jun 26, 2025
53c528b
Update crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs
quexten Jun 26, 2025
c51e779
Update crates/bitwarden-crypto/src/keys/device_key.rs
quexten Jun 26, 2025
6c8092a
Update crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs
quexten Jun 26, 2025
954a6a5
Clean up symmetric crypto key
quexten Jun 26, 2025
948baa5
Merge branch 'km/cose-content-format' of github.com:bitwarden/sdk-intโ€ฆ
quexten Jun 26, 2025
b2f5211
Fix encryptable docs
quexten Jun 26, 2025
f0b6ec5
Cleanup
quexten Jun 26, 2025
390463c
Small cleanup
quexten Jun 26, 2025
a2e243e
Implement password-protected key envelope
quexten Jun 27, 2025
616786e
Add cfg(test) to function
quexten Jun 27, 2025
b913762
Cargo format
quexten Jun 27, 2025
120f8c6
Fix clippy errors
quexten Jun 27, 2025
b1a615a
Merge branch 'main' into km/beeep/safe-password-protected-key-envelope
quexten Jul 7, 2025
25700ef
Fix private const
quexten Jul 7, 2025
690c6df
Cleanup and add tests
quexten Jul 29, 2025
fabee16
Fix clippy errors
quexten Jul 29, 2025
b81de59
Merge branch 'main' into km/beeep/safe-password-protected-key-envelope
quexten Jul 29, 2025
fc6c32b
Add password protected key envelope initialization
quexten Jul 31, 2025
162a083
Cleanup
quexten Jul 31, 2025
2e6e9e5
Fix clippy
quexten Jul 31, 2025
1986d62
Add tests and fix builds
quexten Jul 31, 2025
41ba6c3
Cleanup and increase test coverage
quexten Jul 31, 2025
8e5fa67
Fix pin envelope initialization
quexten Jul 31, 2025
1fef06f
Update crates/bitwarden-crypto/examples/protect_key_with_password.rs
quexten Aug 11, 2025
b85806a
Update crates/bitwarden-crypto/examples/protect_key_with_password.rs
quexten Aug 11, 2025
7848fa6
Update crates/bitwarden-crypto/examples/protect_key_with_password.rs
quexten Aug 11, 2025
32aacd7
Update crates/bitwarden-crypto/src/safe/password_protected_key_enveloโ€ฆ
quexten Aug 11, 2025
6fd28c2
Update crates/bitwarden-crypto/src/safe/password_protected_key_enveloโ€ฆ
quexten Aug 11, 2025
d601b0c
Update crates/bitwarden-crypto/src/safe/README.md
quexten Aug 11, 2025
6b11472
Update crates/bitwarden-crypto/src/safe/README.md
quexten Aug 11, 2025
b8198bf
Fix typo
quexten Aug 11, 2025
0892687
Re-generate test vectors
quexten Aug 11, 2025
3e03ec3
Cargo fmt
quexten Aug 11, 2025
9345408
Prettier fix formatting
quexten Aug 11, 2025
6fb4e97
Fix link formatting
quexten Aug 11, 2025
f685b35
Merge branch 'main' into km/beeep/safe-password-protected-key-envelope
quexten Aug 12, 2025
02f438c
Merge branch 'km/beeep/safe-password-protected-key-envelope' into km/โ€ฆ
quexten Aug 13, 2025
8b447b3
Move clone
quexten Aug 13, 2025
9cad777
Only expose stop-gap key envelope api on wasm
quexten Aug 13, 2025
ca1a346
Move imports to internal feature
quexten Aug 13, 2025
2efaf4c
Move imports to wasm flagged section
quexten Aug 13, 2025
4a99de2
Fix deadlock in PinEnvelope sdk init
quexten Aug 14, 2025
1357d4b
Cargo fmt
quexten Aug 14, 2025
fbdced4
Add enroll_pin_with_encrypted_pin
quexten Aug 15, 2025
50d2164
Apply feedback
quexten Aug 15, 2025
8ff72f3
Apply feedback
quexten Aug 15, 2025
e65a3d4
Merge branch 'km/beeep/safe-password-protected-key-envelope' of githuโ€ฆ
quexten Aug 15, 2025
3355c82
Update docs
quexten Aug 15, 2025
10f97df
Clean-up recipient parsing
quexten Aug 15, 2025
06baa9e
Make legacy-key content format non-public
quexten Aug 15, 2025
6414e1d
Fix formatting
quexten Aug 15, 2025
9c78d35
Update crates/bitwarden-crypto/src/cose.rs
quexten Aug 15, 2025
01a2903
Merge branch 'main' into km/beeep/safe-password-protected-key-envelope
quexten Aug 15, 2025
2c29cad
Lift-in changes from follow-up PR
quexten Aug 15, 2025
8e911af
Lift-in non-generic wrapper from follow-up PR
quexten Aug 15, 2025
d9c70f7
Fix clippy warnings
quexten Aug 15, 2025
fab6509
Lift in uniffi support for PasswordProtectedKeyEnvelope
quexten Aug 15, 2025
17cdef1
Cargo fmt
quexten Aug 15, 2025
a8f5353
Fix unused import warning on some builds
quexten Aug 15, 2025
eb6bff0
Cargo fmt
quexten Aug 15, 2025
cd53925
Merge branch 'km/beeep/safe-password-protected-key-envelope' into km/โ€ฆ
quexten Aug 15, 2025
f3c5004
Merge branch 'km/pin-unlock' of github.com:bitwarden/sdk-internal intโ€ฆ
quexten Aug 15, 2025
4291314
Fix build
quexten Aug 15, 2025
660aaf1
Undo uniffi changes
quexten Aug 15, 2025
ecab3dc
Attempt to fix build
quexten Aug 15, 2025
9e79c97
Properly expose uniffi calls for PinEnvelope
quexten Aug 15, 2025
b127343
Add test coverage for pin envelope
quexten Aug 15, 2025
9c293f5
Lift PasswordProtectedKeyEnvelopeError to CryptoClientError
quexten Aug 15, 2025
0b97c1a
Merge branch 'main' into km/pin-unlock
quexten Aug 19, 2025
e30b8b0
Update crates/bitwarden-core/src/key_management/crypto.rs
quexten Aug 22, 2025
cb8bfb5
Revert "Update crates/bitwarden-core/src/key_management/crypto.rs"
quexten Aug 22, 2025
c41601a
Merge branch 'main' into km/pin-unlock
quexten Aug 22, 2025
d96a110
Fix build
quexten Aug 22, 2025
6c7c1ec
Merge branch 'main' into km/pin-unlock
quexten Aug 26, 2025
8f0ce39
Fix test
quexten Aug 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/bitwarden-core/src/client/encryption_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ pub enum EncryptionSettingsError {

#[error(transparent)]
UserIdAlreadySetError(#[from] UserIdAlreadySetError),

#[error("Wrong Pin")]
WrongPin,
Comment on lines +48 to +50
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: What error do we return if you pass in the wrong password? It would seem reasonable that we return a similar error for both.

I suspect we derive the master key and then attempt to decrypt the user key which would fail and you'd get a decrypt error? Maybe it would be more elegant to handle that with a better error message in which case both wrong password and pin could use the same error?

Copy link
Contributor Author

@quexten quexten Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With master-key decryption you cannot know if:

  • Your email is different to the one used on creation
  • Your kdf settings are different to the one used on creation
  • Your password is different "wrong" to the one used on creation
  • (Your master-key-wrapped-user-key was tampered with)

and all of these would result in the same error. In most cases the true reason would be "wrong password" but there are reasonable cases such as the KDF settings being out of sync, that would lead to the same error path.

With the PasswordProtectedKeyEnvelope, it's only:

  • Your password is different "wrong"
  • (Your key envelope was tampered with)
    so for me it feels more appropriate to use the "WrongPin" error.

(I'm OK changing both to return "WrongPassword", but then we should document that this could also mean that KDF/salt(email) can be wrong for masterkey encrypted items).

}

#[allow(clippy::large_enum_variant)]
Expand Down
30 changes: 29 additions & 1 deletion crates/bitwarden-core/src/client/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ use crate::{
login_method::UserLoginMethod,
},
error::NotAuthenticatedError,
key_management::{crypto::InitUserCryptoRequest, SecurityState, SignedSecurityState},
key_management::{
crypto::InitUserCryptoRequest, PasswordProtectedKeyEnvelope, SecurityState,
SignedSecurityState,
},
};

/// Represents the user's keys, that are encrypted by the user key, and the signed security state.
Expand Down Expand Up @@ -309,6 +312,31 @@ impl InternalClient {
self.initialize_user_crypto_decrypted_key(decrypted_user_key, key_state)
}

#[cfg(feature = "internal")]
pub(crate) fn initialize_user_crypto_pin_envelope(
&self,
pin: String,
pin_protected_user_key_envelope: PasswordProtectedKeyEnvelope,
key_state: UserKeyState,
) -> Result<(), EncryptionSettingsError> {
let decrypted_user_key = {
// Note: This block ensures ctx is dropped. Otherwise it would cause a deadlock when
// initializing the user crypto
use crate::key_management::SymmetricKeyId;
let ctx = &mut self.key_store.context_mut();
let decrypted_user_key_id = pin_protected_user_key_envelope
.unseal(SymmetricKeyId::Local("tmp_unlock_pin"), &pin, ctx)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dani-garcia did we have any convention for local IDs? Or is it just the wild west.

Copy link
Contributor Author

@quexten quexten Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the approach here is to push the other work dani proposed ( https://bitwarden.atlassian.net/browse/PM-18102), so that we don't have to deal with naming collisions..

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'd like to see that merged so we don't have to care about naming in situations like this.

.map_err(|_| EncryptionSettingsError::WrongPin)?;

// Allowing deprecated here, until a refactor to pass the Local key ids to
// `initialized_user_crypto_decrypted_key`
#[allow(deprecated)]
ctx.dangerous_get_symmetric_key(decrypted_user_key_id)?
.clone()
};
self.initialize_user_crypto_decrypted_key(decrypted_user_key, key_state)
}

#[cfg(feature = "secrets")]
pub(crate) fn initialize_crypto_single_org_key(
&self,
Expand Down
120 changes: 115 additions & 5 deletions crates/bitwarden-core/src/key_management/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ use std::collections::HashMap;

use base64::{engine::general_purpose::STANDARD, Engine};
use bitwarden_crypto::{
dangerous_get_v2_rotated_account_keys, AsymmetricCryptoKey, CoseSerializable, CryptoError,
EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes,
SignatureAlgorithm, SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey,
UnsignedSharedKey, UserKey,
dangerous_get_v2_rotated_account_keys, safe::PasswordProtectedKeyEnvelopeError,
AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable,
KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, PrimitiveEncryptable, SignatureAlgorithm,
SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey,
UserKey,
};
use bitwarden_error::bitwarden_error;
use schemars::JsonSchema;
Expand All @@ -23,7 +24,8 @@ use crate::{
client::{encryption_settings::EncryptionSettingsError, LoginMethod, UserLoginMethod},
error::StatefulCryptoError,
key_management::{
AsymmetricKeyId, SecurityState, SignedSecurityState, SigningKeyId, SymmetricKeyId,
non_generic_wrappers::PasswordProtectedKeyEnvelope, AsymmetricKeyId, SecurityState,
SignedSecurityState, SigningKeyId, SymmetricKeyId,
},
Client, NotAuthenticatedError, OrganizationId, UserId, VaultLockedError, WrongPasswordError,
};
Expand All @@ -39,6 +41,8 @@ pub enum CryptoClientError {
VaultLocked(#[from] VaultLockedError),
#[error(transparent)]
Crypto(#[from] bitwarden_crypto::CryptoError),
#[error(transparent)]
PasswordProtectedKeyEnvelope(#[from] PasswordProtectedKeyEnvelopeError),
}

/// State used for initializing the user cryptographic state.
Expand Down Expand Up @@ -68,6 +72,7 @@ pub struct InitUserCryptoRequest {
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
#[allow(clippy::large_enum_variant)]
pub enum InitUserCryptoMethod {
/// Password
Password {
Expand All @@ -89,6 +94,13 @@ pub enum InitUserCryptoMethod {
/// this.
pin_protected_user_key: EncString,
},
/// PIN Envelope
PinEnvelope {
/// The user's PIN
pin: String,
/// The user's symmetric crypto key, encrypted with the PIN-protected key envelope.
pin_protected_user_key_envelope: PasswordProtectedKeyEnvelope,
},
/// Auth request
AuthRequest {
/// Private Key generated by the `crate::auth::new_auth_request`.
Expand Down Expand Up @@ -173,6 +185,16 @@ pub(super) async fn initialize_user_crypto(
key_state,
)?;
}
InitUserCryptoMethod::PinEnvelope {
pin,
pin_protected_user_key_envelope,
} => {
client.internal.initialize_user_crypto_pin_envelope(
pin,
pin_protected_user_key_envelope,
key_state,
)?;
}
InitUserCryptoMethod::AuthRequest {
request_private_key,
method,
Expand Down Expand Up @@ -315,6 +337,38 @@ pub(super) fn update_password(
})
}

/// Request for deriving a pin protected user key
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct EnrollPinResponse {
/// [UserKey] protected by PIN
pub pin_protected_user_key_envelope: PasswordProtectedKeyEnvelope,
/// PIN protected by [UserKey]
pub user_key_encrypted_pin: EncString,
}

pub(super) fn enroll_pin(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โ“ Does this need to be exposed to uniffi? I'm not seeing in the Swift bindings.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this! It seems we still separate out clients which is confusing, but it should be fixed in the latest revision of the PR, and I have verified this gets emitted to the swift bindings locally.

client: &Client,
pin: String,
) -> Result<EnrollPinResponse, CryptoClientError> {
let key_store = client.internal.get_key_store();
let mut ctx = key_store.context_mut();

let key_envelope =
PasswordProtectedKeyEnvelope(bitwarden_crypto::safe::PasswordProtectedKeyEnvelope::seal(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: We already import PasswordProtectedKeyEnvelope, use the short path.

Suggested change
PasswordProtectedKeyEnvelope(bitwarden_crypto::safe::PasswordProtectedKeyEnvelope::seal(
PasswordProtectedKeyEnvelope(PasswordProtectedKeyEnvelope::seal(

Copy link
Contributor Author

@quexten quexten Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, my bad I should have not merged this. The imported PasswordProtectedKeyEnvelope is the non-generic wrapper. The contained envelope that uses the long path is the generic struct from crypto. So they are different structs.

Maybe they should have different naming after all..
I've undone the merge now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea that naming is unfortunate.

SymmetricKeyId::User,
&pin,
&ctx,
)?);
let encrypted_pin = pin.encrypt(&mut ctx, SymmetricKeyId::User)?;
Ok(EnrollPinResponse {
pin_protected_user_key_envelope: key_envelope,
user_key_encrypted_pin: encrypted_pin,
})
}

/// Request for deriving a pin protected user key
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
Expand Down Expand Up @@ -937,6 +991,62 @@ mod tests {
assert_eq!(client_key, client3_key);
}

#[tokio::test]
async fn test_initialize_user_crypto_pin_envelope() {
let user_key = "5yKAZ4TSSEGje54MV5lc5ty6crkqUz4xvl+8Dm/piNLKf6OgRi2H0uzttNTXl9z6ILhkmuIXzGpAVc2YdorHgQ==";
let test_pin = "1234";

let client1 = Client::new(None);
initialize_user_crypto(
&client1,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
email: "[email protected]".into(),
private_key: make_key_pair(user_key.to_string())
.unwrap()
.user_key_encrypted_private_key,
signing_key: None,
security_state: None,
method: InitUserCryptoMethod::DecryptedKey {
decrypted_user_key: user_key.to_string(),
},
},
)
.await
.unwrap();

let enroll_response = client1.crypto().enroll_pin(test_pin.to_string()).unwrap();

let client2 = Client::new(None);
initialize_user_crypto(
&client2,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
// NOTE: THIS CHANGES KDF SETTINGS. We ensure in this test that even with different
// KDF settings the pin can unlock the user key.
kdf_params: Kdf::PBKDF2 {
iterations: 600_000.try_into().unwrap(),
},
email: "[email protected]".into(),
private_key: make_key_pair(user_key.to_string())
.unwrap()
.user_key_encrypted_private_key,
signing_key: None,
security_state: None,
method: InitUserCryptoMethod::PinEnvelope {
pin: test_pin.to_string(),
pin_protected_user_key_envelope: enroll_response
.pin_protected_user_key_envelope,
},
},
)
.await
.unwrap();
}

#[test]
fn test_enroll_admin_password_reset() {
let client = Client::new(None);
Expand Down
95 changes: 88 additions & 7 deletions crates/bitwarden-core/src/key_management/crypto_client.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use bitwarden_crypto::CryptoError;
use bitwarden_crypto::{CryptoError, Decryptable};
#[cfg(feature = "internal")]
use bitwarden_crypto::{EncString, UnsignedSharedKey};
#[cfg(feature = "wasm")]
Expand All @@ -9,18 +9,23 @@ use super::crypto::{
DeriveKeyConnectorRequest, EnrollAdminPasswordResetError, MakeKeyPairResponse,
VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse,
};
#[cfg(any(feature = "wasm", test))]
use crate::key_management::PasswordProtectedKeyEnvelope;
#[cfg(feature = "internal")]
use crate::key_management::crypto::{
derive_pin_key, derive_pin_user_key, enroll_admin_password_reset, get_user_encryption_key,
initialize_org_crypto, initialize_user_crypto, update_password, DerivePinKeyResponse,
InitOrgCryptoRequest, InitUserCryptoRequest, UpdatePasswordResponse,
use crate::key_management::{
crypto::{
derive_pin_key, derive_pin_user_key, enroll_admin_password_reset, get_user_encryption_key,
initialize_org_crypto, initialize_user_crypto, update_password, DerivePinKeyResponse,
InitOrgCryptoRequest, InitUserCryptoRequest, UpdatePasswordResponse,
},
SymmetricKeyId,
};
use crate::{
client::encryption_settings::EncryptionSettingsError,
error::StatefulCryptoError,
key_management::crypto::{
get_v2_rotated_account_keys, make_v2_keys_for_v1_user, CryptoClientError,
UserCryptoV2KeysResponse,
enroll_pin, get_v2_rotated_account_keys, make_v2_keys_for_v1_user, CryptoClientError,
EnrollPinResponse, UserCryptoV2KeysResponse,
},
Client,
};
Expand Down Expand Up @@ -80,6 +85,45 @@ impl CryptoClient {
) -> Result<UserCryptoV2KeysResponse, StatefulCryptoError> {
get_v2_rotated_account_keys(&self.client)
}

/// Protects the current user key with the provided PIN. The result can be stored and later
/// used to initialize another client instance by using the PIN and the PIN key with
/// `initialize_user_crypto`.
pub fn enroll_pin(&self, pin: String) -> Result<EnrollPinResponse, CryptoClientError> {
enroll_pin(&self.client, pin)
}

/// Protects the current user key with the provided PIN. The result can be stored and later
/// used to initialize another client instance by using the PIN and the PIN key with
/// `initialize_user_crypto`. The provided pin is encrypted with the user key.
pub fn enroll_pin_with_encrypted_pin(
&self,
// Note: This will be replaced by `EncString` with https://bitwarden.atlassian.net/browse/PM-24775
encrypted_pin: String,
) -> Result<EnrollPinResponse, CryptoClientError> {
let encrypted_pin: EncString = encrypted_pin.parse()?;
let pin = encrypted_pin.decrypt(
&mut self.client.internal.get_key_store().context_mut(),
SymmetricKeyId::User,
)?;
enroll_pin(&self.client, pin)
}

/// Decrypts a `PasswordProtectedKeyEnvelope`, returning the user key, if successful.
/// This is a stop-gap solution, until initialization of the SDK is used.
#[cfg(any(feature = "wasm", test))]
pub fn unseal_password_protected_key_envelope(
&self,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐Ÿ’ก Shall we add some unit test coverage ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test for the enroll -> re-enroll with encrypted -> unseal flow.

pin: String,
envelope: PasswordProtectedKeyEnvelope,
) -> Result<Vec<u8>, CryptoClientError> {
let mut ctx = self.client.internal.get_key_store().context_mut();
let key_slot = SymmetricKeyId::Local("unseal_password_protected_key_envelope");
envelope.unseal(key_slot, pin.as_str(), &mut ctx)?;
#[allow(deprecated)]
let key = ctx.dangerous_get_symmetric_key(key_slot)?;
Ok(key.to_encoded().to_vec())
}
}

impl CryptoClient {
Expand Down Expand Up @@ -140,3 +184,40 @@ impl Client {
}
}
}

#[cfg(test)]
mod tests {
use bitwarden_crypto::{BitwardenLegacyKeyBytes, SymmetricCryptoKey};

use super::*;
use crate::client::test_accounts::test_bitwarden_com_account;

#[tokio::test]
async fn test_enroll_pin_envelope() {
// Initialize a test client with user crypto
let client = Client::init_test_account(test_bitwarden_com_account()).await;
let user_key_initial =
SymmetricCryptoKey::try_from(client.crypto().get_user_encryption_key().await.unwrap())
.unwrap();

// Enroll with a PIN, then re-enroll
let pin = "1234";
let enroll_response = client.crypto().enroll_pin(pin.to_string()).unwrap();
let re_enroll_response = client
.crypto()
.enroll_pin_with_encrypted_pin(enroll_response.user_key_encrypted_pin.to_string())
.unwrap();

let secret = BitwardenLegacyKeyBytes::from(
client
.crypto()
.unseal_password_protected_key_envelope(
pin.to_string(),
re_enroll_response.pin_protected_user_key_envelope,
)
.unwrap(),
);
let user_key_final = SymmetricCryptoKey::try_from(&secret).unwrap();
assert_eq!(user_key_initial, user_key_final);
}
}
1 change: 0 additions & 1 deletion crates/bitwarden-core/src/key_management/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ pub use crypto_client::CryptoClient;

#[cfg(feature = "internal")]
mod non_generic_wrappers;
#[allow(unused_imports)]
#[cfg(feature = "internal")]
pub(crate) use non_generic_wrappers::*;
#[cfg(feature = "internal")]
Expand Down
24 changes: 22 additions & 2 deletions crates/bitwarden-uniffi/src/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use bitwarden_core::key_management::crypto::{
DeriveKeyConnectorRequest, DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest,
UpdatePasswordResponse,
DeriveKeyConnectorRequest, DerivePinKeyResponse, EnrollPinResponse, InitOrgCryptoRequest,
InitUserCryptoRequest, UpdatePasswordResponse,
};
use bitwarden_crypto::{EncString, UnsignedSharedKey};

Expand Down Expand Up @@ -67,6 +67,26 @@ impl CryptoClient {
.map_err(Error::MobileCrypto)?)
}

/// Protects the current user key with the provided PIN. The result can be stored and later
/// used to initialize another client instance by using the PIN and the PIN key with
/// `initialize_user_crypto`.
pub fn enroll_pin(&self, pin: String) -> Result<EnrollPinResponse> {
Ok(self.0.enroll_pin(pin).map_err(Error::MobileCrypto)?)
}

/// Protects the current user key with the provided PIN. The result can be stored and later
/// used to initialize another client instance by using the PIN and the PIN key with
/// `initialize_user_crypto`. The provided pin is encrypted with the user key.
pub fn enroll_pin_with_encrypted_pin(
&self,
encrypted_pin: EncString,
) -> Result<EnrollPinResponse> {
Ok(self
.0
.enroll_pin_with_encrypted_pin(encrypted_pin.to_string())
.map_err(Error::MobileCrypto)?)
}

pub fn enroll_admin_password_reset(&self, public_key: String) -> Result<UnsignedSharedKey> {
Ok(self
.0
Expand Down
Loading