Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error handling improvements #70

Merged
merged 28 commits into from
Nov 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c6e3ca7
Start work on normalizing keyring names (#60).
brotskydotcom Nov 8, 2021
8b32038
Merge branch 'update-dependencies' into issue-60
adobeDan Nov 9, 2021
4a68211
Finish refactoring; fix #60 on Mac
adobeDan Nov 10, 2021
3983b55
Merge branch 'update-dependencies' into issue-60
adobeDan Nov 10, 2021
cc397b5
Get Linux to compile.
adobeDan Nov 10, 2021
9587247
Get Windows to a compilable state.
adobeDan Nov 11, 2021
a393a3c
Fix typo in Windows tests.
adobeDan Nov 11, 2021
681ef03
Merge branch 'master' into issue-60
adobeDan Nov 11, 2021
43475c2
Update error handling to provide cross platform error codes (start wi…
adobeDan Nov 12, 2021
da5f00c
Get windows and linux onto the new error handling.
adobeDan Nov 13, 2021
0b8a0c4
Move Windows tests onto the new error scheme
adobeDan Nov 13, 2021
762bf23
Merge branch 'master' into error-handling-improvements
adobeDan Nov 13, 2021
5ff53c7
Finish error work and credential improvements.
adobeDan Nov 14, 2021
993906a
Fix typo; improve example code.
adobeDan Nov 14, 2021
3d16934
Fix typo; improve example code.
adobeDan Nov 14, 2021
0a3cc66
Further fixups and enhancements on Mac
adobeDan Nov 14, 2021
107c851
Apply last commit changes to Win and Linux
adobeDan Nov 14, 2021
e286c76
Review changes and match Linux to Mac.
adobeDan Nov 15, 2021
ddf7b01
Restructure errors, do renames, and finish docs.
adobeDan Nov 16, 2021
543f7c5
Start fleshing out tests
adobeDan Nov 18, 2021
1bcadf9
More tests
brotskydotcom Nov 19, 2021
f5737d2
More platform error tests.
brotskydotcom Nov 19, 2021
60b607b
Finish tests.
brotskydotcom Nov 19, 2021
56051c9
A lot of cleanup based on usage.
brotskydotcom Nov 21, 2021
39771fc
Rename working cli example back to cli.
brotskydotcom Nov 21, 2021
01ebb89
Add verbose mode to the CLI.
brotskydotcom Nov 21, 2021
1c680a3
Remove serial testing restriction.
brotskydotcom Nov 22, 2021
9964e87
Clean up based on usage and experimentation.
brotskydotcom Nov 24, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
# don't check in Cargo.lock because this is a library project
# see https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# don't check in .cargo directory because not every developer
# uses it; the typical use is to enable cross-compiling.
/.cargo/
# don't check in the binary and testing artifacts
target
fuzz
libtest.rmeta
# profiling
*.profdata
*.profraw

9 changes: 3 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ repository = "https://github.com/hwchen/keyring-rs.git"
version = "0.10.4"
edition = "2018"

[features]
default = []
macos-specify-keychain = []

[target.'cfg(target_os = "macos")'.dependencies]
security-framework = "2.4.2"

Expand All @@ -24,10 +20,11 @@ byteorder = "1.2.1"
winapi = { version = "0.3", features = ["wincred", "minwindef"] }

[dev-dependencies]
clap = "2.33"
rpassword = "5.0"
rand = "0.8.4"
serial_test = "0.5.1"
doc-comment = "0.3.3"
structopt = "0.3.25"
whoami = "1.2.0"

[target.'cfg(target_os = "macos")'.dev-dependencies]
tempfile = "3.1.0"
138 changes: 40 additions & 98 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 @@ -15,122 +13,56 @@ __Currently supports Linux, macOS, and Windows.__ Please file issues if you have

To use this library in your project add the following to your `Cargo.toml` file:

```
```toml
[dependencies]
keyring = "0.10.1"
keyring = "0.10"
```

This will give you access to the `keyring` crate in your code. Now you can use
the `new` function to get an instance of the `Keyring` struct. The `new`
function expects a `service` name and an `username` with which it accesses
the password.

You can get a password from the OS keyring with the `get_password` function.

```rust,no_run
extern crate keyring;

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
let service = "my_application_name";
let username = "username";

let keyring = keyring::Keyring::new(&service, &username);

let password = keyring.get_password()?;
println!("The password is '{}'", password);
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.

Ok(())
}
```

Passwords can also be added to the keyring using the `set_password` function.
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,no_run
```rust
extern crate keyring;

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
let service = "my_application_name";
let username = "username";
let service = "my_application";
let username = "my_name";
let entry = keyring::Entry::new(&service, &username);

let keyring = keyring::Keyring::new(&service, &username);
let password = "topS3cr3tP4$$w0rd";
entry.set_password(&password)?;

let password = "topS3cr3tP4$$w0rd";
keyring.set_password(&password)?;
let password = entry.get_password()?;
println!("My password is '{}'", password);

let password = keyring.get_password()?;
println!("The password is '{}'", password);
entry.delete_password()?;
println!("My password has been deleted");

Ok(())
}
```

And they can be deleted with the `delete_password` function.

```rust,no_run
extern crate keyring;

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
let service = "my_application_name";
let username = "username";

let keyring = keyring::Keyring::new(&service, &username);

keyring.delete_password()?;

println!("The password has been deleted");

Ok(())
}
```

On macOS, keychain object from specific path can be opened using `Keyring::use_keychain` which gives the flexibility to open non-default keychains. Note that this is currently feature-gated, and is considered unstable, and is subject to change without a semver major version change.

In Cargo.toml, you need to turn the feature on:
```toml
keyring = { version = "0.10.0", features = ["macos-specify-keychain"] }
```

```rust,no_run
extern crate keyring;

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
let service = "my_application_name";
let username = "username";

let keyring = keyring::Keyring::use_keychain(Path::new("/Library/Keychains/System.keychain"), &service, &username);

let password = "topS3cr3tP4$$w0rd";
keyring.set_password(&password)?;

let password = keyring.get_password()?;
println!("The password is '{}'", password);

Ok(())
Ok(())
}
```

## Errors

The `get_password`, `set_password` and `delete_password` functions return a
`Result` which, if the operation was unsuccessful, can yield a `KeyringError`.
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.

The `KeyringError` struct implements the `error::Error` and `fmt::Display`
traits, so it can be queried for a cause and an description using methods of
the same name.
## Conventions and Caveats

## Caveats
* 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

* The application name is hardcoded to be `rust-keyring`.
* 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 @@ -140,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 credential name is currently hardcoded to be `username.service`, due to a [reported issue](https://github.com/jaraco/keyring/issues/47). This breaks compatibility with 3rd-party applications, and is being fixed.
* 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 keychain 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 All @@ -178,8 +120,8 @@ Thanks to the following for helping make this library better, whether through co
- @steveatinfincia
- @bhkaminski
- @MaikKlein
- @brotskydotcom

### 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.

Loading