Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/bitwarden-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
136 changes: 20 additions & 116 deletions crates/bitwarden-core/src/client/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>, 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<String>, Option<String>, Option<i64>) {
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
Expand Down Expand Up @@ -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<String, CryptoError> {
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<String>,
access_token: Option<String>,
refresh_token: Option<String>,
expires_on: Option<i64>,
}

// 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<String>,
access_token: Option<String>,
refresh_token: Option<String>,
expires_on: Option<i64>,
}

// 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,
Expand Down
4 changes: 3 additions & 1 deletion crates/bw/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
4 changes: 3 additions & 1 deletion crates/bw/src/auth/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>) -> Result<()> {
let email = text_prompt_when_none("Email", email)?;

Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions crates/bw/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
90 changes: 90 additions & 0 deletions crates/bw/src/auth/tmp_session.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
access_token: Option<String>,
refresh_token: Option<String>,
expires_on: Option<i64>,
}

pub(crate) async fn export_session(client: &Client) -> Result<String, CryptoError> {
// 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(())
}
10 changes: 4 additions & 6 deletions crates/bw/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -53,11 +53,9 @@ async fn process_commands(command: Commands, session: Option<String>) -> 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))?;
}

Expand Down
Loading