warg_client/keyring/
mod.rs

1//! Utilities for interacting with keyring and performing signing operations.
2
3use crate::config::Config;
4use crate::RegistryUrl;
5use indexmap::IndexSet;
6use secrecy::Secret;
7use warg_crypto::signing::PrivateKey;
8
9mod error;
10use error::KeyringAction;
11pub use error::KeyringError;
12
13pub mod flatfile;
14
15/// Interface to a pluggable keyring backend
16#[derive(Debug)]
17pub struct Keyring {
18    imp: Box<keyring::CredentialBuilder>,
19    name: &'static str,
20}
21
22/// Result type for keyring errors.
23pub type Result<T, E = KeyringError> = std::result::Result<T, E>;
24
25impl Keyring {
26    #[cfg(target_os = "linux")]
27    /// List of supported credential store backends
28    pub const SUPPORTED_BACKENDS: &'static [&'static str] =
29        &["secret-service", "flat-file", "linux-keyutils", "mock"];
30    #[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
31    /// List of supported credential store backends
32    pub const SUPPORTED_BACKENDS: &'static [&'static str] =
33        &["secret-service", "flat-file", "mock"];
34    #[cfg(target_os = "windows")]
35    /// List of supported credential store backends
36    pub const SUPPORTED_BACKENDS: &'static [&'static str] = &["windows", "flat-file", "mock"];
37    #[cfg(target_os = "macos")]
38    /// List of supported credential store backends
39    pub const SUPPORTED_BACKENDS: &'static [&'static str] = &["macos", "flat-file", "mock"];
40    #[cfg(target_os = "ios")]
41    /// List of supported credential store backends
42    pub const SUPPORTED_BACKENDS: &'static [&'static str] = &["ios", "flat-file", "mock"];
43    #[cfg(not(any(
44        target_os = "linux",
45        target_os = "freebsd",
46        target_os = "openbsd",
47        target_os = "macos",
48        target_os = "ios",
49        target_os = "windows",
50    )))]
51    /// List of supported credential store backends
52    pub const SUPPORTED_BACKENDS: &'static [&'static str] = &["flat-file", "mock"];
53
54    /// The default backend when no configuration option is set
55    pub const DEFAULT_BACKEND: &'static str = Self::SUPPORTED_BACKENDS[0];
56
57    /// Returns a human-readable description of a keyring backend.
58    pub fn describe_backend(backend: &str) -> &'static str {
59        match backend {
60            "secret-service" => "Freedesktop.org secret service (GNOME Keyring or KWallet)",
61            "linux-keyutils" => "Linux kernel memory-based keystore (lacks persistence, not suitable for desktop use)",
62            "windows" => "Windows Credential Manager",
63            "macos" => "MacOS Keychain",
64            "ios" => "Apple iOS Keychain",
65            "flat-file" => "Unencrypted flat files in your warg config directory",
66            "mock" => "Mock credential store with no persistence (for testing only)",
67            _ => "(no description available)"
68        }
69    }
70
71    fn load_backend(backend: &str) -> Result<Box<keyring::CredentialBuilder>> {
72        if !Self::SUPPORTED_BACKENDS.contains(&backend) {
73            return Err(KeyringError::unknown_backend(backend.to_owned()));
74        }
75
76        #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
77        if backend == "secret-service" {
78            return Ok(keyring::secret_service::default_credential_builder());
79        }
80
81        #[cfg(target_os = "linux")]
82        if backend == "linux-keyutils" {
83            return Ok(keyring::keyutils::default_credential_builder());
84        }
85
86        #[cfg(target_os = "macos")]
87        if backend == "macos" {
88            return Ok(keyring::macos::default_credential_builder());
89        }
90
91        #[cfg(target_os = "ios")]
92        if backend == "ios" {
93            return Ok(keyring::ios::default_credential_builder());
94        }
95
96        #[cfg(target_os = "windows")]
97        if backend == "windows" {
98            return Ok(keyring::windows::default_credential_builder());
99        }
100
101        if backend == "flat-file" {
102            return Ok(Box::new(
103                flatfile::FlatfileCredentialBuilder::new()
104                    .map_err(|e| KeyringError::backend_init_failure("flat-file", e))?,
105            ));
106        }
107
108        if backend == "mock" {
109            return Ok(keyring::mock::default_credential_builder());
110        }
111
112        unreachable!("missing logic for backend {backend}")
113    }
114
115    /// Instantiate a new keyring.
116    ///
117    /// The argument should be an element of [Self::SUPPORTED_BACKENDS].
118    pub fn new(backend: &str) -> Result<Self> {
119        Self::load_backend(backend).map(|imp| Self {
120            imp,
121            // Get an equivalent &'static str from our &str
122            name: Self::SUPPORTED_BACKENDS
123                .iter()
124                .find(|s| **s == backend)
125                .expect("successfully-loaded backend should be found in SUPPORTED_BACKENDS"),
126        })
127    }
128
129    /// Instantiate a new keyring using the backend specified in a configuration file.
130    pub fn from_config(config: &Config) -> Result<Self> {
131        if let Some(ref backend) = config.keyring_backend {
132            Self::new(backend.as_str())
133        } else {
134            Self::new(Self::DEFAULT_BACKEND)
135        }
136    }
137
138    /// Gets the auth token entry for the given registry and key name.
139    pub fn get_auth_token_entry(&self, registry_url: &RegistryUrl) -> Result<keyring::Entry> {
140        let label = format!("warg-auth-token:{}", registry_url.safe_label());
141        let cred = self
142            .imp
143            .build(None, &label, &registry_url.safe_label())
144            .map_err(|e| {
145                KeyringError::auth_token_access_error(
146                    self.name,
147                    registry_url,
148                    KeyringAction::Open,
149                    e,
150                )
151            })?;
152        Ok(keyring::Entry::new_with_credential(cred))
153    }
154
155    /// Gets the auth token
156    pub fn get_auth_token(&self, registry_url: &RegistryUrl) -> Result<Option<Secret<String>>> {
157        let entry = self.get_auth_token_entry(registry_url)?;
158        match entry.get_password() {
159            Ok(secret) => Ok(Some(Secret::from(secret))),
160            Err(keyring::Error::NoEntry) => Ok(None),
161            Err(e) => Err(KeyringError::auth_token_access_error(
162                self.name,
163                registry_url,
164                KeyringAction::Get,
165                e,
166            )),
167        }
168    }
169
170    /// Deletes the auth token
171    pub fn delete_auth_token(&self, registry_url: &RegistryUrl) -> Result<()> {
172        let entry = self.get_auth_token_entry(registry_url)?;
173        entry.delete_credential().map_err(|e| {
174            KeyringError::auth_token_access_error(self.name, registry_url, KeyringAction::Delete, e)
175        })
176    }
177
178    /// Sets the auth token
179    pub fn set_auth_token(&self, registry_url: &RegistryUrl, token: &str) -> Result<()> {
180        let entry = self.get_auth_token_entry(registry_url)?;
181        entry.set_password(token).map_err(|e| {
182            KeyringError::auth_token_access_error(self.name, registry_url, KeyringAction::Set, e)
183        })
184    }
185
186    /// Gets the signing key entry for the given registry and key name.
187    pub fn get_signing_key_entry(
188        &self,
189        registry_url: Option<&str>,
190        keys: &IndexSet<String>,
191        home_url: Option<&str>,
192    ) -> Result<keyring::Entry> {
193        if let Some(registry_url) = registry_url {
194            let user = if keys.contains(registry_url) {
195                registry_url
196            } else {
197                "default"
198            };
199            let cred = self
200                .imp
201                .build(None, "warg-signing-key", user)
202                .map_err(|e| {
203                    KeyringError::signing_key_access_error(
204                        self.name,
205                        Some(registry_url),
206                        KeyringAction::Open,
207                        e,
208                    )
209                })?;
210            Ok(keyring::Entry::new_with_credential(cred))
211        } else {
212            if let Some(url) = home_url {
213                if keys.contains(url) {
214                    let cred = self
215                        .imp
216                        .build(
217                            None,
218                            "warg-signing-key",
219                            &RegistryUrl::new(url)
220                                .map_err(|e| {
221                                    KeyringError::signing_key_access_error(
222                                        self.name,
223                                        Some(url),
224                                        KeyringAction::Open,
225                                        e,
226                                    )
227                                })?
228                                .safe_label(),
229                        )
230                        .map_err(|e| {
231                            KeyringError::signing_key_access_error(
232                                self.name,
233                                Some(url),
234                                KeyringAction::Open,
235                                e,
236                            )
237                        })?;
238                    return Ok(keyring::Entry::new_with_credential(cred));
239                }
240            }
241
242            if keys.contains("default") {
243                let cred = self
244                    .imp
245                    .build(None, "warg-signing-key", "default")
246                    .map_err(|e| {
247                        KeyringError::signing_key_access_error(
248                            self.name,
249                            None::<&str>,
250                            KeyringAction::Open,
251                            e,
252                        )
253                    })?;
254                return Ok(keyring::Entry::new_with_credential(cred));
255            }
256
257            Err(KeyringError::no_default_signing_key(self.name))
258        }
259    }
260
261    /// Gets the signing key for the given registry registry_label and key name.
262    pub fn get_signing_key(
263        &self,
264        // If being called by a cli key command, this will always be a cli flag
265        // If being called by a client publish command, this could also be supplied by namespace map config
266        registry_url: Option<&str>,
267        keys: &IndexSet<String>,
268        home_url: Option<&str>,
269    ) -> Result<PrivateKey> {
270        let entry = self.get_signing_key_entry(registry_url, keys, home_url)?;
271
272        match entry.get_password() {
273            Ok(secret) => PrivateKey::decode(secret).map_err(|e| {
274                KeyringError::signing_key_access_error(
275                    self.name,
276                    registry_url,
277                    KeyringAction::Get,
278                    anyhow::Error::from(e),
279                )
280            }),
281            Err(e) => Err(KeyringError::signing_key_access_error(
282                self.name,
283                registry_url,
284                KeyringAction::Get,
285                e,
286            )),
287        }
288    }
289
290    /// Sets the signing key for the given registry host and key name.
291    pub fn set_signing_key(
292        &self,
293        registry_url: Option<&str>,
294        key: &PrivateKey,
295        keys: &mut IndexSet<String>,
296        home_url: Option<&str>,
297    ) -> Result<()> {
298        let entry = self.get_signing_key_entry(registry_url, keys, home_url)?;
299        entry.set_password(&key.encode()).map_err(|e| {
300            KeyringError::signing_key_access_error(self.name, registry_url, KeyringAction::Set, e)
301        })
302    }
303
304    /// Deletes the signing key for the given registry host and key name.
305    pub fn delete_signing_key(
306        &self,
307        registry_url: Option<&str>,
308        keys: &IndexSet<String>,
309        home_url: Option<&str>,
310    ) -> Result<()> {
311        let entry = self.get_signing_key_entry(registry_url, keys, home_url)?;
312        entry.delete_credential().map_err(|e| {
313            KeyringError::signing_key_access_error(
314                self.name,
315                registry_url,
316                KeyringAction::Delete,
317                e,
318            )
319        })
320    }
321}