1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
//! The person at the keyboard, on a door with no lys block (ACCESS-003 R2).
//!
//! A loopback address proves nothing: any process on the machine can reach
//! it. So the door makes a key of 32 random bytes when it starts and prints
//! one line to its own standard output, the origin's `/local` with the key
//! as its query. Whoever opens that link is at the door's terminal. The key
//! is held only in this process's memory, written nowhere else, compared in
//! constant time, and spent by its first use: a second use, a wrong key and
//! no key are refused in the same words. The session it opens names the
//! local owner of the door's process, has no expiry while the door runs and
//! ends when the door stops or the person signs out.
use std::collections::BTreeSet;
use std::sync::Mutex;
use lys_pass::Error;
use serde_json::{Value, json};
use super::session::{SignedIn, Started, lock, session_id};
use super::signin::Issuer;
use crate::serve::contracts::Caller;
use crate::serve::storage::Failure;
/// The one refusal of `/local`, the same for a spent, a wrong and a missing
/// key, so a refusal says nothing about the key.
pub(super) fn refused() -> Failure {
Failure::new(
"door_local_key_refused",
"this link does not open Studio; use the link the door printed when it started",
)
}
/// Studio's sign-in on a door with no lys block.
pub struct Local {
/// The local owner of the door's process.
owner: Caller,
/// The printed key until its first use.
key: Mutex<Option<String>>,
/// The sessions it opened, while the door runs.
sessions: Mutex<BTreeSet<String>>,
}
impl Local {
/// A sign-in for `owner`, with a new key: the key is answered once, for
/// the door to print, and kept only here.
///
/// # Errors
///
/// `door_random_unavailable` when the system's random source cannot be read.
pub fn new(owner: Caller) -> Result<(Self, String), Failure> {
let key = session_id()?;
Ok((
Self {
owner,
key: Mutex::new(Some(key.clone())),
sessions: Mutex::new(BTreeSet::new()),
},
key,
))
}
/// Who is at the keyboard.
#[must_use]
pub const fn owner(&self) -> &Caller {
&self.owner
}
/// Open a session with `offered`, spending the key; answers its id.
///
/// # Errors
///
/// [`refused`] for a spent, wrong or missing key; `door_poisoned`, or
/// `door_random_unavailable` before the key is spent.
pub fn open(&self, offered: Option<&str>) -> Result<String, Failure> {
let id = session_id()?;
let mut key = lock(&self.key)?;
let matches = key
.as_deref()
.zip(offered)
.is_some_and(|(held, offered)| same(held.as_bytes(), offered.as_bytes()));
if !matches {
return Err(refused());
}
*key = None;
drop(key);
lock(&self.sessions)?.insert(id.clone());
Ok(id)
}
/// Whether `id` is a session this door opened and has not ended.
///
/// # Errors
///
/// `door_poisoned`.
pub fn holds(&self, id: &str) -> Result<bool, Failure> {
Ok(lock(&self.sessions)?.contains(id))
}
/// The session line for `id`: whose it is, and that it is local.
///
/// # Errors
///
/// `session_unknown`, or `door_poisoned`.
pub fn session(&self, id: &str) -> Result<Value, Failure> {
if !self.holds(id)? {
return Err(Failure::new("session_unknown", "sign in to Studio first"));
}
Ok(json!({"holder": self.owner.subject(), "local": true}))
}
/// End session `id`, answering whether it was held.
///
/// # Errors
///
/// `door_poisoned`.
pub fn end(&self, id: &str) -> Result<bool, Failure> {
Ok(lock(&self.sessions)?.remove(id))
}
}
/// The issuer of a door with no lys block: it signs nobody in through Lys,
/// so every sign-in step is refused by name. The door's routes refuse
/// `/signin` and `/callback` before it is reached.
pub struct NoLys;
impl Issuer for NoLys {
fn start(&self) -> Result<Started, Error> {
Err(Error::Invalid(NO_LYS))
}
fn finish(&self, _: &str, _: &str, _: &str, _: &Started) -> Result<SignedIn, Error> {
Err(Error::Invalid(NO_LYS))
}
fn refresh(&self, _: &str) -> Result<SignedIn, Error> {
Err(Error::Invalid(NO_LYS))
}
}
const NO_LYS: &str = "this door has no lys block";
/// The link the door prints at its start: `origin`'s `/local` with `key`.
#[must_use]
pub fn link(origin: &str, key: &str) -> String {
format!("{origin}/local?key={key}")
}
/// Whether `held` and `offered` are equal, in time that depends on their
/// lengths alone: every byte is compared whatever the first difference.
fn same(held: &[u8], offered: &[u8]) -> bool {
if held.len() != offered.len() {
return false;
}
held.iter()
.zip(offered)
.fold(0_u8, |differs, (left, right)| differs | (left ^ right))
== 0
}
#[cfg(test)]
#[path = "local_signin_tests.rs"]
mod tests;