1#![doc = include_str!("../README.md")]
2
3use bitwarden_cli::{install_color_eyre, text_prompt_when_none, Color};
4use bitwarden_core::{auth::RegisterRequest, ClientSettings};
5use bitwarden_generators::{
6 GeneratorClientsExt, PassphraseGeneratorRequest, PasswordGeneratorRequest,
7};
8use clap::{command, Args, CommandFactory, Parser, Subcommand};
9use color_eyre::eyre::Result;
10use inquire::Password;
11use render::Output;
12
13mod auth;
14mod render;
15
16#[derive(Parser, Clone)]
17#[command(name = "Bitwarden CLI", version, about = "Bitwarden CLI", long_about = None)]
18struct Cli {
19 #[command(subcommand)]
21 command: Option<Commands>,
22
23 #[arg(short = 'o', long, global = true, value_enum, default_value_t = Output::JSON)]
24 output: Output,
25
26 #[arg(short = 'c', long, global = true, value_enum, default_value_t = Color::Auto)]
27 color: Color,
28}
29
30#[derive(Subcommand, Clone)]
31enum Commands {
32 Login(LoginArgs),
33
34 #[command(long_about = "Register")]
35 Register {
36 #[arg(short = 'e', long, help = "Email address")]
37 email: Option<String>,
38
39 name: Option<String>,
40
41 password_hint: Option<String>,
42
43 #[arg(short = 's', long, global = true, help = "Server URL")]
44 server: Option<String>,
45 },
46
47 #[command(long_about = "Manage vault items")]
48 Item {
49 #[command(subcommand)]
50 command: ItemCommands,
51 },
52
53 #[command(long_about = "Pull the latest vault data from the server")]
54 Sync {},
55
56 #[command(long_about = "Password and passphrase generators")]
57 Generate {
58 #[command(subcommand)]
59 command: GeneratorCommands,
60 },
61}
62
63#[derive(Args, Clone)]
64struct LoginArgs {
65 #[command(subcommand)]
66 command: LoginCommands,
67
68 #[arg(short = 's', long, global = true, help = "Server URL")]
69 server: Option<String>,
70}
71
72#[derive(Subcommand, Clone)]
73enum LoginCommands {
74 Password {
75 #[arg(short = 'e', long, help = "Email address")]
76 email: Option<String>,
77 },
78 ApiKey {
79 client_id: Option<String>,
80 client_secret: Option<String>,
81 },
82 Device {
83 #[arg(short = 'e', long, help = "Email address")]
84 email: Option<String>,
85 device_identifier: Option<String>,
86 },
87}
88
89#[derive(Subcommand, Clone)]
90enum ItemCommands {
91 Get { id: String },
92 Create {},
93}
94
95#[derive(Subcommand, Clone)]
96enum GeneratorCommands {
97 Password(PasswordGeneratorArgs),
98 Passphrase(PassphraseGeneratorArgs),
99}
100
101#[derive(Args, Clone)]
102struct PasswordGeneratorArgs {
103 #[arg(short = 'l', long, action, help = "Include lowercase characters (a-z)")]
104 lowercase: bool,
105
106 #[arg(short = 'u', long, action, help = "Include uppercase characters (A-Z)")]
107 uppercase: bool,
108
109 #[arg(short = 'n', long, action, help = "Include numbers (0-9)")]
110 numbers: bool,
111
112 #[arg(
113 short = 's',
114 long,
115 action,
116 help = "Include special characters (!@#$%^&*)"
117 )]
118 special: bool,
119
120 #[arg(long, default_value = "16", help = "Length of generated password")]
121 length: u8,
122}
123
124#[derive(Args, Clone)]
125struct PassphraseGeneratorArgs {
126 #[arg(long, default_value = "3", help = "Number of words in the passphrase")]
127 words: u8,
128 #[arg(long, default_value = " ", help = "Separator between words")]
129 separator: char,
130 #[arg(long, action, help = "Capitalize the first letter of each word")]
131 capitalize: bool,
132 #[arg(long, action, help = "Include a number in one of the words")]
133 include_number: bool,
134}
135
136#[tokio::main(flavor = "current_thread")]
137async fn main() -> Result<()> {
138 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
139
140 process_commands().await
141}
142
143async fn process_commands() -> Result<()> {
144 let cli = Cli::parse();
145
146 install_color_eyre(cli.color)?;
147
148 let Some(command) = cli.command else {
149 let mut cmd = Cli::command();
150 cmd.print_help()?;
151 return Ok(());
152 };
153
154 match command.clone() {
155 Commands::Login(args) => {
156 let settings = args.server.map(|server| ClientSettings {
157 api_url: format!("{}/api", server),
158 identity_url: format!("{}/identity", server),
159 ..Default::default()
160 });
161 let client = bitwarden_core::Client::new(settings);
162
163 match args.command {
164 LoginCommands::Password { email } => {
166 auth::login_password(client, email).await?;
167 }
168 LoginCommands::ApiKey {
169 client_id,
170 client_secret,
171 } => auth::login_api_key(client, client_id, client_secret).await?,
172 LoginCommands::Device {
173 email,
174 device_identifier,
175 } => {
176 auth::login_device(client, email, device_identifier).await?;
177 }
178 }
179 return Ok(());
180 }
181 Commands::Register {
182 email,
183 name,
184 password_hint,
185 server,
186 } => {
187 let settings = server.map(|server| ClientSettings {
188 api_url: format!("{}/api", server),
189 identity_url: format!("{}/identity", server),
190 ..Default::default()
191 });
192 let client = bitwarden_core::Client::new(settings);
193
194 let email = text_prompt_when_none("Email", email)?;
195 let password = Password::new("Password").prompt()?;
196
197 client
198 .auth()
199 .register(&RegisterRequest {
200 email,
201 name,
202 password,
203 password_hint,
204 })
205 .await?;
206 }
207 _ => {}
208 }
209
210 let client = bitwarden_core::Client::new(None);
212
213 match command {
215 Commands::Login(_) => unreachable!(),
216 Commands::Register { .. } => unreachable!(),
217 Commands::Item { command: _ } => todo!(),
218 Commands::Sync {} => todo!(),
219 Commands::Generate { command } => match command {
220 GeneratorCommands::Password(args) => {
221 let password = client.generator().password(PasswordGeneratorRequest {
222 lowercase: args.lowercase,
223 uppercase: args.uppercase,
224 numbers: args.numbers,
225 special: args.special,
226 length: args.length,
227 ..Default::default()
228 })?;
229
230 println!("{}", password);
231 }
232 GeneratorCommands::Passphrase(args) => {
233 let passphrase = client.generator().passphrase(PassphraseGeneratorRequest {
234 num_words: args.words,
235 word_separator: args.separator.to_string(),
236 capitalize: args.capitalize,
237 include_number: args.include_number,
238 })?;
239
240 println!("{}", passphrase);
241 }
242 },
243 };
244
245 Ok(())
246}
247
248#[cfg(test)]
249mod tests {
250 #[test]
251 fn it_works() {
252 assert_eq!(2 + 2, 4);
253 }
254}