Skip to content

Add-userid-to-encryption-methods #278

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 22, 2025
Merged
1 change: 1 addition & 0 deletions crates/bitwarden-core/src/auth/auth_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ mod tests {
new_device
.crypto()
.initialize_user_crypto(InitUserCryptoRequest {
user_id: Some(uuid::Uuid::new_v4()),
kdf_params: kdf,
email: email.to_owned(),
private_key: private_key.to_owned(),
Expand Down
1 change: 1 addition & 0 deletions crates/bitwarden-core/src/auth/login/auth_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
client
.crypto()
.initialize_user_crypto(InitUserCryptoRequest {
user_id: None,

Check warning on line 118 in crates/bitwarden-core/src/auth/login/auth_request.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-core/src/auth/login/auth_request.rs#L118

Added line #L118 was not covered by tests
kdf_params: kdf,
email: auth_req.email,
private_key: require!(r.private_key),
Expand Down
1 change: 1 addition & 0 deletions crates/bitwarden-core/src/client/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ impl Client {

Self {
internal: Arc::new(InternalClient {
user_id: RwLock::new(None),
tokens: RwLock::new(Tokens::default()),
login_method: RwLock::new(None),
#[cfg(feature = "internal")]
Expand Down
9 changes: 9 additions & 0 deletions crates/bitwarden-core/src/client/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub(crate) struct Tokens {

#[derive(Debug)]
pub struct InternalClient {
pub(crate) user_id: RwLock<Option<Uuid>>,
pub(crate) tokens: RwLock<Tokens>,
pub(crate) login_method: RwLock<Option<Arc<LoginMethod>>>,

Expand Down Expand Up @@ -172,6 +173,14 @@ impl InternalClient {
&self.key_store
}

pub fn set_user_id(&self, user_id: Uuid) {
*self.user_id.write().expect("RwLock is not poisoned") = Some(user_id);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

issue?: it seems odd to have a public function that changes the user_id without any additional context required. I feel like the user_id should only ever change when the client is initialized and possibly never again after that. Maybe there's some edge case where we'd want a client without Crypto but tied to a user? If that is the case then I think this function should instead be called something like init_user_id() and only allow itself to be called once. However, it's probably more likely that we'll split up the client into user and non-user scoped clients and then just require the user_id on creation.

Either way, I think we should remove the ability to change the user_id. My opinion is that it should be bundled into the initialize_user_crypto functions below, but I'm not married to that solution

Copy link
Member

@dani-garcia dani-garcia May 21, 2025

Choose a reason for hiding this comment

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

Yeah I agree, separate user-bound and non-user bounds clients would allow us to require user id during construction and enforce no changes to it, until we have those we can use a OnceLock to ensure the ID is only initialized once:

    pub(crate) user_id: OnceLock<Uuid>,

    pub fn init_user_id(&self, user_id: Uuid) -> Result<(), UserIdAlreadySetError> {
        self.user_id
            .set(user_id)
            .map_err(|_| UserIdAlreadySetError)
    }

    pub fn get_user_id(&self) -> Option<Uuid> {
        self.user_id.get().map(|x| *x)
    }

Copy link
Member Author

Choose a reason for hiding this comment

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

There definitely are user-bound, but not crypto-inited clients -- that's a locked SDK.

The thought was, that I only need the UserId during encryption (for now) and I'm not 100% familiar with our current usages and I wanted to minimize the impact of the breaking change.

I can certainly revise. I agree it makes sense to init userId only once -- that's an easy change -- Actually making the pure, environment-bound, and user-bound client contexts feel out of scope here, though. Do you agree?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh yes splitting them up is way out of scope, we're gonna be working on that with Auth if I remember correctly. I was just thinking ahead a little, I could've been a little clearer there :) I think OnceLock that Dani suggested is a perfectly sound solution until we eventually start splitting stuff

Copy link
Member Author

Choose a reason for hiding this comment

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

This is done (468bcda), but there is an unfortunate side effect that crypto initialization can now error with a userId already set.

This will have to be updated once the clients are split, but for now is in line with the expectation that UserId is sent into crypto initialization. Once I implemented it, I got to thinking that for the same reasons it makes sense to error should multiple init_user_crypto requests be sent into a single client, so hopefully this can just morph into a CryptoAlreadyInitialized error or some such.

pub fn get_user_id(&self) -> Option<Uuid> {
*self.user_id.read().expect("RwLock is not poisoned")
}

#[cfg(feature = "internal")]
pub(crate) fn initialize_user_crypto_master_key(
&self,
Expand Down
2 changes: 2 additions & 0 deletions crates/bitwarden-core/src/client/test_accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
pub fn test_bitwarden_com_account() -> TestAccount {
TestAccount {
user: InitUserCryptoRequest {
user_id: Some(uuid::uuid!("060000fb-0922-4dd3-b170-6e15cb5df8c8")),
kdf_params: Kdf::PBKDF2 {
iterations: 600_000.try_into().unwrap(),
},
Expand Down Expand Up @@ -174,6 +175,7 @@
pub fn test_legacy_user_key_account() -> TestAccount {
TestAccount {
user: InitUserCryptoRequest {
user_id: Some(uuid::uuid!("060000fb-0922-4dd3-b170-6e15cb5df8c8")),

Check warning on line 178 in crates/bitwarden-core/src/client/test_accounts.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-core/src/client/test_accounts.rs#L178

Added line #L178 was not covered by tests
kdf_params: Kdf::PBKDF2 {
iterations: 600_000.try_into().unwrap(),
},
Expand Down
10 changes: 10 additions & 0 deletions crates/bitwarden-core/src/mobile/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub enum MobileCryptoError {
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct InitUserCryptoRequest {
pub user_id: Option<uuid::Uuid>,
/// The user's KDF parameters, as received from the prelogin request
pub kdf_params: Kdf,
/// The user's email address
Expand Down Expand Up @@ -131,6 +132,10 @@ pub async fn initialize_user_crypto(

let private_key: EncString = req.private_key.parse()?;

if let Some(user_id) = req.user_id {
client.internal.set_user_id(user_id);
}

match req.method {
InitUserCryptoMethod::Password { password, user_key } => {
let user_key: EncString = user_key.parse()?;
Expand Down Expand Up @@ -564,6 +569,7 @@ mod tests {
initialize_user_crypto(
& client,
InitUserCryptoRequest {
user_id: Some(uuid::Uuid::new_v4()),
kdf_params: kdf.clone(),
email: "[email protected]".into(),
private_key: priv_key.to_owned(),
Expand All @@ -583,6 +589,7 @@ mod tests {
initialize_user_crypto(
&client2,
InitUserCryptoRequest {
user_id: Some(uuid::Uuid::new_v4()),
kdf_params: kdf.clone(),
email: "[email protected]".into(),
private_key: priv_key.to_owned(),
Expand Down Expand Up @@ -638,6 +645,7 @@ mod tests {
initialize_user_crypto(
& client,
InitUserCryptoRequest {
user_id: Some(uuid::Uuid::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
Expand All @@ -659,6 +667,7 @@ mod tests {
initialize_user_crypto(
&client2,
InitUserCryptoRequest {
user_id: Some(uuid::Uuid::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
Expand Down Expand Up @@ -701,6 +710,7 @@ mod tests {
initialize_user_crypto(
&client3,
InitUserCryptoRequest {
user_id: Some(uuid::Uuid::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
Expand Down
1 change: 1 addition & 0 deletions crates/bitwarden-core/tests/register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async fn test_register_initialize_crypto() {
client
.crypto()
.initialize_user_crypto(InitUserCryptoRequest {
user_id: Some(uuid::Uuid::new_v4()),
kdf_params: kdf,
email: email.to_owned(),
private_key: register_response.keys.private.to_string(),
Expand Down
28 changes: 25 additions & 3 deletions crates/bitwarden-fido/src/authenticator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use bitwarden_core::{Client, VaultLockedError};
use bitwarden_crypto::CryptoError;
use bitwarden_vault::{CipherError, CipherView};
use bitwarden_vault::{CipherError, CipherView, EncryptionContext};
use itertools::Itertools;
use log::error;
use passkey::{
Expand Down Expand Up @@ -431,6 +431,8 @@
) -> Result<(), StatusCode> {
#[derive(Debug, Error)]
enum InnerError {
#[error("Client User Id has not been set")]
MissingUserId,
#[error(transparent)]
VaultLocked(#[from] VaultLockedError),
#[error(transparent)]
Expand All @@ -454,6 +456,12 @@
rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
options: passkey::types::ctap2::get_assertion::Options,
) -> Result<(), InnerError> {
let user_id = this
.authenticator
.client
.internal
.get_user_id()
.ok_or(InnerError::MissingUserId)?;

Check warning on line 464 in crates/bitwarden-fido/src/authenticator.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-fido/src/authenticator.rs#L459-L464

Added lines #L459 - L464 were not covered by tests
let cred = try_from_credential_full(cred, user, rp, options)?;

// Get the previously selected cipher and add the new credential to it
Expand Down Expand Up @@ -481,7 +489,10 @@

this.authenticator
.credential_store
.save_credential(encrypted)
.save_credential(EncryptionContext {
cipher: encrypted,
encrypted_for: user_id,
})

Check warning on line 495 in crates/bitwarden-fido/src/authenticator.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-fido/src/authenticator.rs#L492-L495

Added lines #L492 - L495 were not covered by tests
.await?;

Ok(())
Expand All @@ -498,6 +509,8 @@
async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> {
#[derive(Debug, Error)]
enum InnerError {
#[error("Client User Id has not been set")]
MissingUserId,
#[error(transparent)]
VaultLocked(#[from] VaultLockedError),
#[error(transparent)]
Expand All @@ -521,6 +534,12 @@
this: &mut CredentialStoreImpl<'_>,
cred: Passkey,
) -> Result<(), InnerError> {
let user_id = this
.authenticator
.client
.internal
.get_user_id()
.ok_or(InnerError::MissingUserId)?;

Check warning on line 542 in crates/bitwarden-fido/src/authenticator.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-fido/src/authenticator.rs#L537-L542

Added lines #L537 - L542 were not covered by tests
// Get the previously selected cipher and update the credential
let selected = this.authenticator.get_selected_credential()?;

Expand Down Expand Up @@ -550,7 +569,10 @@

this.authenticator
.credential_store
.save_credential(encrypted)
.save_credential(EncryptionContext {
cipher: encrypted,
encrypted_for: user_id,
})

Check warning on line 575 in crates/bitwarden-fido/src/authenticator.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-fido/src/authenticator.rs#L572-L575

Added lines #L572 - L575 were not covered by tests
.await?;

Ok(())
Expand Down
4 changes: 2 additions & 2 deletions crates/bitwarden-fido/src/traits.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use bitwarden_vault::{Cipher, CipherListView, CipherView, Fido2CredentialNewView};
use bitwarden_vault::{CipherListView, CipherView, EncryptionContext, Fido2CredentialNewView};
use passkey::authenticator::UIHint;
use thiserror::Error;

Expand Down Expand Up @@ -43,7 +43,7 @@ pub trait Fido2CredentialStore: Send + Sync {

async fn all_credentials(&self) -> Result<Vec<CipherListView>, Fido2CallbackError>;

async fn save_credential(&self, cred: Cipher) -> Result<(), Fido2CallbackError>;
async fn save_credential(&self, cred: EncryptionContext) -> Result<(), Fido2CallbackError>;
}

#[derive(Clone)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ class MainActivity : FragmentActivity() {

client.crypto().initializeUserCrypto(
InitUserCryptoRequest(
userId = null,
kdfParams = kdf,
email = EMAIL,
privateKey = loginBody.PrivateKey,
Expand Down Expand Up @@ -334,6 +335,7 @@ class MainActivity : FragmentActivity() {
GlobalScope.launch {
client.crypto().initializeUserCrypto(
InitUserCryptoRequest(
userId = null,
kdfParams = kdf,
email = EMAIL,
privateKey = privateKey!!,
Expand Down Expand Up @@ -371,6 +373,7 @@ class MainActivity : FragmentActivity() {
GlobalScope.launch {
client.crypto().initializeUserCrypto(
InitUserCryptoRequest(
userId = null,
kdfParams = kdf,
email = EMAIL,
privateKey = privateKey!!,
Expand Down
6 changes: 3 additions & 3 deletions crates/bitwarden-uniffi/src/platform/fido2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
PublicKeyCredentialAuthenticatorAttestationResponse, PublicKeyCredentialRpEntity,
PublicKeyCredentialUserEntity,
};
use bitwarden_vault::{Cipher, CipherListView, CipherView, Fido2CredentialNewView};
use bitwarden_vault::{CipherListView, CipherView, EncryptionContext, Fido2CredentialNewView};

use crate::error::{Error, Result};

Expand Down Expand Up @@ -245,7 +245,7 @@

async fn all_credentials(&self) -> Result<Vec<CipherListView>, Fido2CallbackError>;

async fn save_credential(&self, cred: Cipher) -> Result<(), Fido2CallbackError>;
async fn save_credential(&self, cred: EncryptionContext) -> Result<(), Fido2CallbackError>;
}

// Because uniffi doesn't support external traits, we have to make a copy of the trait here.
Expand All @@ -271,7 +271,7 @@
self.0.all_credentials().await.map_err(Into::into)
}

async fn save_credential(&self, cred: Cipher) -> Result<(), BitFido2CallbackError> {
async fn save_credential(&self, cred: EncryptionContext) -> Result<(), BitFido2CallbackError> {

Check warning on line 274 in crates/bitwarden-uniffi/src/platform/fido2.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-uniffi/src/platform/fido2.rs#L274

Added line #L274 was not covered by tests
self.0.save_credential(cred).await.map_err(Into::into)
}
}
Expand Down
4 changes: 2 additions & 2 deletions crates/bitwarden-uniffi/src/vault/ciphers.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use bitwarden_vault::{Cipher, CipherListView, CipherView, Fido2CredentialView};
use bitwarden_vault::{Cipher, CipherListView, CipherView, EncryptionContext, Fido2CredentialView};
use uuid::Uuid;

use crate::{error::Error, Result};
Expand All @@ -9,7 +9,7 @@
#[uniffi::export]
impl CiphersClient {
/// Encrypt cipher
pub fn encrypt(&self, cipher_view: CipherView) -> Result<Cipher> {
pub fn encrypt(&self, cipher_view: CipherView) -> Result<EncryptionContext> {

Check warning on line 12 in crates/bitwarden-uniffi/src/vault/ciphers.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-uniffi/src/vault/ciphers.rs#L12

Added line #L12 was not covered by tests
Ok(self.0.encrypt(cipher_view).map_err(Error::Encrypt)?)
}

Expand Down
5 changes: 4 additions & 1 deletion crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ struct ContentView: View {

try await clientCrypto.initializeUserCrypto(
req: InitUserCryptoRequest(
userId: nil,
kdfParams: kdf,
email: EMAIL,
privateKey: loginData.PrivateKey,
Expand Down Expand Up @@ -246,6 +247,7 @@ struct ContentView: View {
let key = biometricRetrieveValue()!

try await clientCrypto.initializeUserCrypto(req: InitUserCryptoRequest(
userId: nil,
kdfParams: kdf,
email: EMAIL,
privateKey: privateKey,
Expand All @@ -272,6 +274,7 @@ struct ContentView: View {
let pinProtectedUserKey = defaults.string(forKey: "pinProtectedUserKey")!

try await clientCrypto.initializeUserCrypto(req: InitUserCryptoRequest(
userId: nil,
kdfParams: kdf,
email: EMAIL,
privateKey: privateKey,
Expand Down Expand Up @@ -417,7 +420,7 @@ class Fido2CredentialStoreImpl: Fido2CredentialStore {
abort()
}

func saveCredential(cred: BitwardenSdk.Cipher) async throws {
func saveCredential(cred: BitwardenSdk.EncryptionContext) async throws {
print("SAVED CREDENTIAL")
}
}
11 changes: 11 additions & 0 deletions crates/bitwarden-vault/src/cipher/cipher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ pub enum CipherRepromptType {
Password = 1,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[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 EncryptionContext {
/// The Id of the user that encrypted the cipher. It should always represent a UserId, even for
/// Organization-owned ciphers
pub encrypted_for: Uuid,
pub cipher: Cipher,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
Expand Down
Loading