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, Subcommand};
8
9use crate::render::CommandResult;
10
11#[derive(Args, Clone)]
12#[command(
13    about = "Generate a password/passphrase.",
14    after_help = r#"Notes:
15    Default options are `-uln --length 14`.
16    Minimum `length` is 5.
17    Minimum `words` is 3.
18
19Examples:
20    bw generate
21    bw generate -u -l --length 18
22    bw generate -ulns --length 25
23    bw generate -ul
24    bw generate -p --separator _
25    bw generate -p --words 5 --separator space
26    bw generate -p --words 5 --separator empty
27    "#
28)]
29pub struct GenerateArgs {
30    // Password arguments
31    #[arg(short = 'u', long, action, help = "Include uppercase characters (A-Z)")]
32    pub uppercase: bool,
33
34    #[arg(short = 'l', long, action, help = "Include lowercase characters (a-z)")]
35    pub lowercase: bool,
36
37    #[arg(short = 'n', long, action, help = "Include numbers (0-9)")]
38    pub number: bool,
39
40    #[arg(
41        short = 's',
42        long,
43        action,
44        help = "Include special characters (!@#$%^&*)"
45    )]
46    pub special: bool,
47
48    #[arg(long, default_value = "14", help = "Length of generated password")]
49    pub length: u8,
50
51    // Default is 0 so the cascade below (`min_number > 0 → enable numbers`) only triggers when
52    // the user explicitly passed the flag. When `-n` is enabled but `--min-number` is omitted,
53    // the SDK's `get_minimum` still enforces at least one digit.
54    #[arg(
55        long,
56        alias = "minNumber",
57        default_value = "0",
58        help = "Minimum number of numeric characters"
59    )]
60    pub min_number: u8,
61
62    #[arg(
63        long,
64        alias = "minSpecial",
65        default_value = "0",
66        help = "Minimum number of special characters"
67    )]
68    pub min_special: u8,
69
70    #[arg(long, action, help = "Avoid ambiguous characters")]
71    pub ambiguous: bool,
72
73    // Passphrase arguments
74    #[arg(short = 'p', long, action, help = "Generate a passphrase")]
75    pub passphrase: bool,
76
77    #[arg(long, default_value = "6", help = "Number of words in the passphrase")]
78    pub words: u8,
79
80    #[arg(long, default_value = "-", help = "Separator between words")]
81    pub separator: String,
82
83    #[arg(long, action, help = "Title case passphrase.")]
84    pub capitalize: bool,
85
86    #[arg(
87        long,
88        alias = "includeNumber",
89        action,
90        help = "Include a number in one of the words"
91    )]
92    pub include_number: bool,
93}
94
95impl GenerateArgs {
96    pub fn run(self, client: &PasswordManagerClient) -> CommandResult {
97        let result = if self.passphrase {
98            client.generator().passphrase(PassphraseGeneratorRequest {
99                // Silently clamp to the SDK's supported range, matching the Angular clients'
100                // `fitToBounds` in `passphrase-policy-constraints.ts`.
101                num_words: self
102                    .words
103                    .clamp(MINIMUM_PASSPHRASE_NUM_WORDS, MAXIMUM_PASSPHRASE_NUM_WORDS),
104                word_separator: normalize_separator(self.separator),
105                capitalize: self.capitalize,
106                include_number: self.include_number,
107            })?
108        } else {
109            // When the user selects no charset, default to lowercase + uppercase + number,
110            // matching the legacy CLI.
111            let any_explicit = self.lowercase || self.uppercase || self.number || self.special;
112            let lowercase = if any_explicit { self.lowercase } else { true };
113            let uppercase = if any_explicit { self.uppercase } else { true };
114            // Cascade `--min-number` / `--min-special` > 0 into enabling the charset, matching
115            // `PasswordGeneratorOptionsEvaluator.applyPolicy` in the Angular clients.
116            let number = if any_explicit {
117                self.number || self.min_number > 0
118            } else {
119                true
120            };
121            let special = self.special || self.min_special > 0;
122
123            client.generator().password(PasswordGeneratorRequest {
124                lowercase,
125                uppercase,
126                numbers: number,
127                special,
128                length: self
129                    .length
130                    .clamp(MINIMUM_PASSWORD_LENGTH, MAXIMUM_PASSWORD_LENGTH),
131                min_number: Some(
132                    self.min_number
133                        .clamp(MINIMUM_MIN_CHAR_COUNT, MAXIMUM_MIN_CHAR_COUNT),
134                ),
135                min_special: Some(
136                    self.min_special
137                        .clamp(MINIMUM_MIN_CHAR_COUNT, MAXIMUM_MIN_CHAR_COUNT),
138                ),
139                avoid_ambiguous: self.ambiguous,
140                ..Default::default()
141            })?
142        };
143
144        Ok(result.into())
145    }
146}
147
148/// Map CLI-level separator input ("space", "empty", or a string) to the single character the
149/// generator expects.
150fn normalize_separator(separator: String) -> String {
151    match separator.as_str() {
152        "space" => " ".to_string(),
153        "empty" => String::new(),
154        s if s.len() > 1 => s.chars().next().map(|c| c.to_string()).unwrap_or_default(),
155        _ => separator,
156    }
157}
158
159#[derive(Args, Clone)]
160pub struct GetSendArgs {
161    pub id: String,
162}
163
164#[derive(Args, Clone)]
165pub struct ImportArgs {
166    /// Format to import from
167    pub format: Option<String>,
168    /// Filepath to data to import
169    pub input: Option<String>,
170
171    #[arg(long, help = "List formats")]
172    pub formats: bool,
173
174    #[arg(
175        long,
176        alias = "organizationid",
177        help = "ID of the organization to import to."
178    )]
179    pub organization_id: Option<String>,
180}
181
182#[derive(Args, Clone)]
183pub struct ExportArgs {
184    #[arg(long, help = "Output directory or filename.")]
185    pub output: Option<String>,
186
187    #[arg(long, help = "Export file format.")]
188    pub format: Option<String>,
189
190    #[arg(
191        long,
192        help = "Use password to encrypt instead of your Bitwarden account encryption key."
193    )]
194    pub password: Option<String>,
195
196    #[arg(
197        long,
198        alias = "organizationid",
199        help = "Organization id for an organization."
200    )]
201    pub organization_id: Option<String>,
202}
203
204#[derive(Args, Clone)]
205pub struct SendArgs {
206    /// The data to Send
207    pub data: Option<String>,
208
209    #[arg(short = 'f', long, help = "Specifies that <data> is a filepath.")]
210    pub file: bool,
211
212    #[arg(
213        short = 'd',
214        long = "deleteInDays",
215        help = "The number of days in the future to set deletion date.",
216        default_value = "7"
217    )]
218    pub delete_in_days: String,
219
220    #[arg(long, help = "Optional password to access this Send.")]
221    pub password: Option<String>,
222
223    #[arg(
224        short = 'a',
225        long = "maxAccessCount",
226        help = "The amount of max possible accesses."
227    )]
228    pub max_access_count: Option<u32>,
229
230    #[arg(long, help = "Hide <data> in web by default.")]
231    pub hidden: bool,
232
233    #[arg(short = 'n', long, help = "The name of the Send.")]
234    pub name: Option<String>,
235
236    #[arg(long, help = "Notes to add to the Send.")]
237    pub notes: Option<String>,
238
239    #[arg(
240        long = "fullObject",
241        help = "Specifies that the full Send object should be returned."
242    )]
243    pub full_object: bool,
244
245    #[command(subcommand)]
246    pub command: Option<SendCommands>,
247}
248
249#[derive(Subcommand, Clone, Debug)]
250pub enum SendCommands {
251    #[command(about = "List all the Sends owned by you.")]
252    List,
253
254    #[command(about = "Get json templates for send objects.")]
255    Template { object: String },
256
257    #[command(about = "Get Sends owned by you.")]
258    Get {
259        id: String,
260
261        #[arg(long, help = "Specify a file path to save a File-type Send to.")]
262        output: Option<String>,
263
264        #[arg(long, help = "Only return the access url.")]
265        text: bool,
266    },
267
268    #[command(about = "Access a Bitwarden Send from a url.")]
269    Receive {
270        url: String,
271
272        #[arg(long, help = "Optional password for the Send.")]
273        password: Option<String>,
274
275        #[arg(long, help = "Specify a file path to save a File-type Send to.")]
276        obj: Option<String>,
277    },
278
279    #[command(about = "Create a Send.")]
280    Create {
281        encoded_json: Option<String>,
282
283        #[arg(short = 'f', long, help = "Path to the file to Send.")]
284        file: Option<String>,
285
286        #[arg(long, help = "Text to Send.")]
287        text: Option<String>,
288
289        #[arg(
290            short = 'd',
291            long = "deleteInDays",
292            help = "The number of days in the future to set deletion date.",
293            default_value = "7"
294        )]
295        delete_in_days: String,
296
297        #[arg(
298            long = "maxAccessCount",
299            help = "The maximum number of times this Send can be accessed."
300        )]
301        max_access_count: Option<u32>,
302
303        #[arg(long, help = "Hide text.")]
304        hidden: bool,
305
306        #[arg(short = 'n', long, help = "The name of the Send.")]
307        name: Option<String>,
308
309        #[arg(long, help = "Notes to add to the Send.")]
310        notes: Option<String>,
311
312        #[arg(long, help = "Optional password to access this Send.")]
313        password: Option<String>,
314
315        #[arg(
316            long = "fullObject",
317            help = "Return full Send object instead of access url."
318        )]
319        full_object: bool,
320    },
321
322    #[command(about = "Edit a Send.")]
323    Edit {
324        encoded_json: Option<String>,
325
326        #[arg(long, help = "Overrides the itemId provided in encodedJson.")]
327        itemid: Option<String>,
328
329        #[arg(
330            short = 'd',
331            long = "deleteInDays",
332            help = "The number of days in the future to set deletion date."
333        )]
334        delete_in_days: Option<String>,
335
336        #[arg(
337            long = "maxAccessCount",
338            help = "The maximum number of times this Send can be accessed."
339        )]
340        max_access_count: Option<u32>,
341
342        #[arg(long, help = "Hide text.")]
343        hidden: bool,
344    },
345
346    #[command(about = "Removes the saved password from a Send.")]
347    RemovePassword { id: String },
348
349    #[command(about = "Delete a Send.")]
350    Delete { id: String },
351}
352
353#[derive(Args, Clone)]
354pub struct ReceiveArgs {
355    /// URL to access Send from
356    pub url: String,
357
358    #[arg(long, help = "Optional password for the Send.")]
359    pub password: Option<String>,
360
361    #[arg(long, help = "Specify a file path to save a File-type Send to.")]
362    pub obj: Option<String>,
363}