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 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 .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_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 None => {
95 legacy_temp_login().await?;
96 None
97 }
98 };
99
100 let ctx = ClientContext { global, user };
101
102 match command {
103 Commands::Login(args) => args.run().await,
105 Commands::Logout => todo!(),
106
107 Commands::Lock(args) => args.dispatch(ctx).await,
109 Commands::Unlock(_args) => todo!(),
110
111 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 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 Commands::Confirm { .. } => todo!(),
131 Commands::DeviceApproval => todo!(),
132 Commands::Move(_args) => todo!(),
133
134 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 Commands::Serve(_args) => todo!(),
148 }
149}
150
151async 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
192async 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 StateRegistry::new_with_db(db_config()?, get_sdk_managed_migrations())
222 .await?
223 .wipe()
224 .await
225 .ok();
226
227 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 device_identifier: None,
297 bitwarden_client_version: Some(env!("CARGO_PKG_VERSION").to_string()),
298 bitwarden_package_type: Some("cli".to_string()),
299 });
300}