Skip to content

Commit 6ad384a

Browse files
authored
[PM-30144] Add KeyContext support for MasterPasswordUnlockData (#680)
## 🎟️ Tracking <!-- Paste the link to the Jira or GitHub issue or otherwise describe / point to where this change is coming from. --> https://bitwarden.atlassian.net/browse/PM-30144 ## 📔 Objective <!-- Describe what the purpose of this PR is, for example what bug you're fixing or new feature you're adding. --> Part of the SDK-key-rotation PR chain. This makes master-password-unlock-data support key context, such that the user-key does not have to be read out of the key-context (which is generally a pattern we want to migrate away from). ## 🚨 Breaking Changes <!-- Does this PR introduce any breaking changes? If so, please describe the impact and migration path for clients. If you're unsure, the automated TypeScript compatibility check will run when you open/update this PR and provide feedback. For breaking changes: 1. Describe what changed in the client interface 2. Explain why the change was necessary 3. Provide migration steps for client developers 4. Link to any paired client PRs if needed Otherwise, you can remove this section. --> ## ⏰ Reminders before review - Contributor guidelines followed - All formatters and local linters executed and passed - Written new unit and / or integration tests where applicable - Protected functional changes with optionality (feature flags) - Used internationalization (i18n) for all UI strings - CI builds passed - Communicated to DevOps any deployment requirements - Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team ## 🦮 Reviewer guidelines <!-- Suggested interactions but feel free to use (or not) as you desire! --> - 👍 (`:+1:`) or similar for great changes - 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info - ❓ (`:question:`) for questions - 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion - 🎨 (`:art:`) for suggestions / improvements - ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention - 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt - ⛏ (`:pick:`) for minor or nitpick changes
1 parent 23dbe0d commit 6ad384a

File tree

2 files changed

+121
-14
lines changed

2 files changed

+121
-14
lines changed

crates/bitwarden-core/src/key_management/crypto.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,6 @@ pub(super) fn make_update_kdf(
323323
) -> Result<UpdateKdfResponse, CryptoClientError> {
324324
let key_store = client.internal.get_key_store();
325325
let ctx = key_store.context();
326-
// FIXME: [PM-18099] Once MasterKey deals with KeyIds, this should be updated
327-
#[allow(deprecated)]
328-
let user_key = ctx.dangerous_get_symmetric_key(SymmetricKeyId::User)?;
329326

330327
let login_method = client
331328
.internal
@@ -341,8 +338,9 @@ pub(super) fn make_update_kdf(
341338

342339
let authentication_data = MasterPasswordAuthenticationData::derive(password, new_kdf, email)
343340
.map_err(|_| CryptoClientError::InvalidKdfSettings)?;
344-
let unlock_data = MasterPasswordUnlockData::derive(password, new_kdf, email, user_key)
345-
.map_err(|_| CryptoClientError::InvalidKdfSettings)?;
341+
let unlock_data =
342+
MasterPasswordUnlockData::derive(password, new_kdf, email, SymmetricKeyId::User, &ctx)
343+
.map_err(|_| CryptoClientError::InvalidKdfSettings)?;
346344
let old_authentication_data = MasterPasswordAuthenticationData::derive(
347345
password,
348346
&client
@@ -986,7 +984,7 @@ pub(crate) fn make_user_jit_master_password_registration(
986984
let user_key = ctx.dangerous_get_symmetric_key(user_key_id)?.to_owned();
987985

988986
let master_password_unlock_data =
989-
MasterPasswordUnlockData::derive(&master_password, &kdf, &salt, &user_key)
987+
MasterPasswordUnlockData::derive(&master_password, &kdf, &salt, user_key_id, &ctx)
990988
.map_err(MakeKeysError::MasterPasswordDerivation)?;
991989

992990
let master_password_authentication_data =

crates/bitwarden-core/src/key_management/master_password.rs

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ use bitwarden_api_api::models::{
55
MasterPasswordUnlockDataRequestModel,
66
master_password_unlock_response_model::MasterPasswordUnlockResponseModel,
77
};
8-
use bitwarden_crypto::{EncString, Kdf, MasterKey, SymmetricCryptoKey};
8+
use bitwarden_crypto::{EncString, Kdf, KeyIds, KeyStoreContext, MasterKey, SymmetricCryptoKey};
99
use bitwarden_encoding::B64;
1010
use bitwarden_error::bitwarden_error;
1111
use serde::{Deserialize, Serialize};
12+
use tracing::Level;
1213
#[cfg(feature = "wasm")]
1314
use wasm_bindgen::prelude::*;
1415

@@ -34,6 +35,9 @@ pub enum MasterPasswordError {
3435
/// Generic crypto error
3536
#[error(transparent)]
3637
Crypto(#[from] bitwarden_crypto::CryptoError),
38+
/// The provided password is incorrect
39+
#[error("Wrong password")]
40+
WrongPassword,
3741
}
3842

3943
/// Represents the data required to unlock with the master password.
@@ -55,7 +59,21 @@ pub struct MasterPasswordUnlockData {
5559
}
5660

5761
impl MasterPasswordUnlockData {
58-
pub(crate) fn derive(
62+
/// Unwrap the user key into the key store context using the provided password.
63+
pub fn unwrap_to_context<Ids: KeyIds>(
64+
&self,
65+
password: &str,
66+
ctx: &mut KeyStoreContext<Ids>,
67+
) -> Result<Ids::Symmetric, MasterPasswordError> {
68+
let master_key = MasterKey::derive(password, &self.salt, &self.kdf)
69+
.map_err(|_| MasterPasswordError::InvalidKdfConfiguration)?;
70+
let user_key = master_key
71+
.decrypt_user_key(self.master_key_wrapped_user_key.clone())
72+
.map_err(|_| MasterPasswordError::WrongPassword)?;
73+
Ok(ctx.add_local_symmetric_key(user_key))
74+
}
75+
76+
pub(crate) fn derive_ref(
5977
password: &str,
6078
kdf: &Kdf,
6179
salt: &str,
@@ -73,6 +91,22 @@ impl MasterPasswordUnlockData {
7391
master_key_wrapped_user_key,
7492
})
7593
}
94+
95+
/// Derive master password unlock data from a password and user key in the key store.
96+
#[tracing::instrument(skip(password, salt, ctx))]
97+
pub fn derive<Ids: KeyIds>(
98+
password: &str,
99+
kdf: &Kdf,
100+
salt: &str,
101+
user_key_id: Ids::Symmetric,
102+
ctx: &KeyStoreContext<Ids>,
103+
) -> Result<Self, MasterPasswordError> {
104+
tracing::event!(Level::INFO, "deriving master password unlock data");
105+
// Temporary workaround until lower level functions also work on the key context
106+
#[expect(deprecated)]
107+
let key = ctx.dangerous_get_symmetric_key(user_key_id)?;
108+
Self::derive_ref(password, kdf, salt, key)
109+
}
76110
}
77111

78112
impl TryFrom<&MasterPasswordUnlockResponseModel> for MasterPasswordUnlockData {
@@ -138,11 +172,10 @@ pub struct MasterPasswordAuthenticationData {
138172
}
139173

140174
impl MasterPasswordAuthenticationData {
141-
pub(crate) fn derive(
142-
password: &str,
143-
kdf: &Kdf,
144-
salt: &str,
145-
) -> Result<Self, MasterPasswordError> {
175+
/// Derive master password authentication data from a password, KDF, and salt.
176+
#[tracing::instrument(skip(password, kdf, salt))]
177+
pub fn derive(password: &str, kdf: &Kdf, salt: &str) -> Result<Self, MasterPasswordError> {
178+
tracing::event!(Level::INFO, "deriving master password authentication data");
146179
let master_key = MasterKey::derive(password, salt, kdf)
147180
.map_err(|_| MasterPasswordError::InvalidKdfConfiguration)?;
148181
let hash = master_key.derive_master_key_hash(
@@ -194,8 +227,10 @@ fn kdf_to_kdf_request_model(kdf: &Kdf) -> KdfRequestModel {
194227
#[cfg(test)]
195228
mod tests {
196229
use bitwarden_api_api::models::{KdfType, MasterPasswordUnlockKdfResponseModel};
230+
use bitwarden_crypto::KeyStore;
197231

198232
use super::*;
233+
use crate::key_management::{KeyIds, SymmetricKeyId};
199234

200235
const TEST_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=";
201236
const TEST_INVALID_USER_KEY: &str = "-1.8UClLa8IPE1iZT7chy5wzQ==|6PVfHnVk5S3XqEtQemnM5yb4JodxmPkkWzmDRdfyHtjORmvxqlLX40tBJZ+CKxQWmS8tpEB5w39rbgHg/gqs0haGdZG4cPbywsgGzxZ7uNI=";
@@ -211,7 +246,7 @@ mod tests {
211246
};
212247
let salt = TEST_SALT.to_string();
213248
let user_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
214-
let data = MasterPasswordUnlockData::derive(TEST_PASSWORD, &kdf, &salt, &user_key)
249+
let data = MasterPasswordUnlockData::derive_ref(TEST_PASSWORD, &kdf, &salt, &user_key)
215250
.expect("Failed to derive master password unlock data");
216251
assert_eq!(data.salt, salt);
217252
assert!(matches!(data.kdf, Kdf::PBKDF2 { iterations } if iterations.get() == 600_000));
@@ -463,4 +498,78 @@ mod tests {
463498
let result = MasterPasswordUnlockData::try_from(&response);
464499
assert!(matches!(result, Err(MasterPasswordError::KdfMalformed)));
465500
}
501+
502+
#[test]
503+
fn test_unwrap_to_context_success() {
504+
// Derive master password unlock data from a known password and user key
505+
let kdf = Kdf::PBKDF2 {
506+
iterations: NonZeroU32::new(600_000).expect("non-zero"),
507+
};
508+
let user_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
509+
let data = MasterPasswordUnlockData::derive_ref(TEST_PASSWORD, &kdf, TEST_SALT, &user_key)
510+
.expect("Failed to derive master password unlock data");
511+
512+
// Create a key store and unwrap the user key into the context
513+
let store: KeyStore<KeyIds> = KeyStore::default();
514+
let mut ctx = store.context_mut();
515+
let key_id = data
516+
.unwrap_to_context::<KeyIds>(TEST_PASSWORD, &mut ctx)
517+
.expect("Failed to unwrap to context");
518+
519+
// Verify that the key was added to the context
520+
assert!(ctx.has_symmetric_key(key_id));
521+
522+
// Verify the unwrapped key matches the original
523+
#[expect(deprecated)]
524+
let unwrapped_key = ctx
525+
.dangerous_get_symmetric_key(key_id)
526+
.expect("Failed to get symmetric key");
527+
assert_eq!(*unwrapped_key, user_key);
528+
}
529+
530+
#[test]
531+
fn test_unwrap_to_context_wrong_password() {
532+
// Derive master password unlock data from a known password and user key
533+
let kdf = Kdf::PBKDF2 {
534+
iterations: NonZeroU32::new(600_000).expect("non-zero"),
535+
};
536+
let user_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
537+
let data = MasterPasswordUnlockData::derive_ref(TEST_PASSWORD, &kdf, TEST_SALT, &user_key)
538+
.expect("Failed to derive master password unlock data");
539+
540+
// Attempt to unwrap with wrong password
541+
let store: KeyStore<KeyIds> = KeyStore::default();
542+
let mut ctx = store.context_mut();
543+
let result = data.unwrap_to_context::<KeyIds>("wrong_password", &mut ctx);
544+
545+
assert!(matches!(result, Err(MasterPasswordError::WrongPassword)));
546+
}
547+
548+
#[test]
549+
fn test_unwrap_to_context_persists_key() {
550+
// Derive master password unlock data from a known password and user key
551+
let kdf = Kdf::PBKDF2 {
552+
iterations: NonZeroU32::new(600_000).expect("non-zero"),
553+
};
554+
let user_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
555+
let data = MasterPasswordUnlockData::derive_ref(TEST_PASSWORD, &kdf, TEST_SALT, &user_key)
556+
.expect("Failed to derive master password unlock data");
557+
558+
// Create a key store and unwrap the user key into the context
559+
let store: KeyStore<KeyIds> = KeyStore::default();
560+
{
561+
let mut ctx = store.context_mut();
562+
let local_key_id = data
563+
.unwrap_to_context::<KeyIds>(TEST_PASSWORD, &mut ctx)
564+
.expect("Failed to unwrap to context");
565+
566+
// Persist the local key to the User key slot
567+
ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User)
568+
.expect("Failed to persist symmetric key");
569+
}
570+
571+
// Verify the key is accessible with the User key id in a new context
572+
let ctx = store.context();
573+
assert!(ctx.has_symmetric_key(SymmetricKeyId::User));
574+
}
466575
}

0 commit comments

Comments
 (0)