1use std::time::{SystemTime, UNIX_EPOCH};
28
29use base64::Engine as _;
30use parking_lot::Mutex;
31use ring::rand::SecureRandom as _;
32use ring::signature::{ED25519, Ed25519KeyPair, KeyPair as _, UnparsedPublicKey};
33use serde::{Deserialize, Serialize};
34
35use crate::config::Config;
36use crate::remote::link::LinkDeviceKey;
37
38const B64: base64::engine::GeneralPurpose = base64::engine::general_purpose::STANDARD;
39
40const KEYS_TRUSTED_FOR: i64 = 30 * 24 * 60 * 60;
43
44const DIAL: &str = "koan-nearby-v1 dial";
45const LISTEN: &str = "koan-nearby-v1 listen";
46const SESSION: &str = "koan-nearby-v1 session";
47const COMMAND: &str = "koan-nearby-v1 command";
48const REPORT: &str = "koan-nearby-v1 report";
49
50const SIGNS_REPORTS: &str = "reports.";
53
54pub fn new_device_key() -> Option<String> {
56 let rng = ring::rand::SystemRandom::new();
57 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).ok()?;
58 Some(B64.encode(pkcs8.as_ref()))
59}
60
61fn keypair_from(kept: &str) -> Option<Ed25519KeyPair> {
62 let pkcs8 = B64.decode(kept).ok()?;
63 Ed25519KeyPair::from_pkcs8(&pkcs8).ok()
64}
65
66fn keypair() -> Option<Ed25519KeyPair> {
69 let cfg = Config::cached();
70 if !cfg.remote.enabled || cfg.remote.api_key.is_empty() {
71 return None;
72 }
73 if let Some(pair) = keypair_from(&cfg.remote.device_key) {
74 return Some(pair);
75 }
76 let made = new_device_key()?;
77 if let Err(e) = Config::persist(|c| c.remote.device_key = made.clone()) {
78 log::warn!("proof: could not keep a device key: {e}");
79 return None;
80 }
81 keypair_from(&made)
82}
83
84pub fn public_key() -> Option<String> {
86 keypair().map(|pair| B64.encode(pair.public_key().as_ref()))
87}
88
89pub fn nonce() -> Option<String> {
94 let mut bytes = [0u8; 32];
95 ring::rand::SystemRandom::new().fill(&mut bytes).ok()?;
96 Some(format!("{SIGNS_REPORTS}{}", B64.encode(bytes)))
97}
98
99pub fn signs_reports(listen_nonce: &str, dial_nonce: &str) -> bool {
101 listen_nonce.starts_with(SIGNS_REPORTS) && dial_nonce.starts_with(SIGNS_REPORTS)
102}
103
104fn message(label: &str, fields: &[&[u8]]) -> Vec<u8> {
107 let mut out = Vec::new();
108 for field in std::iter::once(label.as_bytes()).chain(fields.iter().copied()) {
109 out.extend_from_slice(&(field.len() as u32).to_be_bytes());
110 out.extend_from_slice(field);
111 }
112 out
113}
114
115fn dial_message(listener: &str, dialer: &str, listen_nonce: &str, dial_nonce: &str) -> Vec<u8> {
117 message(
118 DIAL,
119 &[
120 listener.as_bytes(),
121 dialer.as_bytes(),
122 listen_nonce.as_bytes(),
123 dial_nonce.as_bytes(),
124 ],
125 )
126}
127
128fn listen_message(
131 dialer: &str,
132 listener: &str,
133 dial_nonce: &str,
134 listen_nonce: &str,
135 verified: bool,
136) -> Vec<u8> {
137 message(
138 LISTEN,
139 &[
140 dialer.as_bytes(),
141 listener.as_bytes(),
142 dial_nonce.as_bytes(),
143 listen_nonce.as_bytes(),
144 &[verified as u8],
145 ],
146 )
147}
148
149fn sign(pair: &Ed25519KeyPair, msg: &[u8]) -> String {
150 B64.encode(pair.sign(msg).as_ref())
151}
152
153pub fn sign_dial(
155 listener: &str,
156 dialer: &str,
157 listen_nonce: &str,
158 dial_nonce: &str,
159) -> Option<String> {
160 let pair = keypair()?;
161 Some(sign(
162 &pair,
163 &dial_message(listener, dialer, listen_nonce, dial_nonce),
164 ))
165}
166
167pub fn sign_listen(
169 dialer: &str,
170 listener: &str,
171 dial_nonce: &str,
172 listen_nonce: &str,
173 verified: bool,
174) -> Option<String> {
175 let pair = keypair()?;
176 Some(sign(
177 &pair,
178 &listen_message(dialer, listener, dial_nonce, listen_nonce, verified),
179 ))
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
184pub enum Peer {
185 Own,
187 Shared(String),
189}
190
191#[derive(Debug, Clone)]
194pub struct Proven {
195 pub peer: Peer,
196 key: Vec<u8>,
197}
198
199fn verify_with(keys: &[LinkDeviceKey], id: &str, msg: &[u8], sig: &str) -> Option<Proven> {
204 let sig = B64.decode(sig).ok()?;
205 keys.iter().filter(|k| k.id == id).find_map(|k| {
206 let key = B64.decode(&k.key).ok()?;
207 UnparsedPublicKey::new(&ED25519, &key)
208 .verify(msg, &sig)
209 .ok()?;
210 Some(Proven {
211 peer: match &k.owner {
212 None => Peer::Own,
213 Some(owner) => Peer::Shared(owner.clone()),
214 },
215 key,
216 })
217 })
218}
219
220pub fn verify_dial(
222 listener: &str,
223 dialer: &str,
224 listen_nonce: &str,
225 dial_nonce: &str,
226 sig: &str,
227) -> Option<Proven> {
228 verify_with(
229 &kept(),
230 dialer,
231 &dial_message(listener, dialer, listen_nonce, dial_nonce),
232 sig,
233 )
234}
235
236pub fn verify_listen(
238 dialer: &str,
239 listener: &str,
240 dial_nonce: &str,
241 listen_nonce: &str,
242 verified: bool,
243 sig: &str,
244) -> Option<Proven> {
245 verify_with(
246 &kept(),
247 listener,
248 &listen_message(dialer, listener, dial_nonce, listen_nonce, verified),
249 sig,
250 )
251}
252
253pub struct Session {
256 transcript: [u8; 32],
257 label: &'static str,
259 seq: u64,
262}
263
264impl Session {
265 pub fn new(listener: &str, dialer: &str, listen_nonce: &str, dial_nonce: &str) -> Self {
268 Self::of(COMMAND, listener, dialer, listen_nonce, dial_nonce)
269 }
270
271 pub fn reports(listener: &str, dialer: &str, listen_nonce: &str, dial_nonce: &str) -> Self {
273 Self::of(REPORT, listener, dialer, listen_nonce, dial_nonce)
274 }
275
276 fn of(
277 label: &'static str,
278 listener: &str,
279 dialer: &str,
280 listen_nonce: &str,
281 dial_nonce: &str,
282 ) -> Self {
283 let digest = ring::digest::digest(
284 &ring::digest::SHA256,
285 &message(
286 SESSION,
287 &[
288 listener.as_bytes(),
289 dialer.as_bytes(),
290 listen_nonce.as_bytes(),
291 dial_nonce.as_bytes(),
292 ],
293 ),
294 );
295 let mut transcript = [0u8; 32];
296 transcript.copy_from_slice(digest.as_ref());
297 Self {
298 transcript,
299 label,
300 seq: 0,
301 }
302 }
303
304 fn signed_message(&self, seq: u64, text: &str) -> Vec<u8> {
305 message(
306 self.label,
307 &[&self.transcript, &seq.to_be_bytes(), text.as_bytes()],
308 )
309 }
310
311 pub fn sign(&mut self, text: &str) -> Option<(u64, String)> {
314 let pair = keypair()?;
315 self.sign_with(&pair, text)
316 }
317
318 fn sign_with(&mut self, pair: &Ed25519KeyPair, text: &str) -> Option<(u64, String)> {
319 self.seq += 1;
320 Some((self.seq, sign(pair, &self.signed_message(self.seq, text))))
321 }
322
323 pub fn accept(&mut self, by: &Proven, seq: u64, sig: &str, text: &str) -> bool {
326 if seq <= self.seq {
327 return false;
328 }
329 let Ok(sig) = B64.decode(sig) else {
330 return false;
331 };
332 let ok = UnparsedPublicKey::new(&ED25519, &by.key)
333 .verify(&self.signed_message(seq, text), &sig)
334 .is_ok();
335 if ok {
336 self.seq = seq;
337 }
338 ok
339 }
340}
341
342#[derive(Debug, Clone, Default, Serialize, Deserialize)]
347struct Kept {
348 account: Option<String>,
352 at: i64,
353 keys: Vec<LinkDeviceKey>,
354}
355
356pub fn account_of(cfg: &Config) -> Option<String> {
360 let server = crate::remote::link::library_fingerprint(cfg)?;
361 Some(format!("{server}/{}", cfg.remote.username))
362}
363
364static KEPT: Mutex<Option<Kept>> = Mutex::new(None);
365
366fn kept_path() -> std::path::PathBuf {
367 crate::config::config_dir().join("device-keys.json")
368}
369
370fn now() -> i64 {
371 SystemTime::now()
372 .duration_since(UNIX_EPOCH)
373 .map(|d| d.as_secs() as i64)
374 .unwrap_or_default()
375}
376
377pub fn keep(keys: Vec<LinkDeviceKey>, account: Option<String>) {
383 let kept = Kept {
384 account,
385 at: now(),
386 keys,
387 };
388 if let Ok(json) = serde_json::to_string(&kept) {
389 let _ = std::fs::write(kept_path(), json);
390 }
391 *KEPT.lock() = Some(kept);
392}
393
394fn kept() -> Vec<LinkDeviceKey> {
397 let mut held = KEPT.lock();
398 if held.is_none() {
399 *held = std::fs::read_to_string(kept_path())
400 .ok()
401 .and_then(|text| serde_json::from_str(&text).ok());
402 }
403 let Some(kept) = held.as_ref() else {
404 return Vec::new();
405 };
406 let account = account_of(&Config::cached());
407 trusted(kept, account.as_deref(), now())
408}
409
410fn trusted(kept: &Kept, account: Option<&str>, now: i64) -> Vec<LinkDeviceKey> {
411 if account.is_none() || kept.account.as_deref() != account || now - kept.at > KEYS_TRUSTED_FOR {
412 return Vec::new();
413 }
414 kept.keys.clone()
415}
416
417pub fn forget() {
420 *KEPT.lock() = Some(Kept::default());
421 let _ = std::fs::remove_file(kept_path());
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427
428 fn pair() -> (Ed25519KeyPair, String) {
429 let made = new_device_key().unwrap();
430 let pair = keypair_from(&made).unwrap();
431 let public = B64.encode(pair.public_key().as_ref());
432 (pair, public)
433 }
434
435 fn key(id: &str, public: &str, owner: Option<&str>) -> LinkDeviceKey {
436 LinkDeviceKey {
437 id: id.into(),
438 key: public.into(),
439 owner: owner.map(Into::into),
440 }
441 }
442
443 #[test]
444 fn a_dialler_proves_itself_with_its_own_key_only() {
445 let (phone, phone_pub) = pair();
446 let (thief, thief_pub) = pair();
447 let keys = vec![key("phone", &phone_pub, None)];
448 let msg = dial_message("mac", "phone", "nl", "nd");
449 let proven = verify_with(&keys, "phone", &msg, &sign(&phone, &msg)).unwrap();
450 assert_eq!(proven.peer, Peer::Own);
451 assert!(verify_with(&keys, "phone", &msg, &sign(&thief, &msg)).is_none());
453 assert!(
454 verify_with(
455 &[key("x", &thief_pub, None)],
456 "phone",
457 &msg,
458 &sign(&thief, &msg)
459 )
460 .is_none()
461 );
462 assert!(verify_with(&keys, "phone", &msg, "AAAA").is_none());
463 assert!(verify_with(&keys, "phone", &msg, "not base64!").is_none());
464 }
465
466 #[test]
467 fn a_recorded_handshake_fails_against_a_fresh_nonce() {
468 let (phone, phone_pub) = pair();
469 let keys = vec![key("phone", &phone_pub, None)];
470 let recorded = sign(&phone, &dial_message("mac", "phone", "old", "nd"));
471 let fresh = dial_message("mac", "phone", "new", "nd");
472 assert!(verify_with(&keys, "phone", &fresh, &recorded).is_none());
473 let elsewhere = dial_message("tv", "phone", "old", "nd");
475 assert!(verify_with(&keys, "phone", &elsewhere, &recorded).is_none());
476 }
477
478 #[test]
479 fn one_ends_signature_cannot_pass_as_the_others() {
480 let (mac, mac_pub) = pair();
481 let keys = vec![key("mac", &mac_pub, None)];
482 let listened = sign(&mac, &listen_message("phone", "mac", "nd", "nl", true));
485 let as_dial = dial_message("phone", "mac", "nd", "nl");
486 assert!(verify_with(&keys, "mac", &as_dial, &listened).is_none());
487 }
488
489 #[test]
492 fn a_key_with_an_owner_never_proves_an_own_device() {
493 let (own, own_pub) = pair();
494 let (theirs, theirs_pub) = pair();
495 let keys = vec![
496 key("phone", &own_pub, None),
497 key("phone", &theirs_pub, Some("kim")),
498 ];
499 let msg = dial_message("mac", "phone", "nl", "nd");
500 assert_eq!(
501 verify_with(&keys, "phone", &msg, &sign(&theirs, &msg))
502 .unwrap()
503 .peer,
504 Peer::Shared("kim".into())
505 );
506 assert_eq!(
507 verify_with(&keys, "phone", &msg, &sign(&own, &msg))
508 .unwrap()
509 .peer,
510 Peer::Own
511 );
512 }
513
514 #[test]
517 fn keys_from_the_last_accounts_link_prove_nothing_for_the_next() {
518 let _guard = crate::config::tests::PERSIST_LOCK
519 .lock()
520 .unwrap_or_else(|e| e.into_inner());
521 let dir = tempfile::tempdir().unwrap();
522 crate::config::set_config_dir(dir.path());
523 let sign_in = |user: &str| {
524 Config::persist(|c| {
525 c.remote.enabled = true;
526 c.remote.url = "http://koan.test".into();
527 c.remote.username = user.into();
528 c.remote.api_key = "k".into();
529 })
530 .unwrap();
531 };
532 sign_in("jo");
533 let jo = account_of(&Config::cached());
534 sign_in("kim");
535 keep(vec![key("phone", "K", None)], jo);
536 assert!(kept().is_empty(), "jo's link, kim signed in");
537 keep(vec![key("phone", "K", None)], account_of(&Config::cached()));
538 assert_eq!(kept().len(), 1, "kim's own link");
539 forget();
540 assert!(kept().is_empty());
541 }
542
543 #[test]
544 fn signed_commands_are_bound_to_their_session_and_order() {
545 let (phone, phone_pub) = pair();
546 let keys = vec![key("phone", &phone_pub, None)];
547 let msg = dial_message("mac", "phone", "nl", "nd");
548 let proven = verify_with(&keys, "phone", &msg, &sign(&phone, &msg)).unwrap();
549
550 let mut sending = Session::new("mac", "phone", "nl", "nd");
551 let mut receiving = Session::new("mac", "phone", "nl", "nd");
552 let (s1, sig1) = sending.sign_with(&phone, r#"{"type":"pause"}"#).unwrap();
553 let (s2, sig2) = sending.sign_with(&phone, r#"{"type":"next"}"#).unwrap();
554
555 assert!(
556 !receiving.accept(&proven, s1, &sig1, r#"{"type":"sync"}"#),
557 "altered"
558 );
559 assert!(receiving.accept(&proven, s1, &sig1, r#"{"type":"pause"}"#));
560 assert!(
561 !receiving.accept(&proven, s1, &sig1, r#"{"type":"pause"}"#),
562 "replayed"
563 );
564 assert!(receiving.accept(&proven, s2, &sig2, r#"{"type":"next"}"#));
565 assert!(
566 !receiving.accept(&proven, s1, &sig1, r#"{"type":"pause"}"#),
567 "out of order"
568 );
569
570 let mut other = Session::new("mac", "phone", "nl2", "nd");
572 let (s, sig) = other.sign_with(&phone, r#"{"type":"pause"}"#).unwrap();
573 let mut fresh = Session::new("mac", "phone", "nl", "nd");
574 assert!(!fresh.accept(&proven, s, &sig, r#"{"type":"pause"}"#));
575 }
576
577 #[test]
578 fn a_signed_command_is_never_taken_for_a_report() {
579 let (mac, mac_pub) = pair();
580 let keys = vec![key("mac", &mac_pub, None)];
581 let msg = listen_message("phone", "mac", "nd", "nl", true);
582 let proven = verify_with(&keys, "mac", &msg, &sign(&mac, &msg)).unwrap();
583 let text = r#"{"type":"state"}"#;
584 let (seq, sig) = Session::new("mac", "phone", "nl", "nd")
585 .sign_with(&mac, text)
586 .unwrap();
587 assert!(!Session::reports("mac", "phone", "nl", "nd").accept(&proven, seq, &sig, text));
588 let (seq, sig) = Session::reports("mac", "phone", "nl", "nd")
589 .sign_with(&mac, text)
590 .unwrap();
591 assert!(Session::reports("mac", "phone", "nl", "nd").accept(&proven, seq, &sig, text));
592 }
593
594 #[test]
595 fn reports_are_signed_only_when_both_nonces_say_so() {
596 let (a, b) = (nonce().unwrap(), nonce().unwrap());
597 assert!(signs_reports(&a, &b));
598 let older = B64.encode([7u8; 32]);
600 assert!(!signs_reports(&a, &older));
601 assert!(!signs_reports(&older, &b));
602 }
603
604 #[test]
605 fn kept_keys_hold_for_their_account_and_a_month() {
606 let cfg = |url: &str, user: &str| {
607 let mut c = Config::default();
608 c.remote.enabled = true;
609 c.remote.url = url.into();
610 c.remote.username = user.into();
611 c.remote.api_key = "k".into();
612 account_of(&c)
613 };
614 let jo = cfg("http://koan.test", "jo");
615 let kept = Kept {
616 account: jo.clone(),
617 at: 1_000_000,
618 keys: vec![key("phone", "K", None)],
619 };
620 assert_eq!(trusted(&kept, jo.as_deref(), 1_000_000 + 60).len(), 1);
621 assert_eq!(
622 trusted(&kept, cfg("http://KOAN.test/", "jo").as_deref(), 1_000_000).len(),
623 1,
624 "the same server, spelt differently"
625 );
626 assert!(
627 trusted(&kept, cfg("http://koan.test", "Jo").as_deref(), 1_000_000).is_empty(),
628 "usernames are matched exactly, as the server matches them"
629 );
630 let kim = cfg("http://koan.test", "kim");
631 assert!(
632 trusted(&kept, kim.as_deref(), 1_000_000).is_empty(),
633 "another account"
634 );
635 let elsewhere = cfg("http://other.test", "jo");
636 assert!(
637 trusted(&kept, elsewhere.as_deref(), 1_000_000).is_empty(),
638 "another server"
639 );
640 assert!(trusted(&kept, None, 1_000_000).is_empty(), "signed out");
641 assert!(
642 trusted(&kept, jo.as_deref(), 1_000_000 + KEYS_TRUSTED_FOR + 1).is_empty(),
643 "too old"
644 );
645 }
646}