1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
/*!

# Keyring

This is a cross-platform library that does storage and retrieval of passwords (and other credential-like secrets) in the underlying platform secure store. A top-level introduction to the library's usage, as well as a small code sample, may be found in [the library's entry on crates.io](https://crates.io/crates/keyring). Currently supported platforms are Linux, Windows, MacOS, and iOS.

## Design

This module uses platform-native credential managers: secret service on Linux, the Credential Manager on Windows, and the Secure Keychain on Mac and iOS.  Each entry constructed with `Entry::new(service, username)` is mapped to a credential using platform-specific 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. In addition, the `get_password_and_credential` method on an entry can be used retrieve the underlying credential data along with the password.

### Linux

On Linux, the secret service is used as the platform credential store.  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 a 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 ()
{
    read -rsp "Password: " pass
    echo -n "$pass" | gnome-keyring-daemon --unlock
    unset pass
}
```

Trying to access a locked keychain on a headless Linux 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

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 username 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 and iOS

MacOS/iOS credential stores are called keychains.  On iOS there is only one of these, but on Mac the OS automatically creates three of them (or four if removable media is being used).  Generic credentials on Mac/iOS 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 on Mac, 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_).

On Mac, 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`.  On iOS, the target parameter is ignored.

(_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.)

## Caveats

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.

Accessing the same keychain entry from multiple threads simultaneously can produce odd results, even deadlocks.  This is because the system calls to the platform credential managers may use the same thread discipline, and so may be serialized quite differently than the client-side calls.  On MacOS, for example, all calls to access the keychain are serialized in an order that is independent of when they are made.

Because credentials identified with empty service, user, or target names are handled inconsistently at the platform layer, the library had inconsistent (and arguably buggy) behavior in this case.  As of version 1.2, this inconsistency was eliminated by having the library always fail on access when using credentials created with empty strings via `new` or `new_with_target`.  The prior platform-specific behavior can still be accessed by using `new_with_credential` to produce the same credential that would have been produced before the change.

A better way to handle empty strings (and other problematic argument values) would be to allow `Entry` creation to fail gracefully on arguments that are known not to work on a given platform.  That would be a breaking API change, however, so it will have to wait until the next major version.

 */
pub mod credential;
pub mod error;

use credential::{Platform, PlatformCredential};
pub use error::{Error, Result};

/// return the runtime `Platform` so cross-platform
/// code can know what kind of credential is in use.
pub fn platform() -> Platform {
    platform::platform()
}

// Platform-specific implementations
#[cfg_attr(target_os = "linux", path = "linux.rs")]
#[cfg_attr(target_os = "windows", path = "windows.rs")]
#[cfg_attr(target_os = "macos", path = "macos.rs")]
#[cfg_attr(target_os = "ios", path = "ios.rs")]
mod platform;

#[derive(Debug)]
pub struct Entry {
    target: PlatformCredential,
}

impl Entry {
    /// Create an entry for the given service and username.
    /// This maps to a target credential in the default keychain.
    ///
    /// This call never fails, because there is no actual platform access
    /// performed when the credential object is created.  But if you specify
    /// empty strings for any of the arguments, any attempt to use the
    /// credential will fail with a `NoEntry` error.  And if you specify a
    /// string that exceeds platform limits, you will get a `TooLong` error.
    pub fn new(service: &str, username: &str) -> Entry {
        Entry {
            target: credential::default_target(&platform(), None, service, username),
        }
    }

    /// Create an entry for the given target, service, and username.
    /// On Linux and Mac, the target is interpreted as naming the collection/keychain
    /// to store the credential.  On Windows, the target is used directly as
    /// the _target name_ of the credential.
    ///
    /// This call never fails, because there is no actual platform access
    /// performed when the credential object is created.  But if you specify
    /// empty strings for any of the arguments, any attempt to use the
    /// credential will fail with a `NoEntry` error.  And if you specify a
    /// string that exceeds platform limits, you will get a `TooLong` error.
    pub fn new_with_target(target: &str, service: &str, username: &str) -> Entry {
        Entry {
            target: credential::default_target(&platform(), Some(target), service, username),
        }
    }

