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::{
10    client_state::{AnyState, BwCommand},
11    render::CommandResult,
12};
13
14mod file_output;
15mod receive;
16mod send;
17pub use send::SendArgs;
18
19#[derive(Args, Clone)]
20#[command(
21    about = "Generate a password/passphrase.",
22    after_help = r#"Notes:
23    Default options are `-uln --length 14`.
24    Minimum `length` is 5.
25    Minimum `words` is 3.
26
27Examples:
28    bw generate
29    bw generate -u -l --length 18
30    bw generate -ulns --length 25
31    bw generate -ul
32    bw generate -p --separator _
33    bw generate -p --words 5 --separator space
34    bw generate -p --words 5 --separator empty
35    "#
36)]
37pub struct GenerateArgs {
38    // Password arguments
39    #[arg(short = 'u', long, action, help = "Include uppercase characters (A-Z)")]
40    pub uppercase: bool,
41
42    #[arg(short = 'l', long, action, help = "Include lowercase characters (a-z)")]
43    pub lowercase: bool,
44
45    #[arg(short = 'n', long, action, help = "Include numbers (0-9)")]
46    pub number: bool,
47
48    #[arg(
49        short = 's',
50        long,
51        action,
52        help = "Include special characters (!@#$%^&*)"
53    )]
54    pub special: bool,
55
56    #[arg(long, default_value = "14", help = "Length of generated password")]
57    pub length: u8,
58
59    // Default is 0 so the cascade below (`min_number > 0 → enable numbers`) only triggers when
60    // the user explicitly passed the flag. When `-n` is enabled but `--min-number` is omitted,
61    // the SDK's `get_minimum` still enforces at least one digit.
62    #[arg(
63        long,
64        alias = "minNumber",
65        default_value = "0",
66        help = "Minimum number of numeric characters"
67    )]
68    pub min_number: u8,
69
70    #[arg(
71        long,
72        alias = "minSpecial",
73        default_value = "0",
74        help = "Minimum number of special characters"
75    )]
76    pub min_special: u8,
77
78    #[arg(long, action, help = "Avoid ambiguous characters")]
79    pub ambiguous: bool,
80
81    // Passphrase arguments
82    #[arg(short = 'p', long, action, help = "Generate a passphrase")]
83    pub passphrase: bool,
84
85    #[arg(long, default_value = "6", help = "Number of words in the passphrase")]
86    pub words: u8,
87
88    #[arg(long, default_value = "-", help = "Separator between words")]
89    pub separator: String,
90
91    #[arg(long, action, help = "Title case passphrase.")]
92    pub capitalize: bool,
93
94    #[arg(
95        long,
96        alias = "includeNumber",
97        action,
98        help = "Include a number in one of the words"
99    )]
100    pub include_number: bool,
101}
102
103impl GenerateArgs {
104    pub fn run(self, client: &PasswordManagerClient) -> CommandResult {
105        let result = if self.passphrase {
106            client.generator().passphrase(PassphraseGeneratorRequest {
107                // Silently clamp to the SDK's supported range, matching the Angular clients'
108                // `fitToBounds` in `passphrase-policy-constraints.ts`.
109                num_words: self
110                    .words
111                    .clamp(MINIMUM_PASSPHRASE_NUM_WORDS, MAXIMUM_PASSPHRASE_NUM_WORDS),
112                word_separator: normalize_separator(self.separator),
113                capitalize: self.capitalize,
114                include_number: self.include_number,
115            })?
116        } else {
117            // When the user selects no charset, default to lowercase + uppercase + number,
118            // matching the legacy CLI.
119            let any_explicit = self.lowercase || self.uppercase || self.number || self.special;
120            let lowercase = if any_explicit { self.lowercase } else { true };
121            let uppercase = if any_explicit { self.uppercase } else { true };
122            // Cascade `--min-number` / `--min-special` > 0 into enabling the charset, matching
123            // `PasswordGeneratorOptionsEvaluator.applyPolicy` in the Angular clients.
124            let number = if any_explicit {
125                self.number || self.min_number > 0
126            } else {
127                true
128            };
129            let special = self.special || self.min_special > 0;
130
131            client.generator().password(PasswordGeneratorRequest {
132                lowercase,
133                uppercase,
134                numbers: number,
135                special,
136                length: self
137                    .length
138                    .clamp(MINIMUM_PASSWORD_LENGTH, MAXIMUM_PASSWORD_LENGTH),
139                min_number: Some(
140                    self.min_number
141                        .clamp(MINIMUM_MIN_CHAR_COUNT, MAXIMUM_MIN_CHAR_COUNT),
142                ),
143                min_special: Some(
144                    self.min_special
145                        .clamp(MINIMUM_MIN_CHAR_COUNT, MAXIMUM_MIN_CHAR_COUNT),
146                ),
147                avoid_ambiguous: self.ambiguous,
148                ..Default::default()
149            })?
150        };
151
152        Ok(result.into())
153    }
154}
155
156/// Map CLI-level separator input ("space", "empty", or a string) to the single character the
157/// generator expects.
158fn normalize_separator(separator: String) -> String {
159    match separator.as_str() {
160        "space" => " ".to_string(),
161        "empty" => String::new(),
162        s if s.len() > 1 => s.chars().next().map(|c| c.to_string()).unwrap_or_default(),
163        _ => separator,
164    }
165}
166
167#[derive(Args, Clone)]
168pub struct GetSendArgs {
169    pub id: String,
170}
171
172#[derive(Args, Clone)]
173pub struct ImportArgs {
174    /// Format to import from
175    pub format: Option<String>,
176    /// Filepath to data to import
177    pub input: Option<String>,
178
179    #[arg(long, help = "List formats")]
180    pub formats: bool,
181
182    #[arg(
183        long,
184        alias = "organizationid",
185        help = "ID of the organization to import to."
186    )]
187    pub organization_id: Option<String>,
188}
189
190#[derive(Args, Clone)]
191pub struct ExportArgs {
192    #[arg(long, help = "Output directory or filename.")]
193    pub output: Option<String>,
194
195    #[arg(long, help = "Export file format.")]
196    pub format: Option<String>,
197
198    #[arg(
199        long,
200        help = "Use password to encrypt instead of your Bitwarden account encryption key."
201    )]
202    pub password: Option<String>,
203
204    #[arg(
205        long,
206        alias = "organizationid",
207        help = "Organization id for an organization."
208    )]
209    pub organization_id: Option<String>,
210}
211
212/// `bw receive <url>`. Field-identical to [`send::SendReceiveArgs`] (`bw send receive`) — the two
213/// are the same command reachable under two names, and both delegate to
214/// [`receive::run_receive`].
215#[derive(Args, Clone)]
216#[command(after_help = "Notes:
217    If a password is required, the provided password is used or the user is prompted.")]
218pub struct ReceiveArgs {
219    /// URL to access Send from
220    pub url: String,
221
222    #[arg(long, help = "Optional password for the Send.")]
223    pub password: Option<String>,
224
225    #[arg(long, help = "Environment variable storing the Send's password.")]
226    pub passwordenv: Option<String>,
227
228    #[arg(
229        long,
230        help = "Path to a file containing the Send's password as its first line."
231    )]
232    pub passwordfile: Option<String>,
233
234    // The internal field is `output_path` (not `output`) to avoid clashing with the top-level
235    // `Cli::output` (the `-o` rendered-output-format arg) — same convention as
236    // `SendGetArgs::output_path`. User-facing long flag stays `--output` to match both that
237    // sibling command and the legacy CLI.
238    #[arg(
239        long = "output",
240        help = "Specify a file path to save a File-type Send to."
241    )]
242    pub output_path: Option<String>,
243
244    #[arg(
245        long = "fullObject",
246        alias = "full-object",
247        help = "Return the Send's json object rather than its content."
248    )]
249    pub full_object: bool,
250}
251
252impl BwCommand for ReceiveArgs {
253    // `receive` is the one Send flow that needs no session at all: the key comes from the url
254    // fragment and the token from the anonymous send-access grant.
255    type Client = AnyState;
256
257    async fn run(self, _: AnyState) -> CommandResult {
258        receive::run_receive(receive::ReceiveInputs {
259            url: self.url,
260            password: self.password,
261            passwordenv: self.passwordenv,
262            passwordfile: self.passwordfile,
263            output_path: self.output_path,
264            full_object: self.full_object,
265        })
266        .await
267    }
268}