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, params};
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 person.default_device = person.default_device.filter(|id| *id == d.id);
39 Ok(Json(person))
40}
41
42#[derive(Deserialize)]
43pub struct Range {
44 from: i64,
45 to: i64,
46}
47
48pub async fn track(
49 State(s): State<AppState>,
50 d: Device,
51 Query(q): Query<Range>,
52) -> Result<Json<Vec<Point>>> {
53 let db = s.db();
54 let a = own_access(&db, d.user_id)?;
55 Ok(Json(track_points(&db, &a, d.id, q.from, q.to)?))
56}
57
58const PAIR_SECS: i64 = 300;
59
60struct Pairing {
61 user_id: i64,
62 session_hash: Vec<u8>,
63 challenge: String,
64 name: String,
65 expires_at: i64,
66}
67
68/// Codes from the web UI that an app can exchange for a device token, once.
69#[derive(Default)]
70pub struct Pairings(Mutex<HashMap<String, Pairing>>);
71
72impl Pairings {
73 fn put(&self, user: &User, challenge: String, name: String) -> String {
74 let (code, _) = auth::new_secret();
75 let now = now();
76 let mut map = self.0.lock().unwrap();
77 map.retain(|_, p| p.expires_at > now);
78 let p = Pairing {
79 user_id: user.id,
80 session_hash: user.session_hash.clone(),
81 challenge,
82 name,
83 expires_at: now + PAIR_SECS,
84 };
85 map.insert(code.clone(), p);
86 code
87 }
88
89 /// The code is gone after one attempt, so a wrong verifier cannot be retried.
90 fn take(&self, code: &str, verifier: &str) -> Option<Pairing> {
91 let p = self.0.lock().unwrap().remove(code)?;
92 let challenge = auth::hex(&Sha256::digest(verifier.as_bytes()));
93 (p.expires_at > now() && challenge == p.challenge).then_some(p)
94 }
95}
96
97/// The app sends the SHA-256 of a secret it keeps. Only that app can then use the code.
98pub async fn pair_begin(
99 State(s): State<AppState>,
100 user: User,
101 Json(b): Json<PairBegin>,
102) -> Result<Json<PairCode>> {
103 let challenge = b.challenge.to_ascii_lowercase();
104 if challenge.len() != 64 || !challenge.bytes().all(|c| c.is_ascii_hexdigit()) {
105 return Err(Error::BadRequest(
106 "challenge must be a SHA-256 in hex".into(),
107 ));
108 }
109 let name = check_device_name(&b.name)?.to_owned();
110 Ok(Json(PairCode {
111 code: s.pairings.put(&user, challenge, name),
112 }))
113}
114
115pub async fn pair_finish(
116 State(s): State<AppState>,
117 Json(b): Json<PairFinish>,
118) -> Result<Json<DeviceToken>> {
119 let p = s
120 .pairings
121 .take(&b.code, &b.verifier)
122 .ok_or(Error::NotFound)?;
123 let db = s.db();
124 // A code lives only as long as the session that began it. Sign-outs and password resets end it too.
125 let live: bool = db.query_row(
126 "SELECT EXISTS (SELECT 1 FROM sessions WHERE token_hash = ?1 AND user_id = ?2 AND expires_at > ?3)",
127 params![p.session_hash, p.user_id, now()],
128 |r| r.get(0),
129 )?;
130 if !live {
131 return Err(Error::NotFound);
132 }
133 Ok(Json(insert_device(&db, p.user_id, &p.name)?))
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn a_code_needs_its_verifier_and_works_once() {
142 let p = Pairings::default();
143 let user = User {
144 id: 7,
145 username: "a".into(),
146 is_admin: false,
147 session_hash: vec![1],
148 signed_in_at: 0,
149 };
150 let challenge = auth::hex(&Sha256::digest(b"secret"));
151 let code = p.put(&user, challenge.clone(), "phone".into());
152 assert!(
153 p.take(&code, "secret")
154 .is_some_and(|p| p.user_id == 7 && p.name == "phone")
155 );
156 assert!(p.take(&code, "secret").is_none());
157 let code = p.put(&user, challenge, "phone".into());
158 assert!(p.take(&code, "guess").is_none());
159 assert!(p.take(&code, "secret").is_none());
160 }
161
162 #[tokio::test]
163 async fn a_code_dies_with_its_session() {
164 let s = crate::test_state();
165 let id = crate::insert_user(&s.db(), "a", "h", false).unwrap();
166 s.db()
167 .execute(
168 "INSERT INTO sessions (token_hash, user_id, expires_at) VALUES (x'01', ?1, 9999999999)",
169 [id],
170 )
171 .unwrap();
172 let user = User {
173 id,
174 username: "a".into(),
175 is_admin: false,
176 session_hash: vec![1],
177 signed_in_at: 0,
178 };
179 let pair = |code: String| {
180 pair_finish(
181 State(s.clone()),
182 Json(PairFinish {
183 code,
184 verifier: "secret".into(),
185 }),
186 )
187 };
188 let challenge = auth::hex(&Sha256::digest(b"secret"));
189 let code = s.pairings.put(&user, challenge.clone(), "phone".into());
190 assert!(pair(code).await.is_ok());
191 let code = s.pairings.put(&user, challenge, "phone".into());
192 crate::reset_password(&mut s.db(), id, "new").unwrap();
193 assert!(matches!(pair(code).await, Err(Error::NotFound)));
194 }
195}
196