use sequoia_keystore::{sequoia_directories, sequoia_ipc::IPCPolicy};
use tracing::{debug, trace, warn};
use super::{
cert::{Cert, Fingerprint},
error,
};
pub struct KeyStore {
pub(crate) inner: sequoia_keystore::Keystore,
}
impl KeyStore {
const ENV: &'static str = "SETT_KEYSTORE";
pub fn default_location() -> Result<std::path::PathBuf, error::PgpError> {
Ok(sequoia_directories::Home::new(None)
.map_err(error::PgpError::from)?
.data_dir(sequoia_directories::Component::Keystore))
}
pub async fn open(path: Option<&std::path::Path>) -> Result<Self, error::PgpError> {
let c = sequoia_keystore::Context::configure()
.home(if let Some(p) = path {
p.to_path_buf()
} else if let Some(p) = std::env::var_os(Self::ENV) {
p.into()
} else {
Self::default_location()?
})
.ipc_policy(IPCPolicy::Robust)
.build()
.map_err(error::PgpError::from)?;
let mut keystore = Self {
inner: sequoia_keystore::Keystore::connect(&c).map_err(error::PgpError::from)?,
};
if let Err(error) = keystore.migrate().await {
tracing::warn!(?error, "failed to migrate key store");
}
Ok(keystore)
}
async fn migrate(&mut self) -> Result<(), error::Error> {
use std::collections::BTreeSet;
const SEC_STORE_DEFAULT_DIR: &str = "pgp.cert.d.sec";
const SEC_STORE_ENV_VARIABLE: &str = "PGP_CERT_D_SEC";
let key_store_path = if let Some(p) = std::env::var_os(SEC_STORE_ENV_VARIABLE) {
p.into()
} else {
dirs::data_dir()
.ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, "missing data dir")
})?
.join(SEC_STORE_DEFAULT_DIR)
};
async fn process_dir(
keystore: &mut KeyStore,
dir_path: &std::path::Path,
) -> Result<(), error::Error> {
for maybe_tsk in std::fs::read_dir(dir_path)? {
let mut remove = Vec::with_capacity(1);
let maybe_tsk = maybe_tsk.map_err(error::PgpError::from)?.path();
let path_str = maybe_tsk.display();
tracing::debug!(path = %path_str, "migrating TSK file");
let Ok(cert_parser) = super::cert::parse_certs(std::fs::File::open(&maybe_tsk)?)
else {
tracing::warn!(path = %path_str, "failed to parse TSK file");
continue;
};
for cert in cert_parser {
let keys: BTreeSet<_> = cert
.0
.keys()
.secret()
.map(|ka| Fingerprint(ka.key().fingerprint()))
.collect();
let imported_keys = BTreeSet::from_iter(
keystore
.import(cert)
.await
.map_err(error::PgpError::from)?
.into_iter()
.map(|k| k.fingerprint()),
);
tracing::info!(
path = %path_str,
fingerprints = Vec::from_iter(imported_keys.iter().map(std::string::ToString::to_string)).join(", "),
"migrated private keys");
if keys != imported_keys {
tracing::warn!(
path = %path_str,
expected = ?keys,
imported = ?imported_keys,
"failed to migrate all private keys"
);
remove.push(false);
} else {
remove.push(true);
}
}
if remove.iter().all(|r| *r) {
tracing::debug!(path = %path_str, "removing TSK file");
std::fs::remove_file(&maybe_tsk)?;
}
}
if std::fs::read_dir(dir_path)?.count() == 0 {
tracing::debug!(
path = %dir_path.to_string_lossy(),
"removing empty directory"
);
std::fs::remove_dir(dir_path)?;
}
Ok(())
}
if key_store_path.is_dir() {
for entry in std::fs::read_dir(&key_store_path)? {
let entry = entry?.path();
if entry.is_dir() {
process_dir(self, &entry).await?;
} else if entry.is_file() {
if entry.ends_with("writelock") {
std::fs::remove_file(&entry)?;
}
}
}
process_dir(self, &key_store_path).await?;
}
Ok(())
}
pub async fn open_ephemeral() -> Result<Self, error::PgpError> {
Ok(Self {
inner: sequoia_keystore::Keystore::connect(
&sequoia_keystore::Context::configure()
.ephemeral()
.ipc_policy(IPCPolicy::Robust)
.build()
.map_err(error::PgpError::from)?,
)
.map_err(error::PgpError::from)?,
})
}
pub async fn import(&mut self, cert: Cert) -> Result<Vec<Key>, error::PgpError> {
let mut keys = Vec::new();
for mut backend in self
.inner
.backends_async()
.await
.map_err(error::PgpError::from)?
{
if backend.id_async().await.map_err(error::PgpError::from)? == "softkeys" {
for (import_status, key_handle) in backend
.import_async(&cert.0)
.await
.map_err(error::PgpError::from)?
{
let key = Key { inner: key_handle };
tracing::debug!(?import_status, fingerprint=%key.fingerprint(), "imported private key");
keys.push(key);
}
}
}
Ok(keys)
}
pub async fn list(&mut self) -> Result<Vec<Key>, error::PgpError> {
let mut keys = std::collections::BTreeMap::new();
for mut backend in self
.inner
.backends_async()
.await
.map_err(error::PgpError::from)?
{
for mut device in backend
.devices_async()
.await
.map_err(error::PgpError::from)?
{
for key_handle in device.keys_async().await.map_err(error::PgpError::from)? {
keys.insert(key_handle.fingerprint(), Key { inner: key_handle });
}
}
}
Ok(keys.into_values().collect())
}
pub async fn find_key(
&mut self,
fingerprint: Fingerprint,
) -> Result<Vec<Key>, error::PgpError> {
Ok(self
.inner
.find_key_async(fingerprint.0.into())
.await
.map_err(error::PgpError::from)?
.into_iter()
.map(|key_handle| Key { inner: key_handle })
.collect())
}
pub async fn delete_key(&mut self, fingerprint: Fingerprint) -> Result<(), error::PgpError> {
let mut keys = self
.inner
.find_key_async(fingerprint.0.clone().into())
.await
.map_err(error::PgpError::from)?;
if keys.is_empty() {
return Err(error::PgpError::from(
"key deletion failed, no key matching the fingerprint found",
));
}
if keys.len() != 1 {
return Err(error::PgpError::from(
"key deletion failed, found multiple keys matching the fingerprint",
));
}
keys.first_mut()
.unwrap()
.delete_secret_key_material_async()
.await
.map_err(error::PgpError::from)?;
tracing::info!(%fingerprint, "deleted private key");
Ok(())
}
pub(crate) async fn export(
&mut self,
fingerprint: &sequoia_openpgp::Fingerprint,
) -> Result<
sequoia_openpgp::packet::Key<
sequoia_openpgp::packet::key::SecretParts,
sequoia_openpgp::packet::key::UnspecifiedRole,
>,
error::PgpError,
> {
let mut errors = Vec::new();
for mut key in self
.inner
.find_key_async(fingerprint.into())
.await
.map_err(error::PgpError::from)?
{
let exported_key = key.export_async().await;
match exported_key {
Ok(exported_key) => return Ok(exported_key),
Err(e) => errors.push(e),
}
}
Err(error::PgpError::Error(format!(
"Unable to export key: {errors:?}"
)))
}
}
#[derive(Clone)]
pub struct Key {
pub(crate) inner: sequoia_keystore::Key,
}
impl Key {
pub fn fingerprint(&self) -> Fingerprint {
Fingerprint(self.inner.fingerprint())
}
pub async fn unlock<F, Fut>(&mut self, password: F) -> Result<(), error::PgpError>
where
F: Fn(super::cert::Fingerprint) -> Fut,
Fut: std::future::Future<Output = crate::secret::Secret>,
{
match self.inner.locked_async().await {
Ok(sequoia_keystore::Protection::Unlocked) => {
trace!("Key is unlocked");
Ok(())
}
Ok(sequoia_keystore::Protection::Password(_)) => {
if let Ok(()) = self
.inner
.unlock_async(password(self.fingerprint()).await.as_inner().clone())
.await
{
trace!("Unlocked key with the provided password");
Ok(())
} else {
let err_msg = format!(
"Failed to unlock key ({}) with the provided password",
self.inner.fingerprint()
);
debug!(err_msg);
Err(error::PgpError::Error(err_msg))
}
}
Ok(_) => {
trace!("Externally protected key");
Ok(())
}
Err(e) => {
let err_msg = format!("Failed to check key lock status, {e}");
warn!(err_msg);
Err(error::PgpError::Error(err_msg))
}
}
}
}
#[cfg(test)]
mod tests {
use super::KeyStore;
fn assert_keystore_empty(location: &std::path::Path) {
assert!(location.is_dir());
assert!(
!std::fs::read_dir(location)
.unwrap()
.any(|p| p.as_ref().unwrap().path().is_dir())
);
}
#[tokio::test]
async fn custom_keystore_location() {
let location = tempfile::tempdir().unwrap().keep().join("keystore");
KeyStore::open(Some(&location)).await.unwrap();
assert_keystore_empty(&location);
}
}