Skip to main content

rusthound_ce/transport/
ldap.rs

1//! Run a LDAP enumeration and parse results
2//!
3//! This module will prepare your connection and request the LDAP server to retrieve all the information needed to create the json files.
4//!
5//! rusthound sends only one request to the LDAP server, if the result of this one is higher than the limit of the LDAP server limit it will be split in several requests to avoid having an error 4 (LDAP_SIZELIMIT_EXCEED).
6//!
7//! Example in rust
8//!
9//! ```ignore
10//! let search = ldap_search(...)
11//! ```
12
13// use crate::errors::Result;
14use crate::banner::progress_bar;
15use crate::storage::Storage;
16use crate::utils::format::domain_to_dc;
17
18use colored::Colorize;
19use indicatif::ProgressBar;
20use ldap3::adapters::{Adapter, EntriesOnly};
21use ldap3::exop::WhoAmI;
22use ldap3::{adapters::PagedResults, controls::RawControl, LdapConnAsync, LdapConnSettings};
23use ldap3::{Scope, SearchEntry};
24use log::{info, debug, error, trace};
25use std::io::{self, Write, stdin};
26use std::collections::HashMap;
27use std::error::Error;
28use std::process;
29
30/// Function to request all AD values.
31#[allow(clippy::too_many_arguments)]
32pub async fn ldap_search<S: Storage<LdapSearchEntry>>(
33    ldaps: bool,
34    ip: Option<&str>,
35    port: Option<u16>,
36    domain: &str,
37    ldapfqdn: Option<&str>,
38    username: Option<&str>,
39    password: Option<&str>,
40    hashes: Option<&str>,
41    kerberos: bool,
42    // Certificate authentication (Pass-the-Certificate / Schannel)
43    pfx: Option<&str>,
44    pfx_pass: Option<&str>,
45    crt: Option<&str>,
46    key: Option<&str>,
47    ldapfilter: &str,
48    storage: &mut S,
49) -> Result<usize, Box<dyn Error>> {
50    // Certificate authentication is enabled when a PFX or a CRT+KEY is provided.
51    let use_cert = pfx.is_some() || crt.is_some();
52
53    // Certificate auth transport:
54    //   - with --ldaps : LDAPS 636, implicit Schannel mapping, no bind.
55    //   - without      : LDAP 389 + StartTLS, SASL EXTERNAL bind (default).
56    // So StartTLS is used for cert auth unless --ldaps is explicitly requested.
57    let starttls = use_cert && !ldaps;
58
59    // Non-certificate modes keep the user-provided `ldaps` value unchanged;
60    // cert over LDAPS forces the ldaps scheme, cert over StartTLS keeps 389.
61    let effective_ldaps = if use_cert { !starttls } else { ldaps };
62
63    // Certificate transport needs a port consistent with the scheme, otherwise
64    // `prepare_ldap_url` (which also treats port 636 as LDAPS) would build an
65    // ldaps:// URL while StartTLS is enabled, or vice-versa. When the user does
66    // not pass an explicit port, pick the default for the chosen cert transport:
67    //   StartTLS -> 389, LDAPS -> 636. A user-supplied port is respected.
68    let effective_port = if use_cert {
69        port.or(Some(if starttls { 389 } else { 636 }))
70    } else {
71        port
72    };
73
74    // Construct LDAP args (URL scheme follows effective_ldaps)
75    let ldap_args = ldap_constructor(
76        effective_ldaps, ip, effective_port, domain, ldapfqdn, username, password, hashes, kerberos, use_cert,
77    )?;
78
79    // LDAP connection settings
80    let mut consettings = LdapConnSettings::new()
81        .set_conn_timeout(std::time::Duration::from_secs(10))
82        .set_no_tls_verify(true);
83
84    // Attach the client certificate config when doing certificate auth.
85    if use_cert {
86        let config = crate::transport::cert::build_client_config(pfx, pfx_pass, crt, key)?;
87        consettings = consettings.set_config(config);
88        if starttls {
89            consettings = consettings.set_starttls(true);
90        }
91    }
92
93    let (conn, mut ldap) = LdapConnAsync::with_settings(consettings, &ldap_args.s_url).await?;
94    ldap3::drive!(conn);
95
96    if use_cert {
97        // Pass-the-Certificate. AD maps the client certificate to an account at
98        // the TLS layer (Schannel). Over LDAPS the mapping is implicit (no bind);
99        // over StartTLS we authenticate with SASL EXTERNAL. In both cases the
100        // mapped identity is confirmed with a whoami before collection.
101        if starttls {
102            debug!("Trying certificate authentication (StartTLS + SASL EXTERNAL)");
103            match ldap.sasl_external_bind().await.and_then(|r| r.success()) {
104                Ok(_) => {}
105                Err(err) => {
106                    error!(
107                        "Certificate SASL EXTERNAL bind failed on {} Active Directory. \
108                         Some DCs refuse it over StartTLS; try LDAPS. Reason: {err}\n",
109                        domain.to_uppercase().bold().red()
110                    );
111                    process::exit(0x0100);
112                }
113            }
114        } else {
115            debug!("Trying certificate authentication (LDAPS, implicit Schannel mapping)");
116        }
117
118        // Confirm the mapped identity via whoami (both transports).
119        match ldap.extended(WhoAmI).await.map(|r| r.success()) {
120            Ok(Ok((exop, _))) => {
121                let who = exop
122                    .val
123                    .as_ref()
124                    .map(|v| String::from_utf8_lossy(v).to_string())
125                    .unwrap_or_default();
126                if who.is_empty() {
127                    error!(
128                        "Certificate not mapped by {} Active Directory (empty whoami). \
129                         Check the certificate SID and the DC enforcement mode (KB5014754).\n",
130                        domain.to_uppercase().bold().red()
131                    );
132                    process::exit(0x0100);
133                }
134                info!(
135                    "Connected to {} Active Directory via certificate as {}!",
136                    domain.to_uppercase().bold().green(),
137                    who.bold().green()
138                );
139                info!("Starting data collection...");
140            }
141            Ok(Err(err)) => {
142                error!(
143                    "Certificate authentication failed on {} Active Directory. Reason: {err}\n",
144                    domain.to_uppercase().bold().red()
145                );
146                process::exit(0x0100);
147            }
148            Err(err) => {
149                error!(
150                    "Certificate authentication request failed on {} Active Directory. Reason: {err}\n",
151                    domain.to_uppercase().bold().red()
152                );
153                process::exit(0x0100);
154            }
155        }
156    } else if let Some(ref ntlm_password) = ldap_args.s_ntlm_password {
157        debug!("Trying to connect with sasl_ntlm_bind() function (NTLM pass-the-hash)");
158        let res = ldap
159            .sasl_ntlm_bind(&ldap_args.s_username, ntlm_password)
160            .await?
161            .success();
162        match res {
163            Ok(_res) => {
164                info!(
165                    "Connected to {} Active Directory via NTLM!",
166                    domain.to_uppercase().bold().green()
167                );
168                info!("Starting data collection...");
169            }
170            Err(err) => {
171                error!(
172                    "Failed to authenticate to {} Active Directory via NTLM. Reason: {err}\n",
173                    domain.to_uppercase().bold().red()
174                );
175                process::exit(0x0100);
176            }
177        }
178    } else if !kerberos {
179        debug!("Trying to connect with simple_bind() function (username:password)");
180        let res = ldap
181            .simple_bind(&ldap_args.s_username, &ldap_args.s_password)
182            .await?
183            .success();
184        match res {
185            Ok(_res) => {
186                info!(
187                    "Connected to {} Active Directory!",
188                    domain.to_uppercase().bold().green()
189                );
190                info!("Starting data collection...");
191            }
192            Err(err) => {
193                error!(
194                    "Failed to authenticate to {} Active Directory. Reason: {err}\n",
195                    domain.to_uppercase().bold().red()
196                );
197                process::exit(0x0100);
198            }
199        }
200    } else {
201        debug!("Trying to connect with sasl_gssapi_bind() function (kerberos session)");
202        if let Some(fqdn) = ldapfqdn.filter(|f| !f.is_empty()) {
203            #[cfg(not(feature = "nogssapi"))]
204            gssapi_connection(&mut ldap, fqdn, &domain).await?;
205            #[cfg(feature = "nogssapi")]
206            {
207                error!("Kerberos auth and GSSAPI not compatible with current os!");
208                process::exit(0x0100);
209            }
210        } else {
211            error!(
212                "Need Domain Controller FQDN to bind GSSAPI connection. Please use '{}'\n",
213                "-f DC01.DOMAIN.LAB".bold()
214            );
215            process::exit(0x0100);
216        }
217    }
218
219    // // Prepare LDAP result vector
220    let mut total = 0; // for progress bar
221
222    // Request all namingContexts for current DC
223    let res = match get_all_naming_contexts(&mut ldap).await {
224        Ok(res) => {
225            trace!("naming_contexts: {:?}", &res);
226            res
227        }
228        Err(err) => {
229            error!("No namingContexts found! Reason: {err}\n");
230            process::exit(0x0100);
231        }
232    };
233
234    // namingContexts: DC=domain,DC=local
235    // namingContexts: CN=Configuration,DC=domain,DC=local (needed for AD CS datas)
236    if res.iter().any(|s| s.contains("Configuration")) {
237        for cn in &res {
238            //  Control 1: Set control LDAP_SERVER_SD_FLAGS_OID to get nTSecurityDescriptor
239            // https://ldapwiki.com/wiki/LDAP_SERVER_SD_FLAGS_OID
240            let sd_flags = RawControl {
241                ctype: String::from("1.2.840.113556.1.4.801"),
242                crit: true,
243                val: Some(vec![48, 3, 2, 1, 5]),    // SEQUENCE { INTEGER 5 }
244            };
245
246            // Control 2: LDAP_SERVER_SHOW_DELETED_OID (deleted objects)
247            // https://ldapwiki.com/wiki/IsDeleted 
248            let show_deleted = RawControl {
249                ctype: String::from("1.2.840.113556.1.4.417"),
250                crit: false,    // false ignored if not supported
251                val: None,
252            };
253
254            let controls = vec![sd_flags, show_deleted];
255            ldap.with_controls(controls.to_owned());
256
257            info!("Ldap filter : {}", ldapfilter.bold().green());
258            let _s_filter = ldapfilter;
259
260            // Every 999 max value in ldap response (err 4 ldap)
261            let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
262                Box::new(EntriesOnly::new()),
263                Box::new(PagedResults::new(999)),
264            ];
265
266            // Streaming search with adaptaters and filters
267            let mut search = ldap
268                .streaming_search_with(
269                    adapters, // Adapter which fetches Search results with a Paged Results control.
270                    cn,
271                    Scope::Subtree,
272                    _s_filter,
273                    vec!["*", "nTSecurityDescriptor"],
274                    // Without the presence of this control, the server returns an SD only when the SD attribute name is explicitly mentioned in the requested attribute list.
275                    // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/932a7a8d-8c93-4448-8093-c79b7d9ba499
276                )
277                .await?;
278
279            // Wait and get next values
280            let pb = ProgressBar::new(1);
281            let mut count = 0;
282            while let Some(entry) = search.next().await? {
283                let entry = SearchEntry::construct(entry);
284                //trace!("{:?}", &entry);
285                total += 1;
286                // Manage progress bar
287                count += 1;
288                progress_bar(
289                    pb.to_owned(),
290                    "LDAP objects retrieved".to_string(),
291                    count,
292                    "#".to_string(),
293                );
294
295                storage.add(entry.into())?;
296            }
297            pb.finish_and_clear();
298
299            let res = search.finish().await.success();
300            match res {
301                Ok(_res) => info!("All data collected for NamingContext {}", &cn.bold()),
302                Err(err) => {
303                    error!("No data collected on {}! Reason: {err}", &cn.bold().red());
304                }
305            }
306        }
307
308        ldap.unbind().await?;
309    }
310
311    // drop ldap before final flush,
312    // otherwise it will warn about an i/o error
313    // "LDAP connection error: I/O error: Connection reset by peer (os error 54)"
314    drop(ldap);
315    if total == 0 {
316        error!("No LDAP objects found! Exiting...");
317        process::exit(0x0100);
318    }
319
320    storage.flush()?;
321
322    // Return the vector with the result
323    Ok(total)
324}
325
326/// Structure containing the LDAP connection arguments.
327struct LdapArgs {
328    s_url: String,
329    _s_dc: Vec<String>,
330    _s_email: String,
331    s_username: String,
332    s_password: String,
333    s_ntlm_password: Option<String>,
334}
335
336/// Function to prepare LDAP arguments.
337#[allow(clippy::too_many_arguments)]
338fn ldap_constructor(
339    ldaps: bool,
340    ip: Option<&str>,
341    port: Option<u16>,
342    domain: &str,
343    ldapfqdn: Option<&str>,
344    username: Option<&str>,
345    password: Option<&str>,
346    hashes: Option<&str>,
347    kerberos: bool,
348    // When true, no username/password is needed (identity comes from the cert)
349    use_cert: bool,
350) -> Result<LdapArgs, Box<dyn Error>> {
351    // Prepare ldap url
352    let s_url = prepare_ldap_url(ldaps, ip, port, domain);
353
354    // Prepare full DC chain
355    let s_dc = prepare_ldap_dc(domain);
356
357    let use_ntlm = hashes.is_some();
358
359    // Username prompt (skipped for certificate auth: identity comes from the cert)
360    let mut s = String::new();
361    let mut _s_username: String;
362    if username.is_none() && !kerberos && !use_cert {
363        print!("Username: ");
364        io::stdout().flush()?;
365        stdin()
366            .read_line(&mut s)
367            .expect("Did not enter a correct username");
368        io::stdout().flush()?;
369        if let Some('\n') = s.chars().next_back() {
370            s.pop();
371        }
372        if let Some('\r') = s.chars().next_back() {
373            s.pop();
374        }
375        _s_username = s.to_owned();
376    } else {
377        _s_username = username.unwrap_or("not set").to_owned();
378    }
379
380    // Format username and email
381    let mut s_email: String = "".to_owned();
382    if !_s_username.contains("@") {
383        s_email.push_str(&_s_username.to_string());
384        s_email.push_str("@");
385        s_email.push_str(domain);
386        if !use_ntlm {
387            _s_username = s_email.to_string();
388        }
389    } else {
390        s_email = _s_username.to_string().to_lowercase();
391    }
392
393    // For NTLM, format username as DOMAIN\user for sspi
394    if use_ntlm && !_s_username.contains("\\") && !_s_username.contains("@") {
395        let domain_upper = domain.split('.').next().unwrap_or(domain).to_uppercase();
396        _s_username = format!("{}\\{}", domain_upper, _s_username);
397    }
398
399    // Validate and build NTLM password from NT hash if provided
400    let s_ntlm_password = match hashes {
401        Some(hash) => {
402            let clean = hash.trim();
403            // Accept [NTHASH, :NTHASH, LMHASH:NTHASH]
404            let nt = match clean.split_once(':') {
405                Some((_lm, nt)) => nt,
406                None => clean,
407            };
408            if nt.len() != 32 || !nt.chars().all(|c| c.is_ascii_hexdigit()) {
409                error!("Invalid NT hash: must be exactly 32 hex characters (e.g. aad3b435b51404eeaad3b435b51404ee)");
410                process::exit(0x0100);
411            }
412            Some(nt_hash_to_ntlm_password(nt))
413        }
414        None => None,
415    };
416
417    // Password prompt (skip for NTLM hash, Kerberos, and certificate auth)
418    let mut _s_password: String = String::new();
419    if !use_ntlm && !_s_username.contains("not set") && !kerberos && !use_cert {
420        _s_password = match password {
421            Some(p) => p.to_owned(),
422            None => rpassword::prompt_password("Password: ").unwrap_or("not set".to_string()),
423        };
424    } else {
425        _s_password = password.unwrap_or("not set").to_owned();
426    }
427    
428    // Print infos if verbose mod is set
429    debug!("IP: {}", match ip {
430        Some(ip) => ip,
431        None => "not set"
432    });
433    debug!("PORT: {}", match port {
434        Some(p) => {
435            p.to_string()
436        },
437        None => "not set".to_owned()
438    });
439    debug!("FQDN: {}", ldapfqdn.unwrap_or("not set"));
440    debug!("Url: {}", s_url);
441    debug!("Domain: {}", domain);
442    debug!("Username: {}", _s_username);
443    debug!("Email: {}", s_email.to_lowercase());
444    if use_cert {
445        debug!("Auth: certificate (Pass-the-Certificate)");
446    } else if use_ntlm {
447        debug!("Auth: NTLM pass-the-hash");
448    } else {
449        debug!("Password: {}", _s_password);
450    }
451    debug!("DC: {:?}", s_dc);
452    debug!("Kerberos: {:?}", kerberos);
453
454    Ok(LdapArgs {
455        s_url: s_url.to_string(),
456        _s_dc: s_dc,
457        _s_email: s_email.to_string().to_lowercase(),
458        s_username: if use_ntlm {
459            _s_username.to_string()
460        } else {
461            s_email.to_string().to_lowercase()
462        },
463        s_password: _s_password.to_string(),
464        s_ntlm_password,
465    })
466}
467
468/// Encode an NT hash into a password string that triggers pass-the-hash
469/// in the sspi crate's NTLM implementation.
470fn nt_hash_to_ntlm_password(hex_hash: &str) -> String {
471    let upper = hex_hash.to_uppercase();
472    let bytes = upper.as_bytes();
473
474    let mut password = String::new();
475
476    for pair in bytes.chunks(2) {
477        let low_byte = pair[0] as u32;
478        let high_byte = if pair.len() > 1 { pair[1] as u32 } else { 0 };
479        let code_point = (high_byte << 8) | low_byte;
480        password.push(char::from_u32(code_point).unwrap_or('\0'));
481    }
482
483    // Pad to exceed the 512-byte SSPI_CREDENTIALS_HASH_LENGTH_OFFSET
484    for _ in 0..256 {
485        password.push('\0');
486    }
487
488    password
489}
490
491/// Function to prepare LDAP url.
492fn prepare_ldap_url(
493    ldaps: bool,
494    ip: Option<&str>,
495    port: Option<u16>,
496    domain: &str
497) -> String {
498    let protocol = if ldaps || port.unwrap_or(0) == 636 {
499        "ldaps"
500    } else {
501        "ldap"
502    };
503
504    let target = match ip {
505        Some(ip) => ip,
506        None => domain,
507    };
508
509    match port {
510        Some(port) => {
511            format!("{protocol}://{target}:{port}")
512        }
513        None => {
514            format!("{protocol}://{target}")
515        }
516    }
517}
518
519/// Function to prepare LDAP DC from DOMAIN.LOCAL
520pub fn prepare_ldap_dc(domain: &str) -> Vec<String> {
521
522    let mut dc: String = "".to_owned();
523    let mut naming_context: Vec<String> = Vec::new();
524
525    // Format DC
526    if !domain.contains(".") {
527        dc.push_str("DC=");
528        dc.push_str(domain);
529        naming_context.push(dc[..].to_string());
530    }
531    else {
532        naming_context.push(domain_to_dc(domain));
533    }
534
535    // For ADCS values
536    naming_context.push(format!("{}{}", "CN=Configuration,", &dc[..])); 
537    naming_context
538}
539
540/// Function to make GSSAPI ldap connection.
541#[cfg(not(feature = "nogssapi"))]
542async fn gssapi_connection(
543    ldap: &mut ldap3::Ldap,
544    ldapfqdn: &str,
545    domain: &str,
546) -> Result<(), Box<dyn Error>> {
547    let res = ldap.sasl_gssapi_bind(ldapfqdn).await?.success();
548    match res {
549        Ok(_res) => {
550            info!("Connected to {} Active Directory!", domain.to_uppercase().bold().green());
551            info!("Starting data collection...");
552        }
553        Err(err) => {
554            error!("Failed to authenticate to {} Active Directory. Reason: {err}\n", domain.to_uppercase().bold().red());
555            process::exit(0x0100);
556        }
557    }
558    Ok(())
559}
560
561/// Get all namingContext for DC
562pub async fn get_all_naming_contexts(
563    ldap: &mut ldap3::Ldap
564) -> Result<Vec<String>, Box<dyn Error>> {
565    // Every 999 max value in ldap response (err 4 ldap)
566    let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
567        Box::new(EntriesOnly::new()),
568        Box::new(PagedResults::new(999)),
569    ];
570
571    // First LDAP request to get all namingContext
572    let mut search = ldap.streaming_search_with(
573        adapters,
574        "", 
575        Scope::Base,
576        "(objectClass=*)",
577        vec!["namingContexts"],
578    ).await?;
579
580    // Prepare LDAP result vector
581    let mut rs: Vec<SearchEntry> = Vec::new();
582    while let Some(entry) = search.next().await? {
583        let entry = SearchEntry::construct(entry);
584        rs.push(entry);
585    }
586    let res = search.finish().await.success();
587
588    // Prepare vector for all namingContexts result
589    let mut naming_contexts: Vec<String> = Vec::new();
590    match res {
591        Ok(_res) => {
592            debug!("All namingContexts collected!");
593            for result in rs {
594                let result_attrs: HashMap<String, Vec<String>> = result.attrs;
595
596                for (_key, value) in &result_attrs {
597                    for naming_context in value {
598                        debug!("namingContext found: {}",&naming_context.bold().green());
599                        naming_contexts.push(naming_context.to_string());
600                    }
601                }
602            }
603            
604            // Put CN=Schema first so schema_guid_map is complete before ACEs are parsed
605            naming_contexts.sort_by_key(|cn| {
606                if cn.contains("CN=Schema") { 0 }
607                else if cn.to_lowercase().starts_with("dc=") { 1 }
608                else if cn.contains("CN=Configuration") { 2 }
609                else { 3 }
610            });
611
612            // Trace sorted naming contexts order
613            for (i, nc) in naming_contexts.iter().enumerate() {
614                trace!("NamingContext order [{}]: {}", i, nc);
615            }
616
617            return Ok(naming_contexts)
618        }
619        Err(err) => {
620            error!("No namingContexts found! Reason: {err}");
621        }
622    }
623    // Empty result if no namingContexts found
624    Ok(Vec::new())
625}
626
627// New type to implement Serialize and Deserialize for SearchEntry
628#[derive(Debug, Clone, bincode::Encode, bincode::Decode)]
629pub struct LdapSearchEntry {
630    /// Entry DN.
631    pub dn: String,
632    /// Attributes.
633    pub attrs: HashMap<String, Vec<String>>,
634    /// Binary-valued attributes.
635    pub bin_attrs: HashMap<String, Vec<Vec<u8>>>,
636}
637
638impl From<SearchEntry> for LdapSearchEntry {
639    fn from(entry: SearchEntry) -> Self {
640        LdapSearchEntry {
641            dn: entry.dn,
642            attrs: entry.attrs,
643            bin_attrs: entry.bin_attrs,
644        }
645    }
646}
647
648impl From<LdapSearchEntry> for SearchEntry {
649    fn from(entry: LdapSearchEntry) -> Self {
650        SearchEntry {
651            dn: entry.dn,
652            attrs: entry.attrs,
653            bin_attrs: entry.bin_attrs,
654        }
655    }
656}
657
658#[cfg(test)]
659mod tests {
660    use super::*;
661
662    #[test]
663    fn nt_hash_encoding_roundtrip() {
664        let hash = "aad3b435b51404eeaad3b435b51404ee";
665        let password = nt_hash_to_ntlm_password(hash);
666
667        let utf16_bytes: Vec<u8> = password
668            .encode_utf16()
669            .flat_map(|u| u.to_le_bytes())
670            .collect();
671
672        assert!(utf16_bytes.len() > 512);
673
674        let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
675        assert_eq!(hash_portion.len(), 32);
676
677        let expected_hex = hash.to_uppercase();
678        let expected_bytes = expected_hex.as_bytes();
679        assert_eq!(hash_portion, expected_bytes);
680    }
681
682    #[test]
683    fn nt_hash_encoding_all_zeros() {
684        let hash = "00000000000000000000000000000000";
685        let password = nt_hash_to_ntlm_password(hash);
686
687        let utf16_bytes: Vec<u8> = password
688            .encode_utf16()
689            .flat_map(|u| u.to_le_bytes())
690            .collect();
691
692        assert!(utf16_bytes.len() > 512);
693        let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
694        assert_eq!(hash_portion, b"00000000000000000000000000000000");
695    }
696
697    #[test]
698    fn nt_hash_encoding_all_f() {
699        let hash = "ffffffffffffffffffffffffffffffff";
700        let password = nt_hash_to_ntlm_password(hash);
701
702        let utf16_bytes: Vec<u8> = password
703            .encode_utf16()
704            .flat_map(|u| u.to_le_bytes())
705            .collect();
706
707        assert!(utf16_bytes.len() > 512);
708        let hash_portion = &utf16_bytes[..utf16_bytes.len() - 512];
709        assert_eq!(hash_portion, b"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
710    }
711}