use keyring::Entry;
pub trait KeyStore {
fn save_seed(&self, seed: &[u8; 32]) -> Result<(), KeyStoreError>;
fn load_seed(&self) -> Result<[u8; 32], KeyStoreError>;
fn delete_seed(&self) -> Result<(), KeyStoreError>;
}
pub struct KeyringStore {
entry: Entry,
}
impl KeyringStore {
pub fn new(service: &str, account: &str) -> Result<Self, KeyStoreError> {
Ok(Self {
entry: Entry::new(service, account)?,
})
}
}
impl KeyStore for KeyringStore {
fn save_seed(&self, seed: &[u8; 32]) -> Result<(), KeyStoreError> {
self.entry.set_secret(seed)?;
Ok(())
}
fn load_seed(&self) -> Result<[u8; 32], KeyStoreError> {
let secret = match self.entry.get_secret() {
Ok(secret) => secret,
Err(keyring::Error::NoEntry) => return Err(KeyStoreError::NotFound),
Err(e) => return Err(e.into()),
};
let actual = secret.len();
secret
.try_into()
.map_err(|_| KeyStoreError::InvalidSeedLength { actual })
}
fn delete_seed(&self) -> Result<(), KeyStoreError> {
match self.entry.delete_credential() {
Ok(()) => Ok(()),
Err(keyring::Error::NoEntry) => Ok(()),
Err(e) => Err(e.into()),
}
}
}
#[cfg(target_os = "linux")]
pub struct LinuxKeyutilsStore {
entry: keyring_core::Entry,
}
#[cfg(target_os = "linux")]
impl LinuxKeyutilsStore {
pub fn new(service: &str, account: &str) -> Result<Self, KeyStoreError> {
use keyring_core::api::CredentialStoreApi;
let store = linux_keyutils_keyring_store::Store::new()?;
let entry = store.build(service, account, None)?;
Ok(Self { entry })
}
}
#[cfg(target_os = "linux")]
impl KeyStore for LinuxKeyutilsStore {
fn save_seed(&self, seed: &[u8; 32]) -> Result<(), KeyStoreError> {
self.entry.set_secret(seed)?;
Ok(())
}
fn load_seed(&self) -> Result<[u8; 32], KeyStoreError> {
let secret = match self.entry.get_secret() {
Ok(secret) => secret,
Err(keyring_core::Error::NoEntry) => return Err(KeyStoreError::NotFound),
Err(e) => return Err(e.into()),
};
let actual = secret.len();
secret
.try_into()
.map_err(|_| KeyStoreError::InvalidSeedLength { actual })
}
fn delete_seed(&self) -> Result<(), KeyStoreError> {
match self.entry.delete_credential() {
Ok(()) => Ok(()),
Err(keyring_core::Error::NoEntry) => Ok(()),
Err(e) => Err(e.into()),
}
}
}
#[derive(Debug)]
pub enum KeyStoreError {
NotFound,
InvalidSeedLength { actual: usize },
Backend(keyring::Error),
}
impl std::fmt::Display for KeyStoreError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
KeyStoreError::NotFound => write!(f, "no seed stored under this identity"),
KeyStoreError::InvalidSeedLength { actual } => {
write!(f, "stored secret is {actual} bytes, expected 32")
}
KeyStoreError::Backend(e) => write!(f, "platform secure store error: {e}"),
}
}
}
impl std::error::Error for KeyStoreError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
KeyStoreError::Backend(e) => Some(e),
_ => None,
}
}
}
impl From<keyring::Error> for KeyStoreError {
fn from(e: keyring::Error) -> Self {
match e {
keyring::Error::NoEntry => KeyStoreError::NotFound,
other => KeyStoreError::Backend(other),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "linux")]
static KEYRING_TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(target_os = "linux")]
fn test_store() -> LinuxKeyutilsStore {
LinuxKeyutilsStore::new("macula-rust-test", "keystore-round-trip-test-entry")
.expect("Store::new/build should succeed -- keyutils is always available on Linux")
}
#[cfg(target_os = "linux")]
#[test]
fn save_then_load_returns_the_same_seed() {
let _guard = KEYRING_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let store = test_store();
let seed = [0x42u8; 32];
let result = (|| -> Result<(), KeyStoreError> {
store.save_seed(&seed)?;
let loaded = store.load_seed()?;
assert_eq!(loaded, seed);
Ok(())
})();
store.delete_seed().expect("cleanup delete should succeed");
result.expect("save/load round trip should succeed");
}
#[cfg(target_os = "linux")]
#[test]
fn load_before_any_save_reports_not_found() {
let _guard = KEYRING_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let store = test_store();
let _ = store.delete_seed();
assert!(matches!(store.load_seed(), Err(KeyStoreError::NotFound)));
}
#[cfg(target_os = "linux")]
#[test]
fn delete_is_idempotent() {
let _guard = KEYRING_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let store = test_store();
store.save_seed(&[0x7Fu8; 32]).expect("save");
store.delete_seed().expect("first delete");
store
.delete_seed()
.expect("second delete on an absent entry");
}
}