use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use koan_relay_client::{Assertion, Client, Error, Registered, Slot, client_data};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub const RELAY: &str = koan_relay_client::DEFAULT_URL;
pub trait Attester: Send + Sync {
fn attest(&self, client_data_hash: Vec<u8>) -> Option<(Vec<u8>, Vec<u8>)>;
fn assert(&self, key_id: Vec<u8>, client_data_hash: Vec<u8>) -> Option<Vec<u8>>;
}
static ATTESTER: Mutex<Option<Arc<dyn Attester>>> = Mutex::new(None);
pub fn set_attester(attester: Arc<dyn Attester>) {
*ATTESTER.lock() = Some(attester);
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct Registration {
slot: String,
token: String,
sandbox: bool,
handle: String,
revoker: String,
expires: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Key {
key_id: String,
sealed: String,
production: bool,
expires: u64,
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct Store {
servers: HashMap<String, Registration>,
key: Option<Key>,
#[serde(default)]
revoking: Vec<String>,
}
fn path() -> PathBuf {
crate::config::config_dir().join("push-relay.json")
}
fn load() -> Store {
std::fs::read(path())
.ok()
.and_then(|b| serde_json::from_slice(&b).ok())
.unwrap_or_default()
}
fn save(store: &Store) {
let Ok(json) = serde_json::to_vec_pretty(store) else {
return;
};
if let Err(e) = crate::config::write_private(&path(), &json) {
log::warn!("push relay: saving registrations: {e}");
}
}
static WORKING: Mutex<()> = Mutex::new(());
fn now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
}
fn sha256(data: &[u8]) -> Vec<u8> {
Sha256::digest(data).to_vec()
}
fn slot_of(hex_slot: &str) -> Option<Slot> {
let bytes = hex::decode(hex_slot).ok()?;
bytes.try_into().ok()
}
pub fn handle_for(server: &str, token: &str, sandbox: bool) -> Option<String> {
let _working = WORKING.lock();
let mut store = load();
if !settle_revocations(&mut store) {
save(&store);
return None;
}
let entry = store.servers.entry(server.to_string()).or_default();
if entry.slot.is_empty() {
let mut slot = [0u8; 16];
getrandom::fill(&mut slot).ok()?;
entry.slot = hex::encode(slot);
}
let fresh = entry.token == token
&& entry.sandbox == sandbox
&& !entry.handle.is_empty()
&& entry.expires > now() + 30 * 24 * 60 * 60;
if fresh {
return Some(entry.handle.clone());
}
let slot = slot_of(&entry.slot)?;
let token_bytes = hex::decode(token).ok()?;
let client = Client::new(RELAY).ok()?;
let registered = register(&client, &mut store.key, &token_bytes, &slot, sandbox);
save(&store);
let registered = match registered? {
Ok(r) => r,
Err(e) => {
log::info!("push relay: registering: {e}");
return None;
}
};
let entry = store.servers.entry(server.to_string()).or_default();
entry.token = token.to_string();
entry.sandbox = sandbox;
entry.handle = registered.handle.clone();
entry.revoker = registered.revoker;
entry.expires = registered.expires;
save(&store);
Some(registered.handle)
}
fn register(
client: &Client,
held: &mut Option<Key>,
token: &[u8],
slot: &Slot,
sandbox: bool,
) -> Option<Result<Registered, Error>> {
let attested = (|| {
let attester = ATTESTER.lock().clone()?;
let key = attested_key(client, &attester, held, sandbox)?;
let challenge = client.challenge().ok()?;
let data = client_data(&challenge, token, slot, sandbox);
let assertion = attester.assert(hex::decode(&key.key_id).ok()?, sha256(&data))?;
Some(client.register_attested(
token,
slot,
sandbox,
&Assertion {
key: &key.sealed,
challenge: &challenge,
assertion: &assertion,
},
))
})();
if let Some(Err(Error::Bad(why))) = &attested {
log::info!("push relay: the attested key was refused: {why}");
*held = None;
}
match attested {
Some(registered) => Some(registered),
None if sandbox => Some(client.register(token, slot, true)),
None => {
log::warn!("push relay: App Attest is unavailable, so this device cannot register");
None
}
}
}
pub fn server_of_slot(slot: &str) -> Option<String> {
load()
.servers
.into_iter()
.find(|(_, r)| r.slot.eq_ignore_ascii_case(slot))
.map(|(server, _)| server)
}
fn attested_key(
client: &Client,
attester: &Arc<dyn Attester>,
held: &mut Option<Key>,
sandbox: bool,
) -> Option<Key> {
if let Some(key) = held.as_ref()
&& key.production == !sandbox
&& key.expires > now() + 24 * 60 * 60
{
return Some(key.clone());
}
let challenge = client.challenge().ok()?;
let (key_id, attestation) = attester.attest(sha256(challenge.as_bytes()))?;
let mut verified = client.attest(&key_id, &attestation, &challenge);
for wait in [2, 10, 30] {
match &verified {
Err(Error::Transport(_) | Error::Unavailable(_) | Error::Limited(_)) => {
std::thread::sleep(std::time::Duration::from_secs(wait));
verified = client.attest(&key_id, &attestation, &challenge);
}
_ => break,
}
}
match verified {
Ok(attested) => {
let key = Key {
key_id: hex::encode(key_id),
sealed: attested.key,
production: attested.production,
expires: attested.expires,
};
*held = Some(key.clone());
Some(key)
}
Err(e) => {
log::info!("push relay: attesting: {e}");
None
}
}
}
pub fn activity_handle(server: &str, token: &str) -> Option<String> {
let _working = WORKING.lock();
let store = load();
let entry = store.servers.get(server)?;
let slot = slot_of(&entry.slot)?;
let token_bytes = hex::decode(token).ok()?;
match Client::new(RELAY)
.ok()?
.register_activity(&entry.revoker, &token_bytes, &slot)
{
Ok(a) => Some(a.handle),
Err(e) => {
log::info!("push relay: registering a Live Activity: {e}");
None
}
}
}
pub fn revoke(server: &str) {
let _working = WORKING.lock();
let mut store = load();
let Some(entry) = store.servers.remove(server) else {
return;
};
if !entry.revoker.is_empty() {
store.revoking.push(entry.revoker);
}
for other in store.servers.values_mut() {
other.handle.clear();
other.revoker.clear();
}
save(&store);
settle_revocations(&mut store);
save(&store);
}
fn settle_revocations(store: &mut Store) -> bool {
if store.revoking.is_empty() {
return true;
}
let Ok(client) = Client::new(RELAY) else {
return false;
};
store
.revoking
.retain(|revoker| match client.revoke(revoker) {
Ok(()) | Err(Error::Gone) => false,
Err(e) => {
log::info!("push relay: revoking: {e}");
true
}
});
store.revoking.is_empty()
}