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