    /// Create an entry that uses the given credential for storage.  Callers can use
    /// their own algorithm to produce a platform-specific credential spec for the
    /// given service and username and then call this entry with that value.
    ///
    /// This call never fails, because there is no actual platform access
    /// performed when the credential object is created.  But if you specify
    /// a platform credential that contains empty or invalid attributes, you
    /// may get errors or surprises when attempting to use the credential.
    pub fn new_with_credential(target: &PlatformCredential) -> Result<Entry> {
        if target.matches_platform(&platform()) {
            Ok(Entry {
                target: target.clone(),
            })
        } else {
            Err(Error::WrongCredentialPlatform)
        }
    }

    /// Set the password for this entry.  Any other platform-specific
    /// annotations are determined by the mapper that was used
    /// to create the credential.
    pub fn set_password(&self, password: &str) -> Result<()> {
        self.validate_or_no_entry()?;
        platform::set_password(&self.target, password)
    }

    /// Retrieve the password saved for this entry.
    /// Returns a `NoEntry` error is there isn't one.
    pub fn get_password(&self) -> Result<String> {
        self.validate_or_no_entry()?;
        let mut map = self.target.clone();
        platform::get_password(&mut map)
    }

    /// Retrieve the password and all the other fields
    /// set in the platform-specific credential.  This
    /// allows retrieving metadata on the credential that
    /// were saved by external applications.
    pub fn get_password_and_credential(&self) -> Result<(String, PlatformCredential)> {
        self.validate_or_no_entry()?;
        let mut map = self.target.clone();
        let password = platform::get_password(&mut map)?;
        Ok((password, map))
    }

    /// Delete the password for this entry.  (Although the entry
    /// itself follows the Rust structure lifecycle, deleting
    /// the password deletes the platform credential from secure storage.)
    pub fn delete_password(&self) -> Result<()> {
        platform::delete_password(&self.target)
    }

    /// Validate the arguments given to credential create were not empty.  If they were,
    /// return a NoEntry generic error to prevent the user using the credential.
    fn validate_or_no_entry(&self) -> Result<()> {
        match self.target {
            PlatformCredential::Invalid => Err(Error::NoEntry),
            _ => Ok(()),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::credential::default_target;

    #[test]
    fn test_invalid_credential_creation() {
        let entry = Entry::new("service", "");
        assert!(matches!(entry.target, PlatformCredential::Invalid));
        assert!(entry.set_password("foo").is_err());
        assert!(entry.get_password().is_err());
        assert!(entry.get_password_and_credential().is_err());
        let entry = Entry::new("", "username");
        assert!(matches!(entry.target, PlatformCredential::Invalid));
        let entry = Entry::new_with_target("", "service", "username");
        assert!(matches!(entry.target, PlatformCredential::Invalid));
        let result = Entry::new_with_credential(&PlatformCredential::Invalid);
        assert!(result.is_err());
    }

    #[test]
    fn test_default_initial_and_retrieved_map() {
        let name = generate_random_string();
        let expected_target = default_target(&platform(), None, &name, &name);
        let entry = Entry::new(&name, &name);
        assert_eq!(entry.target, expected_target);
        entry.set_password("ignored").unwrap();
        let (_, target) = entry.get_password_and_credential().unwrap();
        assert_eq!(target, expected_target);
        // don't leave password around.
        entry.delete_password().unwrap();
    }

    #[test]
    fn test_targeted_initial_and_retrieved_map() {
        let name = generate_random_string();
        let expected_target = default_target(&platform(), Some(&name), &name, &name);
        let entry = Entry::new_with_target(&name, &name, &name);
        assert_eq!(entry.target, expected_target);
        // can only test targeted credentials on Windows
        if matches!(platform(), Platform::Windows) {
            entry.set_password("ignored").unwrap();
            let (_, target) = entry.get_password_and_credential().unwrap();
            assert_eq!(target, expected_target);
            // don't leave password around.
            entry.delete_password().unwrap();
        }
    }

    fn generate_random_string() -> String {
        // from the Rust Cookbook:
        // https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html
        use rand::{distributions::Alphanumeric, thread_rng, Rng};
        thread_rng()
            .sample_iter(&Alphanumeric)
            .take(30)
            .map(char::from)
            .collect()
    }
}