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