//! What an app sees with its device token, and pairing an app with an account. use std::collections::HashMap; use std::sync::Mutex; use api::{DeviceToken, PairBegin, PairCode, PairFinish, Person, Point, Trail}; use axum::Json; use axum::extract::{Query, State}; use rusqlite::Connection; use serde::Deserialize; use sha2::{Digest, Sha256}; use crate::auth::{self, Device, User}; use crate::routes::{Access, check_device_name, insert_device, person_for, track_points}; use crate::{AppState, Error, now}; type Result = std::result::Result; fn own_access(db: &Connection, user_id: i64) -> Result { let username = db.query_row("SELECT username FROM users WHERE id = ?1", [user_id], |r| { r.get(0) })?; Ok(Access { owner: user_id, username, share: None, all_devices: true, trail: Trail::All, precision_m: 0, }) } /// The device's owner, with only this device. pub async fn me(State(s): State, d: Device) -> Result> { let db = s.db(); let mut person = person_for(&db, own_access(&db, d.user_id)?)?; person.devices.retain(|x| x.id == d.id); Ok(Json(person)) } #[derive(Deserialize)] pub struct Range { from: i64, to: i64, } pub async fn track( State(s): State, d: Device, Query(q): Query, ) -> Result>> { let db = s.db(); let a = own_access(&db, d.user_id)?; Ok(Json(track_points(&db, &a, d.id, q.from, q.to)?)) } const PAIR_SECS: i64 = 300; struct Pairing { user_id: i64, challenge: String, name: String, expires_at: i64, } /// Codes from the web UI that an app can exchange for a device token, once. #[derive(Default)] pub struct Pairings(Mutex>); impl Pairings { fn put(&self, user_id: i64, challenge: String, name: String) -> String { let (code, _) = auth::new_secret(); let now = now(); let mut map = self.0.lock().unwrap(); map.retain(|_, p| p.expires_at > now); let p = Pairing { user_id, challenge, name, expires_at: now + PAIR_SECS, }; map.insert(code.clone(), p); code } /// The code is gone after one attempt, so a wrong verifier cannot be retried. fn take(&self, code: &str, verifier: &str) -> Option<(i64, String)> { let p = self.0.lock().unwrap().remove(code)?; let challenge = hex::encode(Sha256::digest(verifier.as_bytes())); (p.expires_at > now() && challenge == p.challenge).then_some((p.user_id, p.name)) } } /// The app sends the SHA-256 of a secret it keeps. Only that app can then use the code. pub async fn pair_begin( State(s): State, user: User, Json(b): Json, ) -> Result> { let challenge = b.challenge.to_ascii_lowercase(); if challenge.len() != 64 || !challenge.bytes().all(|c| c.is_ascii_hexdigit()) { return Err(Error::BadRequest( "challenge must be a SHA-256 in hex".into(), )); } let name = check_device_name(&b.name)?.to_owned(); Ok(Json(PairCode { code: s.pairings.put(user.id, challenge, name), })) } pub async fn pair_finish( State(s): State, Json(b): Json, ) -> Result> { let (user_id, name) = s .pairings .take(&b.code, &b.verifier) .ok_or(Error::NotFound)?; Ok(Json(insert_device(&s.db(), user_id, &name)?)) } #[cfg(test)] mod tests { use super::*; #[test] fn a_code_needs_its_verifier_and_works_once() { let p = Pairings::default(); let challenge = hex::encode(Sha256::digest(b"secret")); let code = p.put(7, challenge.clone(), "phone".into()); assert_eq!(p.take(&code, "secret"), Some((7, "phone".into()))); assert_eq!(p.take(&code, "secret"), None); let code = p.put(7, challenge, "phone".into()); assert_eq!(p.take(&code, "guess"), None); assert_eq!(p.take(&code, "secret"), None); } }