Skip to main content

bw/
main.rs

1#![doc = include_str!("../README.md")]
2#![allow(
3    clippy::print_stdout,
4    clippy::print_stderr,
5    reason = "The CLI uses stdout/stderr for user interaction"
6)]
7
8use std::sync::Arc;
9
10use bitwarden_auth::token_management::PasswordManagerTokenHandler;
11use bitwarden_cli::install_color_eyre;
12use bitwarden_core::{
13    DeviceType, GlobalClient, HostPlatformInfo, client::persisted_state::BaseUrls,
14    get_host_platform_info, init_host_platform_info,
15};
16use bitwarden_pm::{
17    PasswordManagerClient, SaveStateData, SessionKey, UnlockMethod,
18    migrations::get_sdk_managed_migrations,
19};
20use bitwarden_state::{DatabaseConfiguration, registry::StateRegistry};
21use clap::{CommandFactory, Parser};
22use color_eyre::eyre::Result;
23use tracing_subscriber::{
24    EnvFilter, prelude::__tracing_subscriber_SubscriberExt as _, util::SubscriberInitExt as _,
25};
26
27use crate::{
28    client_state::{BwCommandExt, ClientContext},
29    command::*,
30    platform::appdata_dir,
31    render::CommandResult,
32};
33
34mod admin_console;
35mod auth;
36mod client_state;
37mod command;
38mod dirt;
39mod key_management;
40mod platform;
41mod render;
42mod tools;
43mod vault;
44
45#[tokio::main(flavor = "current_thread")]
46async fn main() -> Result<()> {
47    // the log level hierarchy is determined by:
48    //    - if RUST_LOG is detected at runtime
49    //    - if RUST_LOG is provided at compile time
50    //    - default to INFO
51    let filter = EnvFilter::builder()
52        .with_default_directive(
53            option_env!("RUST_LOG")
54                .unwrap_or("info")
55                .parse()
56                .expect("should provide valid log level at compile time."),
57        )
58        // parse directives from the RUST_LOG environment variable,
59        // overriding the default directive for matching targets.
60        .from_env_lossy();
61
62    tracing_subscriber::registry()
63        .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
64        .with(filter)
65        .init();
66
67    init_cli_platform_info();
68
69    let cli = Cli::parse();
70    install_color_eyre(cli.color)?;
71    let render_config = render::RenderConfig::new(&cli);
72
73    let Some(command) = cli.command else {
74        let mut cmd = Cli::command();
75        cmd.print_help()?;
76        return Ok(());
77    };
78
79    let result = process_commands(command, cli.session).await;
80
81    // Render the result of the command
82    render_config.render_result(result)
83}
84
85async fn process_commands(command: Commands, session: Option<SessionKey>) -> CommandResult {
86    let global = GlobalClient::new();
87
88    let user = match rehydrate_user(session).await? {
89        Some(client) => Some(client),
90        // Legacy stop-gap: with no persisted session, bootstrap one from the BW_EMAIL / BW_PASSWORD
91        // env vars. `legacy_temp_login` logs in, persists the session, and prints a `BW_SESSION`
92        // key; we then exit so the user can export it and re-run. Removed once `bw login` writes
93        // its own session state.
94        None => {
95            legacy_temp_login().await?;
96            None
97        }
98    };
99
100    let ctx = ClientContext { global, user };
101
102    match command {
103        // Auth commands
104        Commands::Login(args) => args.run().await,
105        Commands::Logout => todo!(),
106
107        // KM commands
108        Commands::Lock(args) => args.dispatch(ctx).await,
109        Commands::Unlock(_args) => todo!(),
110
111        // Platform commands
112        Commands::Sync(args) => args.dispatch(ctx).await,
113        Commands::Encode(args) => args.dispatch(ctx).await,
114        Commands::Config { command } => command.dispatch(ctx).await,
115        Commands::Completion(args) => args.dispatch(ctx).await,
116
117        Commands::Update { .. } => todo!(),
118
119        Commands::Status(_) => todo!(),
120
121        // Vault commands
122        Commands::List { .. } => todo!(),
123        Commands::Get { command } => command.run(),
124        Commands::Create { command } => command.run(),
125        Commands::Edit { .. } => todo!(),
126        Commands::Delete { .. } => todo!(),
127        Commands::Restore(_args) => todo!(),
128
129        // Admin console commands
130        Commands::Confirm { .. } => todo!(),
131        Commands::DeviceApproval => todo!(),
132        Commands::Move(_args) => todo!(),
133
134        // Tools commands
135        Commands::Generate(args) => {
136            let client = ctx
137                .user
138                .unwrap_or_else(|| bitwarden_pm::PasswordManagerClient::new(None));
139            args.run(&client)
140        }
141        Commands::Import(_args) => todo!(),
142        Commands::Export(_args) => todo!(),
143        Commands::Send(_args) => todo!(),
144        Commands::Receive(_args) => todo!(),
145
146        // Server commands
147        Commands::Serve(_args) => todo!(),
148    }
149}
150
151/// Rehydrate a [`PasswordManagerClient`] from the persisted session database.
152///
153/// Returns `Ok(None)` when no session has been persisted yet. Failures to load from state or unlock
154/// with the provided session key are logged as warnings and fail gracefully into a logged out or
155/// locked state respectively, to match the old CLI behavior.
156async fn rehydrate_user(session: Option<SessionKey>) -> Result<Option<PasswordManagerClient>> {
157    let registry =
158        match StateRegistry::new_with_db(db_config()?, get_sdk_managed_migrations()).await {
159            Ok(r) => r,
160            Err(e) => {
161                tracing::warn!("Failed to open session database: {e}");
162                return Ok(None);
163            }
164        };
165
166    let token_handler = Arc::new(PasswordManagerTokenHandler::default());
167    let client = match PasswordManagerClient::load_from_state(token_handler, registry).await {
168        Ok(c) => c,
169        Err(e) => {
170            tracing::warn!("Failed to initialize from session: {e}");
171            return Ok(None);
172        }
173    };
174
175    if let Some(key) = session
176        && let Err(e) = client.unlock().unlock(UnlockMethod::SessionKey(key)).await
177    {
178        tracing::warn!("Failed to unlock with provided session key: {e}");
179        return Ok(Some(client));
180    }
181
182    Ok(Some(client))
183}
184
185fn db_config() -> Result<DatabaseConfiguration> {
186    Ok(DatabaseConfiguration::Sqlite {
187        db_name: "user".to_string(),
188        folder_path: appdata_dir()?,
189    })
190}
191
192/// One-shot bootstrap login for commands that need an authenticated user before `bw login` writes
193/// its own session state. When the `BW_EMAIL` / `BW_PASSWORD` env vars are set, logs in against the
194/// persisted session database, persists the resulting session state (tokens, login method, account
195/// crypto state, and a session-key envelope), and mints a session key which it prints for the user
196/// to export as `BW_SESSION` and reuse on subsequent runs. Does nothing when the env vars are
197/// unset.
198async fn legacy_temp_login() -> Result<()> {
199    use bitwarden_core::{
200        ClientBuilder, UserId,
201        auth::{JwtToken, login::PasswordLoginRequest},
202        client::persisted_state::AUTHENTICATION_TOKENS,
203        key_management::account_cryptographic_state::WrappedAccountCryptographicState,
204    };
205    use color_eyre::eyre::eyre;
206
207    let (Ok(email), Ok(password)) = (std::env::var("BW_EMAIL"), std::env::var("BW_PASSWORD"))
208    else {
209        return Ok(());
210    };
211
212    let urls = BaseUrls {
213        api_url: "https://api.bitwarden.com".into(),
214        identity_url: "https://identity.bitwarden.com".into(),
215    };
216
217    let settings = get_host_platform_info()
218        .to_client_settings(urls.api_url.clone(), urls.identity_url.clone());
219
220    // Clear any existing session state
221    StateRegistry::new_with_db(db_config()?, get_sdk_managed_migrations())
222        .await?
223        .wipe()
224        .await
225        .ok();
226
227    // `wipe` leaves its registry unusable, so open a fresh one to back the login client.
228    let registry = StateRegistry::new_with_db(db_config()?, get_sdk_managed_migrations()).await?;
229    let client = PasswordManagerClient(
230        ClientBuilder::new()
231            .with_settings(settings)
232            .with_token_handler(Arc::new(PasswordManagerTokenHandler::default()))
233            .with_state(registry)
234            .build(),
235    );
236
237    client
238        .0
239        .auth()
240        .login_password(&PasswordLoginRequest {
241            email: email.clone(),
242            password,
243            two_factor: None,
244        })
245        .await?;
246
247    let tokens = client
248        .platform()
249        .state()
250        .setting(AUTHENTICATION_TOKENS)?
251        .get()
252        .await?
253        .ok_or_else(|| eyre!("login did not persist authentication tokens"))?;
254
255    let user_id: UserId = tokens.access_token.parse::<JwtToken>()?.sub.parse()?;
256
257    let crypto_state = {
258        let store = client.0.internal.get_key_store();
259        WrappedAccountCryptographicState::get_from_key_store(&store.context())?
260    };
261
262    let save_registry =
263        StateRegistry::new_with_db(db_config()?, get_sdk_managed_migrations()).await?;
264    PasswordManagerClient::save_to_state(
265        SaveStateData {
266            user_id,
267            email: email.clone(),
268            urls,
269            crypto_state,
270        },
271        &save_registry,
272    )
273    .await?;
274
275    let session = client.unlock().generate_session_key().await?;
276
277    println!("Logged in as {email} via legacy temp login");
278    println!("Use the following session key to unlock your vault:");
279    println!("export BW_SESSION={session}");
280    std::process::exit(0);
281}
282
283fn init_cli_platform_info() {
284    let device_type = if cfg!(target_os = "windows") {
285        DeviceType::WindowsCLI
286    } else if cfg!(target_os = "macos") {
287        DeviceType::MacOsCLI
288    } else {
289        DeviceType::LinuxCLI
290    };
291
292    init_host_platform_info(HostPlatformInfo {
293        user_agent: format!("Bitwarden_CLI/{}", env!("CARGO_PKG_VERSION")),
294        device_type,
295        // Stable identifier comes from session persistence (PM-35206).
296        device_identifier: None,
297        bitwarden_client_version: Some(env!("CARGO_PKG_VERSION").to_string()),
298        bitwarden_package_type: Some("cli".to_string()),
299    });
300}