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 239
/*!
Defines the credential model for each supported platform.
This crate has a very simple model of a keyring: it has any number
of entries, each of which is identified by a <username, service> pair,
has no other metadata, and has a UTF-8 string as its "password".
Furthermore, there is only one keyring.
This crate runs on several different platforms, each of which has its
own secure storage system with its own model for what constitutes a
"generic" secure credential: where it is stored, how it is identified,
what metadata it has, and what kind of "password" it allows. These
platform credentials provide the persistence for keyring entries.
In order to bridge the gap between the keyring entry model and each
platform's credential model, this crate uses a "credential mapper":
a function which maps from keyring entries to platform credentials.
The inputs to a credential mapper are the platform, optional target
specification, service, and username of the keyring entry; its output
is a platform-specific "recipe" for identifying and annotating the
platform credential which the crate will use for this entry.
This module provides a credential model for each supported platform,
and a credential mapper which the crate uses by default. The default
credential mapper can be "advised" by providing a suggested "target"
when creating an entry: on Linux and Mac this target is interpreted
as the collection/keychain to put the credential in; on Windows this
target is taken literally as the "target name" of the credential.
Clients who want to use a different algorithm for mapping service/username
pairs to platform credentials can also provide the specific credential spec
they want to use when creating the entry.
See the [top-level library documentation](https://docs.rs/keyring) for
more information about the platform-specific credential mapping.
Or read the code here :).
*/
use std::collections::HashMap;
/// The supported platforms.
#[derive(Debug)]
pub enum Platform {
Linux,
Windows,
MacOs,
Ios,
}
/// Linux supports multiple credential stores, each named by a string.
/// Credentials in a store are identified by an arbitrary collection
/// of attributes, and each can have "label" metadata for use in
/// graphical editors.
#[derive(Debug, Clone, PartialEq)]
pub struct LinuxCredential {
pub collection: String,
pub attributes: HashMap<String, String>,
pub label: String,
}
impl LinuxCredential {
/// Using strings in the credential map makes managing the lifetime
/// of the credential much easier. But since the secret service expects
/// a map from &str to &str, we have this utility to transform the
/// credential's map into one of the right form.
pub fn attributes(&self) -> HashMap<&str, &str> {
self.attributes
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect()
}
}
/// Windows has only one credential store, and each credential is identified
/// by a single string called the "target name". But generic credentials
/// also have three pieces of metadata with suggestive names.
#[derive(Debug, Clone, PartialEq)]
pub struct WinCredential {
pub username: String,
pub target_name: String,
pub target_alias: String,
pub comment: String,
}
/// MacOS supports multiple OS-provided credential stores, and used to support creating
/// arbitrary new credential stores (but that has been deprecated). Credentials on
/// Mac also can have "type" but we don't reflect that here because the type is actually
/// opaque once set and is only used in the Keychain UI.
#[derive(Debug, Clone, PartialEq)]
pub struct MacCredential {
pub domain: MacKeychainDomain,
pub service: String,
pub account: String,
}
#[derive(Debug, Clone, PartialEq)]
/// There are four pre-defined Mac keychains. Now that file-based keychains are
/// deprecated, those are the only domains that can be accessed.
pub enum MacKeychainDomain {
User,
System,
Common,
Dynamic,
}
impl From<&str> for MacKeychainDomain {
/// Target specifications are strings, but on Mac we map them
/// to keychoin domains. We accept any case in the string,
/// but the value has to match a known keychain domain name
/// or else we assume the login keychain is meant.
fn from(keychain: &str) -> Self {
match keychain.to_ascii_lowercase().as_str() {
"system" => MacKeychainDomain::System,
"common" => MacKeychainDomain::Common,
"dynamic" => MacKeychainDomain::Dynamic,
_ => MacKeychainDomain::User,
}
}
}
impl From<Option<&str>> for MacKeychainDomain {
fn from(keychain: Option<&str>) -> Self {
match keychain {
None => MacKeychainDomain::User,
Some(str) => str.into(),
}
}
}
/// iOS credentials all go in the user keychain identified by service and account.
#[derive(Debug, Clone, PartialEq)]
pub struct IosCredential {
pub service: String,
pub account: String,
}
#[derive(Debug, Clone, PartialEq)]
/// While defined cross-platform, instantiated platform
/// credentials always contain just the model for the
/// current runtime `Platform`.
///
/// Because you can create credentials for any strings,
/// but platform handling of empty target attributes is
/// not well defined, we have a special Invalid case
/// which we use in this case to ensure the created
/// credential has no functionality.
pub enum PlatformCredential {
Linux(LinuxCredential),
Win(WinCredential),
Mac(MacCredential),
Ios(IosCredential),
Invalid,
}
impl PlatformCredential {
pub fn matches_platform(&self, os: &Platform) -> bool {
match self {
PlatformCredential::Linux(_) => matches!(os, Platform::Linux),
PlatformCredential::Win(_) => matches!(os, Platform::Windows),
PlatformCredential::Mac(_) => matches!(os, Platform::MacOs),
PlatformCredential::Ios(_) => matches!(os, Platform::Ios),
PlatformCredential::Invalid => false,
}
}
}
/// Create the default target credential for a keyring entry. The caller
/// can provide an optional target parameter to influence the mapping.
///
/// If any of the provided strings are empty, the credential returned is
/// invalid, to prevent it being used. This is because platform behavior
/// around empty strings for attributes is undefined.
pub fn default_target(
platform: &Platform,
target: Option<&str>,
service: &str,
username: &str,
) -> PlatformCredential {
if service.is_empty() || username.is_empty() || target.unwrap_or("none").is_empty() {
return PlatformCredential::Invalid;
}
const VERSION: &str = env!("CARGO_PKG_VERSION");
let custom = if target.is_none() {
"entry"
} else {
"custom entry"
};
let metadata = format!(
"keyring-rs v{} {} for service '{}', user '{}'",
VERSION, custom, service, username
);
match platform {
Platform::Linux => PlatformCredential::Linux(LinuxCredential {
collection: target.unwrap_or("default").to_string(),
attributes: HashMap::from([
("service".to_string(), service.to_string()),
("username".to_string(), username.to_string()),
("application".to_string(), "rust-keyring".to_string()),
]),
label: metadata,
}),
Platform::Windows => {
if let Some(keychain) = target {
PlatformCredential::Win(WinCredential {
// Note: Since Windows doesn't support multiple keychains,
// and since it's nice for clients to have control over
// the target_name directly, we use the `keychain` value
// as the target name if it's specified non-default.
username: username.to_string(),
target_name: keychain.to_string(),
target_alias: String::new(),
comment: metadata,
})
} else {
PlatformCredential::Win(WinCredential {
// Note: default concatenation of user and service name is
// used because windows uses target_name as sole identifier.
// See the README for more rationale. Also see this issue
// for Python: https://github.com/jaraco/keyring/issues/47
username: username.to_string(),
target_name: format!("{}.{}", username, service),
target_alias: String::new(),
comment: metadata,
})
}
}
Platform::MacOs => PlatformCredential::Mac(MacCredential {
domain: target.into(),
service: service.to_string(),
account: username.to_string(),
}),
Platform::Ios => PlatformCredential::Ios(IosCredential {
service: service.to_string(),
account: username.to_string(),
}),
}
}