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 #[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 #[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 #[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 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 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 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
156fn 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 pub format: Option<String>,
176 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#[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 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 #[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 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}