mssql_auth/
sql_auth.rs

1//! SQL Server authentication implementation.
2//!
3//! This module provides SQL Server username/password authentication,
4//! which sends credentials via the TDS Login7 packet.
5
6use std::borrow::Cow;
7
8use crate::credentials::Credentials;
9use crate::error::AuthError;
10use crate::provider::{AuthData, AuthMethod, AuthProvider};
11
12/// SQL Server authenticator for username/password authentication.
13///
14/// This provider handles traditional SQL Server authentication where
15/// credentials are sent via the Login7 packet with password obfuscation.
16///
17/// # Security Note
18///
19/// The password is obfuscated (XOR + nibble swap), not encrypted.
20/// Always use TLS encryption for the connection.
21///
22/// # Example
23///
24/// ```rust
25/// use mssql_auth::SqlServerAuth;
26///
27/// let auth = SqlServerAuth::new("sa", "Password123!");
28/// ```
29#[derive(Clone)]
30pub struct SqlServerAuth {
31    username: Cow<'static, str>,
32    password: Cow<'static, str>,
33}
34
35impl SqlServerAuth {
36    /// Create a new SQL Server authenticator with credentials.
37    pub fn new(
38        username: impl Into<Cow<'static, str>>,
39        password: impl Into<Cow<'static, str>>,
40    ) -> Self {
41        Self {
42            username: username.into(),
43            password: password.into(),
44        }
45    }
46
47    /// Create from existing credentials.
48    ///
49    /// Returns an error if the credentials are not SQL Server credentials.
50    pub fn from_credentials(credentials: &Credentials) -> Result<Self, AuthError> {
51        match credentials {
52            Credentials::SqlServer { username, password } => Ok(Self {
53                username: Cow::Owned(username.to_string()),
54                password: Cow::Owned(password.to_string()),
55            }),
56            _ => Err(AuthError::UnsupportedMethod(
57                "SqlServerAuth requires SQL Server credentials".into(),
58            )),
59        }
60    }
61
62    /// Get the username.
63    #[must_use]
64    pub fn username(&self) -> &str {
65        &self.username
66    }
67
68    /// Encode a password for SQL Server Login7 packet.
69    ///
70    /// SQL Server uses a simple XOR-based obfuscation for passwords
71    /// in Login7 packets. This is NOT encryption - it's just obfuscation.
72    /// The connection should always be encrypted via TLS.
73    ///
74    /// # Algorithm
75    ///
76    /// For each UTF-16 code unit:
77    /// 1. XOR each byte with 0xA5
78    /// 2. Swap the high and low nibbles
79    #[must_use]
80    pub fn encode_password(password: &str) -> Vec<u8> {
81        password
82            .encode_utf16()
83            .flat_map(|c| {
84                let byte1 = (c & 0xFF) as u8;
85                let byte2 = (c >> 8) as u8;
86
87                // XOR with 0xA5 and swap nibbles
88                let encoded1 = (byte1 ^ 0xA5).rotate_right(4);
89                let encoded2 = (byte2 ^ 0xA5).rotate_right(4);
90
91                [encoded1, encoded2]
92            })
93            .collect()
94    }
95}
96
97impl AuthProvider for SqlServerAuth {
98    fn method(&self) -> AuthMethod {
99        AuthMethod::SqlServer
100    }
101
102    fn authenticate(&self) -> Result<AuthData, AuthError> {
103        tracing::debug!(
104            username = %self.username,
105            "authenticating with SQL Server credentials"
106        );
107
108        let password_bytes = Self::encode_password(&self.password);
109
110        Ok(AuthData::SqlServer {
111            username: self.username.to_string(),
112            password_bytes,
113        })
114    }
115}
116
117impl std::fmt::Debug for SqlServerAuth {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.debug_struct("SqlServerAuth")
120            .field("username", &self.username)
121            .field("password", &"[REDACTED]")
122            .finish()
123    }
124}
125
126// Keep the old SqlAuthenticator for backward compatibility
127/// SQL Server authenticator (legacy API).
128///
129/// This is kept for backward compatibility. Prefer using [`SqlServerAuth`] instead.
130#[deprecated(since = "0.2.0", note = "Use SqlServerAuth instead")]
131pub struct SqlAuthenticator;
132
133#[allow(deprecated)]
134impl SqlAuthenticator {
135    /// Create a new SQL authenticator.
136    #[must_use]
137    pub fn new() -> Self {
138        Self
139    }
140
141    /// Encode a password for SQL Server Login7 packet.
142    #[must_use]
143    pub fn encode_password(password: &str) -> Vec<u8> {
144        SqlServerAuth::encode_password(password)
145    }
146}
147
148#[allow(deprecated)]
149impl Default for SqlAuthenticator {
150    fn default() -> Self {
151        Self::new()
152    }
153}
154
155#[cfg(test)]
156#[allow(clippy::unwrap_used, clippy::panic)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_password_encoding() {
162        // Test that password encoding produces expected output
163        let encoded = SqlServerAuth::encode_password("test");
164        assert!(!encoded.is_empty());
165        assert_eq!(encoded.len(), 8); // 4 UTF-16 chars * 2 bytes each
166    }
167
168    #[test]
169    fn test_password_encoding_known_value() {
170        // Test against known encoded value
171        // "a" in UTF-16LE is 0x61, 0x00
172        // 0x61 ^ 0xA5 = 0xC4, nibble swap = 0x4C
173        // 0x00 ^ 0xA5 = 0xA5, nibble swap = 0x5A
174        let encoded = SqlServerAuth::encode_password("a");
175        assert_eq!(encoded, vec![0x4C, 0x5A]);
176    }
177
178    #[test]
179    fn test_sql_server_auth_provider() {
180        let auth = SqlServerAuth::new("sa", "Password123!");
181
182        assert_eq!(auth.method(), AuthMethod::SqlServer);
183        assert_eq!(auth.username(), "sa");
184
185        let data = auth.authenticate().unwrap();
186        match data {
187            AuthData::SqlServer {
188                username,
189                password_bytes,
190            } => {
191                assert_eq!(username, "sa");
192                assert!(!password_bytes.is_empty());
193            }
194            _ => panic!("Expected SqlServer auth data"),
195        }
196    }
197
198    #[test]
199    fn test_from_credentials() {
200        let creds = Credentials::sql_server("user", "pass");
201        let auth = SqlServerAuth::from_credentials(&creds).unwrap();
202        assert_eq!(auth.username(), "user");
203    }
204
205    #[test]
206    fn test_from_credentials_wrong_type() {
207        let creds = Credentials::azure_token("token");
208        let result = SqlServerAuth::from_credentials(&creds);
209        assert!(result.is_err());
210    }
211
212    #[test]
213    fn test_debug_redacts_password() {
214        let auth = SqlServerAuth::new("sa", "secret");
215        let debug = format!("{:?}", auth);
216        assert!(debug.contains("sa"));
217        assert!(!debug.contains("secret"));
218        assert!(debug.contains("[REDACTED]"));
219    }
220}