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