mod delete;
mod format;
mod import;
mod keys;
mod listing;
mod part;
mod pending;
pub use delete::{DeletePreview, PendingDelete};
pub use pending::{
ImportCertificate, ImportChoices, ImportDisposition, ImportLocation, ImportPreview,
ImportReport, ImportRoot, PendingImport,
};
mod roots;
mod signed;
pub use roots::{MAX_ROOTS, RootKind, RootSource, TrustedRoot};
pub use signed::{
DuplicateChoice, MAX_SIGNED_KEY_LEN, MAX_SIGNED_QSOS, MAX_SIGNED_STATION_LEN,
MAX_SIGNED_STATIONS, PreviouslySigned, SignedLog, SignedQso, SigningPlan, SigningSelection,
duplicate_key,
};
pub use import::ExpiringCertificate;
pub use listing::{ListedCertificate, Listing};
pub use part::{PartError, RestoreReport};
pub use keys::{KeyStoreError, MAX_KEYS, OrphanDetection, SERVICE, TEST_SERVICE};
use keys::{KeyStore, MemoryKeyStore, OsKeyStore};
use std::fmt;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use crate::certificate::Certificate;
use crate::request::GeneratedRequest;
use crate::signing::{KeyError, TqslKey};
use crate::station::StationLocation;
use crate::tbk::BackedUpCertificate;
pub const VERSION: u32 = 3;
pub const MAX_FILE_LEN: usize = 32 * 1024 * 1024;
pub const MAX_CERTIFICATES: usize = 1024;
pub const MAX_LOCATIONS: usize = 4096;
pub const MAX_PENDING_REQUESTS: usize = 1024;
pub const MAX_NAME_LEN: usize = 255;
pub const MAX_KEY_LEN: usize = 2560;
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum KeyBackend {
Memory,
Os,
OsTest,
}
#[derive(Clone, Debug)]
pub struct StoredCertificate {
pub id: String,
pub certificate: Certificate,
pub key_name: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StoredLocation {
pub name: String,
pub station: StationLocation,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PendingRequest {
pub id: String,
pub callsign: String,
pub dxcc: u32,
pub public_key_der: Vec<u8>,
pub created_at: u64,
pub key_name: String,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct OpenReport {
pub missing_keys: Vec<String>,
pub missing_request_keys: Vec<String>,
pub orphan_detection: OrphanDetection,
pub orphaned_keys: Vec<String>,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum StoreError {
Log(crate::signed_log::LogError),
Io(io::Error),
Durability(io::Error),
KeyStore(KeyStoreError),
Key(KeyError),
NewerVersion {
found: u32,
supported: u32,
},
Limit(&'static str),
Invalid(&'static str),
AlreadyExists,
Conflict,
SignedMeanwhile,
NotFound,
BuiltInRoot,
Random,
Rollback {
write: Box<StoreError>,
cleanup: KeyStoreError,
},
}
impl fmt::Display for StoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Log(error) => write!(f, "the log: {error}"),
Self::Io(e) => write!(f, "the store file: {e}"),
Self::Durability(e) => write!(
f,
"the store was replaced but its durability sync failed: {e}"
),
Self::KeyStore(e) => write!(f, "the credential store: {e}"),
Self::Key(e) => write!(f, "the signing key: {e}"),
Self::NewerVersion { found, supported } => write!(
f,
"store version {found} is newer than supported version {supported}"
),
Self::Limit(what) => write!(f, "the store exceeds its limit for {what}"),
Self::Invalid(what) => write!(f, "invalid store: {what}"),
Self::AlreadyExists => f.write_str("the store entry already exists"),
Self::Conflict => {
f.write_str("the store changed since inspection; inspect again before committing")
}
Self::SignedMeanwhile => f.write_str(
"a QSO in this log was signed by another run meanwhile; nothing was written",
),
Self::NotFound => f.write_str("the store entry or its key is missing"),
Self::BuiltInRoot => f.write_str(
"that is one of LoTW's roots or CAs built into QG Trust, which are always trusted \
and cannot be removed",
),
Self::Random => f.write_str("secure randomness is unavailable"),
Self::Rollback { write, cleanup } => write!(
f,
"{write}; the new credential could not be removed: {cleanup}"
),
}
}
}
impl std::error::Error for StoreError {}
impl From<io::Error> for StoreError {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
impl From<KeyStoreError> for StoreError {
fn from(error: KeyStoreError) -> Self {
Self::KeyStore(error)
}
}
impl From<KeyError> for StoreError {
fn from(error: KeyError) -> Self {
Self::Key(error)
}
}
#[derive(Default)]
struct Data {
signed_qsos: std::collections::BTreeMap<String, SignedQso>,
certificates: Vec<StoredCertificate>,
locations: Vec<StoredLocation>,
default_location: Option<String>,
pending_requests: Vec<PendingRequest>,
roots: Vec<roots::AcceptedRoot>,
}
pub struct Store {
dir: PathBuf,
keys: Box<dyn KeyStore>,
import_identity: std::sync::Arc<()>,
#[cfg(test)]
fail_sync: std::sync::atomic::AtomicBool,
#[cfg(test)]
lock_observer: std::sync::Mutex<Option<std::sync::mpsc::Sender<bool>>>,
}
impl Store {
pub fn open(
dir: impl AsRef<Path>,
backend: KeyBackend,
) -> Result<(Self, OpenReport), StoreError> {
let keys: Box<dyn KeyStore> = match backend {
KeyBackend::Memory => Box::new(MemoryKeyStore::new()),
KeyBackend::Os => Box::new(OsKeyStore::new(false)),
KeyBackend::OsTest => Box::new(OsKeyStore::new(true)),
};
Self::open_with_keys(dir.as_ref(), keys)
}
fn open_with_keys(
dir: &Path,
keys: Box<dyn KeyStore>,
) -> Result<(Self, OpenReport), StoreError> {
create_directory(dir, sync_directory)?;
let store = Self {
dir: dir.to_path_buf(),
keys,
import_identity: std::sync::Arc::new(()),
#[cfg(test)]
fail_sync: std::sync::atomic::AtomicBool::new(false),
#[cfg(test)]
lock_observer: std::sync::Mutex::new(None),
};
let _lock = store.lock()?;
let data = store.read()?;
let mut report = OpenReport {
orphan_detection: store.keys.orphan_detection(),
..OpenReport::default()
};
for cert in &data.certificates {
if let Some(name) = &cert.key_name
&& !store.keys.exists(name)?
{
report.missing_keys.push(cert.id.clone());
}
}
for request in &data.pending_requests {
if !store.keys.exists(&request.key_name)? {
report.missing_request_keys.push(request.id.clone());
}
}
if report.orphan_detection == OrphanDetection::Unavailable {
return Ok((store, report));
}
let names = store.keys.names()?;
if names.len() > keys::MAX_KEYS {
return Err(StoreError::Limit("credential names"));
}
for name in names {
if !keys::valid_name(&name, "key:") {
return Err(StoreError::Invalid("credential name"));
}
if !data
.certificates
.iter()
.any(|c| c.key_name.as_ref() == Some(&name))
&& !data.pending_requests.iter().any(|r| r.key_name == name)
{
report.orphaned_keys.push(name);
}
}
report.orphaned_keys.sort();
report.orphaned_keys.dedup();
Ok((store, report))
}
pub fn certificates(&self) -> Result<Vec<StoredCertificate>, StoreError> {
let _lock = self.lock()?;
Ok(self.read()?.certificates)
}
pub fn locations(&self) -> Result<Vec<StoredLocation>, StoreError> {
let _lock = self.lock()?;
Ok(self.read()?.locations)
}
pub fn default_location(&self) -> Result<Option<String>, StoreError> {
let _lock = self.lock()?;
Ok(self.read()?.default_location)
}
pub fn set_default_location(&self, name: &str) -> Result<(), StoreError> {
let _lock = self.lock()?;
let mut data = self.read()?;
if !data.locations.iter().any(|l| l.name == name) {
return Err(StoreError::NotFound);
}
data.default_location = Some(name.into());
self.replace(&format::write(&data)?)
}
pub fn clear_default_location(&self) -> Result<(), StoreError> {
let _lock = self.lock()?;
let mut data = self.read()?;
data.default_location = None;
self.replace(&format::write(&data)?)
}
pub fn add_certificate_with_key(
&self,
entry: &BackedUpCertificate,
password: Option<&[u8]>,
) -> Result<String, StoreError> {
let _lock = self.lock()?;
let mut data = self.read()?;
let cert = entry
.certificate
.as_ref()
.ok_or(StoreError::Invalid("certificate is required"))?;
let keyless = keyless_twin(&data, cert);
if keyless.is_none() {
check_certificate(&data, cert)?;
}
let key = entry.key.as_ref().ok_or(StoreError::NotFound)?;
if key.callsign() != cert.callsign() || key.request_dxcc().is_some_and(|d| d != cert.dxcc())
{
return Err(StoreError::Invalid(
"key identity does not match certificate",
));
}
let der = key.pkcs8_der(password)?;
if der.len() > MAX_KEY_LEN {
return Err(StoreError::Limit("credential bytes"));
}
TqslKey::from_pkcs8_der(&der, cert.public_key_der())?;
let name = keys::new_name("key:")?;
if let Some(i) = keyless {
if self.keys.exists(&name)? {
return Err(StoreError::AlreadyExists);
}
data.certificates[i].key_name = Some(name.clone());
let id = data.certificates[i].id.clone();
let xml = format::write(&data)?;
self.put_and_replace(&name, &der, &xml)?;
return Ok(id);
}
let id = keys::new_name("cert:")?;
if self.keys.exists(&name)? || data.certificates.iter().any(|c| c.id == id) {
return Err(StoreError::AlreadyExists);
}
data.certificates.push(StoredCertificate {
id: id.clone(),
certificate: cert.clone(),
key_name: Some(name.clone()),
});
let xml = format::write(&data)?;
self.put_and_replace(&name, &der, &xml)?;
Ok(id)
}
pub fn store_request(&self, request: &GeneratedRequest) -> Result<String, StoreError> {
let _lock = self.lock()?;
let mut data = self.read()?;
if data.pending_requests.len() >= MAX_PENDING_REQUESTS {
return Err(StoreError::Limit("pending requests"));
}
if data
.pending_requests
.iter()
.any(|r| r.public_key_der == request.public)
{
return Err(StoreError::AlreadyExists);
}
if request.private.len() > MAX_KEY_LEN {
return Err(StoreError::Limit("credential bytes"));
}
TqslKey::from_pkcs8_der(&request.private, &request.public)?;
let name = keys::new_name("key:")?;
let id = keys::new_name("request:")?;
if self.keys.exists(&name)? || data.pending_requests.iter().any(|r| r.id == id) {
return Err(StoreError::AlreadyExists);
}
data.pending_requests.push(PendingRequest {
id: id.clone(),
callsign: request.callsign.clone(),
dxcc: request.dxcc,
public_key_der: request.public.clone(),
created_at: request.created_at,
key_name: name.clone(),
});
let xml = format::write(&data)?;
self.put_and_replace(&name, &request.private, &xml)?;
Ok(id)
}
pub fn pending_requests(&self) -> Result<Vec<PendingRequest>, StoreError> {
let _lock = self.lock()?;
Ok(self.read()?.pending_requests)
}
pub fn abandon_request(&self, id: &str) -> Result<(), StoreError> {
let _lock = self.lock()?;
let mut data = self.read()?;
let i = data
.pending_requests
.iter()
.position(|r| r.id == id)
.ok_or(StoreError::NotFound)?;
let request = data.pending_requests.remove(i);
self.replace(&format::write(&data)?)?;
self.keys.delete(&request.key_name)?;
Ok(())
}
pub fn add_requested_certificate(
&self,
certificate: &Certificate,
) -> Result<String, StoreError> {
let _lock = self.lock()?;
let mut data = self.read()?;
check_certificate(&data, certificate)?;
let i = data
.pending_requests
.iter()
.position(|r| {
r.public_key_der == certificate.public_key_der()
&& r.callsign == certificate.callsign()
&& r.dxcc == certificate.dxcc()
})
.ok_or(StoreError::NotFound)?;
let key_name = data.pending_requests[i].key_name.clone();
let der = self.keys.get(&key_name)?.ok_or(StoreError::NotFound)?;
if der.len() > MAX_KEY_LEN {
return Err(StoreError::Limit("credential bytes"));
}
TqslKey::from_pkcs8_der(&der, certificate.public_key_der())?;
let id = keys::new_name("cert:")?;
if data.certificates.iter().any(|c| c.id == id) {
return Err(StoreError::AlreadyExists);
}
data.certificates.push(StoredCertificate {
id: id.clone(),
certificate: certificate.clone(),
key_name: Some(key_name),
});
data.pending_requests.remove(i);
self.replace(&format::write(&data)?)?;
Ok(id)
}
pub fn add_certificate(&self, certificate: &Certificate) -> Result<String, StoreError> {
let _lock = self.lock()?;
let mut data = self.read()?;
check_certificate(&data, certificate)?;
let id = keys::new_name("cert:")?;
if data.certificates.iter().any(|c| c.id == id) {
return Err(StoreError::AlreadyExists);
}
data.certificates.push(StoredCertificate {
id: id.clone(),
certificate: certificate.clone(),
key_name: None,
});
self.replace(&format::write(&data)?)?;
Ok(id)
}
pub fn delete_certificate(&self, id: &str) -> Result<(), StoreError> {
self.commit_delete(self.inspect_delete(id)?)
}
pub fn add_location(&self, name: &str, station: &StationLocation) -> Result<(), StoreError> {
self.save_location(name, station, false)
}
pub fn replace_location(
&self,
name: &str,
station: &StationLocation,
) -> Result<(), StoreError> {
self.save_location(name, station, true)
}
fn save_location(
&self,
name: &str,
station: &StationLocation,
replace: bool,
) -> Result<(), StoreError> {
let _lock = self.lock()?;
let mut data = self.read()?;
format::check_name(name)?;
let i = data.locations.iter().position(|l| l.name == name);
match (i, replace) {
(Some(_), false) => return Err(StoreError::AlreadyExists),
(None, true) => return Err(StoreError::NotFound),
(Some(i), true) => data.locations[i].station = station.clone(),
(None, false) => {
if data.locations.len() >= MAX_LOCATIONS {
return Err(StoreError::Limit("locations"));
}
if data.locations.is_empty() {
data.default_location = Some(name.into());
}
data.locations.push(StoredLocation {
name: name.into(),
station: station.clone(),
});
}
}
self.replace(&format::write(&data)?)
}
pub fn delete_location(&self, name: &str) -> Result<(), StoreError> {
let _lock = self.lock()?;
let mut data = self.read()?;
let i = data
.locations
.iter()
.position(|l| l.name == name)
.ok_or(StoreError::NotFound)?;
data.locations.remove(i);
if data.default_location.as_deref() == Some(name) {
data.default_location = None;
}
self.replace(&format::write(&data)?)
}
pub fn with_key<T>(
&self,
cert_id: &str,
sign: impl FnOnce(&TqslKey) -> T,
) -> Result<T, StoreError> {
let _lock = self.lock()?;
let data = self.read()?;
let cert = data
.certificates
.iter()
.find(|c| c.id == cert_id)
.ok_or(StoreError::NotFound)?;
let name = cert.key_name.as_ref().ok_or(StoreError::NotFound)?;
let der = self.keys.get(name)?.ok_or(StoreError::NotFound)?;
if der.len() > MAX_KEY_LEN {
return Err(StoreError::Limit("credential bytes"));
}
let key = TqslKey::from_pkcs8_der(&der, cert.certificate.public_key_der())?;
drop(der);
Ok(sign(&key))
}
fn put_and_replace(&self, name: &str, der: &[u8], xml: &str) -> Result<(), StoreError> {
self.keys.put(name, der)?;
if let Err(write) = self.replace(xml) {
if matches!(write, StoreError::Durability(_)) {
return Err(write);
}
if let Err(cleanup) = self.keys.delete(name) {
return Err(StoreError::Rollback {
write: Box::new(write),
cleanup,
});
}
return Err(write);
}
Ok(())
}
fn lock(&self) -> Result<File, StoreError> {
let lock = owner_file()
.create(true)
.truncate(false)
.open(self.dir.join("store.lock"))?;
#[cfg(test)]
if let Some(observer) = self.lock_observer.lock().expect("lock observer").as_ref() {
match lock.try_lock() {
Ok(()) => return Ok(lock),
Err(std::fs::TryLockError::WouldBlock) => {
observer.send(true).expect("blocked lock observed");
}
Err(std::fs::TryLockError::Error(e)) => return Err(e.into()),
}
}
lock.lock()?;
Ok(lock)
}
fn read(&self) -> Result<Data, StoreError> {
read_file(&self.dir.join("store.xml"))
}
fn replace(&self, xml: &str) -> Result<(), StoreError> {
let partial = self.dir.join("store.xml.partial");
match fs::remove_file(&partial) {
Ok(()) => (),
Err(e) if e.kind() == io::ErrorKind::NotFound => (),
Err(e) => return Err(e.into()),
}
let result = (|| {
let mut file = owner_file().create_new(true).open(&partial)?;
file.write_all(xml.as_bytes())?;
file.sync_all()?;
drop(file);
fs::rename(&partial, self.dir.join("store.xml"))?;
Ok::<_, StoreError>(())
})();
if result.is_err() {
let _ = fs::remove_file(&partial);
}
result?;
#[cfg(test)]
if self.fail_sync.load(std::sync::atomic::Ordering::Relaxed) {
return Err(StoreError::Durability(io::Error::other(
"injected sync failure after rename",
)));
}
sync_replacement(&self.dir).map_err(StoreError::Durability)?;
Ok(())
}
}
fn read_file(path: &Path) -> Result<Data, StoreError> {
let bytes = match crate::files::read_regular_file(path, MAX_FILE_LEN) {
Ok(bytes) => bytes,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Data::default()),
Err(e) if e.kind() == io::ErrorKind::InvalidData => {
return Err(StoreError::Limit("file bytes"));
}
Err(e) => return Err(e.into()),
};
format::read(std::str::from_utf8(&bytes).map_err(|_| StoreError::Invalid("not UTF-8"))?)
}
fn sync_replacement(dir: &Path) -> io::Result<()> {
#[cfg(windows)]
{
OpenOptions::new()
.write(true)
.open(dir.join("store.xml"))?
.sync_all()?;
}
sync_directory(dir)
}
fn sync_directory(dir: &Path) -> io::Result<()> {
#[cfg(unix)]
File::open(dir)?.sync_all()?;
#[cfg(not(unix))]
let _ = dir;
Ok(())
}
fn create_directory(dir: &Path, sync: impl Fn(&Path) -> io::Result<()>) -> io::Result<()> {
const PENDING: &str = ".qgt-directory-pending";
let dir = std::path::absolute(dir)?;
let mut missing = Vec::new();
let mut ancestor = dir.as_path();
while !ancestor.try_exists()? {
missing.push(ancestor);
ancestor = ancestor
.parent()
.ok_or_else(|| io::Error::other("no existing store ancestor"))?;
}
let mut chain = Vec::new();
if !ancestor.join(PENDING).try_exists()?
&& let Some(parent) = ancestor.parent()
&& parent.join(PENDING).try_exists()?
{
chain.push(ancestor);
ancestor = parent;
}
for path in ancestor.ancestors() {
if !path.join(PENDING).try_exists()? {
break;
}
chain.push(path);
}
chain.reverse();
chain.extend(missing.into_iter().rev());
#[cfg(unix)]
let builder = {
use std::os::unix::fs::DirBuilderExt;
let mut builder = fs::DirBuilder::new();
builder.mode(0o700);
builder
};
#[cfg(not(unix))]
let builder = fs::DirBuilder::new();
for (index, path) in chain.iter().enumerate() {
match builder.create(path) {
Ok(()) => (),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists && path.is_dir() => (),
Err(e) => return Err(e),
}
match owner_file().create_new(true).open(path.join(PENDING)) {
Ok(marker) => marker.sync_all()?,
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => (),
Err(e) => return Err(e),
}
if index > 0 {
sync(chain[index - 1])?;
}
sync(path)?;
}
for path in &chain {
match fs::remove_file(path.join(PENDING)) {
Ok(()) => (),
Err(e) if e.kind() == io::ErrorKind::NotFound => (),
Err(e) => return Err(e),
}
sync(path)?;
}
if chain.is_empty() {
sync(&dir)?;
}
Ok(())
}
fn keyless_twin(data: &Data, cert: &Certificate) -> Option<usize> {
data.certificates
.iter()
.position(|c| c.key_name.is_none() && c.certificate.der() == cert.der())
}
fn check_certificate(data: &Data, cert: &Certificate) -> Result<(), StoreError> {
if data.certificates.len() >= MAX_CERTIFICATES {
return Err(StoreError::Limit("certificates"));
}
if data
.certificates
.iter()
.any(|c| c.certificate.der() == cert.der())
{
return Err(StoreError::AlreadyExists);
}
Ok(())
}
fn owner_file() -> OpenOptions {
let mut options = OpenOptions::new();
options.read(true).write(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
options
}
pub fn default_dir() -> Option<PathBuf> {
default_dir_from(|name| {
std::env::var_os(name)
.filter(|v| !v.is_empty())
.map(PathBuf::from)
})
}
fn default_dir_from(env: impl Fn(&str) -> Option<PathBuf>) -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
env("APPDATA").map(|p| p.join("qso-graph").join("trust"))
}
#[cfg(target_os = "macos")]
{
env("HOME").map(|p| p.join("Library/Application Support/io.qso-graph.trust"))
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
env("XDG_DATA_HOME")
.filter(|p| p.is_absolute())
.or_else(|| env("HOME").map(|p| p.join(".local/share")))
.map(|p| p.join("qso-graph-trust"))
}
}
#[cfg(test)]
mod tests;