bw/
main.rs

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    // Optional as a workaround for https://github.com/clap-rs/clap/issues/3572
19    #[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                // FIXME: Rust CLI will not support password login!
164                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    // Not login, assuming we have a config
192    let client = bitwarden_core::Client::new(None);
193
194    // And finally we process all the commands which require authentication
195    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}