1use std::{
11 fmt,
12 hint::black_box,
13 ptr,
14 sync::atomic::{fence, Ordering},
15};
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum Tier {
19 Anon,
20 Read,
21 Write,
22 Approve,
23}
24
25#[derive(Debug, Copy, Clone, PartialEq, Eq)]
26#[non_exhaustive]
27pub enum AuthGate {
28 Read,
29 Write,
30 WriteApprove,
31 Delete,
32}
33
34#[derive(Clone)]
35pub struct Tokens {
36 pub(crate) read: Option<NonEmptyBytes>,
37 pub(crate) write: Option<NonEmptyBytes>,
38 pub(crate) approve: Option<NonEmptyBytes>,
39}
40
41#[derive(Clone)]
46pub(crate) struct NonEmptyBytes(Vec<u8>);
47
48impl NonEmptyBytes {
49 pub(crate) fn new(bytes: impl Into<Vec<u8>>) -> Option<Self> {
50 let bytes = bytes.into();
51 Self::is_valid(&bytes).then_some(Self(bytes))
52 }
53
54 pub(crate) fn is_valid(bytes: &[u8]) -> bool {
55 !bytes.is_empty()
56 && std::str::from_utf8(bytes)
57 .map(|value| !value.trim().is_empty())
58 .unwrap_or(true)
59 }
60
61 pub(crate) fn as_slice(&self) -> &[u8] {
62 &self.0
63 }
64
65 pub(crate) fn into_vec(mut self) -> Vec<u8> {
66 std::mem::take(&mut self.0)
67 }
68}
69
70pub fn is_valid_token(bytes: &[u8]) -> bool {
77 NonEmptyBytes::is_valid(bytes)
78}
79
80impl Drop for NonEmptyBytes {
81 fn drop(&mut self) {
82 wipe_vec_allocation(&mut self.0);
83 }
84}
85
86impl fmt::Debug for NonEmptyBytes {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 f.write_str("NonEmptyBytes(..)")
89 }
90}
91
92fn wipe_vec_allocation(bytes: &mut Vec<u8>) {
93 let ptr = bytes.as_mut_ptr();
98 for index in 0..bytes.capacity() {
99 unsafe {
100 ptr::write_volatile(ptr.add(index), 0);
101 }
102 }
103 fence(Ordering::SeqCst);
104}
105
106impl Tokens {
107 pub fn read_required(&self) -> bool {
108 self.read.is_some()
109 }
110
111 pub(crate) fn check_token_bytes(&self, candidate: &[u8]) -> Tier {
112 if !NonEmptyBytes::is_valid(candidate) {
115 return Tier::Anon;
116 }
117 if let Some(t) = &self.approve {
119 if ct_eq(candidate, t.as_slice()) {
120 return Tier::Approve;
121 }
122 }
123 if let Some(t) = &self.write {
124 if ct_eq(candidate, t.as_slice()) {
125 return Tier::Write;
126 }
127 }
128 if let Some(t) = &self.read {
129 if ct_eq(candidate, t.as_slice()) {
130 return Tier::Read;
131 }
132 }
133 Tier::Anon
134 }
135}
136
137pub fn ct_eq(a: &[u8], b: &[u8]) -> bool {
146 if a.len() != b.len() {
147 return false;
148 }
149 let mut diff: u8 = 0;
150 for (x, y) in a.iter().zip(b.iter()) {
151 diff |= x ^ y;
152 }
153 black_box(diff) == 0
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::{can_delete, can_read, can_write, test_support::test_core};
160
161 fn token(bytes: &[u8]) -> NonEmptyBytes {
162 NonEmptyBytes::new(bytes.to_vec()).unwrap()
163 }
164
165 #[test]
166 fn invalid_token_candidates_never_match() {
167 let tokens = Tokens {
168 read: Some(token(b"reader")),
169 write: Some(token(b"writer")),
170 approve: Some(token(b"approve")),
171 };
172
173 assert_eq!(tokens.check_token_bytes(b""), Tier::Anon);
174 assert_eq!(tokens.check_token_bytes(b" \t\r\n"), Tier::Anon);
175 assert_eq!(tokens.check_token_bytes("\u{2003}".as_bytes()), Tier::Anon);
176 }
177
178 #[test]
179 fn var_log_requires_approve_token() {
180 assert!(!can_write("var/log", Tier::Anon));
181 assert!(!can_write("var/log", Tier::Read));
182 assert!(!can_write("var/log", Tier::Write));
183 assert!(can_write("var/log", Tier::Approve));
184 assert!(!can_write("var/log/deletes", Tier::Anon));
185 assert!(!can_write("var/log/deletes", Tier::Read));
186 assert!(!can_write("var/log/deletes", Tier::Write));
187 assert!(can_write("var/log/deletes", Tier::Approve));
188 }
189
190 #[test]
191 fn delete_requires_approve_token() {
192 assert!(!can_delete(Tier::Anon));
193 assert!(!can_delete(Tier::Read));
194 assert!(!can_delete(Tier::Write));
195 assert!(can_delete(Tier::Approve));
196 }
197
198 #[test]
199 fn system_namespace_roots_require_approve_even_if_called_directly() {
200 for name in ["lib", "etc", "boot", "usr"] {
201 assert!(!can_write(name, Tier::Anon), "{name}");
202 assert!(!can_write(name, Tier::Read), "{name}");
203 assert!(!can_write(name, Tier::Write), "{name}");
204 assert!(can_write(name, Tier::Approve), "{name}");
205 }
206 }
207
208 #[test]
209 fn non_log_var_still_accepts_auth_token() {
210 assert!(!can_write("var/cache/rag", Tier::Anon));
211 assert!(!can_write("var/cache/rag", Tier::Read));
212 assert!(can_write("var/cache/rag", Tier::Write));
213 assert!(can_write("var/cache/rag", Tier::Approve));
214 }
215
216 #[test]
217 fn read_token_is_optional_but_gates_reads_when_set() {
218 let (mut core, dir) = test_core("read-token");
219 assert!(can_read(&core, Tier::Anon));
220
221 core.tokens.read = NonEmptyBytes::new(b"reader".to_vec());
222 assert!(!can_read(&core, Tier::Anon));
223 assert!(can_read(&core, Tier::Read));
224 assert!(can_read(&core, Tier::Write));
225 assert!(can_read(&core, Tier::Approve));
226
227 let _ = std::fs::remove_dir_all(dir);
228 }
229
230 #[test]
231 fn public_token_validity_matches_core_token_gate() {
232 assert!(is_valid_token(b"reader"));
233 assert!(is_valid_token(&[0xff, 0xfe]));
234 assert!(!is_valid_token(b""));
235 assert!(!is_valid_token(b" \t\r\n"));
236 }
237
238 #[test]
239 fn wipe_vec_allocation_clears_spare_capacity() {
240 let mut bytes = Vec::with_capacity(8);
241 bytes.extend_from_slice(b"key");
242 let ptr = bytes.as_mut_ptr();
243 let cap = bytes.capacity();
244 unsafe {
245 for index in bytes.len()..cap {
246 ptr.add(index).write(b'x');
247 }
248 }
249
250 wipe_vec_allocation(&mut bytes);
251
252 unsafe {
253 bytes.set_len(cap);
254 }
255 assert!(bytes.iter().all(|byte| *byte == 0));
256 }
257
258 #[test]
259 fn nonempty_raw_tokens_still_authenticate() {
260 let tokens = Tokens {
261 read: Some(token(b"reader")),
262 write: Some(token(b"writer")),
263 approve: Some(token(b"approve")),
264 };
265
266 assert_eq!(tokens.check_token_bytes(b"reader"), Tier::Read);
267 assert_eq!(tokens.check_token_bytes(b"writer"), Tier::Write);
268 assert_eq!(tokens.check_token_bytes(b"approve"), Tier::Approve);
269 assert_eq!(tokens.check_token_bytes(b"missing"), Tier::Anon);
270 }
271}