Skip to content

Commit 2e2e915

Browse files
Merge pull request #222 from soywod/secret-service-with-keyutils
Provide keyutils with persistence-after-reboot using secret-service
2 parents 9a4184c + f59afd5 commit 2e2e915

File tree

7 files changed

+342
-33
lines changed

7 files changed

+342
-33
lines changed

.github/workflows/ci.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ jobs:
5656
- "async-secret-service,async-io,crypto-rust"
5757
- "async-secret-service,tokio,crypto-openssl"
5858
- "async-secret-service,async-io,crypto-openssl"
59+
- "linux-native-sync-persistent,crypto-rust"
60+
- "linux-native-sync-persistent,crypto-openssl"
61+
- "linux-native-async-persistent,tokio,crypto-rust"
62+
- "linux-native-async-persistent,async-io,crypto-rust"
63+
- "linux-native-async-persistent,tokio,crypto-openssl"
64+
- "linux-native-async-persistent,async-io,crypto-openssl"
5965

6066
steps:
6167
- name: Install CI dependencies

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ linux-native = ["dep:linux-keyutils"]
1717
apple-native = ["dep:security-framework"]
1818
windows-native = ["dep:windows-sys", "dep:byteorder"]
1919

20+
linux-native-sync-persistent = ["linux-native", "sync-secret-service"]
21+
linux-native-async-persistent = ["linux-native", "async-secret-service"]
2022
sync-secret-service = ["dep:dbus-secret-service"]
2123
async-secret-service = ["dep:secret-service", "dep:zbus"]
2224
crypto-rust = ["dbus-secret-service?/crypto-rust", "secret-service?/crypto-rust"]

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,14 @@ This crate allows clients to "bring their own credential store" by providing tra
5656

5757
This crate provides built-in implementations of the following platform-specific credential stores:
5858

59-
* _Linux_: The DBus-based Secret Service and the kernel keyutils.
59+
* _Linux_: The DBus-based Secret Service, the kernel keyutils, and a combo of the two.
6060
* _FreeBSD_, _OpenBSD_: The DBus-based Secret Service.
6161
* _macOS_, _iOS_: The local keychain.
6262
* _Windows_: The Windows Credential Manager.
6363

6464
To enable the stores you want, you use features: there is one feature for each possibly-included credential store. If you specify a feature (e.g., `dbus-secret-service`) _and_ your target platform (e.g., `freebsd`) supports that credential store, it will be included as the default credential store in that build. That way you can have a build command that specifies a single credential store for each of your target platforms, and use that same build command for all targets.
6565

66-
If you don't enable any credential stores that are supported on a given platform, or you enable multiple credential stores for some platform, the _mock_ keystore will be the default on that platform. See the [developer docs](https://docs.rs/keyring/) for details of which features control the inclusion of which credential stores (and which platforms each credential store targets).
66+
If you don't enable any credential stores that are supported on a given platform, the _mock_ keystore will be the default on that platform. See the [developer docs](https://docs.rs/keyring/) for details of which features control the inclusion of which credential stores.
6767

6868
### Platform-specific issues
6969

build-xplat-docs.sh

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
#!/bin/bash
2-
cargo doc --no-deps --features=linux-native --target aarch64-unknown-linux-musl $OPEN_DOCS
3-
cargo doc --no-deps --features=windows-native --target aarch64-pc-windows-msvc $OPEN_DOCS
4-
cargo doc --no-deps --features=apple-native --target aarch64-apple-darwin $OPEN_DOCS
5-
cargo doc --no-deps --features=apple-native --target aarch64-apple-ios $OPEN_DOCS
2+
if [[ "$OSTYPE" == "linux"* ]]; then
3+
cargo doc --no-deps --features=linux-native-sync-persistent $OPEN_DOCS
4+
cargo doc --no-deps --features=sync-secret-service $OPEN_DOCS
5+
cargo doc --no-deps --features=linux-native $OPEN_DOCS
6+
elif [[ "$OSTYPE" == "darwin"* ]]; then
7+
cargo doc --no-deps --features=linux-native --target aarch64-unknown-linux-musl $OPEN_DOCS
8+
cargo doc --no-deps --features=windows-native --target aarch64-pc-windows-msvc $OPEN_DOCS
9+
cargo doc --no-deps --features=apple-native --target aarch64-apple-darwin $OPEN_DOCS
10+
cargo doc --no-deps --features=apple-native --target aarch64-apple-ios $OPEN_DOCS
11+
fi

