Skip to content

Commit

Permalink
Clean up based on usage and experimentation.
Browse files Browse the repository at this point in the history
1. Rename `new_in_keychain` to `new_with_target` and use the target on all platforms.
2. Enhacen tests to test the new functionality.
3. Revamp the CLI example to use new functionality, maintaining backward compatibility.
4. Revamp the docs and the readme to talk about what happens on each platform.
5. Test to make sure there is backward compatibility with earlier releases.
  • Loading branch information
brotskydotcom committed Nov 24, 2021
1 parent 1c680a3 commit 9964e87
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 103 deletions.
32 changes: 22 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
[![Crates.io](https://img.shields.io/crates/v/keyring.svg?style=flat-square)](https://crates.io/crates/keyring)
[![API Documentation on docs.rs](https://docs.rs/keyring/badge.svg)](https://docs.rs/keyring)

A cross-platorm library and utility to manage passwords.

Online [docs](https://docs.rs/keyring) are currently limited to linux, as cross-platform autogenerated docs are not a thing yet. For osx or windows, try `cargo doc -p keyring --open`.
A cross-platorm library to manage passwords, with a fully-developed example that provides a command-line interface.

Published on [crates.io](https://crates.io/crates/keyring)

Expand All @@ -25,7 +23,7 @@ the `new` function to create a new `Entry` on the keyring. The `new`
function expects a `service` name and an `username` which together identify
the item.

Passwords can be added to an item using its `set_password` method. They can then be read back using the `get_password` method, and deleted using the `delete_password` method. (Note that persistence of the `Entry` is determined via Rust rules, so deleting the password doesn't delete the item.)
Passwords can be added to an item using its `set_password` method. They can then be read back using the `get_password` method, and deleted using the `delete_password` method. (Note that persistence of the `Entry` is determined via Rust rules, so deleting the password doesn't delete the entry, but it does delete the platform credential.)

```rust
extern crate keyring;
Expand Down Expand Up @@ -54,12 +52,17 @@ fn main() -> Result<(), Box<dyn Error>> {

The `get_password`, `set_password` and `delete_password` functions return a `Result` which, if the operation was unsuccessful, can yield a `keyring::Error` with a platform-independent code that describes the error.

## Caveats
## Conventions and Caveats

* This module manipulates passwords as UTF-8 encoded strings, so if a 3rd party has stored a non-Unicode password then retrieving that password will return an error. The error in that case will have the raw bytes attached, so you can access them.
* This module uses platform-native credential managers: secret service on Linux, the Credential Manager on Windows, and the Secure Keychain on Mac. Each keyring `Entry` (identified by service and username) is mapped to a specific platform credential using conventions described below.
* To facilitate interoperability with third-party software, there are alternate constructors for keyring entries - `Entry::new_with_target` and `Entry::new_with_credential` - that use different conventions to map entries to credentials. See below and the module documentation for how they work. In addition, the `get_password_and_credential` method on an entry can be used retrieve the underlying credential information.
* This module manipulates passwords as UTF-8 encoded strings, so if a third party has stored an arbitrary byte string then retrieving that password will return an error. The error in that case will have the raw bytes attached, so you can access them.

### Linux

* Secret-service groups credentials into collections, and identifies each credential in a collection using a set of key-value pairs (called _attributes_). In addition, secret-service allows for a label on each credential for use in UI-based clients.
* For a given service/username pair, `Entry::new` maps to a credential in the default (login) secret-service collection. This credential has matching `service` and `username` attributes, and an additional `application` attribute of `rust-keyring`.
* You can map an entry to non-default secret-service collection by passing the collection's name as the `target` parameter to `Entry::new_with_target`. This module doesn't ever create collections, so trying to access an entry in a named collection before externally creating and unlocking it will result in a `NoStorageAccess` error.
* If you are running on a headless linux box, you will need to unlock the Gnome login keyring before you can use it. The following `bash` function may be very helpful.
```shell
function unlock-keyring ()
Expand All @@ -69,19 +72,29 @@ function unlock-keyring ()
unset pass
}
```
* Trying to access a locked keychain on a headless box often returns the platform error that displays as `SS error: prompt dismissed`. This refers to the fact that there is no GUI running that can be used to prompt for a keychain unlock.

### Windows

* The default Windows approach to storing credentials doesn't allow storing passwords for different users against the same service. You can override this approach, see the `IdentityMapper` type for details.
* There is only one credential store on Windows. Generic credentials in this store are identified by a single string (called the _target name_). They also have a number of non-identifying but manipulable attributes: a username, a comment, and a target alias.
* For a given service/username pair, this module uses the concatenated string `username.service` as the mapped credential's target name. (This allows multiple users to store passwords for the same service.) It also fills the usrename and comment fields with appropriate strings.
* Because the Windows credential manager doesn't support multiple keychains, and because many Windows programs use _only_ the service name as the credential target name, the `Entry::new_with_target` call uses the target parameter as the credential's target name rather than concatenating the username and service. So if you have a custom algorithm you want to use for computing the Windows target name (such as just the service name), you can specify the target name directly (along with the usual service and username values).

### MacOS

* Accessing the same keychain entry from multiple threads simultaneously is generally a bad idea, and can cause deadlocks.
* MacOS credential stores are called keychains, and the OS automatically creates three of them (or four if removable media is being used). Generic credentials on Mac can be identified by a large number of _key/value_ attributes; this module (currently) uses only the _account_ and _name_ attributes.
* For a given service/username pair, this module uses a generic credential in the User (login) keychain whose _account_ is the username and and whose _name_ is the service. In the _Keychain Access_ UI, generic credentials created by this module show up in the passwords area (with their _where_ field equal to their _name_), but _Note_ entries on Mac are also generic credentials and can be accessed by this module if you know their _account_ value (which is not displayed by _Keychain Access_).
* You can specify targeting a different keychain by passing the keychain's (case-insensitive) name as the target parameter to `Entry::new_with_target`. Any name other than one of the OS-supplied keychains (User, Common, System, and Dynamic) will be mapped to `User`. (_N.B._ The latest versions of the MacOS SDK no longer support creation of file-based keychains, so this module's experimental support for those has been removed.)
* Accessing the same keychain entry from multiple threads simultaneously is generally a bad idea, and can cause deadlocks. This is because MacOS serializes all access and does so in unpredicatable ways. There is no issue with accessing different entries from multiple threads.

## Sample Application

The keychain-rs project contains a sample application: a command-line interface to the keychain in `cli.rs` in the examples directory. This can be a great way to explore how the library is used, and it allows experimentation with the use of different service names, user names, and targets. When run in "singly verbose" mode (-v), it outputs the retrieved credentials on each `get` run. When run in "doubly verbose" mode (-vv), it also outputs any errors returned. This can be a great way to see which errors result from which conditions on each platform.

## Dev Notes

* We build using GitHub CI.
* Each tag is built on Ubuntu x64, Win 10 x64, and Mac x64. The `cli` example executable is posted with the tag.
* Each tag is built on Ubuntu x64, Win 10 x64, and Mac intel x64. The `cli` sample executable is posted for all platforms with the tag.

## License

Expand Down Expand Up @@ -112,4 +125,3 @@ Thanks to the following for helping make this library better, whether through co
### Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

84 changes: 37 additions & 47 deletions examples/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@ extern crate keyring;
use keyring::{Entry, Error};

#[derive(Debug, StructOpt)]
#[structopt(author = "github.com/hwchen/keyring-rs")]
/// Keyring CLI: A command-line interface to platform secure storage
pub struct Cli {
#[structopt(short, parse(from_occurrences))]
/// Specify once to retrieve all aspects of credentials on get.
/// Specify twice to provide structure print of all errors in addition to messages.
pub verbose: u8,

#[structopt(short, long, default_value = "default")]
/// The keychain to use, if the platform supports more than one.
pub keychain: String,
#[structopt(short, long)]
/// The target for the entry.
pub target: Option<String>,

#[structopt(short, long, default_value = "keyring")]
/// The service name to store/retrieve the password for.
#[structopt(short, long, default_value = "keyring-cli")]
/// The service name for the entry
pub service: String,

#[structopt(short, long)]
Expand Down Expand Up @@ -48,90 +49,79 @@ fn main() {
}

fn execute_args(args: &Cli) {
let keychain = args.keychain.clone();
let username = args.username.clone().unwrap_or_else(whoami::username);
let entry = Entry::new_in_keychain(&keychain, &args.service, &username);
let username = if let Some(username) = &args.username {
username.clone()
} else {
whoami::username()
};
let entry = if let Some(target) = args.target.as_ref() {
Entry::new_with_target(target, &args.service, &username)
} else {
Entry::new(&args.service, &username)
};
match &args.command {
Command::Set {
password: Some(password),
} => execute_set_password(args, &entry, password),
} => execute_set_password(&username, args.verbose, &entry, password),
Command::Set { password: None } => {
if let Ok(password) = read_password_from_tty(Some("Password: ")) {
execute_set_password(args, &entry, &password)
execute_set_password(&username, args.verbose, &entry, &password)
} else {
eprintln!("(Failed to read password, so none set.)")
}
}
Command::Get => execute_get_password_and_credential(args, &entry),
Command::Delete => execute_delete_password(args, &entry),
Command::Get => execute_get_password_and_credential(&username, args.verbose, &entry),
Command::Delete => execute_delete_password(&username, args.verbose, &entry),
}
}

fn execute_set_password(args: &Cli, entry: &Entry, password: &str) {
fn execute_set_password(username: &str, verbose: u8, entry: &Entry, password: &str) {
match entry.set_password(password) {
Ok(()) => println!("Password set successfully"),
Err(Error::NoStorageAccess(err)) => {
eprintln!("Couldn't set the password: {}", err);
if args.verbose > 1 {
eprintln!("Error details: {:?}", err);
}
}
Ok(()) => println!("(Password for user '{}' set successfully)", username),
Err(err) => {
eprintln!("Unexpected error setting the password: {}", err);
if args.verbose > 1 {
eprintln!("Couldn't set password for user '{}': {}", username, err);
if verbose > 1 {
eprintln!("Error details: {:?}", err);
}
}
}
}

fn execute_get_password_and_credential(args: &Cli, entry: &Entry) {
fn execute_get_password_and_credential(username: &str, verbose: u8, entry: &Entry) {
match entry.get_password_and_credential() {
Ok((password, credential)) => {
println!("Password is '{}'", &password);
if args.verbose > 0 {
println!("The password for user '{}' is '{}'", username, &password);
if verbose > 0 {
println!("Credential is: {:?}", credential)
}
}
Err(Error::NoEntry) => {
eprintln!("(No password found)");
if args.verbose > 1 {
eprintln!("(No password found for user '{}')", username);
if verbose > 1 {
eprintln!("Error details: {:?}", Error::NoEntry);
}
}
Err(Error::NoStorageAccess(err)) => {
eprintln!("Couldn't retrieve the password: {}", err);
if args.verbose > 1 {
eprintln!("Error details: {:?}", err);
}
}
Err(err) => {
eprintln!("Unexpected error retrieving the password: {}", err);
if args.verbose > 1 {
eprintln!("Couldn't get password for user '{}': {}", username, err);
if verbose > 1 {
eprintln!("Error details: {:?}", err);
}
}
}
}

fn execute_delete_password(args: &Cli, entry: &Entry) {
fn execute_delete_password(username: &str, verbose: u8, entry: &Entry) {
match entry.delete_password() {
Ok(()) => println!("(Password deleted)"),
Ok(()) => println!("(Password for user '{}' deleted)", username),
Err(Error::NoEntry) => {
eprintln!("(No password found)");
if args.verbose > 1 {
eprintln!("(No password for user '{}' found)", username);
if verbose > 1 {
eprintln!("Error details: {:?}", Error::NoEntry);
}
}
Err(Error::NoStorageAccess(err)) => {
eprintln!("Couldn't delete the password: {}", err);
if args.verbose > 1 {
eprintln!("Error details: {:?}", err);
}
}
Err(err) => {
eprintln!("Unexpected error retrieving the password: {}", err);
if args.verbose > 1 {
eprintln!("Couldn't delete password for user '{}': {}", username, err);
if verbose > 1 {
eprintln!("Error details: {:?}", err);
}
}
Expand Down
Loading

0 comments on commit 9964e87

Please sign in to comment.