diff --git a/Cargo.lock b/Cargo.lock index d2eb06438..f5bb1236d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -1016,6 +1016,8 @@ dependencies = [ "bat", "bitwarden-cli", "bitwarden-core", + "bitwarden-crypto", + "bitwarden-encoding", "bitwarden-generators", "bitwarden-pm", "bitwarden-vault", diff --git a/crates/bitwarden-core/Cargo.toml b/crates/bitwarden-core/Cargo.toml index b6093d487..2ccf1809d 100644 --- a/crates/bitwarden-core/Cargo.toml +++ b/crates/bitwarden-core/Cargo.toml @@ -35,6 +35,7 @@ wasm = [ "dep:wasm-bindgen-futures", "dep:tsify" ] # WASM support +cli = [] # Temporary functions for WIP bw CLI [dependencies] async-trait = { workspace = true } diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index a5163ee7e..3ed230308 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -208,6 +208,26 @@ impl InternalClient { self.set_api_tokens_internal(token); } + /// Temporary interface for CLI to set tokens. + #[cfg(feature = "cli")] + pub fn cli_set_tokens(&self, token: String, refresh_token: Option, expires_in: u64) { + self.set_tokens(token, refresh_token, expires_in); + } + + /// Temporary interface for CLI to get tokens. + #[cfg(feature = "cli")] + pub fn cli_get_tokens(&self) -> (Option, Option, Option) { + let tokens = self.tokens.read().expect("RwLock is not poisoned"); + match &*tokens { + Tokens::SdkManaged(tokens) => ( + tokens.access_token.clone(), + tokens.refresh_token.clone(), + tokens.expires_on, + ), + Tokens::ClientManaged(_client_managed_tokens) => (None, None, None), + } + } + /// Sets api tokens for only internal API clients, use `set_tokens` for SdkManagedTokens. pub(crate) fn set_api_tokens_internal(&self, token: String) { self.__api_configurations @@ -285,122 +305,6 @@ impl InternalClient { self.user_id.get().copied() } - /// Export a full session containing all data needed to restore the client state - /// This includes the user key, tokens, and encrypted private/signing keys - #[cfg(feature = "internal")] - pub fn export_session(&self) -> Result { - use bitwarden_encoding::B64; - use serde::{Deserialize, Serialize}; - - use crate::key_management::{AsymmetricKeyId, SymmetricKeyId}; - - #[derive(Serialize, Deserialize)] - struct SessionData { - user_key: String, - private_key: Option, - access_token: Option, - refresh_token: Option, - expires_on: Option, - } - - // Get the user encryption key and private key - #[allow(deprecated)] - let (user_key, private_key) = { - let ctx = self.key_store.context(); - let user_key = ctx.dangerous_get_symmetric_key(SymmetricKeyId::User)?; - let private_key = if ctx.has_asymmetric_key(AsymmetricKeyId::UserPrivateKey) { - let key = ctx.dangerous_get_asymmetric_key(AsymmetricKeyId::UserPrivateKey)?; - Some(B64::from(key.to_der()?.as_ref()).to_string()) - } else { - None - }; - (user_key.to_base64().to_string(), private_key) - }; - - // Get the tokens - let tokens = self.tokens.read().expect("RwLock is not poisoned"); - let (access_token, refresh_token, expires_on) = match &*tokens { - Tokens::SdkManaged(sdk_tokens) => ( - sdk_tokens.access_token.clone(), - sdk_tokens.refresh_token.clone(), - sdk_tokens.expires_on, - ), - Tokens::ClientManaged(_) => (None, None, None), - }; - - let session_data = SessionData { - user_key, - private_key, - access_token, - refresh_token, - expires_on, - }; - - // Serialize to JSON and then base64 encode - let json = serde_json::to_string(&session_data).map_err(|_| CryptoError::InvalidKey)?; - let encoded = bitwarden_encoding::B64::from(json.as_bytes()); - - Ok(encoded.to_string()) - } - - /// Import a session and restore the client state - /// This includes restoring the user key, private key, and setting tokens - #[cfg(feature = "internal")] - pub fn import_session(&self, session: &str) -> Result<(), CryptoError> { - use bitwarden_crypto::{AsymmetricCryptoKey, Pkcs8PrivateKeyBytes}; - use bitwarden_encoding::B64; - use serde::{Deserialize, Serialize}; - - use crate::key_management::{AsymmetricKeyId, SymmetricKeyId}; - - #[derive(Serialize, Deserialize)] - struct SessionData { - user_key: String, - private_key: Option, - access_token: Option, - refresh_token: Option, - expires_on: Option, - } - - // Decode from base64 and parse JSON - let decoded = B64::try_from(session.to_string()).map_err(|_| CryptoError::InvalidKey)?; - let json_str = - String::from_utf8(decoded.as_bytes().to_vec()).map_err(|_| CryptoError::InvalidKey)?; - let session_data: SessionData = - serde_json::from_str(&json_str).map_err(|_| CryptoError::InvalidKey)?; - - // Restore the user key and private key - let user_key = SymmetricCryptoKey::try_from(session_data.user_key)?; - - #[allow(deprecated)] - { - let mut ctx = self.key_store.context_mut(); - ctx.set_symmetric_key(SymmetricKeyId::User, user_key)?; - - // Restore private key if present - if let Some(private_key_b64) = session_data.private_key { - let private_key_b64_parsed = - B64::try_from(private_key_b64).map_err(|_| CryptoError::InvalidKey)?; - let private_key_der = Pkcs8PrivateKeyBytes::from(private_key_b64_parsed.as_bytes()); - let private_key = AsymmetricCryptoKey::from_der(&private_key_der)?; - ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?; - } - } - - // Restore the tokens - if let Some(access_token) = session_data.access_token { - *self.tokens.write().expect("RwLock is not poisoned") = - Tokens::SdkManaged(SdkManagedTokens { - access_token: Some(access_token.clone()), - refresh_token: session_data.refresh_token, - expires_on: session_data.expires_on, - }); - self.set_api_tokens_internal(access_token); - } - - Ok(()) - } - #[cfg(feature = "internal")] pub(crate) fn initialize_user_crypto_master_key( &self, diff --git a/crates/bw/Cargo.toml b/crates/bw/Cargo.toml index 174f4df7e..fc7d3d84e 100644 --- a/crates/bw/Cargo.toml +++ b/crates/bw/Cargo.toml @@ -20,7 +20,9 @@ bat = { version = "0.25.0", features = [ "regex-fancy", ], default-features = false } bitwarden-cli = { workspace = true } -bitwarden-core = { workspace = true } +bitwarden-core = { workspace = true, features = ["cli"] } +bitwarden-crypto = { workspace = true } +bitwarden-encoding = { workspace = true } bitwarden-generators = { workspace = true } bitwarden-pm = { workspace = true } bitwarden-vault = { workspace = true } diff --git a/crates/bw/src/auth/login.rs b/crates/bw/src/auth/login.rs index 0475a33d6..7ed033552 100644 --- a/crates/bw/src/auth/login.rs +++ b/crates/bw/src/auth/login.rs @@ -11,6 +11,8 @@ use color_eyre::eyre::{Result, bail}; use inquire::{Password, Text}; use log::{debug, error, info}; +use crate::auth::tmp_session::export_session; + pub(crate) async fn login_password(client: Client, email: Option) -> Result<()> { let email = text_prompt_when_none("Email", email)?; @@ -134,7 +136,7 @@ pub(crate) async fn login_api_key( info!("Synced {} ciphers", sync_result.ciphers.len()); // Export the full session (user key + tokens) - let session = client.internal.export_session()?; + let session = export_session(&client).await?; Ok(session) } diff --git a/crates/bw/src/auth/mod.rs b/crates/bw/src/auth/mod.rs index d5963a76d..fa8a59f25 100644 --- a/crates/bw/src/auth/mod.rs +++ b/crates/bw/src/auth/mod.rs @@ -7,6 +7,9 @@ use inquire::Password; use crate::render::CommandResult; +mod tmp_session; +pub(crate) use tmp_session::*; + // TODO(CLI): This is incompatible with the current node CLI #[derive(Args, Clone)] pub struct LoginArgs { diff --git a/crates/bw/src/auth/tmp_session.rs b/crates/bw/src/auth/tmp_session.rs new file mode 100644 index 000000000..a0a1e4211 --- /dev/null +++ b/crates/bw/src/auth/tmp_session.rs @@ -0,0 +1,90 @@ +//! Contains temporary glue code to recover a session. +//! +//! None of this code should be considered final but rather as a temporary hack until auth +//! persistence is properly implemented. + +use bitwarden_core::Client; +use bitwarden_crypto::{AsymmetricCryptoKey, Pkcs8PrivateKeyBytes}; +use bitwarden_crypto::{CryptoError, SymmetricCryptoKey}; +use bitwarden_encoding::B64; +use serde::{Deserialize, Serialize}; + +use bitwarden_core::key_management::{AsymmetricKeyId, SymmetricKeyId}; + +#[derive(Serialize, Deserialize)] +struct SessionData { + user_key: String, + private_key: Option, + access_token: Option, + refresh_token: Option, + expires_on: Option, +} + +pub(crate) async fn export_session(client: &Client) -> Result { + // Get the user encryption key and private key + #[allow(deprecated)] + let (user_key, private_key) = { + let ctx = client.internal.get_key_store().context(); + let user_key = ctx.dangerous_get_symmetric_key(SymmetricKeyId::User)?; + let private_key = if ctx.has_asymmetric_key(AsymmetricKeyId::UserPrivateKey) { + let key = ctx.dangerous_get_asymmetric_key(AsymmetricKeyId::UserPrivateKey)?; + Some(B64::from(key.to_der()?.as_ref()).to_string()) + } else { + None + }; + (user_key.to_base64().to_string(), private_key) + }; + + // Get the tokens + let (access_token, refresh_token, expires_on) = client.internal.cli_get_tokens(); + + let session_data = SessionData { + user_key, + private_key, + access_token, + refresh_token, + expires_on, + }; + + // Serialize to JSON and then base64 encode + let json = serde_json::to_string(&session_data).map_err(|_| CryptoError::InvalidKey)?; + let encoded = bitwarden_encoding::B64::from(json.as_bytes()); + + Ok(encoded.to_string()) +} + +/// Import a session and restore the client state +/// This includes restoring the user key, private key, and setting tokens +pub(crate) async fn import_session(client: &Client, session: &str) -> Result<(), CryptoError> { + // Decode from base64 and parse JSON + let decoded = B64::try_from(session.to_string()).map_err(|_| CryptoError::InvalidKey)?; + let json_str = + String::from_utf8(decoded.as_bytes().to_vec()).map_err(|_| CryptoError::InvalidKey)?; + let session_data: SessionData = + serde_json::from_str(&json_str).map_err(|_| CryptoError::InvalidKey)?; + + // Restore the user key and private key + let user_key = SymmetricCryptoKey::try_from(session_data.user_key)?; + + #[allow(deprecated)] + { + let mut ctx = client.internal.get_key_store().context_mut(); + ctx.set_symmetric_key(SymmetricKeyId::User, user_key)?; + + // Restore private key if present + if let Some(private_key_b64) = session_data.private_key { + let private_key_b64_parsed = + B64::try_from(private_key_b64).map_err(|_| CryptoError::InvalidKey)?; + let private_key_der = Pkcs8PrivateKeyBytes::from(private_key_b64_parsed.as_bytes()); + let private_key = AsymmetricCryptoKey::from_der(&private_key_der)?; + ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?; + } + } + + // Restore the tokens + client + .internal + .cli_set_tokens(session_data.access_token.unwrap_or_default(), None, 0); + + Ok(()) +} diff --git a/crates/bw/src/main.rs b/crates/bw/src/main.rs index af5f5301c..9f69f783d 100644 --- a/crates/bw/src/main.rs +++ b/crates/bw/src/main.rs @@ -7,7 +7,7 @@ use clap_complete::Shell; use color_eyre::eyre::Result; use env_logger::Target; -use crate::{command::*, render::CommandResult}; +use crate::{auth::import_session, command::*, render::CommandResult}; mod admin_console; mod auth; @@ -53,11 +53,9 @@ async fn process_commands(command: Commands, session: Option) -> Command let client = bitwarden_pm::PasswordManagerClient::new(None); // If a session was provided, import it to restore the client state - if let Some(ref session_str) = session { - client - .0 - .internal - .import_session(session_str) + if let Some(s) = session { + import_session(&client.0, &s) + .await .map_err(|e| color_eyre::eyre::eyre!("Failed to import session: {}", e))?; }