@@ -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 } ;
99use bitwarden_encoding:: B64 ;
1010use bitwarden_error:: bitwarden_error;
1111use serde:: { Deserialize , Serialize } ;
12+ use tracing:: Level ;
1213#[ cfg( feature = "wasm" ) ]
1314use 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
5761impl 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
78112impl TryFrom < & MasterPasswordUnlockResponseModel > for MasterPasswordUnlockData {
@@ -138,11 +172,10 @@ pub struct MasterPasswordAuthenticationData {
138172}
139173
140174impl 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) ]
195228mod 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