Skip to content

Commit 11ace18

Browse files
Merge pull request #212 from brotskydotcom/attributes
Add support for credential-store attributes. Fixes #208.
2 parents d80de84 + 0fff59b commit 11ace18

13 files changed

+689
-157
lines changed

CHANGELOG.md

+22
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
## Version 3.3.0
2+
- Add support for credential-store attributes other than those used by this crate. This allows the creation of credentials that are more compatible with 3rd-party clients, such as the OS-provided GUIs over credentials.
3+
- Make the textual descriptions of entries consistently follow the form `user@service` (or `user@service:target` if a target was specified).
4+
5+
## Version 3.2.1
6+
- Re-enable access to v1 credentials. The fixes of version 3.2 meant that legacy credentials with no target attribute couldn't be accessed.
7+
8+
## Version 3.2.0
9+
- Improve secret-service handling of targets, so that searches on locked items distinguish items with different targets properly.
10+
11+
## Version 3.1.0
12+
- enhance the CLI to allow empty user names and better info about `Ambiguous` credentials.
13+
14+
## Version 3.0.5
15+
- updated docs and clean up dead code. No code changes.
16+
17+
## Version 3.0.4
18+
- expose a cross-platform module alias via the `default` module.
19+
20+
## Version 3.0.3
21+
- fix feature `linux-native`, which was causing compile errors.
22+
123
## Version 3.0.2
224
- add missing implementations for iOS `set_secret` and `get_secret`
325

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ keywords = ["password", "credential", "keychain", "keyring", "cross-platform"]
66
license = "MIT OR Apache-2.0"
77
name = "keyring"
88
repository = "https://github.com/hwchen/keyring-rs.git"
9-
version = "3.2.1"
9+
version = "3.3.0"
1010
rust-version = "1.75"
1111
edition = "2021"
1212
exclude = [".github/"]
@@ -64,6 +64,7 @@ path = "examples/cli.rs"
6464
base64 = "0.22"
6565
clap = { version = "4", features = ["derive", "wrap_help"] }
6666
rpassword = "7"
67+
rprompt = "2"
6768
rand = "0.8"
6869
doc-comment = "0.3"
6970
whoami = "1"

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ Thanks to the following for helping make this library better, whether through co
135135
- @russellbanks
136136
- @ryanavella
137137
- @samuela
138+
- @ShaunSHamilton
138139
- @stankec
139140
- @steveatinfincia
140141
- @Sytten

build-xplat-docs.sh

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/bin/bash
2-
cargo doc --no-deps --target aarch64-unknown-linux-musl $OPEN_DOCS
3-
cargo doc --no-deps --target aarch64-pc-windows-msvc $OPEN_DOCS
4-
cargo doc --no-deps --target aarch64-apple-darwin $OPEN_DOCS
5-
cargo doc --no-deps --target aarch64-apple-ios $OPEN_DOCS
2+
cargo doc --no-deps --features=linux-native --target aarch64-unknown-linux-musl $OPEN_DOCS
3+
cargo doc --no-deps --features=windows-native --target aarch64-pc-windows-msvc $OPEN_DOCS
4+
cargo doc --no-deps --features=apple-native --target aarch64-apple-darwin $OPEN_DOCS
5+
cargo doc --no-deps --features=apple-native --target aarch64-apple-ios $OPEN_DOCS

examples/cli.rs

+175-55
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
extern crate keyring;
22

3-
use clap::Parser;
3+
use clap::{Args, Parser};
4+
use std::collections::HashMap;
45

56
use keyring::{Entry, Error, Result};
67

@@ -21,40 +22,52 @@ fn main() {
2122
};
2223
match &args.command {
2324
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),
2829
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),
3333
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+
}
3444
}
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"),
4046
}
4147
}
4248
Command::Password => match entry.get_password() {
4349
Ok(password) => {
4450
println!("{password}");
45-
args.success_message_for(None, Some(&password));
51+
args.success_message_for(&Value::Password(password));
4652
}
4753
Err(err) => args.error_message_for(err),
4854
},
4955
Command::Secret => match entry.get_secret() {
5056
Ok(secret) => {
5157
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));
5366
}
5467
Err(err) => args.error_message_for(err),
5568
},
5669
Command::Delete => match entry.delete_credential() {
57-
Ok(()) => args.success_message_for(None, None),
70+
Ok(()) => args.success_message_for(&Value::None),
5871
Err(err) => args.error_message_for(err),
5972
},
6073
}
@@ -87,32 +100,58 @@ pub struct Cli {
87100

88101
#[derive(Debug, Parser)]
89102
pub enum Command {
90-
/// Set the password in the secure store
103+
/// Set the password or update the attributes in the secure store
91104
Set {
105+
#[command(flatten)]
106+
what: What,
107+
92108
#[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>,
101113
},
102114
/// Retrieve the (string) password from the secure store
103115
/// and write it to the standard output.
104116
Password,
105117
/// Retrieve the (binary) secret from the secure store
106118
/// and write it in base64 encoding to the standard output.
107119
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.
109123
Delete,
110124
}
111125

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+
112151
impl Cli {
113152
fn description(&self) -> String {
114153
if let Some(target) = &self.target {
115-
format!("[{target}]{}@{}", &self.user, &self.service)
154+
format!("{}@{}:{target}", &self.user, &self.service)
116155
} else {
117156
format!("{}@{}", &self.user, &self.service)
118157
}
@@ -146,6 +185,9 @@ impl Cli {
146185
Command::Secret => {
147186
eprintln!("Couldn't get secret for '{description}': {err}");
148187
}
188+
Command::Attributes => {
189+
eprintln!("Couldn't get attributes for '{description}': {err}");
190+
}
149191
Command::Delete => {
150192
eprintln!("Couldn't delete credential for '{description}': {err}");
151193
}
@@ -155,46 +197,69 @@ impl Cli {
155197
std::process::exit(1)
156198
}
157199

158-
fn success_message_for(&self, secret: Option<&[u8]>, password: Option<&str>) {
200+
fn success_message_for(&self, value: &Value) {
159201
if !self.verbose {
160202
return;
161203
}
162204
let description = self.description();
163205
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) => {
169208
let secret = secret_string(secret);
170209
eprintln!("Set secret for '{description}' to decode of '{secret}'");
171210
}
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+
},
173220
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+
};
180227
}
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+
},
181246
Command::Delete => {
182247
eprintln!("Successfully deleted credential for '{description}'");
183248
}
184249
}
185250
}
186251

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))
196260
}
197-
_ => (None, None),
261+
} else {
262+
panic!("Can't happen: asking for password and attributes on non-set command")
198263
}
199264
}
200265
}
@@ -205,11 +270,66 @@ fn secret_string(secret: &[u8]) -> String {
205270
BASE64_STANDARD.encode(secret)
206271
}
207272

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> {
209280
use base64::prelude::*;
210281

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());
214333
}
334+
attributes
215335
}

0 commit comments

Comments
 (0)