Skip to main content

dpapi_core/
masterkey.rs

1//! DPAPI **master-key file** parser + user master-key derivation.
2//!
3//! This is the on-disk counterpart of the LSASS master-key source: the same
4//! 64-byte master key that [`crate::decrypt_dpapi_blob`] consumes, but recovered
5//! from a master-key *file* (`%APPDATA%\Microsoft\Protect\<SID>\<GUID>`) plus the
6//! user's password (or pre-hashed SHA1), rather than read from LSASS memory.
7//!
8//! Layout and crypto follow impacket's `MasterKeyFile` / `MasterKey.decrypt`
9//! (impacket 0.13.1, `impacket/dpapi.py`). The master-key file is:
10//!
11//! * a fixed 128-byte header — `Version(4)`, `unk1(4)`, `unk2(4)`, `Guid(72,
12//!   UTF-16LE)`, `Unknown(4)`, `Policy(4)`, `Flags(4)`, then four `<u64` length
13//!   fields: `MasterKeyLen`, `BackupKeyLen`, `CredHistLen`, `DomainKeyLen`;
14//! * followed by the four sub-blobs in that order, each exactly its length.
15//!
16//! The **`MasterKey`** sub-blob is itself `Version(4)`, `Salt(16)`,
17//! `IterationCount(4)`, `HashAlgo(4)`, `CryptAlgo(4)`, then the encrypted `data`.
18//!
19//! Derivation (`MasterKey.decrypt`): `deriveKey(preKey, salt, keyLen+ivLen,
20//! rounds, prf=HMAC_H)` — impacket's iterated XOR construction, NOT standard
21//! PBKDF2 — yields `cryptKey || iv`; CBC-decrypt `data`; the trailing 64 bytes are
22//! the master key, the leading 16 are `hmacSalt`, and the next `digestLen` bytes
23//! are an HMAC verified via `HMAC_H(HMAC_H(preKey, hmacSalt), masterKey)`.
24//!
25//! The pre-key for the user path is
26//! `HMAC-SHA1(SHA1(UTF16LE(password)), UTF16LE(sid + "\0"))`
27//! (impacket `deriveKeysFromUser`, SHA1 variant).
28//!
29//! All cryptography uses audited RustCrypto crates — no hand-rolled primitives.
30
31use aes::Aes256;
32use cbc::Decryptor as CbcDec;
33use cipher::{block_padding::NoPadding, BlockDecryptMut, KeyIvInit};
34use des::TdesEde3;
35use hmac::{Hmac, Mac};
36use sha1::{Digest, Sha1};
37
38use forensicnomicon::dpapi::{
39    cipher_alg_info, hash_alg_info, CALG_AES_256, CALG_HMAC, CALG_SHA1, CALG_SHA_512,
40};
41
42use crate::blob::{decode_utf16le, hash_alg};
43use crate::decrypt::hmac_hash;
44use crate::error::DpapiError;
45
46/// Fixed master-key file header size (impacket `len(MasterKeyFile)`).
47const HEADER_LEN: usize = 128;
48/// Size of the decrypted DPAPI master key consumed by blob decryption.
49pub const MASTER_KEY_LEN: usize = 64;
50
51/// A parsed DPAPI master-key file, mirroring impacket's `MasterKeyFile`.
52///
53/// The four sub-blobs are exposed as owned byte vectors (each may be empty when
54/// its length field is zero). `master_key` is the only one needed for the user
55/// password path; `domain_key` feeds the (not-yet-implemented) RSA backup path.
56#[derive(Debug, Clone)]
57pub struct MasterKeyFile {
58    pub version: u32,
59    /// The key GUID as stored (UTF-16LE, NUL-trimmed) — matches the file name.
60    pub guid: String,
61    pub policy: u32,
62    pub flags: u32,
63    pub master_key: Vec<u8>,
64    pub backup_key: Vec<u8>,
65    pub cred_hist: Vec<u8>,
66    pub domain_key: Vec<u8>,
67}
68
69/// The `MasterKey` sub-blob: salt + rounds + alg IDs + encrypted payload.
70#[derive(Debug, Clone)]
71pub struct MasterKey {
72    pub version: u32,
73    pub salt: [u8; 16],
74    pub rounds: u32,
75    pub alg_id_hash: u32,
76    pub alg_id_encrypt: u32,
77    pub data: Vec<u8>,
78}
79
80/// Read a little-endian `u32` at `*pos`, advancing by 4. Out-of-range → error.
81fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32, DpapiError> {
82    let slice: [u8; 4] = data
83        .get(*pos..*pos + 4)
84        .and_then(|s| s.try_into().ok())
85        .ok_or(DpapiError::TooShort {
86            needed: *pos + 4,
87            got: data.len(),
88        })?;
89    *pos += 4;
90    Ok(u32::from_le_bytes(slice))
91}
92
93/// Read a little-endian `u64` at `*pos`, advancing by 8. Out-of-range → error.
94fn read_u64(data: &[u8], pos: &mut usize) -> Result<u64, DpapiError> {
95    let slice: [u8; 8] = data
96        .get(*pos..*pos + 8)
97        .and_then(|s| s.try_into().ok())
98        .ok_or(DpapiError::TooShort {
99            needed: *pos + 8,
100            got: data.len(),
101        })?;
102    *pos += 8;
103    Ok(u64::from_le_bytes(slice))
104}
105
106/// Parse a DPAPI master-key file (impacket `MasterKeyFile` layout).
107///
108/// Validates the 128-byte header is present and that the four declared sub-blob
109/// lengths fit the buffer; a truncated file is rejected loudly with the byte
110/// counts rather than silently yielding a short sub-blob.
111pub fn parse_masterkey_file(data: &[u8]) -> Result<MasterKeyFile, DpapiError> {
112    if data.len() < HEADER_LEN {
113        return Err(DpapiError::TooShort {
114            needed: HEADER_LEN,
115            got: data.len(),
116        });
117    }
118
119    let mut pos = 0usize;
120    let version = read_u32(data, &mut pos)?;
121    let _unk1 = read_u32(data, &mut pos)?;
122    let _unk2 = read_u32(data, &mut pos)?;
123    let guid = decode_utf16le(&data[pos..pos + 72]);
124    pos += 72;
125    let _unknown = read_u32(data, &mut pos)?;
126    let policy = read_u32(data, &mut pos)?;
127    let flags = read_u32(data, &mut pos)?;
128    let master_key_len = read_u64(data, &mut pos)? as usize;
129    let backup_key_len = read_u64(data, &mut pos)? as usize;
130    let cred_hist_len = read_u64(data, &mut pos)? as usize;
131    let domain_key_len = read_u64(data, &mut pos)? as usize;
132    debug_assert_eq!(pos, HEADER_LEN);
133
134    let mut take = |len: usize| -> Result<Vec<u8>, DpapiError> {
135        let slice = data.get(pos..pos + len).ok_or(DpapiError::TooShort {
136            needed: pos + len,
137            got: data.len(),
138        })?;
139        pos += len;
140        Ok(slice.to_vec())
141    };
142
143    let master_key = take(master_key_len)?;
144    let backup_key = take(backup_key_len)?;
145    let cred_hist = take(cred_hist_len)?;
146    let domain_key = take(domain_key_len)?;
147
148    Ok(MasterKeyFile {
149        version,
150        guid,
151        policy,
152        flags,
153        master_key,
154        backup_key,
155        cred_hist,
156        domain_key,
157    })
158}
159
160/// Parse the `MasterKey` sub-blob (impacket `MasterKey` structure).
161pub fn parse_master_key(data: &[u8]) -> Result<MasterKey, DpapiError> {
162    // Version(4) + Salt(16) + Rounds(4) + HashAlgo(4) + CryptAlgo(4) = 32.
163    if data.len() < 32 {
164        return Err(DpapiError::TooShort {
165            needed: 32,
166            got: data.len(),
167        });
168    }
169    let mut pos = 0usize;
170    let version = read_u32(data, &mut pos)?;
171    let mut salt = [0u8; 16];
172    salt.copy_from_slice(&data[pos..pos + 16]);
173    pos += 16;
174    let rounds = read_u32(data, &mut pos)?;
175    let alg_id_hash = read_u32(data, &mut pos)?;
176    let alg_id_encrypt = read_u32(data, &mut pos)?;
177    let payload = data[pos..].to_vec();
178    Ok(MasterKey {
179        version,
180        salt,
181        rounds,
182        alg_id_hash,
183        alg_id_encrypt,
184        data: payload,
185    })
186}
187
188/// Whether the master-key sub-blob's hash module is SHA-512 (vs SHA-1).
189///
190/// Per impacket `MasterKey.decrypt`: `CALG_HMAC` (0x8009) forces the SHA-1
191/// module; every other recognised `HashAlgo` uses its table module — SHA-512 for
192/// `CALG_SHA_512` (0x800e), SHA-1 for `CALG_SHA` (0x8004).
193fn uses_sha512(alg_id_hash: u32) -> Result<bool, DpapiError> {
194    if alg_id_hash == CALG_HMAC {
195        return Ok(false);
196    }
197    hash_alg_info(alg_id_hash)
198        .map(|h| h.is_sha512)
199        .ok_or(DpapiError::UnsupportedAlgId(alg_id_hash))
200}
201
202/// PRF = HMAC over the chosen hash module (SHA-512 vs SHA-1).
203///
204/// Delegates the RustCrypto HMAC wiring to [`crate::decrypt::hmac_hash`] — the
205/// same construction the blob decryptor uses — selecting the module via the
206/// canonical `algId` for the chosen width so a single keyed-HMAC implementation
207/// serves both the on-disk (master-key file) and in-memory (blob) paths.
208fn prf(is_sha512: bool, key: &[u8], msg: &[u8]) -> Result<Vec<u8>, DpapiError> {
209    let alg = hash_alg(if is_sha512 { CALG_SHA_512 } else { CALG_SHA1 });
210    hmac_hash(alg, key, msg)
211}
212
213/// impacket `MasterKey.deriveKey`: iterated key material of length `keylen`.
214///
215/// For each block `i = 1..`: `derived = prf(passphrase, salt || BE32(i))`, then
216/// repeated `count-1` times `derived ^= prf(passphrase, derived)` (full-width
217/// little-endian XOR of equal-length digests). Blocks are concatenated until at
218/// least `keylen` bytes, then truncated. This is the DPAPI variant, distinct from
219/// standard PBKDF2.
220fn derive_key(
221    is_sha512: bool,
222    passphrase: &[u8],
223    salt: &[u8],
224    keylen: usize,
225    count: u32,
226) -> Result<Vec<u8>, DpapiError> {
227    let mut key_material: Vec<u8> = Vec::with_capacity(keylen + 64);
228    let mut i: u32 = 1;
229    while key_material.len() < keylen {
230        let mut u = salt.to_vec();
231        u.extend_from_slice(&i.to_be_bytes());
232        i += 1;
233        let mut derived = prf(is_sha512, passphrase, &u)?;
234        for _ in 0..count.saturating_sub(1) {
235            let actual = prf(is_sha512, passphrase, &derived)?;
236            for (d, a) in derived.iter_mut().zip(actual.iter()) {
237                *d ^= a;
238            }
239        }
240        key_material.extend_from_slice(&derived);
241    }
242    key_material.truncate(keylen);
243    Ok(key_material)
244}
245
246/// Derive the 64-byte master key from a master-key sub-blob and a **pre-key**.
247///
248/// `pre_key` is the per-user pre-key (impacket's `deriveKeysFromUser` output for
249/// the password path, or LSA `UserKey`/`MachineKey` for the SYSTEM path). Mirrors
250/// impacket `MasterKey.decrypt`: derive `cryptKey || iv`, CBC-decrypt, take the
251/// trailing 64 bytes, and verify the embedded HMAC; an HMAC mismatch (wrong
252/// pre-key) is rejected with [`DpapiError::HmacMismatch`] rather than returning
253/// garbage.
254pub fn derive_master_key_from_prekey(
255    mk: &MasterKey,
256    pre_key: &[u8],
257) -> Result<[u8; MASTER_KEY_LEN], DpapiError> {
258    let is_sha512 = uses_sha512(mk.alg_id_hash)?;
259    let cipher = cipher_alg_info(mk.alg_id_encrypt)
260        .ok_or(DpapiError::UnsupportedAlgId(mk.alg_id_encrypt))?;
261    let digest_len = hash_alg_info(mk.alg_id_hash)
262        .ok_or(DpapiError::UnsupportedAlgId(mk.alg_id_hash))?
263        .digest_len;
264
265    let derived = derive_key(
266        is_sha512,
267        pre_key,
268        &mk.salt,
269        cipher.key_len + cipher.iv_len,
270        mk.rounds,
271    )?;
272    let crypt_key = &derived[..cipher.key_len];
273    let iv = &derived[cipher.key_len..cipher.key_len + cipher.iv_len];
274
275    let cleartext = if mk.alg_id_encrypt == CALG_AES_256 {
276        cbc_decrypt_no_pad::<Aes256>(crypt_key, iv, &mk.data)?
277    } else {
278        cbc_decrypt_no_pad::<TdesEde3>(crypt_key, iv, &mk.data)?
279    };
280
281    if cleartext.len() < MASTER_KEY_LEN || cleartext.len() < 16 + digest_len {
282        return Err(DpapiError::DecryptionFailed);
283    }
284    let master_key_bytes = &cleartext[cleartext.len() - MASTER_KEY_LEN..];
285    let hmac_salt = &cleartext[..16];
286    let stored_hmac = &cleartext[16..16 + digest_len];
287
288    // hmacKey = HMAC_H(preKey, hmacSalt); calc = HMAC_H(hmacKey, masterKey)
289    let hmac_key = prf(is_sha512, pre_key, hmac_salt)?;
290    let calc = prf(is_sha512, &hmac_key, master_key_bytes)?;
291    if calc.get(..digest_len) != Some(stored_hmac) {
292        return Err(DpapiError::HmacMismatch);
293    }
294
295    let mut out = [0u8; MASTER_KEY_LEN];
296    out.copy_from_slice(master_key_bytes);
297    Ok(out)
298}
299
300/// CBC-decrypt without unpadding (the DPAPI master-key payload is block-aligned
301/// and carries no PKCS#7 padding — the structure, not a pad byte, bounds it).
302fn cbc_decrypt_no_pad<C>(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, DpapiError>
303where
304    C: cipher::BlockCipher + cipher::BlockDecryptMut + cipher::KeyInit + cipher::BlockSizeUser,
305{
306    let mut buf = ciphertext.to_vec();
307    let dec = CbcDec::<C>::new_from_slices(key, iv).map_err(|_| DpapiError::InvalidKeyLength)?;
308    let out = dec
309        .decrypt_padded_mut::<NoPadding>(&mut buf)
310        .map_err(|_| DpapiError::DecryptionFailed)?;
311    Ok(out.to_vec())
312}
313
314/// Derive the per-user **pre-key** from a SID and a pre-hashed SHA-1 password.
315///
316/// `pwd_sha1` is `SHA1(UTF16LE(password))`. Returns
317/// `HMAC-SHA1(pwd_sha1, UTF16LE(sid + "\0"))` — impacket `deriveKeysFromUser`
318/// key1 (the SHA-1 variant). This is the value to pass to
319/// [`derive_master_key_from_prekey`] for a modern (Vista+) profile.
320pub fn prekey_from_sha1(sid: &str, pwd_sha1: &[u8; 20]) -> Result<[u8; 20], DpapiError> {
321    let sid_utf16 = utf16le_with_nul(sid);
322    let mut mac =
323        Hmac::<Sha1>::new_from_slice(pwd_sha1).map_err(|_| DpapiError::InvalidKeyLength)?;
324    mac.update(&sid_utf16);
325    let out = mac.finalize().into_bytes();
326    let mut k = [0u8; 20];
327    k.copy_from_slice(&out);
328    Ok(k)
329}
330
331/// Derive the pre-key directly from a plaintext password and SID.
332///
333/// `pwd_sha1 = SHA1(UTF16LE(password))`, then [`prekey_from_sha1`].
334pub fn prekey_from_password(sid: &str, password: &str) -> Result<[u8; 20], DpapiError> {
335    let mut h = Sha1::new();
336    h.update(utf16le(password));
337    let digest: [u8; 20] = h.finalize().into();
338    prekey_from_sha1(sid, &digest)
339}
340
341/// Full user-password path: parse the file, derive the pre-key, decrypt the key.
342///
343/// Convenience over the sub-steps for the common case (a single master-key file
344/// + the user's password). Returns the 64-byte master key on success.
345pub fn derive_master_key_from_password(
346    file: &MasterKeyFile,
347    sid: &str,
348    password: &str,
349) -> Result<[u8; MASTER_KEY_LEN], DpapiError> {
350    let mk = parse_master_key(&file.master_key)?;
351    let pre_key = prekey_from_password(sid, password)?;
352    derive_master_key_from_prekey(&mk, &pre_key)
353}
354
355/// Decrypt the master key via the **domain RSA backup key** (`DomainKey` sub-blob).
356///
357/// Next sub-step, not implemented this pass: the `DomainKey` sub-blob holds the
358/// master key wrapped with the domain controller's RSA *backup* key (the
359/// `DPAPI_DOMAIN_RSA_MASTER_KEY` structure, decrypted with the DC's `.pvk`
360/// private key, reversed, then RSA-decrypted — see impacket
361/// `DPAPI_DOMAIN_RSA_MASTER_KEY` + `privatekeyblob_to_pkcs1`). It needs an RSA
362/// implementation, which is out of scope here; this refuses loudly rather than
363/// fabricating a key.
364pub fn derive_master_key_from_domain_backup(
365    _domain_key: &[u8],
366    _pvk: &[u8],
367) -> Result<[u8; MASTER_KEY_LEN], DpapiError> {
368    Err(DpapiError::DomainBackupUnsupported)
369}
370
371/// Encode `s` as UTF-16LE bytes (no trailing NUL).
372fn utf16le(s: &str) -> Vec<u8> {
373    s.encode_utf16().flat_map(u16::to_le_bytes).collect()
374}
375
376/// Encode `s + "\0"` as UTF-16LE bytes (the SID form impacket HMACs over).
377fn utf16le_with_nul(s: &str) -> Vec<u8> {
378    let mut v = utf16le(s);
379    v.extend_from_slice(&[0, 0]);
380    v
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    fn hex(s: &str) -> Vec<u8> {
388        (0..s.len())
389            .step_by(2)
390            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
391            .collect()
392    }
393
394    // ── Independent oracle ──────────────────────────────────────────────────
395    // impacket 0.13.1 `tests/misc/test_dpapi.py::DPAPITests::systemMasterKeyFile`:
396    // a real Windows master-key file (GUID ea95eba8-…) whose 64-byte master key
397    // impacket recovers from the LSA `UserKey` pre-key and asserts against a
398    // value extracted with secretsdump/mimikatz. Confirmed locally:
399    //   $ python3 -c 'from impacket.dpapi import MasterKeyFile, MasterKey; ...'
400    //   decrypted == systemMasterKey  -> True
401    // HashAlgo=0x800e (SHA-512), CryptAlgo=0x6610 (AES-256-CBC), rounds=17400.
402    const SYSTEM_MK_FILE: &str = "020000000000000000000000650061003900350065006200610038002d0062006100300030002d0034006500310061002d0062003400330066002d00350031006500610033003000310037003100640031003100000000000000000006000000b00000000000000090000000000000001400000000000000000000000000000002000000f42f61ea0c9647bf403819898452089ff84300000e80000010660000e2441dec11a6b6f03ffebba1e71473f78da46a52b12caff7df9925ed6ac89d84050ad15cfe88ec50ece201e5a80eb198909ff8c781510f78859b96cfade433c83a7f3fc19926b0280e6a196ef1b0b5e1b3c1ab426120da53f24e5989f8a7d3dde86ac444901401a6df6407f550d197ff27c91abb5c331250b5a7ce58c1f61fd0d656360df58e6f5b5faac639661aa89402000000bdc1f9592357353a2a3ebcf8cedc72bbf84300000e800000106600001231d67ec689fea9f6de6bd66a21e28d5405232df71deae5bf3ab63c6cb2cac08ad17456979d72b70de13afc2b61d05434161191dcdfb24aaac7ed0275f71eff9f936a559aff4be6301ba99d66bd8e07b1a325c73c7a4da97117f80551a3f0a75da9fcc37c4d2bb43b5a9ef1684446ae0300000000000000000000000000000000000000";
403    const SYSTEM_USER_KEY: &str = "458dc597034d8801fc6fe3b342817caabb81a0cb";
404    const SYSTEM_MASTER_KEY: &str = "682a9b8923ff4ca7ce0ef7e4cee061f0ff942cd31c7703ec60792740b2e7d0b1b5115d1ff77e10b77e189e0d6e99d5b668190ecd44fa84e82e049f406e2c2a59";
405
406    #[test]
407    fn parse_system_masterkey_file_header() {
408        let f = parse_masterkey_file(&hex(SYSTEM_MK_FILE)).expect("parse file");
409        assert_eq!(f.version, 2);
410        assert_eq!(f.guid, "ea95eba8-ba00-4e1a-b43f-51ea30171d11");
411        assert_eq!(f.flags, 0x6);
412        assert_eq!(f.master_key.len(), 176);
413        assert_eq!(f.backup_key.len(), 144);
414        assert_eq!(f.cred_hist.len(), 20);
415        assert_eq!(f.domain_key.len(), 0);
416    }
417
418    #[test]
419    fn parse_system_master_key_subblob_fields() {
420        let f = parse_masterkey_file(&hex(SYSTEM_MK_FILE)).expect("parse file");
421        let mk = parse_master_key(&f.master_key).expect("parse mk");
422        assert_eq!(mk.version, 2);
423        assert_eq!(mk.rounds, 17400);
424        assert_eq!(mk.alg_id_hash, 0x800e);
425        assert_eq!(mk.alg_id_encrypt, 0x6610);
426        assert_eq!(mk.salt.to_vec(), hex("f42f61ea0c9647bf403819898452089f"));
427    }
428
429    // Tier-2 (impacket-anchored): the derived 64-byte master key MUST equal the
430    // value impacket's MasterKey.decrypt produces from the same pre-key.
431    #[test]
432    fn derive_system_master_key_matches_impacket() {
433        let f = parse_masterkey_file(&hex(SYSTEM_MK_FILE)).expect("parse file");
434        let mk = parse_master_key(&f.master_key).expect("parse mk");
435        let pre_key = hex(SYSTEM_USER_KEY);
436        let derived = derive_master_key_from_prekey(&mk, &pre_key).expect("derive");
437        assert_eq!(derived.to_vec(), hex(SYSTEM_MASTER_KEY));
438    }
439
440    #[test]
441    fn wrong_prekey_fails_hmac() {
442        let f = parse_masterkey_file(&hex(SYSTEM_MK_FILE)).expect("parse file");
443        let mk = parse_master_key(&f.master_key).expect("parse mk");
444        let bad = [0xABu8; 20];
445        assert!(matches!(
446            derive_master_key_from_prekey(&mk, &bad),
447            Err(DpapiError::HmacMismatch)
448        ));
449    }
450
451    // impacket `deriveKeysFromUser(sid, password)` key1 (SHA-1 path), confirmed:
452    //   sid="S-1-5-21-1455520393-2011455520393-2019809541-4133251990-500",
453    //   password="Admin456" -> 742ab02b5f80ea5658ffecd49491f77e9b3c536a
454    // and SHA1(UTF16LE("Admin456")) = 7ca54db25c28c72a5ec9a43b08bf75937c8b5fc6.
455    const DERIVE_SID: &str = "S-1-5-21-1455520393-2011455520393-2019809541-4133251990-500";
456    const DERIVE_PWD: &str = "Admin456";
457    const DERIVE_PWD_SHA1: &str = "7ca54db25c28c72a5ec9a43b08bf75937c8b5fc6";
458    const DERIVE_PREKEY: &str = "742ab02b5f80ea5658ffecd49491f77e9b3c536a";
459
460    #[test]
461    fn prekey_from_password_matches_impacket() {
462        let k = prekey_from_password(DERIVE_SID, DERIVE_PWD).expect("derive prekey");
463        assert_eq!(k.to_vec(), hex(DERIVE_PREKEY));
464    }
465
466    #[test]
467    fn prekey_from_sha1_matches_impacket() {
468        let mut sha1 = [0u8; 20];
469        sha1.copy_from_slice(&hex(DERIVE_PWD_SHA1));
470        let k = prekey_from_sha1(DERIVE_SID, &sha1).expect("derive prekey");
471        assert_eq!(k.to_vec(), hex(DERIVE_PREKEY));
472    }
473
474    #[test]
475    fn domain_backup_path_refuses() {
476        assert!(matches!(
477            derive_master_key_from_domain_backup(&[0u8; 16], &[0u8; 16]),
478            Err(DpapiError::DomainBackupUnsupported)
479        ));
480    }
481}