tds_protocol/
login7.rs

1//! TDS LOGIN7 packet construction.
2//!
3//! The LOGIN7 packet is sent by the client to authenticate with SQL Server.
4//! It contains client information, credentials, and feature negotiation data.
5//!
6//! ## Packet Structure
7//!
8//! The LOGIN7 packet has a complex structure with:
9//! - Fixed-length header (94 bytes)
10//! - Variable-length data section (strings are UTF-16LE)
11//! - Optional feature extension block
12//!
13//! ## Security Note
14//!
15//! The password is obfuscated (not encrypted) using a simple XOR + bit rotation.
16//! Always use TLS encryption for the connection.
17
18use bytes::{BufMut, Bytes, BytesMut};
19
20use crate::codec::write_utf16_string;
21use crate::version::TdsVersion;
22
23/// LOGIN7 packet header size (fixed portion).
24pub const LOGIN7_HEADER_SIZE: usize = 94;
25
26/// LOGIN7 option flags 1.
27#[derive(Debug, Clone, Copy, Default)]
28pub struct OptionFlags1 {
29    /// Use big-endian byte order.
30    pub byte_order_be: bool,
31    /// Character set (0 = ASCII, 1 = EBCDIC).
32    pub char_ebcdic: bool,
33    /// Floating point representation (0 = IEEE 754, 1 = VAX, 2 = ND5000).
34    pub float_ieee: bool,
35    /// Dump/load off.
36    pub dump_load_off: bool,
37    /// Use DB notification.
38    pub use_db_notify: bool,
39    /// Database is fatal.
40    pub database_fatal: bool,
41    /// Set language warning.
42    pub set_lang_warn: bool,
43}
44
45impl OptionFlags1 {
46    /// Convert to byte.
47    ///
48    /// Per MS-TDS 2.2.6.3 LOGIN7, OptionFlags1 layout:
49    /// - bit 0: fByteOrder (0=little-endian, 1=big-endian)
50    /// - bit 1: fChar (0=ASCII, 1=EBCDIC)
51    /// - bits 2-3: fFloat (0=IEEE 754, 1=VAX, 2=ND5000)
52    /// - bit 4: fDumpLoad
53    /// - bit 5: fUseDB
54    /// - bit 6: fDatabase
55    /// - bit 7: fSetLang
56    #[must_use]
57    pub fn to_byte(&self) -> u8 {
58        let mut flags = 0u8;
59        if self.byte_order_be {
60            flags |= 0x01; // bit 0
61        }
62        if self.char_ebcdic {
63            flags |= 0x02; // bit 1
64        }
65        // Note: fFloat is bits 2-3, IEEE 754 = 0, so leave as 0 for IEEE
66        // float_ieee being true means we use IEEE (which is 0, the default)
67        if self.dump_load_off {
68            flags |= 0x10; // bit 4
69        }
70        if self.use_db_notify {
71            flags |= 0x20; // bit 5
72        }
73        if self.database_fatal {
74            flags |= 0x40; // bit 6
75        }
76        if self.set_lang_warn {
77            flags |= 0x80; // bit 7
78        }
79        flags
80    }
81}
82
83/// LOGIN7 option flags 2.
84#[derive(Debug, Clone, Copy, Default)]
85pub struct OptionFlags2 {
86    /// Language is fatal.
87    pub language_fatal: bool,
88    /// ODBC driver.
89    pub odbc: bool,
90    /// Obsolete: transaction boundary.
91    pub tran_boundary: bool,
92    /// Obsolete: cache connect.
93    pub cache_connect: bool,
94    /// User type (0 = Normal, 1 = Server, 2 = DQ login, 3 = Replication).
95    pub user_type: u8,
96    /// Integrated security.
97    pub integrated_security: bool,
98}
99
100impl OptionFlags2 {
101    /// Convert to byte.
102    #[must_use]
103    pub fn to_byte(&self) -> u8 {
104        let mut flags = 0u8;
105        if self.language_fatal {
106            flags |= 0x01;
107        }
108        if self.odbc {
109            flags |= 0x02;
110        }
111        if self.tran_boundary {
112            flags |= 0x04;
113        }
114        if self.cache_connect {
115            flags |= 0x08;
116        }
117        flags |= (self.user_type & 0x07) << 4;
118        if self.integrated_security {
119            flags |= 0x80;
120        }
121        flags
122    }
123}
124
125/// LOGIN7 type flags.
126#[derive(Debug, Clone, Copy, Default)]
127pub struct TypeFlags {
128    /// SQL type (0 = DFLT, 1 = TSQL).
129    pub sql_type: u8,
130    /// OLEDB driver.
131    pub oledb: bool,
132    /// Read-only intent.
133    pub read_only_intent: bool,
134}
135
136impl TypeFlags {
137    /// Convert to byte.
138    #[must_use]
139    pub fn to_byte(&self) -> u8 {
140        let mut flags = 0u8;
141        flags |= self.sql_type & 0x0F;
142        if self.oledb {
143            flags |= 0x10;
144        }
145        if self.read_only_intent {
146            flags |= 0x20;
147        }
148        flags
149    }
150}
151
152/// LOGIN7 option flags 3.
153#[derive(Debug, Clone, Copy, Default)]
154pub struct OptionFlags3 {
155    /// Change password.
156    pub change_password: bool,
157    /// User instance.
158    pub user_instance: bool,
159    /// Send YUKON binary XML.
160    pub send_yukon_binary_xml: bool,
161    /// Unknown collation handling.
162    pub unknown_collation_handling: bool,
163    /// Feature extension.
164    pub extension: bool,
165}
166
167impl OptionFlags3 {
168    /// Convert to byte.
169    #[must_use]
170    pub fn to_byte(&self) -> u8 {
171        let mut flags = 0u8;
172        if self.change_password {
173            flags |= 0x01;
174        }
175        if self.user_instance {
176            flags |= 0x02;
177        }
178        if self.send_yukon_binary_xml {
179            flags |= 0x04;
180        }
181        if self.unknown_collation_handling {
182            flags |= 0x08;
183        }
184        if self.extension {
185            flags |= 0x10;
186        }
187        flags
188    }
189}
190
191/// Feature extension types.
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193#[repr(u8)]
194pub enum FeatureId {
195    /// Session recovery.
196    SessionRecovery = 0x01,
197    /// Federated authentication.
198    FedAuth = 0x02,
199    /// Column encryption.
200    ColumnEncryption = 0x04,
201    /// Global transactions.
202    GlobalTransactions = 0x05,
203    /// Azure SQL Support for DB.
204    AzureSqlSupport = 0x08,
205    /// Data classification.
206    DataClassification = 0x09,
207    /// UTF-8 support.
208    Utf8Support = 0x0A,
209    /// Azure SQL DNS Caching.
210    AzureSqlDnsCaching = 0x0B,
211    /// Terminator.
212    Terminator = 0xFF,
213}
214
215/// LOGIN7 packet builder.
216#[derive(Debug, Clone)]
217pub struct Login7 {
218    /// TDS version to request.
219    pub tds_version: TdsVersion,
220    /// Requested packet size.
221    pub packet_size: u32,
222    /// Client program version.
223    pub client_prog_version: u32,
224    /// Client process ID.
225    pub client_pid: u32,
226    /// Connection ID (for connection pooling).
227    pub connection_id: u32,
228    /// Option flags 1.
229    pub option_flags1: OptionFlags1,
230    /// Option flags 2.
231    pub option_flags2: OptionFlags2,
232    /// Type flags.
233    pub type_flags: TypeFlags,
234    /// Option flags 3.
235    pub option_flags3: OptionFlags3,
236    /// Client timezone offset in minutes.
237    pub client_timezone: i32,
238    /// Client LCID (locale ID).
239    pub client_lcid: u32,
240    /// Hostname (client machine name).
241    pub hostname: String,
242    /// Username for SQL authentication.
243    pub username: String,
244    /// Password for SQL authentication.
245    pub password: String,
246    /// Application name.
247    pub app_name: String,
248    /// Server name.
249    pub server_name: String,
250    /// Unused field.
251    pub unused: String,
252    /// Client library name.
253    pub library_name: String,
254    /// Language.
255    pub language: String,
256    /// Database name.
257    pub database: String,
258    /// Client ID (MAC address, typically zeros).
259    pub client_id: [u8; 6],
260    /// SSPI data for integrated authentication.
261    pub sspi_data: Vec<u8>,
262    /// Attach DB filename (for LocalDB).
263    pub attach_db_file: String,
264    /// New password (for password change).
265    pub new_password: String,
266    /// Feature extensions.
267    pub features: Vec<FeatureExtension>,
268}
269
270/// Feature extension data.
271#[derive(Debug, Clone)]
272pub struct FeatureExtension {
273    /// Feature ID.
274    pub feature_id: FeatureId,
275    /// Feature data.
276    pub data: Bytes,
277}
278
279impl Default for Login7 {
280    fn default() -> Self {
281        #[cfg(feature = "std")]
282        let client_pid = std::process::id();
283        #[cfg(not(feature = "std"))]
284        let client_pid = 0;
285
286        Self {
287            tds_version: TdsVersion::V7_4,
288            packet_size: 4096,
289            client_prog_version: 0,
290            client_pid,
291            connection_id: 0,
292            // Match Tiberius/standard SQL Server client flags
293            option_flags1: OptionFlags1 {
294                use_db_notify: true,
295                database_fatal: true,
296                ..Default::default()
297            },
298            option_flags2: OptionFlags2 {
299                language_fatal: true,
300                odbc: true,
301                ..Default::default()
302            },
303            type_flags: TypeFlags::default(), // TSQL type is in sql_type field
304            option_flags3: OptionFlags3 {
305                unknown_collation_handling: true,
306                ..Default::default()
307            },
308            client_timezone: 0,
309            client_lcid: 0x0409, // English (US)
310            hostname: String::new(),
311            username: String::new(),
312            password: String::new(),
313            app_name: String::from("rust-mssql-driver"),
314            server_name: String::new(),
315            unused: String::new(),
316            library_name: String::from("rust-mssql-driver"),
317            language: String::new(),
318            database: String::new(),
319            client_id: [0u8; 6],
320            sspi_data: Vec::new(),
321            attach_db_file: String::new(),
322            new_password: String::new(),
323            features: Vec::new(),
324        }
325    }
326}
327
328impl Login7 {
329    /// Create a new Login7 packet builder.
330    #[must_use]
331    pub fn new() -> Self {
332        Self::default()
333    }
334
335    /// Set the TDS version.
336    #[must_use]
337    pub fn with_tds_version(mut self, version: TdsVersion) -> Self {
338        self.tds_version = version;
339        self
340    }
341
342    /// Set SQL authentication credentials.
343    #[must_use]
344    pub fn with_sql_auth(
345        mut self,
346        username: impl Into<String>,
347        password: impl Into<String>,
348    ) -> Self {
349        self.username = username.into();
350        self.password = password.into();
351        self.option_flags2.integrated_security = false;
352        self
353    }
354
355    /// Enable integrated (Windows) authentication.
356    #[must_use]
357    pub fn with_integrated_auth(mut self, sspi_data: Vec<u8>) -> Self {
358        self.sspi_data = sspi_data;
359        self.option_flags2.integrated_security = true;
360        self
361    }
362
363    /// Set the database to connect to.
364    #[must_use]
365    pub fn with_database(mut self, database: impl Into<String>) -> Self {
366        self.database = database.into();
367        self
368    }
369
370    /// Set the hostname (client machine name).
371    #[must_use]
372    pub fn with_hostname(mut self, hostname: impl Into<String>) -> Self {
373        self.hostname = hostname.into();
374        self
375    }
376
377    /// Set the application name.
378    #[must_use]
379    pub fn with_app_name(mut self, app_name: impl Into<String>) -> Self {
380        self.app_name = app_name.into();
381        self
382    }
383
384    /// Set the server name.
385    #[must_use]
386    pub fn with_server_name(mut self, server_name: impl Into<String>) -> Self {
387        self.server_name = server_name.into();
388        self
389    }
390
391    /// Set the packet size.
392    #[must_use]
393    pub fn with_packet_size(mut self, packet_size: u32) -> Self {
394        self.packet_size = packet_size;
395        self
396    }
397
398    /// Enable read-only intent for readable secondary connections.
399    #[must_use]
400    pub fn with_read_only_intent(mut self, read_only: bool) -> Self {
401        self.type_flags.read_only_intent = read_only;
402        self
403    }
404
405    /// Add a feature extension.
406    #[must_use]
407    pub fn with_feature(mut self, feature: FeatureExtension) -> Self {
408        self.option_flags3.extension = true;
409        self.features.push(feature);
410        self
411    }
412
413    /// Encode the LOGIN7 packet to bytes.
414    #[must_use]
415    pub fn encode(&self) -> Bytes {
416        let mut buf = BytesMut::with_capacity(512);
417
418        // Calculate variable data offsets
419        // Variable data starts after the 94-byte fixed header
420        let mut offset = LOGIN7_HEADER_SIZE as u16;
421
422        // Pre-calculate all UTF-16 lengths
423        let hostname_len = self.hostname.encode_utf16().count() as u16;
424        let username_len = self.username.encode_utf16().count() as u16;
425        let password_len = self.password.encode_utf16().count() as u16;
426        let app_name_len = self.app_name.encode_utf16().count() as u16;
427        let server_name_len = self.server_name.encode_utf16().count() as u16;
428        let unused_len = self.unused.encode_utf16().count() as u16;
429        let library_name_len = self.library_name.encode_utf16().count() as u16;
430        let language_len = self.language.encode_utf16().count() as u16;
431        let database_len = self.database.encode_utf16().count() as u16;
432        let sspi_len = self.sspi_data.len() as u16;
433        let attach_db_len = self.attach_db_file.encode_utf16().count() as u16;
434        let new_password_len = self.new_password.encode_utf16().count() as u16;
435
436        // Build variable data buffer
437        let mut var_data = BytesMut::new();
438
439        // Hostname
440        let hostname_offset = offset;
441        write_utf16_string(&mut var_data, &self.hostname);
442        offset += hostname_len * 2;
443
444        // Username
445        let username_offset = offset;
446        write_utf16_string(&mut var_data, &self.username);
447        offset += username_len * 2;
448
449        // Password (obfuscated)
450        let password_offset = offset;
451        Self::write_obfuscated_password(&mut var_data, &self.password);
452        offset += password_len * 2;
453
454        // App name
455        let app_name_offset = offset;
456        write_utf16_string(&mut var_data, &self.app_name);
457        offset += app_name_len * 2;
458
459        // Server name
460        let server_name_offset = offset;
461        write_utf16_string(&mut var_data, &self.server_name);
462        offset += server_name_len * 2;
463
464        // Unused / Feature extension pointer
465        let extension_offset = if self.option_flags3.extension {
466            // Calculate feature extension offset after all other data
467            let base = offset
468                + unused_len * 2
469                + library_name_len * 2
470                + language_len * 2
471                + database_len * 2
472                + sspi_len
473                + attach_db_len * 2
474                + new_password_len * 2;
475            // Store the offset where feature extension will be
476            var_data.put_u32_le(base as u32);
477            offset += 4;
478            base
479        } else {
480            let unused_offset = offset;
481            write_utf16_string(&mut var_data, &self.unused);
482            offset += unused_len * 2;
483            unused_offset
484        };
485
486        // Library name
487        let library_name_offset = offset;
488        write_utf16_string(&mut var_data, &self.library_name);
489        offset += library_name_len * 2;
490
491        // Language
492        let language_offset = offset;
493        write_utf16_string(&mut var_data, &self.language);
494        offset += language_len * 2;
495
496        // Database
497        let database_offset = offset;
498        write_utf16_string(&mut var_data, &self.database);
499        offset += database_len * 2;
500
501        // Client ID (6 bytes)
502        // (Already handled in fixed header)
503
504        // SSPI
505        let sspi_offset = offset;
506        var_data.put_slice(&self.sspi_data);
507        offset += sspi_len;
508
509        // Attach DB file
510        let attach_db_offset = offset;
511        write_utf16_string(&mut var_data, &self.attach_db_file);
512        offset += attach_db_len * 2;
513
514        // Change password
515        let new_password_offset = offset;
516        if !self.new_password.is_empty() {
517            Self::write_obfuscated_password(&mut var_data, &self.new_password);
518        }
519        #[allow(unused_assignments)]
520        {
521            offset += new_password_len * 2;
522        }
523
524        // Feature extensions (if any)
525        if self.option_flags3.extension {
526            for feature in &self.features {
527                var_data.put_u8(feature.feature_id as u8);
528                var_data.put_u32_le(feature.data.len() as u32);
529                var_data.put_slice(&feature.data);
530            }
531            var_data.put_u8(FeatureId::Terminator as u8);
532        }
533
534        // Calculate total length
535        let total_length = LOGIN7_HEADER_SIZE + var_data.len();
536
537        // Write fixed header
538        buf.put_u32_le(total_length as u32); // Length
539        buf.put_u32_le(self.tds_version.raw()); // TDS version
540        buf.put_u32_le(self.packet_size); // Packet size
541        buf.put_u32_le(self.client_prog_version); // Client program version
542        buf.put_u32_le(self.client_pid); // Client PID
543        buf.put_u32_le(self.connection_id); // Connection ID
544
545        // Option flags
546        buf.put_u8(self.option_flags1.to_byte());
547        buf.put_u8(self.option_flags2.to_byte());
548        buf.put_u8(self.type_flags.to_byte());
549        buf.put_u8(self.option_flags3.to_byte());
550
551        buf.put_i32_le(self.client_timezone); // Client timezone
552        buf.put_u32_le(self.client_lcid); // Client LCID
553
554        // Variable length field offsets and lengths
555        buf.put_u16_le(hostname_offset);
556        buf.put_u16_le(hostname_len);
557        buf.put_u16_le(username_offset);
558        buf.put_u16_le(username_len);
559        buf.put_u16_le(password_offset);
560        buf.put_u16_le(password_len);
561        buf.put_u16_le(app_name_offset);
562        buf.put_u16_le(app_name_len);
563        buf.put_u16_le(server_name_offset);
564        buf.put_u16_le(server_name_len);
565
566        // Extension offset (or unused)
567        if self.option_flags3.extension {
568            buf.put_u16_le(extension_offset as u16);
569            buf.put_u16_le(4); // Size of offset pointer
570        } else {
571            buf.put_u16_le(extension_offset as u16);
572            buf.put_u16_le(unused_len);
573        }
574
575        buf.put_u16_le(library_name_offset);
576        buf.put_u16_le(library_name_len);
577        buf.put_u16_le(language_offset);
578        buf.put_u16_le(language_len);
579        buf.put_u16_le(database_offset);
580        buf.put_u16_le(database_len);
581
582        // Client ID (6 bytes)
583        buf.put_slice(&self.client_id);
584
585        buf.put_u16_le(sspi_offset);
586        buf.put_u16_le(sspi_len);
587        buf.put_u16_le(attach_db_offset);
588        buf.put_u16_le(attach_db_len);
589        buf.put_u16_le(new_password_offset);
590        buf.put_u16_le(new_password_len);
591
592        // SSPI Long (4 bytes, for SSPI > 65535 bytes)
593        buf.put_u32_le(0);
594
595        // Append variable data
596        buf.put_slice(&var_data);
597
598        buf.freeze()
599    }
600
601    /// Write password with TDS obfuscation.
602    ///
603    /// Per MS-TDS spec: For every byte in the password buffer, the client SHOULD first
604    /// swap the four high bits with the four low bits and then do a bit-XOR with 0xA5.
605    fn write_obfuscated_password(dst: &mut impl BufMut, password: &str) {
606        for c in password.encode_utf16() {
607            let low = (c & 0xFF) as u8;
608            let high = ((c >> 8) & 0xFF) as u8;
609
610            // Step 1: Swap nibbles (rotate by 4 bits)
611            // Step 2: XOR with 0xA5
612            let low_enc = low.rotate_right(4) ^ 0xA5;
613            let high_enc = high.rotate_right(4) ^ 0xA5;
614
615            dst.put_u8(low_enc);
616            dst.put_u8(high_enc);
617        }
618    }
619}
620
621#[cfg(not(feature = "std"))]
622use alloc::string::String;
623#[cfg(not(feature = "std"))]
624use alloc::vec::Vec;
625
626#[cfg(test)]
627#[allow(clippy::unwrap_used)]
628mod tests {
629    use super::*;
630
631    #[test]
632    fn test_login7_default() {
633        let login = Login7::new();
634        assert_eq!(login.tds_version, TdsVersion::V7_4);
635        assert_eq!(login.packet_size, 4096);
636        assert!(login.option_flags2.odbc);
637    }
638
639    #[test]
640    fn test_login7_encode() {
641        let login = Login7::new()
642            .with_hostname("TESTHOST")
643            .with_sql_auth("testuser", "testpass")
644            .with_database("testdb")
645            .with_app_name("TestApp");
646
647        let encoded = login.encode();
648
649        // Check that the packet starts with a valid length
650        assert!(encoded.len() >= LOGIN7_HEADER_SIZE);
651
652        // Check TDS version at offset 4 (after length)
653        let tds_version = u32::from_le_bytes([encoded[4], encoded[5], encoded[6], encoded[7]]);
654        assert_eq!(tds_version, TdsVersion::V7_4.raw());
655    }
656
657    #[test]
658    fn test_password_obfuscation() {
659        // Known test case: "a" should encode to specific bytes
660        let mut buf = BytesMut::new();
661        Login7::write_obfuscated_password(&mut buf, "a");
662
663        // 'a' = 0x0061 in UTF-16LE
664        // Per MS-TDS: swap nibbles FIRST, then XOR with 0xA5
665        // Low byte: 0x61 swap nibbles = 0x16, XOR 0xA5 = 0xB3
666        // High byte: 0x00 swap nibbles = 0x00, XOR 0xA5 = 0xA5
667        assert_eq!(buf.len(), 2);
668        assert_eq!(buf[0], 0xB3);
669        assert_eq!(buf[1], 0xA5);
670    }
671
672    #[test]
673    fn test_option_flags() {
674        let flags1 = OptionFlags1::default();
675        assert_eq!(flags1.to_byte(), 0x00);
676
677        let flags2 = OptionFlags2 {
678            odbc: true,
679            integrated_security: true,
680            ..Default::default()
681        };
682        assert_eq!(flags2.to_byte(), 0x82);
683
684        let flags3 = OptionFlags3 {
685            extension: true,
686            ..Default::default()
687        };
688        assert_eq!(flags3.to_byte(), 0x10);
689    }
690}