1
1
extern crate keyring;
2
2
3
- use clap:: Parser ;
3
+ use clap:: { Args , Parser } ;
4
+ use std:: collections:: HashMap ;
4
5
5
6
use keyring:: { Entry , Error , Result } ;
6
7
@@ -21,40 +22,52 @@ fn main() {
21
22
} ;
22
23
match & args. command {
23
24
Command :: Set { .. } => {
24
- let ( secret , password ) = args. get_password ( ) ;
25
- if let Some ( secret ) = secret {
26
- match entry. set_secret ( & secret) {
27
- Ok ( ( ) ) => args. success_message_for ( Some ( & secret ) , None ) ,
25
+ let value = args. get_password_and_attributes ( ) ;
26
+ match & value {
27
+ Value :: Secret ( secret ) => match entry. set_secret ( secret) {
28
+ Ok ( ( ) ) => args. success_message_for ( & value ) ,
28
29
Err ( err) => args. error_message_for ( err) ,
29
- }
30
- } else if let Some ( password) = password {
31
- match entry. set_password ( & password) {
32
- Ok ( ( ) ) => args. success_message_for ( None , Some ( & password) ) ,
30
+ } ,
31
+ Value :: Password ( password) => match entry. set_password ( password) {
32
+ Ok ( ( ) ) => args. success_message_for ( & value) ,
33
33
Err ( err) => args. error_message_for ( err) ,
34
+ } ,
35
+ Value :: Attributes ( attributes) => {
36
+ let attrs: HashMap < & str , & str > = attributes
37
+ . iter ( )
38
+ . map ( |( k, v) | ( k. as_str ( ) , v. as_str ( ) ) )
39
+ . collect ( ) ;
40
+ match entry. update_attributes ( & attrs) {
41
+ Ok ( ( ) ) => args. success_message_for ( & value) ,
42
+ Err ( err) => args. error_message_for ( err) ,
43
+ }
34
44
}
35
- } else {
36
- if args. verbose {
37
- eprintln ! ( "You must provide a password to the set command" ) ;
38
- }
39
- std:: process:: exit ( 1 )
45
+ _ => panic ! ( "Can't set without a value" ) ,
40
46
}
41
47
}
42
48
Command :: Password => match entry. get_password ( ) {
43
49
Ok ( password) => {
44
50
println ! ( "{password}" ) ;
45
- args. success_message_for ( None , Some ( & password) ) ;
51
+ args. success_message_for ( & Value :: Password ( password) ) ;
46
52
}
47
53
Err ( err) => args. error_message_for ( err) ,
48
54
} ,
49
55
Command :: Secret => match entry. get_secret ( ) {
50
56
Ok ( secret) => {
51
57
println ! ( "{}" , secret_string( & secret) ) ;
52
- args. success_message_for ( Some ( & secret) , None ) ;
58
+ args. success_message_for ( & Value :: Secret ( secret) ) ;
59
+ }
60
+ Err ( err) => args. error_message_for ( err) ,
61
+ } ,
62
+ Command :: Attributes => match entry. get_attributes ( ) {
63
+ Ok ( attributes) => {
64
+ println ! ( "{}" , attributes_string( & attributes) ) ;
65
+ args. success_message_for ( & Value :: Attributes ( attributes) ) ;
53
66
}
54
67
Err ( err) => args. error_message_for ( err) ,
55
68
} ,
56
69
Command :: Delete => match entry. delete_credential ( ) {
57
- Ok ( ( ) ) => args. success_message_for ( None , None ) ,
70
+ Ok ( ( ) ) => args. success_message_for ( & Value :: None ) ,
58
71
Err ( err) => args. error_message_for ( err) ,
59
72
} ,
60
73
}
@@ -87,32 +100,58 @@ pub struct Cli {
87
100
88
101
#[ derive( Debug , Parser ) ]
89
102
pub enum Command {
90
- /// Set the password in the secure store
103
+ /// Set the password or update the attributes in the secure store
91
104
Set {
105
+ #[ command( flatten) ]
106
+ what : What ,
107
+
92
108
#[ clap( value_parser) ]
93
- /// The password to set into the secure store.
94
- /// If it's a valid base64 encoding (with padding),
95
- /// it will be decoded and used to set the binary secret.
96
- /// Otherwise, it will be interpreted as a string password.
97
- /// If no password is specified, it will be
98
- /// collected interactively (without echo)
99
- /// from the terminal.
100
- password : Option < String > ,
109
+ /// The input to parse. If not specified, it will be
110
+ /// read interactively from the terminal. Password/secret
111
+ /// input will not be echoed.
112
+ input : Option < String > ,
101
113
} ,
102
114
/// Retrieve the (string) password from the secure store
103
115
/// and write it to the standard output.
104
116
Password ,
105
117
/// Retrieve the (binary) secret from the secure store
106
118
/// and write it in base64 encoding to the standard output.
107
119
Secret ,
108
- /// Delete the underlying credential from the secure store.
120
+ /// Retrieve attributes available in the secure store.
121
+ Attributes ,
122
+ /// Delete the credential from the secure store.
109
123
Delete ,
110
124
}
111
125
126
+ #[ derive( Debug , Args ) ]
127
+ #[ group( multiple = false , required = true ) ]
128
+ pub struct What {
129
+ #[ clap( short, long, action, help = "The input is a password" ) ]
130
+ password : bool ,
131
+
132
+ #[ clap( short, long, action, help = "The input is a base64-encoded secret" ) ]
133
+ secret : bool ,
134
+
135
+ #[ clap(
136
+ short,
137
+ long,
138
+ action,
139
+ help = "The input is comma-separated, key=val attribute pairs"
140
+ ) ]
141
+ attributes : bool ,
142
+ }
143
+
144
+ enum Value {
145
+ Secret ( Vec < u8 > ) ,
146
+ Password ( String ) ,
147
+ Attributes ( HashMap < String , String > ) ,
148
+ None ,
149
+ }
150
+
112
151
impl Cli {
113
152
fn description ( & self ) -> String {
114
153
if let Some ( target) = & self . target {
115
- format ! ( "[{target}]{}@{ }" , & self . user, & self . service)
154
+ format ! ( "{}@{}:{target }" , & self . user, & self . service)
116
155
} else {
117
156
format ! ( "{}@{}" , & self . user, & self . service)
118
157
}
@@ -146,6 +185,9 @@ impl Cli {
146
185
Command :: Secret => {
147
186
eprintln ! ( "Couldn't get secret for '{description}': {err}" ) ;
148
187
}
188
+ Command :: Attributes => {
189
+ eprintln ! ( "Couldn't get attributes for '{description}': {err}" ) ;
190
+ }
149
191
Command :: Delete => {
150
192
eprintln ! ( "Couldn't delete credential for '{description}': {err}" ) ;
151
193
}
@@ -155,46 +197,69 @@ impl Cli {
155
197
std:: process:: exit ( 1 )
156
198
}
157
199
158
- fn success_message_for ( & self , secret : Option < & [ u8 ] > , password : Option < & str > ) {
200
+ fn success_message_for ( & self , value : & Value ) {
159
201
if !self . verbose {
160
202
return ;
161
203
}
162
204
let description = self . description ( ) ;
163
205
match self . command {
164
- Command :: Set { .. } => {
165
- if let Some ( pw) = password {
166
- eprintln ! ( "Set password for '{description}' to '{pw}'" ) ;
167
- }
168
- if let Some ( secret) = secret {
206
+ Command :: Set { .. } => match value {
207
+ Value :: Secret ( secret) => {
169
208
let secret = secret_string ( secret) ;
170
209
eprintln ! ( "Set secret for '{description}' to decode of '{secret}'" ) ;
171
210
}
172
- }
211
+ Value :: Password ( password) => {
212
+ eprintln ! ( "Set password for '{description}' to '{password}'" ) ;
213
+ }
214
+ Value :: Attributes ( attributes) => {
215
+ eprintln ! ( "The following attributes for '{description}' were sent for update:" ) ;
216
+ eprint_attributes ( attributes) ;
217
+ }
218
+ _ => panic ! ( "Can't set without a value" ) ,
219
+ } ,
173
220
Command :: Password => {
174
- let pw = password . unwrap ( ) ;
175
- eprintln ! ( "Password for '{description}' is '{pw}'" ) ;
176
- }
177
- Command :: Secret => {
178
- let secret = secret_string ( secret . unwrap ( ) ) ;
179
- eprintln ! ( "Secret for '{description}' encodes as {secret}" ) ;
221
+ match value {
222
+ Value :: Password ( password ) => {
223
+ eprintln ! ( "Password for '{description}' is '{password}'" ) ;
224
+ }
225
+ _ => panic ! ( "Wrong value type for command" ) ,
226
+ } ;
180
227
}
228
+ Command :: Secret => match value {
229
+ Value :: Secret ( secret) => {
230
+ let encoded = secret_string ( secret) ;
231
+ eprintln ! ( "Secret for '{description}' encodes as {encoded}" ) ;
232
+ }
233
+ _ => panic ! ( "Wrong value type for command" ) ,
234
+ } ,
235
+ Command :: Attributes => match value {
236
+ Value :: Attributes ( attributes) => {
237
+ if attributes. is_empty ( ) {
238
+ eprintln ! ( "No attributes found for '{description}'" ) ;
239
+ } else {
240
+ eprintln ! ( "Attributes for '{description}' are:" ) ;
241
+ eprint_attributes ( attributes) ;
242
+ }
243
+ }
244
+ _ => panic ! ( "Wrong value type for command" ) ,
245
+ } ,
181
246
Command :: Delete => {
182
247
eprintln ! ( "Successfully deleted credential for '{description}'" ) ;
183
248
}
184
249
}
185
250
}
186
251
187
- fn get_password ( & self ) -> ( Option < Vec < u8 > > , Option < String > ) {
188
- match & self . command {
189
- Command :: Set { password : Some ( pw) } => password_or_secret ( pw) ,
190
- Command :: Set { password : None } => {
191
- if let Ok ( password) = rpassword:: prompt_password ( "Password: " ) {
192
- password_or_secret ( & password)
193
- } else {
194
- ( None , None )
195
- }
252
+ fn get_password_and_attributes ( & self ) -> Value {
253
+ if let Command :: Set { what, input } = & self . command {
254
+ if what. password {
255
+ Value :: Password ( read_password ( input) )
256
+ } else if what. secret {
257
+ Value :: Secret ( decode_secret ( input) )
258
+ } else {
259
+ Value :: Attributes ( parse_attributes ( input) )
196
260
}
197
- _ => ( None , None ) ,
261
+ } else {
262
+ panic ! ( "Can't happen: asking for password and attributes on non-set command" )
198
263
}
199
264
}
200
265
}
@@ -205,11 +270,66 @@ fn secret_string(secret: &[u8]) -> String {
205
270
BASE64_STANDARD . encode ( secret)
206
271
}
207
272
208
- fn password_or_secret ( input : & str ) -> ( Option < Vec < u8 > > , Option < String > ) {
273
+ fn eprint_attributes ( attributes : & HashMap < String , String > ) {
274
+ for ( key, value) in attributes {
275
+ println ! ( " {key}: {value}" ) ;
276
+ }
277
+ }
278
+
279
+ fn decode_secret ( input : & Option < String > ) -> Vec < u8 > {
209
280
use base64:: prelude:: * ;
210
281
211
- match BASE64_STANDARD . decode ( input) {
212
- Ok ( secret) => ( Some ( secret) , None ) ,
213
- Err ( _) => ( None , Some ( input. to_string ( ) ) ) ,
282
+ let encoded = if let Some ( input) = input {
283
+ input. clone ( )
284
+ } else {
285
+ rpassword:: prompt_password ( "Base64 encoding: " ) . unwrap_or_else ( |_| String :: new ( ) )
286
+ } ;
287
+ if encoded. is_empty ( ) {
288
+ return Vec :: new ( ) ;
289
+ }
290
+ match BASE64_STANDARD . decode ( encoded) {
291
+ Ok ( secret) => secret,
292
+ Err ( err) => {
293
+ eprintln ! ( "Sorry, the provided secret data is not base64-encoded: {err}" ) ;
294
+ std:: process:: exit ( 1 ) ;
295
+ }
296
+ }
297
+ }
298
+
299
+ fn read_password ( input : & Option < String > ) -> String {
300
+ if let Some ( input) = input {
301
+ input. clone ( )
302
+ } else {
303
+ rpassword:: prompt_password ( "Password: " ) . unwrap_or_else ( |_| String :: new ( ) )
304
+ }
305
+ }
306
+
307
+ fn attributes_string ( attributes : & HashMap < String , String > ) -> String {
308
+ let strings = attributes
309
+ . iter ( )
310
+ . map ( |( k, v) | format ! ( "{}={}" , k, v) )
311
+ . collect :: < Vec < _ > > ( ) ;
312
+ strings. join ( "," )
313
+ }
314
+
315
+ fn parse_attributes ( input : & Option < String > ) -> HashMap < String , String > {
316
+ let input = if let Some ( input) = input {
317
+ input. clone ( )
318
+ } else {
319
+ rprompt:: prompt_reply ( "Attributes: " ) . unwrap_or_else ( |_| String :: new ( ) )
320
+ } ;
321
+ if input. is_empty ( ) {
322
+ eprintln ! ( "You must specify at least one key=value attribute pair to set" )
323
+ }
324
+ let mut attributes = HashMap :: new ( ) ;
325
+ let parts = input. split ( ',' ) ;
326
+ for s in parts. into_iter ( ) {
327
+ let parts: Vec < & str > = s. split ( "=" ) . collect ( ) ;
328
+ if parts. len ( ) != 2 || parts[ 0 ] . is_empty ( ) {
329
+ eprintln ! ( "Sorry, this part of the attributes string is not a key=val pair: {s}" ) ;
330
+ std:: process:: exit ( 1 ) ;
331
+ }
332
+ attributes. insert ( parts[ 0 ] . to_string ( ) , parts[ 1 ] . to_string ( ) ) ;
214
333
}
334
+ attributes
215
335
}
0 commit comments