@@ -7,10 +7,10 @@ use std::collections::HashMap;
77use uuid:: Uuid ;
88use zeroize:: Zeroizing ;
99
10- use vault_api:: { BaseUrls , BitwardenClient , SyncResponse } ;
10+ use vault_api:: { BaseUrls , BitwardenClient , SyncResponse , TokenResponse } ;
1111use vault_core:: cipher:: { Cipher , decrypt_user_key} ;
1212use vault_core:: kdf:: { KdfParams , derive_master_key, stretch_master_key} ;
13- use vault_ipc:: proto:: Error as IpcError ;
13+ use vault_ipc:: proto:: { ApiKeyCreds , Error as IpcError } ;
1414use vault_store:: VaultCache ;
1515
1616use crate :: state:: { Vault , account_dir_name} ;
@@ -26,8 +26,9 @@ pub async fn perform_unlock(
2626 email : & str ,
2727 password : & [ u8 ] ,
2828 device_id : Option < & str > ,
29+ api_key : Option < & ApiKeyCreds > ,
2930) -> Result < Vault , IpcError > {
30- match online_unlock ( server, email, password, device_id) . await {
31+ match online_unlock ( server, email, password, device_id, api_key ) . await {
3132 Ok ( vault) => Ok ( vault) ,
3233 Err ( IpcError :: Network ( net) ) => {
3334 // Network down — recover from cache if we have one for this account.
@@ -47,6 +48,7 @@ async fn online_unlock(
4748 email : & str ,
4849 password : & [ u8 ] ,
4950 device_id : Option < & str > ,
51+ api_key : Option < & ApiKeyCreds > ,
5052) -> Result < Vault , IpcError > {
5153 let email_lower = email. trim ( ) . to_lowercase ( ) ;
5254 let urls = BaseUrls :: self_hosted ( server) . map_err ( |e| IpcError :: Internal ( e. to_string ( ) ) ) ?;
@@ -70,10 +72,19 @@ async fn online_unlock(
7072 let stretch_enc = Zeroizing :: new ( stretch_enc) ;
7173 let stretch_mac = Zeroizing :: new ( stretch_mac) ;
7274
73- let token = client
74- . login_password ( & email_lower, password, params)
75- . await
76- . map_err ( translate_login_err) ?;
75+ // Authenticate. An API key (request-supplied or previously persisted) uses
76+ // the client_credentials grant, which skips 2FA; otherwise the password
77+ // grant. Either way the master key derived above is what decrypts the vault.
78+ let token = obtain_token (
79+ & mut client,
80+ & email_lower,
81+ password,
82+ params,
83+ api_key,
84+ server,
85+ email,
86+ )
87+ . await ?;
7788
7889 let encrypted_user_key = token
7990 . key
@@ -114,6 +125,81 @@ async fn online_unlock(
114125 Ok ( vault)
115126}
116127
128+ /// Authenticate `client` and return the token. Grant selection: request-supplied
129+ /// API-key creds (persisted on success, so future unlocks reuse them) → a
130+ /// previously stored API key → the password grant. The API-key grants use
131+ /// `client_credentials`, which skips 2FA; the password grant can surface
132+ /// [`IpcError::TwoFactorRequired`].
133+ async fn obtain_token (
134+ client : & mut BitwardenClient ,
135+ email_lower : & str ,
136+ password : & [ u8 ] ,
137+ params : KdfParams ,
138+ api_key : Option < & ApiKeyCreds > ,
139+ server : & str ,
140+ email : & str ,
141+ ) -> Result < TokenResponse , IpcError > {
142+ if let Some ( creds) = api_key {
143+ let token = client
144+ . login_api_key ( & creds. client_id , & creds. client_secret )
145+ . await
146+ . map_err ( api_err) ?;
147+ // Enrollment succeeded — persist the key so plain `unlock` reuses it.
148+ persist_apikey ( server, email, creds) ?;
149+ return Ok ( token) ;
150+ }
151+ if let Some ( stored) =
152+ cache_dir ( server, email) . and_then ( |d| vault_store:: load_apikey_from_dir ( & d) . ok ( ) )
153+ {
154+ return client
155+ . login_api_key ( & stored. client_id , stored. client_secret . as_bytes ( ) )
156+ . await
157+ . map_err ( api_err) ;
158+ }
159+ client
160+ . login_password ( email_lower, password, params)
161+ . await
162+ . map_err ( translate_login_err)
163+ }
164+
165+ /// Persist a request-supplied API key (0600) for the account. The
166+ /// `client_secret` is valid UTF-8 here — `login_api_key` already verified it
167+ /// against the server.
168+ fn persist_apikey ( server : & str , email : & str , creds : & ApiKeyCreds ) -> Result < ( ) , IpcError > {
169+ let dir = cache_dir ( server, email)
170+ . ok_or_else ( || IpcError :: Internal ( "no data directory" . to_owned ( ) ) ) ?;
171+ let client_secret = String :: from_utf8 ( creds. client_secret . clone ( ) )
172+ . map_err ( |_| IpcError :: Internal ( "api-key client_secret is not valid UTF-8" . to_owned ( ) ) ) ?;
173+ let store_creds = vault_store:: ApiKeyCreds {
174+ client_id : creds. client_id . clone ( ) ,
175+ client_secret,
176+ } ;
177+ vault_store:: save_apikey_to_dir ( & dir, & store_creds)
178+ . map_err ( |e| IpcError :: Internal ( format ! ( "write api key: {e}" ) ) ) ?;
179+ Ok ( ( ) )
180+ }
181+
182+ /// Report whether an API key is stored for the account (never the secret).
183+ #[ must_use]
184+ pub fn apikey_status ( server : & str , email : & str ) -> vault_ipc:: proto:: ApiKeyStatus {
185+ let creds = cache_dir ( server, email) . and_then ( |d| vault_store:: load_apikey_from_dir ( & d) . ok ( ) ) ;
186+ vault_ipc:: proto:: ApiKeyStatus {
187+ configured : creds. is_some ( ) ,
188+ client_id : creds. map ( |c| c. client_id ) ,
189+ }
190+ }
191+
192+ /// Forget the stored API key for the account (idempotent — no key is success).
193+ ///
194+ /// # Errors
195+ ///
196+ /// [`IpcError::Internal`] if the data dir can't be located or the removal fails.
197+ pub fn apikey_forget ( server : & str , email : & str ) -> Result < ( ) , IpcError > {
198+ let dir = cache_dir ( server, email)
199+ . ok_or_else ( || IpcError :: Internal ( "no data directory" . to_owned ( ) ) ) ?;
200+ vault_store:: delete_apikey_from_dir ( & dir) . map_err ( |e| IpcError :: Internal ( e. to_string ( ) ) )
201+ }
202+
117203/// Offline unlock from the encrypted local cache — no network. Recovers the
118204/// user key from the master password via the cached protected key, then builds
119205/// a read-only (`client = None`) vault. A wrong password is `BadPassword`.
0 commit comments