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