nut_shell/auth/
mod.rs

1//! Authentication and access control.
2//!
3//! This module provides:
4//! - `AccessLevel` trait for hierarchical permissions (always available)
5//! - `User` struct with username and access level (always available)
6//! - Password hashing and credential providers (feature-gated: `authentication`)
7
8#![cfg_attr(not(feature = "authentication"), allow(unused_imports))]
9
10// Sub-modules
11#[cfg(feature = "authentication")]
12pub mod password;
13
14#[cfg(feature = "authentication")]
15pub mod providers;
16
17// Re-exports
18#[cfg(feature = "authentication")]
19pub use password::Sha256Hasher;
20
21#[cfg(feature = "authentication")]
22pub use providers::ConstCredentialProvider;
23
24/// Access level trait for hierarchical permissions.
25/// Implement this trait to define your application's access hierarchy.
26pub trait AccessLevel: Copy + Clone + PartialOrd + Ord + 'static {
27    /// Parse access level from string.
28    fn from_str(s: &str) -> Option<Self>
29    where
30        Self: Sized;
31
32    /// Convert access level to string representation.
33    fn as_str(&self) -> &'static str;
34}
35
36/// User information.
37///
38/// Contains username, access level, and (when authentication enabled) password hash and salt.
39/// This type is always available, even when authentication feature is disabled.
40#[derive(Debug, Clone)]
41pub struct User<L: AccessLevel> {
42    /// Username (always present)
43    pub username: heapless::String<32>,
44
45    /// User's access level (always present)
46    pub access_level: L,
47
48    /// Password hash
49    #[cfg(feature = "authentication")]
50    pub password_hash: [u8; 32],
51
52    /// Salt for password hashing
53    #[cfg(feature = "authentication")]
54    pub salt: [u8; 16],
55}
56
57impl<L: AccessLevel> User<L> {
58    /// Create a new user without authentication (auth feature disabled).
59    #[cfg(not(feature = "authentication"))]
60    pub fn new(username: &str, access_level: L) -> Result<Self, crate::error::CliError> {
61        let mut user_str = heapless::String::new();
62        user_str
63            .push_str(username)
64            .map_err(|_| crate::error::CliError::BufferFull)?;
65
66        Ok(Self {
67            username: user_str,
68            access_level,
69        })
70    }
71
72    /// Create a new user with authentication (auth feature enabled).
73    #[cfg(feature = "authentication")]
74    pub fn new(
75        username: &str,
76        access_level: L,
77        password_hash: [u8; 32],
78        salt: [u8; 16],
79    ) -> Result<Self, crate::error::CliError> {
80        let mut user_str = heapless::String::new();
81        user_str
82            .push_str(username)
83            .map_err(|_| crate::error::CliError::BufferFull)?;
84
85        Ok(Self {
86            username: user_str,
87            access_level,
88            password_hash,
89            salt,
90        })
91    }
92}
93
94/// Credential provider trait.
95/// Implementations provide user lookup and password verification.
96#[cfg(feature = "authentication")]
97pub trait CredentialProvider<L: AccessLevel> {
98    /// Provider-specific error type
99    type Error;
100
101    /// Find user by username.
102    ///
103    /// Returns `Ok(Some(user))` if found, `Ok(None)` if not found.
104    fn find_user(&self, username: &str) -> Result<Option<User<L>>, Self::Error>;
105
106    /// Verify password for user.
107    ///
108    /// MUST use constant-time comparison to prevent timing attacks.
109    fn verify_password(&self, user: &User<L>, password: &str) -> bool;
110}
111
112/// Password hasher trait.
113///
114/// Provides password hashing and verification with salt.
115/// Must use constant-time comparison for verification.
116#[cfg(feature = "authentication")]
117pub trait PasswordHasher {
118    /// Hash password with salt.
119    ///
120    /// Returns 32-byte hash.
121    fn hash(&self, password: &str, salt: &[u8]) -> [u8; 32];
122
123    /// Verify password against hash using constant-time comparison.
124    ///
125    /// MUST use constant-time comparison to prevent timing attacks.
126    fn verify(&self, password: &str, salt: &[u8], hash: &[u8; 32]) -> bool;
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    // Mock access level for testing
134    #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
135    enum TestAccessLevel {
136        Guest = 0,
137        User = 1,
138        Admin = 2,
139    }
140
141    impl AccessLevel for TestAccessLevel {
142        fn from_str(s: &str) -> Option<Self> {
143            match s {
144                "Guest" => Some(Self::Guest),
145                "User" => Some(Self::User),
146                "Admin" => Some(Self::Admin),
147                _ => None,
148            }
149        }
150
151        fn as_str(&self) -> &'static str {
152            match self {
153                Self::Guest => "Guest",
154                Self::User => "User",
155                Self::Admin => "Admin",
156            }
157        }
158    }
159
160    #[test]
161    fn test_user_creation() {
162        #[cfg(not(feature = "authentication"))]
163        {
164            let user = User::new("alice", TestAccessLevel::User).unwrap();
165            assert_eq!(user.username.as_str(), "alice");
166            assert_eq!(user.access_level, TestAccessLevel::User);
167        }
168
169        #[cfg(feature = "authentication")]
170        {
171            let hash = [42u8; 32];
172            let salt = [99u8; 16];
173            let user = User::new("alice", TestAccessLevel::User, hash, salt).unwrap();
174            assert_eq!(user.username.as_str(), "alice");
175            assert_eq!(user.access_level, TestAccessLevel::User);
176            assert_eq!(user.password_hash, hash);
177            assert_eq!(user.salt, salt);
178        }
179    }
180}