use core::fmt;
use std::fs::{self, OpenOptions};
use std::io::{self, Write as _};
use std::path::{Path, PathBuf};
use std::sync::{PoisonError, RwLock};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::local::{self, Entry};
use super::{KeyId, KeySpec, KeyStore, KeyStoreCapabilities, KeyStoreError};
use crate::jose::{Jwk, JwsAlgorithm};
#[derive(Serialize, Deserialize)]
struct JwkSet {
keys: Vec<Jwk>,
#[serde(flatten)]
additional: IndexMap<String, Value>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct SoftwareKeyStoreOptions {
pub allow_insecure_permissions: bool,
pub create_if_missing: bool,
}
impl SoftwareKeyStoreOptions {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn allow_insecure_permissions(mut self, allow: bool) -> Self {
self.allow_insecure_permissions = allow;
self
}
#[must_use]
pub fn create_if_missing(mut self, create: bool) -> Self {
self.create_if_missing = create;
self
}
}
pub struct SoftwareKeyStore {
path: PathBuf,
keys: RwLock<IndexMap<KeyId, Entry>>,
additional: IndexMap<String, Value>,
}
impl SoftwareKeyStore {
pub fn open(path: impl Into<PathBuf>) -> Result<Self, KeyStoreError> {
Self::open_with(path, SoftwareKeyStoreOptions::default())
}
#[expect(
clippy::needless_pass_by_value,
reason = "options are taken by value, as builders take them, so that they can grow"
)]
pub fn open_with(
path: impl Into<PathBuf>,
options: SoftwareKeyStoreOptions,
) -> Result<Self, KeyStoreError> {
let path = path.into();
if options.create_if_missing && !path.exists() {
return Self::create(path);
}
let data = fs::read(&path).map_err(|source| KeyStoreError::Io {
path: path.clone(),
source,
})?;
if !options.allow_insecure_permissions {
check_permissions(&path)?;
}
let set: JwkSet =
serde_json::from_slice(&data).map_err(|e| KeyStoreError::InvalidKeySet {
path: path.clone(),
reason: "not a JWK set".to_owned(),
source: Some(Box::new(e)),
})?;
let mut keys = IndexMap::with_capacity(set.keys.len());
for jwk in set.keys {
let (id, entry) =
Entry::from_jwk(jwk).map_err(|unusable| unusable.for_key_set(path.clone()))?;
if keys.insert(id.clone(), entry).is_some() {
return Err(KeyStoreError::InvalidKeySet {
path,
reason: format!("key `{id}` is defined twice"),
source: None,
});
}
}
Ok(Self {
path,
keys: RwLock::new(keys),
additional: set.additional,
})
}
pub fn create(path: impl Into<PathBuf>) -> Result<Self, KeyStoreError> {
let path = path.into();
if path.exists() {
return Err(KeyStoreError::Io {
path,
source: io::Error::new(io::ErrorKind::AlreadyExists, "file exists"),
});
}
let store = Self {
path,
keys: RwLock::new(IndexMap::new()),
additional: IndexMap::new(),
};
store.save(&IndexMap::new())?;
Ok(store)
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn key_ids(&self) -> Vec<KeyId> {
self.keys
.read()
.unwrap_or_else(PoisonError::into_inner)
.keys()
.cloned()
.collect()
}
pub fn import(&self, jwk: Jwk) -> Result<KeyId, KeyStoreError> {
let key = KeyId::new(jwk.kid().unwrap_or_default());
let (id, entry) = Entry::from_jwk(jwk).map_err(|unusable| unusable.for_key(key))?;
self.insert(id, entry)
}
fn insert(&self, id: KeyId, entry: Entry) -> Result<KeyId, KeyStoreError> {
let mut keys = self.keys.write().unwrap_or_else(PoisonError::into_inner);
if keys.contains_key(&id) {
return Err(KeyStoreError::AlreadyExists(id));
}
keys.insert(id.clone(), entry);
if let Err(e) = self.save(&keys) {
keys.shift_remove(&id);
return Err(e);
}
Ok(id)
}
fn save(&self, keys: &IndexMap<KeyId, Entry>) -> Result<(), KeyStoreError> {
let set = JwkSet {
keys: keys.values().map(|entry| entry.jwk.clone()).collect(),
additional: self.additional.clone(),
};
let mut json = serde_json::to_vec_pretty(&set).map_err(|e| KeyStoreError::Io {
path: self.path.clone(),
source: io::Error::other(e),
})?;
json.push(b'\n');
write_atomically(&self.path, &json).map_err(|source| KeyStoreError::Io {
path: self.path.clone(),
source,
})
}
}
impl fmt::Debug for SoftwareKeyStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SoftwareKeyStore")
.field("path", &self.path)
.field("keys", &self.key_ids())
.finish_non_exhaustive()
}
}
#[async_trait::async_trait]
impl KeyStore for SoftwareKeyStore {
fn capabilities(&self) -> KeyStoreCapabilities {
KeyStoreCapabilities::new(local::ALGORITHMS.iter().copied())
.with_generate()
.with_delete()
}
async fn public_jwk(&self, key: &KeyId) -> Result<Jwk, KeyStoreError> {
self.keys
.read()
.unwrap_or_else(PoisonError::into_inner)
.get(key)
.map(Entry::public_jwk)
.ok_or_else(|| KeyStoreError::NotFound(key.clone()))
}
async fn sign(
&self,
key: &KeyId,
alg: JwsAlgorithm,
data: &[u8],
) -> Result<Vec<u8>, KeyStoreError> {
self.keys
.read()
.unwrap_or_else(PoisonError::into_inner)
.get(key)
.ok_or_else(|| KeyStoreError::NotFound(key.clone()))?
.sign(key, alg, data)
}
async fn exists(&self, key: &KeyId) -> Result<bool, KeyStoreError> {
Ok(self
.keys
.read()
.unwrap_or_else(PoisonError::into_inner)
.contains_key(key))
}
async fn generate(&self, spec: KeySpec) -> Result<KeyId, KeyStoreError> {
let (id, key) = local::generate(&spec, &mut local::os_random)?;
let entry = Entry::new(&id, key, spec.alg);
self.insert(id, entry)
}
async fn delete(&self, key: &KeyId) -> Result<(), KeyStoreError> {
let mut keys = self.keys.write().unwrap_or_else(PoisonError::into_inner);
let (index, id, entry) = keys
.shift_remove_full(key)
.ok_or_else(|| KeyStoreError::NotFound(key.clone()))?;
if let Err(e) = self.save(&keys) {
keys.shift_insert(index, id, entry);
return Err(e);
}
Ok(())
}
}
#[cfg(unix)]
fn check_permissions(path: &Path) -> Result<(), KeyStoreError> {
use std::os::unix::fs::PermissionsExt as _;
let mode = fs::metadata(path)
.map_err(|source| KeyStoreError::Io {
path: path.to_owned(),
source,
})?
.permissions()
.mode()
& 0o7777;
if mode & 0o077 != 0 {
return Err(KeyStoreError::InsecurePermissions {
path: path.to_owned(),
mode,
});
}
Ok(())
}
#[cfg(not(unix))]
fn check_permissions(_path: &Path) -> Result<(), KeyStoreError> {
Ok(())
}
fn write_atomically(path: &Path, data: &[u8]) -> io::Result<()> {
let dir = match path.parent() {
Some(dir) if !dir.as_os_str().is_empty() => dir,
_ => Path::new("."),
};
let file_name = path
.file_name()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "path has no file name"))?;
let mut nonce = [0u8; 8];
getrandom::fill(&mut nonce).map_err(io::Error::other)?;
let mut tmp_name = std::ffi::OsString::from(".");
tmp_name.push(file_name);
tmp_name.push(format!(".{:016x}.tmp", u64::from_be_bytes(nonce)));
let tmp = dir.join(tmp_name);
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
options.mode(0o600);
}
let result = options.open(&tmp).and_then(|mut file| {
file.write_all(data)?;
file.sync_all()?;
drop(file);
fs::rename(&tmp, path)
});
if result.is_err() {
let _ = fs::remove_file(&tmp);
}
result
}