1#[cfg(feature = "std")]
25extern crate std as alloc;
26
27#[cfg(all(feature = "alloc", not(feature = "std")))]
28extern crate alloc;
29
30use crate::{Verb, ProtocolError, ProtocolResult};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
34#[repr(u8)]
35pub enum AuthLevel {
36 None = 0x00,
38 Session = 0x01,
40 Fido = 0x02,
42 FidoPin = 0x03,
44}
45
46impl AuthLevel {
47 pub fn from_byte(b: u8) -> Option<Self> {
48 match b {
49 0x00 => Some(AuthLevel::None),
50 0x01 => Some(AuthLevel::Session),
51 0x02 => Some(AuthLevel::Fido),
52 0x03 => Some(AuthLevel::FidoPin),
53 _ => None,
54 }
55 }
56
57 pub fn as_byte(self) -> u8 {
58 self as u8
59 }
60
61 pub fn name(self) -> &'static str {
63 match self {
64 AuthLevel::None => "none",
65 AuthLevel::Session => "session",
66 AuthLevel::Fido => "fido",
67 AuthLevel::FidoPin => "fido+pin",
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
74pub struct SessionId([u8; 16]);
75
76impl SessionId {
77 pub fn new(bytes: [u8; 16]) -> Self {
78 SessionId(bytes)
79 }
80
81 pub fn from_slice(slice: &[u8]) -> Option<Self> {
82 if slice.len() != 16 {
83 return None;
84 }
85 let mut bytes = [0u8; 16];
86 bytes.copy_from_slice(slice);
87 Some(SessionId(bytes))
88 }
89
90 pub fn as_bytes(&self) -> &[u8; 16] {
91 &self.0
92 }
93
94 #[cfg(feature = "std")]
96 pub fn random() -> Self {
97 use std::time::{SystemTime, UNIX_EPOCH};
98
99 let now = SystemTime::now()
102 .duration_since(UNIX_EPOCH)
103 .unwrap()
104 .as_nanos();
105
106 let mut bytes = [0u8; 16];
107 for (i, b) in bytes.iter_mut().enumerate() {
108 *b = ((now >> (i * 8)) & 0xFF) as u8;
109 }
110 SessionId(bytes)
111 }
112}
113
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub struct Signature([u8; 32]);
118
119impl Signature {
120 pub fn new(bytes: [u8; 32]) -> Self {
121 Signature(bytes)
122 }
123
124 pub fn from_slice(slice: &[u8]) -> Option<Self> {
125 if slice.len() != 32 {
126 return None;
127 }
128 let mut bytes = [0u8; 32];
129 bytes.copy_from_slice(slice);
130 Some(Signature(bytes))
131 }
132
133 pub fn as_bytes(&self) -> &[u8; 32] {
134 &self.0
135 }
136
137 pub fn empty() -> Self {
139 Signature([0u8; 32])
140 }
141}
142
143impl Default for Signature {
144 fn default() -> Self {
145 Self::empty()
146 }
147}
148
149#[derive(Debug, Clone)]
151pub struct AuthBlock {
152 pub level: AuthLevel,
154 pub session: SessionId,
156 pub signature: Signature,
158}
159
160impl AuthBlock {
161 pub fn new(level: AuthLevel, session: SessionId, signature: Signature) -> Self {
163 AuthBlock {
164 level,
165 session,
166 signature,
167 }
168 }
169
170 pub fn with_session(session: SessionId) -> Self {
172 AuthBlock {
173 level: AuthLevel::Session,
174 session,
175 signature: Signature::empty(),
176 }
177 }
178
179 pub const SIZE: usize = 1 + 16 + 32;
181
182 #[cfg(any(feature = "std", feature = "alloc"))]
184 pub fn encode(&self) -> alloc::vec::Vec<u8> {
185 let mut out = alloc::vec::Vec::with_capacity(Self::SIZE);
186 out.push(self.level.as_byte());
187 out.extend_from_slice(self.session.as_bytes());
188 out.extend_from_slice(self.signature.as_bytes());
189 out
190 }
191
192 pub fn decode(data: &[u8]) -> ProtocolResult<Self> {
194 if data.len() < Self::SIZE {
195 return Err(ProtocolError::InvalidAuthBlock);
196 }
197
198 let level = AuthLevel::from_byte(data[0])
199 .ok_or(ProtocolError::InvalidAuthBlock)?;
200 let session = SessionId::from_slice(&data[1..17])
201 .ok_or(ProtocolError::InvalidAuthBlock)?;
202 let signature = Signature::from_slice(&data[17..49])
203 .ok_or(ProtocolError::InvalidAuthBlock)?;
204
205 Ok(AuthBlock {
206 level,
207 session,
208 signature,
209 })
210 }
211}
212
213#[derive(Debug, Clone)]
215pub struct SecurityContext {
216 session: Option<SessionId>,
218 level: AuthLevel,
220 user: Option<[u8; 32]>,
222}
223
224impl Default for SecurityContext {
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230impl SecurityContext {
231 pub fn new() -> Self {
233 SecurityContext {
234 session: None,
235 level: AuthLevel::None,
236 user: None,
237 }
238 }
239
240 pub fn authenticated(session: SessionId, level: AuthLevel) -> Self {
242 SecurityContext {
243 session: Some(session),
244 level,
245 user: None,
246 }
247 }
248
249 pub fn session(&self) -> Option<&SessionId> {
251 self.session.as_ref()
252 }
253
254 pub fn level(&self) -> AuthLevel {
256 self.level
257 }
258
259 pub fn can_execute(&self, verb: Verb) -> bool {
261 let required = verb.security_level();
262 self.level as u8 >= required
263 }
264
265 pub fn elevate(&mut self, level: AuthLevel, session: SessionId) {
267 if level > self.level {
268 self.level = level;
269 self.session = Some(session);
270 }
271 }
272
273 pub fn is_authenticated(&self) -> bool {
275 self.level > AuthLevel::None
276 }
277
278 pub fn set_user(&mut self, user: [u8; 32]) {
280 self.user = Some(user);
281 }
282}
283
284pub const PROTECTED_PATHS: &[&str] = &[
286 "~/.claude/settings.json",
287 "~/.claude/",
288 "~/.config/",
289 "~/.ssh/",
290 "~/.gnupg/",
291 "/etc/",
292];
293
294pub fn is_protected_path(path: &str) -> bool {
296 for protected in PROTECTED_PATHS {
297 if path.starts_with(protected) || path.contains(protected) {
298 return true;
299 }
300 }
301 false
302}
303
304pub fn path_auth_level(path: &str) -> AuthLevel {
306 if path.contains("/.claude/") || path.contains("/.ssh/") || path.contains("/.gnupg/") {
307 AuthLevel::Fido } else if path.starts_with("/etc/") {
309 AuthLevel::FidoPin } else if is_protected_path(path) {
311 AuthLevel::Session } else {
313 AuthLevel::None
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_auth_levels() {
323 assert!(AuthLevel::FidoPin > AuthLevel::Fido);
324 assert!(AuthLevel::Fido > AuthLevel::Session);
325 assert!(AuthLevel::Session > AuthLevel::None);
326 }
327
328 #[test]
329 fn test_auth_block_roundtrip() {
330 let original = AuthBlock {
331 level: AuthLevel::Fido,
332 session: SessionId::new([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
333 signature: Signature::new([42u8; 32]),
334 };
335
336 let encoded = original.encode();
337 let decoded = AuthBlock::decode(&encoded).unwrap();
338
339 assert_eq!(decoded.level, original.level);
340 assert_eq!(decoded.session.as_bytes(), original.session.as_bytes());
341 assert_eq!(decoded.signature.as_bytes(), original.signature.as_bytes());
342 }
343
344 #[test]
345 fn test_security_context() {
346 let mut ctx = SecurityContext::new();
347
348 assert!(ctx.can_execute(Verb::Scan));
350 assert!(ctx.can_execute(Verb::Ping));
351
352 assert!(!ctx.can_execute(Verb::Permit));
354
355 ctx.elevate(AuthLevel::FidoPin, SessionId::default());
357 assert!(ctx.can_execute(Verb::Permit));
358 }
359
360 #[test]
361 fn test_protected_paths() {
362 assert!(is_protected_path("~/.claude/settings.json"));
363 assert!(is_protected_path("/etc/passwd"));
364 assert!(!is_protected_path("/home/user/projects/foo"));
365 }
366
367 #[test]
368 fn test_path_auth_level() {
369 assert_eq!(path_auth_level("/home/user/file.txt"), AuthLevel::None);
370 assert_eq!(path_auth_level("~/.claude/settings.json"), AuthLevel::Fido);
371 assert_eq!(path_auth_level("/etc/hosts"), AuthLevel::FidoPin);
372 }
373}