Skip to main content

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#[cfg(test)]
127#[allow(clippy::unwrap_used, clippy::panic)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_password_encoding() {
133        // Test that password encoding produces expected output
134        let encoded = SqlServerAuth::encode_password("test");
135        assert!(!encoded.is_empty());
136        assert_eq!(encoded.len(), 8); // 4 UTF-16 chars * 2 bytes each
137    }
138
139    #[test]
140    fn test_password_encoding_known_value() {
141        // Test against known encoded value
142        // "a" in UTF-16LE is 0x61, 0x00
143        // 0x61 ^ 0xA5 = 0xC4, nibble swap = 0x4C
144        // 0x00 ^ 0xA5 = 0xA5, nibble swap = 0x5A
145        let encoded = SqlServerAuth::encode_password("a");
146        assert_eq!(encoded, vec![0x4C, 0x5A]);
147    }
148
149    #[test]
150    fn test_sql_server_auth_provider() {
151        let auth = SqlServerAuth::new("sa", "Password123!");
152
153        assert_eq!(auth.method(), AuthMethod::SqlServer);
154        assert_eq!(auth.username(), "sa");
155
156        let data = auth.authenticate().unwrap();
157        match &data {
158            AuthData::SqlServer {
159                username,
160                password_bytes,
161            } => {
162                assert_eq!(username, "sa");
163                assert!(!password_bytes.is_empty());
164            }
165            _ => panic!("Expected SqlServer auth data"),
166        }
167    }
168
169    #[test]
170    fn test_from_credentials() {
171        let creds = Credentials::sql_server("user", "pass");
172        let auth = SqlServerAuth::from_credentials(&creds).unwrap();
173        assert_eq!(auth.username(), "user");
174    }
175
176    #[test]
177    fn test_from_credentials_wrong_type() {
178        let creds = Credentials::azure_token("token");
179        let result = SqlServerAuth::from_credentials(&creds);
180        assert!(result.is_err());
181    }
182
183    #[test]
184    fn test_debug_redacts_password() {
185        let auth = SqlServerAuth::new("sa", "secret");
186        let debug = format!("{auth:?}");
187        assert!(debug.contains("sa"));
188        assert!(!debug.contains("secret"));
189        assert!(debug.contains("[REDACTED]"));
190    }
191}