device.rs
⎇
Raw
1//! What an app sees with its device token, and pairing an app with an account.
2
3use std::collections::HashMap;
4use std::sync::Mutex;
5
6use api::{DeviceToken, PairBegin, PairCode, PairFinish, Person, Point};
7use axum::Json;
8use axum::extract::{Query, State};
9use rusqlite::Connection;
10use serde::Deserialize;
11use sha2::{Digest, Sha256};
12
13use crate::auth::{self, Device, User};
14use crate::routes::{Access, check_device_name, insert_device, person_for, track_points};
15use crate::{AppState, Error, now};
16
17type Result<T> = std::result::Result<T, Error>;
18
19fn 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_since: Some(0),
29 precision_m: 0,
30 })
31}
32
33/// The device's owner, with only this device.
34pub 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)]
42pub struct Range {
43 from: i64,
44 to: i64,
45}
46
47pub 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
57const PAIR_SECS: i64 = 300;
58
59struct 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)]
68pub struct Pairings(Mutex<HashMap<String, Pairing>>);
69
70impl 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.
95pub 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
112pub 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)]
124mod 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