use std::fmt;
use base64ct::{Base64, Encoding};
use x509_cert::der::Encode;
use zeroize::Zeroizing;
use crate::certificate::{
Certificate, CertificateError, FoundRoot, MAX_CERTIFICATE_LEN, Trust, file_roots,
};
use crate::gzip::{self, GzipError};
use crate::lotw;
use crate::signing::{KeyEntry, KeyError};
use crate::station::{StationError, StationLocation};
use crate::xml::{self, Element};
pub const MAX_TBK_LEN: usize = 256 * 1024 * 1024;
pub const MAX_CERTIFICATES: usize = 1024;
pub const MAX_LOCATIONS: usize = 4096;
pub const MAX_NAME_LEN: usize = 255;
#[derive(Debug)]
pub struct Backup {
pub roots: Vec<Vec<u8>>,
pub authorities: Vec<Vec<u8>>,
pub certificates: Vec<BackedUpCertificate>,
pub locations: Vec<BackedUpLocation>,
pub signed_qsos: Vec<SignedQso>,
pub preferences: Preferences,
pub settings_not_taken: Vec<String>,
pub not_read: Vec<String>,
}
#[derive(Debug)]
pub struct BackedUpCertificate {
pub callsign: String,
pub dxcc: u32,
pub serial: i64,
pub certificate: Option<Certificate>,
pub key: Option<KeyEntry>,
}
#[derive(Debug)]
pub struct BackedUpLocation {
pub name: String,
pub station: StationLocation,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum QthHandling {
Ignore,
#[default]
Refuse,
FromLog,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Preferences {
pub adif_modes: Vec<(String, String)>,
pub cabrillo_contests: Vec<(String, i64, i64)>,
pub ignore_seconds: bool,
pub allow_bad_calls: bool,
pub ignore_log_callsigns: bool,
pub qth: QthHandling,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignedQso {
pub key: String,
pub station: String,
}
#[derive(Debug, PartialEq, Eq)]
pub enum TbkError {
Gzip(GzipError),
TooLarge,
NotText,
Xml {
line: usize,
column: usize,
problem: &'static str,
},
NotABackup,
TooMany(&'static str),
Malformed {
what: String,
},
Authority {
index: usize,
},
Certificate {
callsign: String,
error: CertificateError,
},
Key {
callsign: String,
error: KeyError,
},
Mismatch {
callsign: String,
what: &'static str,
},
Location {
name: String,
error: StationError,
},
RepeatedLocation {
name: String,
},
}
impl fmt::Display for TbkError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Gzip(e) => write!(f, "the backup: {e}"),
Self::TooLarge => write!(f, "the backup is too large"),
Self::NotText => write!(f, "the backup is not text"),
Self::Xml {
line,
column,
problem,
} => {
write!(
f,
"the backup's XML: {problem}, at line {line}, column {column}"
)
}
Self::NotABackup => write!(f, "this is not a TQSL backup"),
Self::TooMany(what) => write!(f, "the backup holds too many {what}"),
Self::Malformed { what } => write!(f, "the backup's {what} is malformed"),
Self::Authority { index } => {
write!(
f,
"the backup's root or CA certificate {} is not a certificate",
index + 1
)
}
Self::Certificate { callsign, error } => write!(f, "{callsign}'s certificate: {error}"),
Self::Key { callsign, error } => write!(f, "{callsign}'s key: {error}"),
Self::Mismatch { callsign, what } => write!(f, "{callsign}: {what}"),
Self::Location { name, error } => write!(f, "station location {name:?}: {error}"),
Self::RepeatedLocation { name } => {
write!(f, "two station locations are named {name:?}")
}
}
}
}
impl std::error::Error for TbkError {}
impl From<GzipError> for TbkError {
fn from(e: GzipError) -> Self {
match e {
GzipError::TooLarge => Self::TooLarge,
e => Self::Gzip(e),
}
}
}
pub fn read(bytes: &[u8], trust: &Trust) -> Result<Backup, TbkError> {
read_with(bytes, trust, false, true)
}
pub fn read_trusting_roots(bytes: &[u8], trust: &Trust) -> Result<Backup, TbkError> {
read_with(bytes, trust, true, true)
}
pub(crate) fn inspect(bytes: &[u8], trust: &Trust) -> Result<Backup, TbkError> {
read_with(bytes, trust, true, false)
}
fn read_with(
bytes: &[u8],
trust: &Trust,
own_roots: bool,
verify: bool,
) -> Result<Backup, TbkError> {
let plain;
let text: &[u8] = if gzip::is_gzip(bytes) {
plain = gzip::decompress(bytes, MAX_TBK_LEN)?;
&plain
} else if bytes.len() > MAX_TBK_LEN {
return Err(TbkError::TooLarge);
} else {
bytes
};
let text = std::str::from_utf8(text).map_err(|_| TbkError::NotText)?;
let root = xml::parse(text).map_err(|e| TbkError::Xml {
line: e.line,
column: e.column,
problem: e.problem,
})?;
if root.name != "TQSL_Configuration" {
return Err(TbkError::NotABackup);
}
let mut backup = Backup {
roots: Vec::new(),
authorities: Vec::new(),
certificates: Vec::new(),
locations: Vec::new(),
signed_qsos: Vec::new(),
preferences: Preferences::default(),
settings_not_taken: Vec::new(),
not_read: Vec::new(),
};
for section in &root.children {
match section.name {
"Certificates" => read_certificates(section, &mut backup)?,
"Locations" => read_locations(section, &mut backup)?,
"DupeDb" => read_signed_qsos(section, &mut backup)?,
"TQSLSettings" => read_settings(section, &mut backup)?,
other => backup.not_read.push(other.to_string()),
}
}
let trust = if own_roots {
let found = backup.found_roots(trust);
trust.with_found(&found)
} else {
trust.clone()
};
for entry in &backup.certificates {
if let Some(cert) = &entry.certificate
&& verify
{
cert.verify(&trust, &backup.authorities)
.map_err(|error| TbkError::Certificate {
callsign: entry.callsign.clone(),
error,
})?;
}
}
Ok(backup)
}
impl Backup {
pub(crate) fn found_roots(&self, trust: &Trust) -> Vec<FoundRoot> {
let users: Vec<Vec<u8>> = self
.certificates
.iter()
.filter_map(|c| c.certificate.as_ref().map(|c| c.der().to_vec()))
.collect();
let supplied: Vec<Vec<u8>> = self
.roots
.iter()
.chain(&self.authorities)
.cloned()
.collect();
file_roots(&users, &supplied, trust)
}
}
fn malformed(what: impl Into<String>) -> TbkError {
TbkError::Malformed { what: what.into() }
}
fn only_child<'e, 'a>(
node: &'e Element<'a>,
name: &'e str,
of: &str,
) -> Result<Option<&'e Element<'a>>, TbkError> {
let mut found = node.children_named(name);
let first = found.next();
if found.next().is_some() {
return Err(malformed(format!("{of} {name}")));
}
Ok(first)
}
fn read_certificates(section: &Element, backup: &mut Backup) -> Result<(), TbkError> {
for entry in §ion.children {
match entry.name {
kind @ ("RootCert" | "CACert") => {
let index = backup.roots.len() + backup.authorities.len();
let pem = entry.text();
let pem = pem.trim();
if pem.len() > MAX_CERTIFICATE_LEN {
return Err(TbkError::Authority { index });
}
let der = crate::certificate::x509_from_pem(pem.as_bytes())
.map_err(|_| TbkError::Authority { index })?
.to_der()
.map_err(|_| TbkError::Authority { index })?;
let list = if kind == "RootCert" {
&mut backup.roots
} else {
&mut backup.authorities
};
if list.len() == MAX_CERTIFICATES {
return Err(TbkError::TooMany("root and CA certificates"));
}
if !list.contains(&der) {
list.push(der);
}
}
"UserCert" => {
if backup.certificates.len() == MAX_CERTIFICATES {
return Err(TbkError::TooMany("callsign certificates"));
}
backup.certificates.push(read_user_certificate(entry)?);
}
_ => return Err(malformed("certificate list")),
}
}
Ok(())
}
fn read_user_certificate(entry: &Element) -> Result<BackedUpCertificate, TbkError> {
let callsign = entry
.attribute("CallSign")
.filter(|c| crate::qso::is_callsign(c))
.ok_or_else(|| malformed("callsign certificate's CallSign"))?
.to_string();
let dxcc = entry
.attribute("dxcc")
.and_then(|d| d.parse::<u32>().ok())
.filter(|&d| d != 0 && lotw::dxcc(d).is_some())
.ok_or_else(|| malformed(format!("{callsign}'s dxcc")))?;
let serial = entry
.attribute("serial")
.and_then(|s| s.trim().parse::<i64>().ok())
.ok_or_else(|| malformed(format!("{callsign}'s serial")))?;
if let Some(other) = entry
.children
.iter()
.find(|c| !matches!(c.name, "SignedCert" | "PrivateKey"))
{
return Err(malformed(format!("{callsign}'s {}", other.name)));
}
let certificate = only_child(entry, "SignedCert", &callsign)?
.map(|node| {
Certificate::from_pem_or_der(node.text().trim().as_bytes()).map_err(|error| {
TbkError::Certificate {
callsign: callsign.clone(),
error,
}
})
})
.transpose()?;
let key = only_child(entry, "PrivateKey", &callsign)?
.map(|node| read_key(&node.text(), &callsign))
.transpose()?;
if certificate.is_none() && serial != 0 {
return Err(malformed(format!("{callsign}'s serial")));
}
if certificate.is_none() && key.is_none() {
return Err(malformed(format!(
"{callsign}'s entry, with neither certificate nor key,"
)));
}
let mismatch = |what| TbkError::Mismatch {
callsign: callsign.clone(),
what,
};
if let Some(cert) = &certificate {
if cert.callsign() != callsign {
return Err(mismatch("the certificate is for another call"));
}
if cert.dxcc() != dxcc {
return Err(mismatch("the certificate is for another DXCC entity"));
}
let written = cert.serial();
let windows = written.is_some_and(|n| n > i64::from(i32::MAX)) && serial == -1;
if written.unwrap_or(-1) != serial && !windows {
return Err(mismatch(
"the certificate's serial number is not its entry's",
));
}
}
if let Some(key) = &key {
if key.callsign() != callsign {
return Err(mismatch("the key was made for another call"));
}
match key.request_dxcc() {
Some(d) if d != dxcc => {
return Err(mismatch("the key was requested for another DXCC entity"));
}
None if certificate.is_none() => {
return Err(mismatch(
"the key waiting for its certificate names no DXCC entity",
));
}
_ => {}
}
if let Some(cert) = &certificate
&& key.public_key_der().as_deref() != Some(cert.public_key_der())
{
return Err(mismatch("the key is not the certificate's"));
}
key.check().map_err(|error| TbkError::Key {
callsign: callsign.clone(),
error,
})?;
}
Ok(BackedUpCertificate {
callsign,
dxcc,
serial,
certificate,
key,
})
}
fn read_key(text: &str, callsign: &str) -> Result<KeyEntry, TbkError> {
let error = |error| TbkError::Key {
callsign: callsign.to_string(),
error,
};
let mut joined = Zeroizing::new(String::with_capacity(text.len()));
joined.extend(text.chars().filter(|c| !c.is_ascii_whitespace()));
let mut decoded = Zeroizing::new(vec![0u8; joined.len() / 4 * 3 + 3]);
let len = Base64::decode(joined.as_bytes(), &mut decoded)
.map_err(|_| error(KeyError::NotAKey))?
.len();
let mut keys = KeyEntry::read_key_file(&decoded[..len]).map_err(error)?;
if keys.len() != 1 {
return Err(error(KeyError::NotAKey));
}
Ok(keys.remove(0))
}
pub(crate) fn location_name(name: Option<&str>, trim: bool) -> Option<String> {
name.map(|name| if trim { name.trim() } else { name })
.filter(|name| !name.trim().is_empty() && name.len() <= MAX_NAME_LEN)
.map(str::to_owned)
}
fn read_locations(section: &Element, backup: &mut Backup) -> Result<(), TbkError> {
for entry in §ion.children {
if entry.name != "Location" {
return Err(malformed("station location list"));
}
if backup.locations.len() == MAX_LOCATIONS {
return Err(TbkError::TooMany("station locations"));
}
let name = location_name(entry.attribute("name").as_deref().map(String::as_str), true)
.ok_or_else(|| malformed("station location's name"))?;
if backup.locations.iter().any(|l| l.name == name) {
return Err(TbkError::RepeatedLocation { name });
}
let values: Vec<(&str, Zeroizing<String>)> =
entry.attributes().filter(|(n, _)| *n != "name").collect();
let fields: Vec<(&str, &str)> = values.iter().map(|(n, v)| (*n, v.as_str())).collect();
let station = StationLocation::new(&fields).map_err(|error| TbkError::Location {
name: name.clone(),
error,
})?;
backup.locations.push(BackedUpLocation { name, station });
}
Ok(())
}
const MAX_SETTINGS: usize = 10_000;
fn read_settings(section: &Element, backup: &mut Backup) -> Result<(), TbkError> {
if section.children.len() > MAX_SETTINGS {
return Err(TbkError::TooMany("settings"));
}
let prefs = &mut backup.preferences;
for entry in §ion.children {
let (Some(name), Some(value)) = (entry.attribute("name"), entry.attribute("Value")) else {
return Err(malformed("setting"));
};
if entry.name != "Setting" {
return Err(malformed("setting"));
}
let group = entry
.attribute("group")
.map(|g| g.to_string())
.unwrap_or_else(|| "/".into());
let flag = |v: &str| v.trim().parse::<i64>().is_ok_and(|n| n != 0);
match (group.as_str(), name.as_str()) {
("/modeMap", adif) => {
let adif = adif.replace("!SLASH!", "/").to_ascii_uppercase();
let lotw = value.trim().to_ascii_uppercase();
let standard = lotw::lotw_mode(&adif, None).is_some_and(|m| m == adif);
match lotw::lotw_mode(&lotw, None).filter(|m| *m == lotw) {
Some(_) if !standard => prefs.adif_modes.push((adif, lotw)),
_ => backup.settings_not_taken.push(format!("/modeMap/{adif}")),
}
}
("/cabrilloMap", contest) => {
let (kind, field) = value.split_once(';').unwrap_or((&value, ""));
match (kind.trim().parse::<i64>(), field.trim().parse::<i64>()) {
(Ok(k @ (0 | 1)), Ok(f @ 6..=64)) => {
prefs
.cabrillo_contests
.push((contest.to_ascii_uppercase(), k, f));
}
_ => backup
.settings_not_taken
.push(format!("/cabrilloMap/{contest}")),
}
}
("/", "IgnoreSeconds") => prefs.ignore_seconds = flag(&value),
("/", "BadCalls") => prefs.allow_bad_calls = flag(&value),
("/", "IgnoreADIFCallsign") => prefs.ignore_log_callsigns = flag(&value),
("/", "LogVerify") => {
prefs.qth = match value.trim() {
"0" => QthHandling::Ignore,
"2" => QthHandling::FromLog,
_ => QthHandling::Refuse,
}
}
(group, name) => backup
.settings_not_taken
.push(format!("{}/{name}", group.trim_end_matches('/'))),
}
}
Ok(())
}
fn read_signed_qsos(section: &Element, backup: &mut Backup) -> Result<(), TbkError> {
for entry in §ion.children {
let key = entry.attribute("key").filter(|_| entry.name == "Dupe");
let Some(key) = key else {
return Err(malformed("signed QSO list"));
};
backup.signed_qsos.push(SignedQso {
key: key.to_string(),
station: entry
.attribute("data")
.map(|d| d.to_string())
.unwrap_or_default(),
});
}
Ok(())
}