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