1use 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#[derive(Debug)]
17pub struct Keyring {
18 imp: Box<keyring::CredentialBuilder>,
19 name: &'static str,
20}
21
22pub type Result<T, E = KeyringError> = std::result::Result<T, E>;
24
25impl Keyring {
26 #[cfg(target_os = "linux")]
27 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 pub const SUPPORTED_BACKENDS: &'static [&'static str] =
33 &["secret-service", "flat-file", "mock"];
34 #[cfg(target_os = "windows")]
35 pub const SUPPORTED_BACKENDS: &'static [&'static str] = &["windows", "flat-file", "mock"];
37 #[cfg(target_os = "macos")]
38 pub const SUPPORTED_BACKENDS: &'static [&'static str] = &["macos", "flat-file", "mock"];
40 #[cfg(target_os = "ios")]
41 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 pub const SUPPORTED_BACKENDS: &'static [&'static str] = &["flat-file", "mock"];
53
54 pub const DEFAULT_BACKEND: &'static str = Self::SUPPORTED_BACKENDS[0];
56
57 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 pub fn new(backend: &str) -> Result<Self> {
119 Self::load_backend(backend).map(|imp| Self {
120 imp,
121 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 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 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, ®istry_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 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 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 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 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 pub fn get_signing_key(
263 &self,
264 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 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 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}