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(),
        }),
    }
}