src/keyutils_persistent.rs

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
/*!
2+
3+
# Linux (keyutils) store with Secret Service backing
4+
5+
This store, contributed by [@soywod](https://github.com/soywod),
6+
uses the [keyutils module](crate::keyutils) as a cache
7+
available to headless processes, while using the
8+
[secret-service module](crate::secret_service)
9+
to provide credential storage beyond reboot.
10+
The expected usage pattern
11+
for this module is as follows:
12+
13+
- Processes that run on headless systems are built with `keyutils` support via the
14+
`linux-native` feature of this crate. After each reboot, these processes
15+
are either launched after the keyutils cache has been reloaded from the secret service,
16+
or (if launched immediately) they wait until the keyutils cache has been reloaded.
17+
- A headed "configuration" process is built with this module that allows its user
18+
to configure the credentials needed by the headless processes. After each reboot,
19+
this process unlocks the secret service (see both the keyutils and secret-service
20+
module for information about how this can be done headlessly, if desired) and then
21+
accesses each of the configured credentials (which loads them into keyutils). At
22+
that point the headless clients can be started (or become active, if already started).
23+
24+
This store works by creating a keyutils entry and a secret-service entry for
25+
each of its entries. Because keyutils entries don't have attributes, entries
26+
in this store don't expose attributes either. Because keyutils entries can't
27+
store empty passwords/secrets, this store's entries can't either.
28+
29+
See the documentation for the `keyutils` and `secret-service` modules if you
30+
want details about how the underlying storage is handled.
31+
*/
32+
33+
use log::debug;
34+
35+
use super::credential::{
36+
Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialPersistence,
37+
};
38+
use super::error::{Error, Result};
39+
use super::keyutils::KeyutilsCredential;
40+
use super::secret_service::{SsCredential, SsCredentialBuilder};
41+
42+
/// Representation of a keyutils-persistent credential.
43+
///
44+
/// The credential owns a [KeyutilsCredential] for in-memory usage and
45+
/// a [SsCredential] for persistence.
46+
#[derive(Debug, Clone)]
47+
pub struct KeyutilsPersistentCredential {
48+
keyutils: KeyutilsCredential,
49+
ss: SsCredential,
50+
}
51+
52+
impl CredentialApi for KeyutilsPersistentCredential {
53+
/// Set a password in the underlying store
54+
fn set_password(&self, password: &str) -> Result<()> {
55+
self.set_secret(password.as_bytes())
56+
}
57+
58+
/// Set a secret in the underlying store
59+
///
60+
/// It sets first the secret in keyutils, then in
61+
/// secret-service. If the latter set fails, the former
62+
/// is reverted.
63+
fn set_secret(&self, secret: &[u8]) -> Result<()> {
64+
let prev_secret = self.keyutils.get_secret();
65+
self.keyutils.set_secret(secret)?;
66+
67+
if let Err(err) = self.ss.set_secret(secret) {
68+
debug!("Failed set of secret-service: {err}; reverting keyutils");
69+
match prev_secret {
70+
Ok(ref secret) => self.keyutils.set_secret(secret),
71+
Err(Error::NoEntry) => self.keyutils.delete_credential(),
72+
Err(err) => Err(err),
73+
}?;
74+
75+
return Err(err);
76+
}
77+
78+
Ok(())
79+
}
80+
81+
/// Retrieve a password from the underlying store
82+
///
83+
/// The password is retrieved from keyutils. In case of error, the
84+
/// password is retrieved from secret-service instead (and
85+
/// keyutils is updated).
86+
fn get_password(&self) -> Result<String> {
87+
match self.keyutils.get_password() {
88+
Ok(password) => {
89+
return Ok(password);
90+
}
91+
Err(err) => {
92+
debug!("Failed get from keyutils: {err}; trying secret service")
93+
}
94+
}
95+
96+
let password = self.ss.get_password().map_err(ambiguous_to_no_entry)?;
97+
self.keyutils.set_password(&password)?;
98+
99+
Ok(password)
100+
}
101+
102+
/// Retrieve a secret from the underlying store
103+
///
104+
/// The secret is retrieved from keyutils. In case of error, the
105+
/// secret is retrieved from secret-service instead (and keyutils
106+
/// is updated).
107+
fn get_secret(&self) -> Result<Vec<u8>> {
108+
match self.keyutils.get_secret() {
109+
Ok(secret) => {
110+
return Ok(secret);
111+
}
112+
Err(err) => {
113+
debug!("Failed get from keyutils: {err}; trying secret service")
114+
}
115+
}
116+
117+
let secret = self.ss.get_secret().map_err(ambiguous_to_no_entry)?;
118+
self.keyutils.set_secret(&secret)?;
119+
120+
Ok(secret)
121+
}
122+
123+
/// Delete a password from the underlying store.
124+
///
125+
/// The credential is deleted from both keyutils and
126+
/// secret-service.
127+
fn delete_credential(&self) -> Result<()> {
128+
if let Err(err) = self.keyutils.delete_credential() {
129+
debug!("cannot delete keyutils credential: {err}");
130+
}
131+
132+
self.ss.delete_credential()
133+
}
134+
135+
fn as_any(&self) -> &dyn std::any::Any {
136+
self
137+
}
138+
139+
fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140+
std::fmt::Debug::fmt(self, f)
141+
}
142+
}
143+
144+
impl KeyutilsPersistentCredential {
145+
/// Create the platform credential for a Keyutils entry.
146+
///
147+
/// This just passes the arguments to the underlying two stores
148+
/// and wraps their results with an entry that holds both.
149+
pub fn new_with_target(target: Option<&str>, service: &str, user: &str) -> Result<Self> {
150+
let ss = SsCredential::new_with_target(target, service, user)?;
151+
let keyutils = KeyutilsCredential::new_with_target(target, service, user)?;
152+
Ok(Self { keyutils, ss })
153+
}
154+
}
155+
156+
/// The builder for keyutils-persistent credentials
157+
#[derive(Debug, Default)]
158+
pub struct KeyutilsPersistentCredentialBuilder {}
159+
160+
/// Returns an instance of the keyutils-persistent credential builder.
161+
///
162+
/// If keyutils-persistent is the default credential store, this is
163+
/// called once when an entry is first created.
164+
pub fn default_credential_builder() -> Box<CredentialBuilder> {
165+
Box::new(KeyutilsPersistentCredentialBuilder {})
166+
}
167+
168+
impl CredentialBuilderApi for KeyutilsPersistentCredentialBuilder {
169+
/// Build a [KeyutilsPersistentCredential] for the given target, service, and user.
170+
fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result<Box<Credential>> {
171+
Ok(Box::new(SsCredential::new_with_target(
172+
target, service, user,
173+
)?))
174+
}
175+
176+
/// Return the underlying builder object with an `Any` type so that it can
177+
/// be downgraded to a [KeyutilsPersistentCredentialBuilder] for platform-specific processing.
178+
fn as_any(&self) -> &dyn std::any::Any {
179+
self
180+
}
181+
182+
/// Return the persistence of this store.
183+
///
184+
/// This store's persistence derives from that of the secret service.
185+
fn persistence(&self) -> CredentialPersistence {
186+
SsCredentialBuilder {}.persistence()
187+
}
188+
}
189+
190+
/// Replace any Ambiguous error with a NoEntry one
191+
fn ambiguous_to_no_entry(err: Error) -> Error {
192+
if let Error::Ambiguous(_) = err {
193+
return Error::NoEntry;
194+
};
195+
196+
err
197+
}
198+
199+
#[cfg(test)]
200+
mod tests {
201+
use crate::{Entry, Error};
202+
203+
use super::KeyutilsPersistentCredential;
204+
205+
fn entry_new(service: &str, user: &str) -> Entry {
206+
crate::tests::entry_from_constructor(
207+
KeyutilsPersistentCredential::new_with_target,
208+
service,
209+
user,
210+
)
211+
}
212+
213+
#[test]
214+
fn test_invalid_parameter() {
215+
let credential = KeyutilsPersistentCredential::new_with_target(Some(""), "service", "user");
216+
assert!(
217+
matches!(credential, Err(Error::Invalid(_, _))),
218+
"Created entry with empty target"
219+
);
220+
}
221+
222+
#[test]
223+
fn test_empty_service_and_user() {
224+
crate::tests::test_empty_service_and_user(entry_new);
225+
}
226+
227+
#[test]
228+
fn test_missing_entry() {
229+
crate::tests::test_missing_entry(entry_new);
230+
}
231+
232+
#[test]
233+
fn test_empty_password() {
234+
let entry = entry_new("empty password service", "empty password user");
235+
assert!(
236+
matches!(entry.set_password(""), Err(Error::Invalid(_, _))),
237+
"Able to set empty password"
238+
);
239+
}
240+
241+
#[test]
242+
fn test_round_trip_ascii_password() {
243+
crate::tests::test_round_trip_ascii_password(entry_new);
244+
}
245+
246+
#[test]
247+
fn test_round_trip_non_ascii_password() {
248+
crate::tests::test_round_trip_non_ascii_password(entry_new);
249+
}
250+
251+
#[test]
252+
fn test_round_trip_random_secret() {
253+
crate::tests::test_round_trip_random_secret(entry_new);
254+
}
255+
256+
#[test]
257+
fn test_update() {
258+
crate::tests::test_update(entry_new);
259+
}
260+
261+
#[test]
262+
fn test_noop_get_update_attributes() {
263+
crate::tests::test_noop_get_update_attributes(entry_new);
264+
}
265+
}

0 commit comments

Comments
 (0)