openmoat_proxy/
session.rs1use std::fmt;
13use std::net::SocketAddr;
14
15use openmoat_core::Taint;
16use zeroize::Zeroizing;
17
18pub const RULE_SESSION: &str = "proxy-session";
21
22pub const MIN_TOKEN_LEN: usize = 32;
24
25const MAX_SESSION_LEN: usize = 128;
27
28pub struct Credential {
30 session: String,
31 token: Zeroizing<Vec<u8>>,
32}
33
34impl fmt::Debug for Credential {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 f.debug_struct("Credential")
38 .field("session", &self.session)
39 .finish_non_exhaustive()
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
45#[non_exhaustive]
46pub enum CredentialError {
47 #[error("a session id is 1 to 128 ASCII letters, digits, `.`, `_` or `-`")]
50 Session,
51 #[error("a session token is at least 32 ASCII letters, digits, `.`, `_` or `-`")]
53 Token,
54}
55
56fn plain(bytes: &[u8]) -> bool {
58 bytes
59 .iter()
60 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
61}
62
63impl Credential {
64 pub fn new(session: &str, token: &[u8]) -> Result<Self, CredentialError> {
66 if session.is_empty() || session.len() > MAX_SESSION_LEN || !plain(session.as_bytes()) {
67 return Err(CredentialError::Session);
68 }
69 if token.len() < MIN_TOKEN_LEN || !plain(token) {
70 return Err(CredentialError::Token);
71 }
72 Ok(Self {
73 session: session.to_owned(),
74 token: Zeroizing::new(token.to_vec()),
75 })
76 }
77
78 #[must_use]
80 pub fn session(&self) -> &str {
81 &self.session
82 }
83
84 #[must_use]
87 pub fn proxy_url(&self, addr: SocketAddr) -> Zeroizing<String> {
88 let token = String::from_utf8_lossy(&self.token);
89 Zeroizing::new(format!("http://{}:{token}@{addr}", self.session))
90 }
91}
92
93pub trait TaintLookup: Send + Sync {
96 fn taint(&self, session: &str) -> Result<Taint, String>;
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
104#[non_exhaustive]
105pub enum AuthError {
106 #[error("this proxy serves sessions only: the request carries no session credential")]
108 Missing,
109 #[error("the session credential is malformed")]
111 Malformed,
112 #[error("the session credential is not valid")]
114 Invalid,
115}
116
117pub struct Sessions<'a> {
119 credentials: Vec<Credential>,
120 taint: &'a dyn TaintLookup,
121}
122
123impl fmt::Debug for Sessions<'_> {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 f.debug_struct("Sessions")
126 .field("credentials", &self.credentials)
127 .finish_non_exhaustive()
128 }
129}
130
131impl<'a> Sessions<'a> {
132 #[must_use]
134 pub fn new(credentials: Vec<Credential>, taint: &'a dyn TaintLookup) -> Self {
135 Self { credentials, taint }
136 }
137
138 pub fn verify<T: AsRef<[u8]>>(&self, headers: &[T]) -> Result<&str, AuthError> {
142 let value = match headers {
143 [] => return Err(AuthError::Missing),
144 [one] => one.as_ref(),
145 _ => return Err(AuthError::Malformed),
146 };
147 let decoded = basic(value).ok_or(AuthError::Malformed)?;
148 let colon = decoded
149 .iter()
150 .position(|b| *b == b':')
151 .ok_or(AuthError::Malformed)?;
152 let (session, token) = (&decoded[..colon], &decoded[colon + 1..]);
153 let mut found = None;
154 for credential in &self.credentials {
155 let id = constant_time_eq(credential.session.as_bytes(), session);
156 let secret = constant_time_eq(&credential.token, token);
157 if id & secret {
158 found = Some(credential.session.as_str());
159 }
160 }
161 found.ok_or(AuthError::Invalid)
162 }
163
164 pub fn taint(&self, session: &str) -> Result<Taint, String> {
166 self.taint.taint(session)
167 }
168}
169
170fn basic(value: &[u8]) -> Option<Zeroizing<Vec<u8>>> {
173 let value = value.trim_ascii();
174 let space = value.iter().position(u8::is_ascii_whitespace)?;
175 let (scheme, rest) = value.split_at(space);
176 if !scheme.eq_ignore_ascii_case(b"basic") {
177 return None;
178 }
179 base64(rest.trim_ascii())
180}
181
182#[must_use]
184pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
185 if a.len() != b.len() {
186 return false;
187 }
188 let diff = a
189 .iter()
190 .zip(b)
191 .fold(0u8, |acc, (x, y)| acc | std::hint::black_box(x ^ y));
192 std::hint::black_box(diff) == 0
193}
194
195fn base64(input: &[u8]) -> Option<Zeroizing<Vec<u8>>> {
197 let data = input
198 .strip_suffix(b"==")
199 .or_else(|| input.strip_suffix(b"="))
200 .unwrap_or(input);
201 let padded = data.len() != input.len();
202 if data.is_empty() || data.len() % 4 == 1 || (padded && !input.len().is_multiple_of(4)) {
203 return None;
204 }
205 let mut out = Zeroizing::new(Vec::with_capacity(data.len() / 4 * 3 + 2));
206 let mut acc: u32 = 0;
207 let mut bits = 0u32;
208 for &c in data {
209 let v = match c {
210 b'A'..=b'Z' => c - b'A',
211 b'a'..=b'z' => c - b'a' + 26,
212 b'0'..=b'9' => c - b'0' + 52,
213 b'+' => 62,
214 b'/' => 63,
215 _ => return None,
216 };
217 acc = (acc << 6) | u32::from(v);
218 bits += 6;
219 if bits >= 8 {
220 bits -= 8;
221 out.push(u8::try_from((acc >> bits) & 0xff).ok()?);
222 acc &= (1 << bits) - 1;
223 }
224 }
225 Some(out)
226}
227
228#[cfg(test)]
229pub(crate) mod tests;