device.rs
| 1 | //! What an app sees with its device token, and pairing an app with an account. |
| 2 | |
| 3 | use std::collections::HashMap; |
| 4 | use std::sync::Mutex; |
| 5 | |
| 6 | use api::{DeviceToken, PairBegin, PairCode, PairFinish, Person, Point, Trail}; |
| 7 | use axum::Json; |
| 8 | use axum::extract::{Query, State}; |
| 9 | use rusqlite::Connection; |
| 10 | use serde::Deserialize; |
| 11 | use sha2::{Digest, Sha256}; |
| 12 | |
| 13 | use crate::auth::{self, Device, User}; |
| 14 | use crate::routes::{Access, check_device_name, insert_device, person_for, track_points}; |
| 15 | use crate::{AppState, Error, now}; |
| 16 | |
| 17 | type Result<T> = std::result::Result<T, Error>; |
| 18 | |
| 19 | fn own_access(db: &Connection, user_id: i64) -> Result<Access> { |
| 20 | let username = db.query_row("SELECT username FROM users WHERE id = ?1", [user_id], |r| { |
| 21 | r.get(0) |
| 22 | })?; |
| 23 | Ok(Access { |
| 24 | owner: user_id, |
| 25 | username, |
| 26 | share: None, |
| 27 | all_devices: true, |
| 28 | trail: Trail::All, |
| 29 | precision_m: 0, |
| 30 | }) |
| 31 | } |
| 32 | |
| 33 | /// The device's owner, with only this device. |
| 34 | pub async fn me(State(s): State<AppState>, d: Device) -> Result<Json<Person>> { |
| 35 | let db = s.db(); |
| 36 | let mut person = person_for(&db, own_access(&db, d.user_id)?)?; |
| 37 | person.devices.retain(|x| x.id == d.id); |
| 38 | Ok(Json(person)) |
| 39 | } |
| 40 | |
| 41 | #[derive(Deserialize)] |
| 42 | pub struct Range { |
| 43 | from: i64, |
| 44 | to: i64, |
| 45 | } |
| 46 | |
| 47 | pub async fn track( |
| 48 | State(s): State<AppState>, |
| 49 | d: Device, |
| 50 | Query(q): Query<Range>, |
| 51 | ) -> Result<Json<Vec<Point>>> { |
| 52 | let db = s.db(); |
| 53 | let a = own_access(&db, d.user_id)?; |
| 54 | Ok(Json(track_points(&db, &a, d.id, q.from, q.to)?)) |
| 55 | } |
| 56 | |
| 57 | const PAIR_SECS: i64 = 300; |
| 58 | |
| 59 | struct Pairing { |
| 60 | user_id: i64, |
| 61 | challenge: String, |
| 62 | name: String, |
| 63 | expires_at: i64, |
| 64 | } |
| 65 | |
| 66 | /// Codes from the web UI that an app can exchange for a device token, once. |
| 67 | #[derive(Default)] |
| 68 | pub struct Pairings(Mutex<HashMap<String, Pairing>>); |
| 69 | |
| 70 | impl Pairings { |
| 71 | fn put(&self, user_id: i64, challenge: String, name: String) -> String { |
| 72 | let (code, _) = auth::new_secret(); |
| 73 | let now = now(); |
| 74 | let mut map = self.0.lock().unwrap(); |
| 75 | map.retain(|_, p| p.expires_at > now); |
| 76 | let p = Pairing { |
| 77 | user_id, |
| 78 | challenge, |
| 79 | name, |
| 80 | expires_at: now + PAIR_SECS, |
| 81 | }; |
| 82 | map.insert(code.clone(), p); |
| 83 | code |
| 84 | } |
| 85 | |
| 86 | /// The code is gone after one attempt, so a wrong verifier cannot be retried. |
| 87 | fn take(&self, code: &str, verifier: &str) -> Option<(i64, String)> { |
| 88 | let p = self.0.lock().unwrap().remove(code)?; |
| 89 | let challenge = hex::encode(Sha256::digest(verifier.as_bytes())); |
| 90 | (p.expires_at > now() && challenge == p.challenge).then_some((p.user_id, p.name)) |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | /// The app sends the SHA-256 of a secret it keeps. Only that app can then use the code. |
| 95 | pub async fn pair_begin( |
| 96 | State(s): State<AppState>, |
| 97 | user: User, |
| 98 | Json(b): Json<PairBegin>, |
| 99 | ) -> Result<Json<PairCode>> { |
| 100 | let challenge = b.challenge.to_ascii_lowercase(); |
| 101 | if challenge.len() != 64 || !challenge.bytes().all(|c| c.is_ascii_hexdigit()) { |
| 102 | return Err(Error::BadRequest( |
| 103 | "challenge must be a SHA-256 in hex".into(), |
| 104 | )); |
| 105 | } |
| 106 | let name = check_device_name(&b.name)?.to_owned(); |
| 107 | Ok(Json(PairCode { |
| 108 | code: s.pairings.put(user.id, challenge, name), |
| 109 | })) |
| 110 | } |
| 111 | |
| 112 | pub async fn pair_finish( |
| 113 | State(s): State<AppState>, |
| 114 | Json(b): Json<PairFinish>, |
| 115 | ) -> Result<Json<DeviceToken>> { |
| 116 | let (user_id, name) = s |
| 117 | .pairings |
| 118 | .take(&b.code, &b.verifier) |
| 119 | .ok_or(Error::NotFound)?; |
| 120 | Ok(Json(insert_device(&s.db(), user_id, &name)?)) |
| 121 | } |
| 122 | |
| 123 | #[cfg(test)] |
| 124 | mod tests { |
| 125 | use super::*; |
| 126 | |
| 127 | #[test] |
| 128 | fn a_code_needs_its_verifier_and_works_once() { |
| 129 | let p = Pairings::default(); |
| 130 | let challenge = hex::encode(Sha256::digest(b"secret")); |
| 131 | let code = p.put(7, challenge.clone(), "phone".into()); |
| 132 | assert_eq!(p.take(&code, "secret"), Some((7, "phone".into()))); |
| 133 | assert_eq!(p.take(&code, "secret"), None); |
| 134 | let code = p.put(7, challenge, "phone".into()); |
| 135 | assert_eq!(p.take(&code, "guess"), None); |
| 136 | assert_eq!(p.take(&code, "secret"), None); |
| 137 | } |
| 138 | } |
| 139 |