use std::sync::OnceLock;
pub const OVERRIDE_ENV: &str = "OPENVTC_SECURE_STORE";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Selection {
Platform,
KernelKeyring,
}
static ACTIVE: OnceLock<Selection> = OnceLock::new();
pub fn parse_override(raw: &str) -> Result<Selection, String> {
match raw {
"" => Ok(Selection::Platform),
"keyutils" if cfg!(target_os = "linux") => Ok(Selection::KernelKeyring),
"keyutils" => Err(format!(
"{OVERRIDE_ENV}=keyutils selects the Linux kernel keyring, which this platform \
does not have. Unset {OVERRIDE_ENV} to use the OS credential store."
)),
"file" => Err(format!(
"{OVERRIDE_ENV}=file selects OpenVTC's encrypted file store, which did-git-sign \
cannot read (it belongs to openvtc-core and holds only passphrase-encrypted \
profile data, so OpenVTC does not keep the signing credential there either). \
Unset {OVERRIDE_ENV} for did-git-sign to use the OS credential store, or set \
{OVERRIDE_ENV}=keyutils on Linux for the kernel keyring (lost on reboot)."
)),
other => Err(format!(
"unknown {OVERRIDE_ENV}={other}. Leave it unset to use the OS credential store \
(the same one OpenVTC uses)."
)),
}
}
pub fn describe(selection: Selection) -> &'static str {
match selection {
Selection::KernelKeyring => "Linux kernel keyring (keyutils)",
Selection::Platform => platform_name(),
}
}
fn platform_name() -> &'static str {
if cfg!(target_os = "linux") {
"Secret Service (DBus)"
} else if cfg!(target_os = "macos") {
"macOS Keychain"
} else if cfg!(target_os = "windows") {
"Windows Credential Manager"
} else {
"OS credential store"
}
}
pub fn describe_active() -> String {
if let Some(sel) = ACTIVE.get() {
return describe(*sel).to_string();
}
match keyring_core::get_default_store() {
Some(store) => store.vendor(),
None => "OS credential store (none registered)".to_string(),
}
}
pub fn active() -> Option<Selection> {
ACTIVE.get().copied()
}
pub fn install() -> anyhow::Result<()> {
let raw = match std::env::var(OVERRIDE_ENV) {
Ok(v) => v,
Err(std::env::VarError::NotPresent) => String::new(),
Err(std::env::VarError::NotUnicode(_)) => {
anyhow::bail!("{OVERRIDE_ENV} is not valid UTF-8")
}
};
let selection = parse_override(&raw).map_err(anyhow::Error::msg)?;
match selection {
Selection::Platform => {
vta_sdk::keyring_init::install_default_store()
.map_err(|e| anyhow::anyhow!("could not open the {}: {e}", platform_name()))?;
}
Selection::KernelKeyring => install_kernel_keyring()?,
}
let _ = ACTIVE.set(selection);
Ok(())
}
#[cfg(target_os = "linux")]
fn install_kernel_keyring() -> anyhow::Result<()> {
let store = linux_keyutils_keyring_store::Store::new()
.map_err(|e| anyhow::anyhow!("could not open the Linux kernel keyring: {e}"))?;
keyring_core::set_default_store(store);
Ok(())
}
#[cfg(not(target_os = "linux"))]
fn install_kernel_keyring() -> anyhow::Result<()> {
unreachable!("parse_override refuses keyutils off Linux")
}
#[derive(Debug, PartialEq, Eq)]
pub enum LegacyLookup {
Absent,
Migrated(String),
FoundNotCopied(String, String),
}
pub fn copy_from_legacy(
primary: &keyring_core::api::CredentialStore,
legacy: &keyring_core::api::CredentialStore,
service: &str,
user: &str,
) -> keyring_core::Result<LegacyLookup> {
let value = match legacy.build(service, user, None)?.get_password() {
Ok(v) => v,
Err(keyring_core::Error::NoEntry) => return Ok(LegacyLookup::Absent),
Err(e) => return Err(e),
};
let copied = primary
.build(service, user, None)
.and_then(|entry| entry.set_password(&value));
Ok(match copied {
Ok(()) => LegacyLookup::Migrated(value),
Err(e) => LegacyLookup::FoundNotCopied(value, e.to_string()),
})
}
pub fn recover_legacy(service: &str, user: &str) -> Option<String> {
if active() != Some(Selection::Platform) {
return None;
}
recover_from_kernel_keyring(service, user)
}
#[cfg(target_os = "linux")]
fn recover_from_kernel_keyring(service: &str, user: &str) -> Option<String> {
let legacy = linux_keyutils_keyring_store::Store::new().ok()?;
let primary = keyring_core::get_default_store()?;
match copy_from_legacy(primary.as_ref(), legacy.as_ref(), service, user) {
Ok(LegacyLookup::Absent) => None,
Ok(LegacyLookup::Migrated(v)) => {
eprintln!(
"did-git-sign: moved the credential '{user}' from the Linux kernel keyring \
(where did-git-sign 0.18.1 and earlier kept it, lost on reboot) to the \
{}.",
platform_name()
);
Some(v)
}
Ok(LegacyLookup::FoundNotCopied(v, why)) => {
eprintln!(
"did-git-sign: found the credential '{user}' in the Linux kernel keyring but \
could not copy it to the {}: {why}. Using it for now; it is lost at reboot.",
platform_name()
);
Some(v)
}
Err(e) => {
tracing::debug!("kernel keyring lookup for {user}: {e}");
None
}
}
}
#[cfg(not(target_os = "linux"))]
fn recover_from_kernel_keyring(_service: &str, _user: &str) -> Option<String> {
None
}
pub fn remove_legacy(service: &str, user: &str) -> bool {
if active() != Some(Selection::Platform) {
return false;
}
#[cfg(target_os = "linux")]
{
use keyring_core::api::CredentialStoreApi;
if let Ok(store) = linux_keyutils_keyring_store::Store::new()
&& let Ok(entry) = store.build(service, user, None)
{
return entry.delete_credential().is_ok();
}
}
let _ = (service, user);
false
}
#[cfg(test)]
mod tests {
use super::*;
use keyring_core::api::CredentialStoreApi;
use keyring_core::mock;
#[test]
fn unset_or_empty_selects_the_platform_store() {
assert_eq!(parse_override(""), Ok(Selection::Platform));
}
#[test]
fn keyutils_selects_the_kernel_keyring_only_on_linux() {
let got = parse_override("keyutils");
if cfg!(target_os = "linux") {
assert_eq!(got, Ok(Selection::KernelKeyring));
} else {
assert!(got.unwrap_err().contains("does not have"));
}
}
#[test]
fn file_is_refused_with_the_reason() {
let err = parse_override("file").unwrap_err();
assert!(err.contains("openvtc-core"), "{err}");
assert!(err.contains(OVERRIDE_ENV), "{err}");
}
#[test]
fn unknown_values_are_errors_not_defaults() {
for v in ["os", "KEYUTILS", " keyutils", "dbus"] {
assert!(parse_override(v).is_err(), "{v} should be refused");
}
}
#[test]
fn store_names_say_where_the_lookup_went() {
assert_eq!(
describe(Selection::KernelKeyring),
"Linux kernel keyring (keyutils)"
);
let platform = describe(Selection::Platform);
if cfg!(target_os = "linux") {
assert_eq!(platform, "Secret Service (DBus)");
} else if cfg!(target_os = "macos") {
assert_eq!(platform, "macOS Keychain");
}
}
#[test]
fn legacy_entry_is_copied_into_the_primary_store() {
let primary = mock::Store::new().unwrap();
let legacy = mock::Store::new().unwrap();
legacy
.build("did-git-sign", "did:x#key-0:vta", None)
.unwrap()
.set_password("creds")
.unwrap();
let got = copy_from_legacy(
primary.as_ref(),
legacy.as_ref(),
"did-git-sign",
"did:x#key-0:vta",
)
.unwrap();
assert_eq!(got, LegacyLookup::Migrated("creds".into()));
let copied = primary
.build("did-git-sign", "did:x#key-0:vta", None)
.unwrap()
.get_password()
.unwrap();
assert_eq!(copied, "creds");
assert!(
legacy
.build("did-git-sign", "did:x#key-0:vta", None)
.unwrap()
.get_password()
.is_ok()
);
}
#[test]
fn legacy_miss_is_absent_and_writes_nothing() {
let primary = mock::Store::new().unwrap();
let legacy = mock::Store::new().unwrap();
let got = copy_from_legacy(primary.as_ref(), legacy.as_ref(), "did-git-sign", "u").unwrap();
assert_eq!(got, LegacyLookup::Absent);
assert!(matches!(
primary
.build("did-git-sign", "u", None)
.unwrap()
.get_password(),
Err(keyring_core::Error::NoEntry)
));
}
#[test]
fn a_failed_copy_still_returns_the_value() {
let primary = mock::Store::new().unwrap();
let legacy = mock::Store::new().unwrap();
legacy
.build("s", "u", None)
.unwrap()
.set_password("v")
.unwrap();
let entry = primary.build("s", "u", None).unwrap();
let cred: &mock::Cred = entry.as_any().downcast_ref().unwrap();
cred.set_error(keyring_core::Error::NoStorageAccess(
"locked".to_string().into(),
));
match copy_from_legacy(primary.as_ref(), legacy.as_ref(), "s", "u").unwrap() {
LegacyLookup::FoundNotCopied(v, why) => {
assert_eq!(v, "v");
assert!(!why.is_empty());
}
other => panic!("expected FoundNotCopied, got {other:?}"),
}
}
#[test]
fn a_legacy_read_error_is_an_error_not_a_miss() {
let primary = mock::Store::new().unwrap();
let legacy = mock::Store::new().unwrap();
let entry = legacy.build("s", "u", None).unwrap();
let cred: &mock::Cred = entry.as_any().downcast_ref().unwrap();
cred.set_error(keyring_core::Error::NoStorageAccess(
"locked".to_string().into(),
));
assert!(copy_from_legacy(primary.as_ref(), legacy.as_ref(), "s", "u").is_err());
}
}