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