Skip to main content

bw/tools/
mod.rs

1use bitwarden_generators::{
2    MAXIMUM_MIN_CHAR_COUNT, MAXIMUM_PASSPHRASE_NUM_WORDS, MAXIMUM_PASSWORD_LENGTH,
3    MINIMUM_MIN_CHAR_COUNT, MINIMUM_PASSPHRASE_NUM_WORDS, MINIMUM_PASSWORD_LENGTH,
4    PassphraseGeneratorRequest, PasswordGeneratorRequest,
5};
6use bitwarden_pm::PasswordManagerClient;
7use clap::Args;
8
9use crate::render::CommandResult;
10
11mod send;
12pub use send::SendArgs;
13
14#[derive(Args, Clone)]
15#[command(
16    about = "Generate a password/passphrase.",
17    after_help = r#"Notes:
18    Default options are `-uln --length 14`.
19    Minimum `length` is 5.
20    Minimum `words` is 3.
21
22Examples:
23    bw generate
24    bw generate -u -l --length 18
25    bw generate -ulns --length 25
26    bw generate -ul
27    bw generate -p --separator _
28    bw generate -p --words 5 --separator space
29    bw generate -p --words 5 --separator empty
30    "#
31)]
32pub struct GenerateArgs {
33    // Password arguments
34    #[arg(short = 'u', long, action, help = "Include uppercase characters (A-Z)")]
35    pub uppercase: bool,
36
37    #[arg(short = 'l', long, action, help = "Include lowercase characters (a-z)")]
38    pub lowercase: bool,
39
40    #[arg(short = 'n', long, action, help = "Include numbers (0-9)")]
41    pub number: bool,
42
43    #[arg(
44        short = 's',
45        long,
46        action,
47        help = "Include special characters (!@#$%^&*)"
48    )]
49    pub special: bool,
50
51    #[arg(long, default_value = "14", help = "Length of generated password")]
52    pub length: u8,
53
54    // Default is 0 so the cascade below (`min_number > 0 → enable numbers`) only triggers when
55    // the user explicitly passed the flag. When `-n` is enabled but `--min-number` is omitted,
56    // the SDK's `get_minimum` still enforces at least one digit.
57    #[arg(
58        long,
59        alias = "minNumber",
60        default_value = "0",
61        help = "Minimum number of numeric characters"
62    )]
63    pub min_number: u8,
64
65    #[arg(
66        long,
67        alias = "minSpecial",
68        default_value = "0",
69        help = "Minimum number of special characters"
70    )]
71    pub min_special: u8,
72
73    #[arg(long, action, help = "Avoid ambiguous characters")]
74    pub ambiguous: bool,
75
76    // Passphrase arguments
77    #[arg(short = 'p', long, action, help = "Generate a passphrase")]
78    pub passphrase: bool,
79
80    #[arg(long, default_value = "6", help = "Number of words in the passphrase")]
81    pub words: u8,
82
83    #[arg(long, default_value = "-", help = "Separator between words")]
84    pub separator: String,
85
86    #[arg(long, action, help = "Title case passphrase.")]
87    pub capitalize: bool,
88
89    #[arg(
90        long,
91        alias = "includeNumber",
92        action,
93        help = "Include a number in one of the words"
94    )]
95    pub include_number: bool,
96}
97
98impl GenerateArgs {
99    pub fn run(self, client: &PasswordManagerClient) -> CommandResult {
100        let result = if self.passphrase {
101            client.generator().passphrase(PassphraseGeneratorRequest {
102                // Silently clamp to the SDK's supported range, matching the Angular clients'
103                // `fitToBounds` in `passphrase-policy-constraints.ts`.
104                num_words: self
105                    .words
106                    .clamp(MINIMUM_PASSPHRASE_NUM_WORDS, MAXIMUM_PASSPHRASE_NUM_WORDS),
107                word_separator: normalize_separator(self.separator),
108                capitalize: self.capitalize,
109                include_number: self.include_number,
110            })?
111        } else {
112            // When the user selects no charset, default to lowercase + uppercase + number,
113            // matching the legacy CLI.
114            let any_explicit = self.lowercase || self.uppercase || self.number || self.special;
115            let lowercase = if any_explicit { self.lowercase } else { true };
116            let uppercase = if any_explicit { self.uppercase } else { true };
117            // Cascade `--min-number` / `--min-special` > 0 into enabling the charset, matching
118            // `PasswordGeneratorOptionsEvaluator.applyPolicy` in the Angular clients.
119            let number = if any_explicit {
120                self.number || self.min_number > 0
121            } else {
122                true
123            };
124            let special = self.special || self.min_special > 0;
125
126            client.generator().password(PasswordGeneratorRequest {
127                lowercase,
128                uppercase,
129                numbers: number,
130                special,
131                length: self
132                    .length
133                    .clamp(MINIMUM_PASSWORD_LENGTH, MAXIMUM_PASSWORD_LENGTH),
134                min_number: Some(
135                    self.min_number
136                        .clamp(MINIMUM_MIN_CHAR_COUNT, MAXIMUM_MIN_CHAR_COUNT),
137                ),
138                min_special: Some(
139                    self.min_special
140                        .clamp(MINIMUM_MIN_CHAR_COUNT, MAXIMUM_MIN_CHAR_COUNT),
141                ),
142                avoid_ambiguous: self.ambiguous,
143                ..Default::default()
144            })?
145        };
146
147        Ok(result.into())
148    }
149}
150
151/// Map CLI-level separator input ("space", "empty", or a string) to the single character the
152/// generator expects.
153fn normalize_separator(separator: String) -> String {
154    match separator.as_str() {
155        "space" => " ".to_string(),
156        "empty" => String::new(),
157        s if s.len() > 1 => s.chars().next().map(|c| c.to_string()).unwrap_or_default(),
158        _ => separator,
159    }
160}
161
162#[derive(Args, Clone)]
163pub struct GetSendArgs {
164    pub id: String,
165}
166
167#[derive(Args, Clone)]
168pub struct ImportArgs {
169    /// Format to import from
170    pub format: Option<String>,
171    /// Filepath to data to import
172    pub input: Option<String>,
173
174    #[arg(long, help = "List formats")]
175    pub formats: bool,
176
177    #[arg(
178        long,
179        alias = "organizationid",
180        help = "ID of the organization to import to."
181    )]
182    pub organization_id: Option<String>,
183}
184
185#[derive(Args, Clone)]
186pub struct ExportArgs {
187    #[arg(long, help = "Output directory or filename.")]
188    pub output: Option<String>,
189
190    #[arg(long, help = "Export file format.")]
191    pub format: Option<String>,
192
193    #[arg(
194        long,
195        help = "Use password to encrypt instead of your Bitwarden account encryption key."
196    )]
197    pub password: Option<String>,
198
199    #[arg(
200        long,
201        alias = "organizationid",
202        help = "Organization id for an organization."
203    )]
204    pub organization_id: Option<String>,
205}
206
207#[derive(Args, Clone)]
208pub struct ReceiveArgs {
209    /// URL to access Send from
210    pub url: String,
211
212    #[arg(long, help = "Optional password for the Send.")]
213    pub password: Option<String>,
214
215    #[arg(long, help = "Specify a file path to save a File-type Send to.")]
216    pub obj: Option<String>,
217}