Skip to content

Commit 9cb38f1

Browse files
Merge pull request #205 from brotskydotcom/issue-204
Always include the target when searching for credentials.
2 parents 0b2ced7 + 13f069f commit 9cb38f1

File tree

4 files changed

+94
-48
lines changed

4 files changed

+94
-48
lines changed

Cargo.toml

+1-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.1.0"
9+
version = "3.2.0"
1010
rust-version = "1.75"
1111
edition = "2021"
1212
exclude = [".github/"]

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ The main API change between v2 and v3 is the addition of support for non-string
9191

9292
Another API change between v2 and v3 is that the notion of a default feature set has gone away: you must now specify explicitly which crate-supported keystores you want included (other than the `mock` keystore, which is always present). So all keyring client developers will need to update their `Cargo.toml` file to use the new features correctly.
9393

94-
All v2 data is fully forward-compatible with v3 data; there have been no changes at all in that respect.
94+
All v2 data is fully forward-compatible with v3 data; there have been no changes at all in that respect. _However_, unlike v2, the v3 implementation of the secret service credential store will _not_ read credentials that were written by the v1 keyring. (For details about why this decision was made, see [this issue](https://github.com/hwchen/keyring-rs/issues/204)). Keyring clients who use the secret service and are still using old v1 credentials should replace those credentials with v2/v3 credentials. The CLI has been extended to allow reading and deleting v1 credentials (and thus provides sample code for how to do this).
9595

9696
The MSRV has been moved to 1.75, and all direct dependencies are at their latest stable versions.
9797

examples/cli.rs

+44-6
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,44 @@ fn main() {
6060
}
6161
}
6262

63+
#[cfg(all(
64+
any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"),
65+
any(feature = "sync-secret-service", feature = "async-secret-service")
66+
))]
67+
mod v1 {
68+
use keyring::{secret_service::SsCredential, Entry, Result};
69+
70+
/// Create a v1-like entry (one with no target attribute)
71+
pub fn new_entry(service: &str, user: &str) -> Result<Entry> {
72+
let cred = SsCredential::new_with_no_target(service, user)?;
73+
Ok(Entry::new_with_credential(Box::new(cred)))
74+
}
75+
}
76+
#[cfg(not(all(
77+
any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"),
78+
any(feature = "sync-secret-service", feature = "async-secret-service")
79+
)))]
80+
mod v1 {
81+
use keyring::Entry;
82+
83+
/// For everything but the secret service, v1 entries are the same as
84+
/// regular entries with the default target.
85+
pub fn new_entry(service: &str, user: &str) -> keyring::Result<Entry> {
86+
Entry::new(service, user)
87+
}
88+
}
89+
6390
#[derive(Debug, Parser)]
6491
#[clap(author = "github.com/hwchen/keyring-rs")]
6592
/// Keyring CLI: A command-line interface to platform secure storage
6693
pub struct Cli {
67-
#[clap(short, long, action)]
68-
/// Write debugging info to stderr, including retrieved passwords
69-
/// and secrets. If an operation fails, error information is provided.
94+
#[clap(short, long, action, verbatim_doc_comment)]
95+
/// Write debugging info to stderr, including retrieved passwords and secrets.
96+
/// If an operation fails, detailed error information is provided.
7097
pub verbose: bool,
7198

7299
#[clap(short, long, value_parser)]
73-
/// The (optional) target for the entry. If none is provided,
74-
/// the entry will be created from the service and user only.
100+
/// The (optional) target for the entry.
75101
pub target: Option<String>,
76102

77103
#[clap(short, long, value_parser, default_value = "keyring-cli")]
@@ -82,6 +108,12 @@ pub struct Cli {
82108
/// The user for the entry.
83109
pub user: String,
84110

111+
#[clap(long, action, verbatim_doc_comment)]
112+
/// Whether to look for v1 entries (that have no target).
113+
/// N.B.: v1 entries can only be read or deleted, not set.
114+
/// This may also find v2/v3 entries that have a target.
115+
pub v1: bool,
116+
85117
#[clap(subcommand)]
86118
pub command: Command,
87119
}
@@ -120,7 +152,13 @@ impl Cli {
120152
}
121153

122154
fn entry_for(&self) -> Result<Entry> {
123-
if let Some(target) = &self.target {
155+
if self.v1 {
156+
if self.target.is_some() {
157+
eprintln!("usage error: You cannot specify both --target and --v1");
158+
std::process::exit(1)
159+
}
160+
v1::new_entry(&self.service, &self.user)
161+
} else if let Some(target) = &self.target {
124162
Entry::new_with_target(target, &self.service, &self.user)
125163
} else {
126164
Entry::new(&self.service, &self.user)

src/secret_service.rs

+48-40
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,26 @@ This provides better compatibility with 3rd party clients that may already
2828
have created items that match the entry, and reduces the chance
2929
of ambiguity in later searches.
3030
31-
## Async runtime required
32-
33-
While this crate uses the secret-service via its blocking API,
34-
the secret-service crate is built on zbus which always talks to the dbus via async calls.
35-
Thus, using the secret-service implies using an async runtime under the covers.
36-
If you are already using an async runtime,
37-
you can use keyring features to make sure that secret-service
38-
uses a compatible runtime. But be careful: if you make keyring calls
39-
on the main thread in this situation, you will likely crash because
40-
you will block the main thread (see\
31+
## keyring v1 incompatibility
32+
33+
In order to fix
34+
[this bug](https://github.com/hwchen/keyring-rs/issues/204)
35+
efficiently, this implementation can no longer access
36+
credentials that have no `target` attribute. Since keyring v1
37+
didn't set this attribute, any old credentials left from v1
38+
will have to be upgraded to a v3-compatible format
39+
using platform-specific code. You can use the new secret-service-specific
40+
entry creation call [new_with_no_target] to
41+
create an [Entry] that will retrieve a v1 password and/or delete it.
42+
43+
## Tokio runtime caution
44+
45+
If you are using the `async-secret-service` with this crate,
46+
and specifying `tokio` as your runtime, be careful:
47+
if you make keyring calls on the main thread, you will likely deadlock (see\
4148
[this issue on GitHub](https://github.com/hwchen/keyring-rs/issues/132)
42-
for details). You will need to spawn a separate thread on which
43-
you make your keyring calls so the main thread doesn't block.
49+
for details). You need to spawn a separate thread on which
50+
you make your keyring calls to avoid this.
4451
4552
## Headless usage
4653
@@ -233,6 +240,26 @@ impl SsCredential {
233240
})
234241
}
235242

243+
/// Create a credential that has *no* target and the given service and user.
244+
///
245+
/// This emulates what keyring v1 did, and can be very handy when you need to
246+
/// access an old v1 credential that's in your secret service default collection.
247+
pub fn new_with_no_target(service: &str, user: &str) -> Result<Self> {
248+
let attributes = HashMap::from([
249+
("service".to_string(), service.to_string()),
250+
("username".to_string(), user.to_string()),
251+
("application".to_string(), "rust-keyring".to_string()),
252+
]);
253+
Ok(Self {
254+
attributes,
255+
label: format!(
256+
"keyring-rs v{} for no target, service '{service}', user '{user}'",
257+
env!("CARGO_PKG_VERSION"),
258+
),
259+
target: None,
260+
})
261+
}
262+
236263
/// Create a credential from an underlying item.
237264
///
238265
/// The created credential will have all the attributes and label
@@ -259,15 +286,15 @@ impl SsCredential {
259286
/// (This is useful if [get_password](SsCredential::get_password)
260287
/// returns an [Ambiguous](ErrorCode::Ambiguous) error.)
261288
pub fn get_all_passwords(&self) -> Result<Vec<String>> {
262-
self.map_matching_items(get_item_password, true)
289+
self.map_matching_items(get_item_password, false)
263290
}
264291

265292
/// If there are multiple matching items for this credential, delete all of them.
266293
///
267294
/// (This is useful if [delete_credential](SsCredential::delete_credential)
268295
/// returns an [Ambiguous](ErrorCode::Ambiguous) error.)
269296
pub fn delete_all_passwords(&self) -> Result<()> {
270-
self.map_matching_items(delete_item, true)?;
297+
self.map_matching_items(delete_item, false)?;
271298
Ok(())
272299
}
273300

@@ -293,27 +320,24 @@ impl SsCredential {
293320
let ss = SecretService::connect(session_type).map_err(platform_failure)?;
294321
let attributes: HashMap<&str, &str> = self.search_attributes().into_iter().collect();
295322
let search = ss.search_items(attributes).map_err(decode_error)?;
296-
let target = self.target.as_ref().ok_or_else(empty_target)?;
297-
let unlocked = matching_target_items(&search.unlocked, target)?;
298-
let locked = matching_target_items(&search.locked, target)?;
299323
if require_unique {
300-
let count = locked.len() + unlocked.len();
324+
let count = search.locked.len() + search.unlocked.len();
301325
if count == 0 {
302326
return Err(ErrorCode::NoEntry);
303327
} else if count > 1 {
304328
let mut creds: Vec<Box<Credential>> = vec![];
305-
for item in locked.into_iter().chain(unlocked.into_iter()) {
329+
for item in search.locked.iter().chain(search.unlocked.iter()) {
306330
let cred = Self::new_from_item(item)?;
307331
creds.push(Box::new(cred))
308332
}
309333
return Err(ErrorCode::Ambiguous(creds));
310334
}
311335
}
312336
let mut results: Vec<T> = vec![];
313-
for item in unlocked.into_iter() {
337+
for item in search.unlocked.iter() {
314338
results.push(f(item)?);
315339
}
316-
for item in locked.into_iter() {
340+
for item in search.locked.iter() {
317341
item.unlock().map_err(decode_error)?;
318342
results.push(f(item)?);
319343
}
@@ -335,6 +359,9 @@ impl SsCredential {
335359
/// but this just selects the ones we search on
336360
fn search_attributes(&self) -> HashMap<&str, &str> {
337361
let mut result: HashMap<&str, &str> = HashMap::new();
362+
if self.target.is_some() {
363+
result.insert("target", self.attributes["target"].as_str());
364+
}
338365
result.insert("service", self.attributes["service"].as_str());
339366
result.insert("username", self.attributes["username"].as_str());
340367
result
@@ -429,25 +456,6 @@ pub fn delete_item(item: &Item) -> Result<()> {
429456
item.delete().map_err(decode_error)
430457
}
431458

432-
/// Given a slice of items, filter out the ones that have an explicit target
433-
/// attribute that doesn't match the given target.
434-
///
435-
/// References to the matching items are returned in a new vector.
436-
pub fn matching_target_items<'a>(
437-
source: &'a [Item<'a>],
438-
target: &str,
439-
) -> Result<Vec<&'a Item<'a>>> {
440-
let mut result: Vec<&'a Item<'a>> = vec![];
441-
for i in source.iter() {
442-
match i.get_attributes().map_err(decode_error)?.get("target") {
443-
None => result.push(i),
444-
Some(item_target) if target.eq(item_target) => result.push(i),
445-
_ => {}
446-
}
447-
}
448-
Ok(result)
449-
}
450-
451459
//
452460
// Error utilities
453461
//

0 commit comments

Comments
 (0)