Skip to content

AeyeOps/efs-offline-recovery

Repository files navigation

EFS Offline Recovery

Offline recovery of Windows EFS-encrypted files when Windows refuses to decrypt them - even with the correct keys.

License: MIT Python Status


Start here

A Windows machine dies. You move the drive to a new system to recover your files, and most of them come back without trouble. The EFS-encrypted ones don't. You have the password, you have the certificate, you have done everything Microsoft documents, and Windows still answers Access Denied. This tool exists for that situation.

The reason it happens: Windows EFS checks the original user's SID baked into each file before it ever consults a key, and once that user account is gone there is no SID any current user can match - so the request is refused before your recovered key is even looked at. The only way through is to bypass the Windows EFS driver entirely and do the cryptography yourself, which is what this tool does.

efs-decrypt does not use the Windows EFS driver at all. It reads the ciphertext directly from the disk, performs the RSA unwrap and the AES decryption in Python, and writes clean copies of your files to an output folder you specify. The originals are never modified.

You need three things:

  • the drive attached and visible as a Windows drive letter
  • the recovered RSA private key as a .pem file
  • Python 3.11+ and an administrator shell

The certificate and the key material come from a backup of the old user profile - specifically the AppData\Roaming\Microsoft\SystemCertificates, AppData\Roaming\Microsoft\Crypto\RSA, and AppData\Roaming\Microsoft\Protect folders under the user that originally encrypted the files. If you have a file-level backup of C:\Users\<name>\ from before the crash (a sync mirror, an image you can mount, anything that preserved those folders), you almost certainly have what you need. How to get the inputs walks through extracting the PEM from that backup.

The Quickstart gets you running in two commands. The rest of this README explains why Windows refuses, how the tool works around it, and the per-sector IV derivation that makes the offline path possible.

This is a recovery tool, not a key-cracking tool. If you do not already have the RSA private key (or a backup of the profile that holds it), nothing here will help you. The upstream recovery work - DPAPI master keys, the CAPI1 RSA blob, the PEM export - is covered briefly in How to get the inputs and in detail in the companion whitepaper.


When to use this

This tool is designed for one specific, painful scenario: you have the right keys, you have the right password, and Windows still refuses to decrypt your files. Use it when all of the following hold:

  1. You have physical (or block-level) access to the drive and can open it raw as \\.\X:.
  2. You have administrator rights and SeBackupPrivilege on the machine running the tool.
  3. You have the user's password, or a viable path to recover it.
  4. The DPAPI blobs under the old profile's AppData\Roaming\Microsoft\Protect\ are intact.
  5. The SystemCertificates / Crypto\RSA\<SID>\ key store on the old profile is intact.
  6. The volume has not been TRIM'd, zeroed, or overwritten since encryption.

If any of those preconditions fail, stop here. This tool cannot help you. It is not a magic wand for encrypted files; it is a surgical instrument for one very specific failure mode in which every other legitimate avenue has been exhausted.

When NOT to use this

This is a recovery tool, not an attack tool. It cannot break EFS without the RSA private key. If you do not already have the password, the DPAPI master keys, or the recovered PEM, start with the upstream recovery workflows first: dpapick3, impacket, and friends. This repo assumes those steps are done and you are holding a PEM key and a DDF that will not decrypt through Windows.

The problem

I ran into a scenario that the Windows EFS subsystem was apparently never designed to handle gracefully: a crashed Windows install, an external drive moved onto a fresh system, a user profile that no longer exists, and an orphaned SID baked into every encrypted file's metadata. The RSA private key could be recovered. The password was known. The certificate thumbprint matched. And yet every Windows-side decryption path returned ERROR_ACCESS_DENIED (5).

I tried cipher /d. Denied. I tried File.ReadAllBytes from .NET under an elevated shell with SeBackupPrivilege. Denied. I tried OpenEncryptedFileRawW with and without CREATE_FOR_EXPORT. Denied. I reimported the certificate into the current user's CAPI1 store, rebound the private key, even materialised a CAPI1 key blob in the exact provider the DDF named - the Microsoft Enhanced Cryptographic Provider v1.0. Still denied. The Windows EFS driver was never consulting my key; it was rejecting the request before the key mattered at all.

There are two root causes that compound:

  1. The DDF SID pre-check. Before the Windows EFS driver attempts to unwrap the FEK, it compares the SID embedded in the Data Decryption Field against the caller's SID. If the DDF was written by a user account on a Windows install that no longer exists, that SID will never match any current user on any current machine. No amount of certificate reimporting fixes this, because the certificate is not the thing being checked.
  2. The NCrypt/CNG vs CAPI1 provider mismatch. EFS private keys written by older Windows versions live in CAPI1 (CryptoAPI 1.0) containers. Modern Windows increasingly routes EFS through CNG (NCrypt*). Even when the DDF correctly names a CAPI1 provider, the EFS driver on newer builds may attempt to open the key through CNG shims that do not find it, and the request fails long before any key material is touched.

The conclusion is unavoidable: if Windows is going to pre-check a SID that no living user owns, the only way forward is to bypass the Windows EFS driver entirely, read the encrypted metadata and raw ciphertext off the disk ourselves, and do the cryptography offline in userspace.

How it works

The tool implements the full EFS V1 decrypt pipeline in pure Python, reading from a raw NTFS volume handle and using only the recovered RSA private key as authority. The pipeline for each file is:

  1. Open \\.\X: and walk the NTFS MFT via dissect.ntfs to locate the target file's MFT entry.
  2. Extract the $LOGGED_UTILITY_STREAM:$EFS attribute (attribute type 0x100) - this is the per-file EFS metadata stream that Windows writes alongside every encrypted file.
  3. Parse the V1 $EFS structure: metadata_length, version, file ID, DDF offset, DRF offset, and the array of Data Decryption Field entries. Each DDF entry carries a SID, a cert thumbprint, a container GUID, a provider name, and a 256-byte encrypted FEK blob.
  4. Find the DDF entry whose thumbprint matches the certificate that issued the PEM key you hold.
  5. RSA-unwrap the encrypted FEK. This is where a subtle detail bites you: CAPI1 stores RSA ciphertext in little-endian byte order, but cryptography's privkey.decrypt expects big-endian. The unwrap is therefore privkey.decrypt(fek_blob[::-1], PKCS1v15()).
  6. Parse the resulting 48-byte plaintext FEK struct: a 16-byte header (keylen × 2, algorithm ID, reserved), followed by a 32-byte key. Validate alg_id == 0x6610 (CALG_AES_256).
  7. Read the raw $DATA ciphertext off the volume via the MFT's non-resident dataruns, cluster by cluster. This bypasses every Windows filesystem wrapper - the bytes you get are exactly what EFS wrote to disk.
  8. Recover the per-file IV_0 (see the next section - this is the hard part).
  9. Decrypt each 512-byte sector under AES-256-CBC, with a per-sector IV derived from IV_0.
  10. Truncate the concatenated plaintext to the real file size read from the $STANDARD_INFORMATION attribute, and write it out.

None of this involves the Windows EFS driver, the CAPI1 key store, the current user's SID, or any Windows API beyond CreateFile on \\.\X:.

The IV derivation breakthrough

This was the part of the project that could have killed it.

EFS encrypts every file as a sequence of 512-byte sectors under AES-256-CBC. Each sector uses the same FEK, but a different IV. The IV of sector k is derived from a per-file IV_0 by a formula that, as far as I could tell after days of searching, is not documented anywhere public - not in MS-EFSR, not in libfsefs, not in the ReactOS EFS reimplementation, and not in any public EFS writeup I could find.

Here is the formula, recovered empirically:

IV_k = IV_0 with:
    bytes[1:3]  (LE u16) += 2k  (mod 0x10000)
    bytes[9:11] (LE u16) += 2k  (mod 0x10000)
    all other bytes unchanged

Twelve of the sixteen IV bytes are constant across all sectors of a file. The other four are two independent 16-bit little-endian counters that both increment by exactly 2 per sector.

How it was recovered

The recovery exploits a happy accident of NTFS's allocation strategy. NTFS allocates files in cluster-aligned chunks. On the target volume, clusters are 64 KB - 128 sectors. For any file whose real size is not a multiple of 512 bytes (and especially any file whose real size is not a multiple of the cluster size), the sectors past real_size are zero-filled before encryption.

For any such zero-fill sector k, AES-CBC of an all-zero plaintext has a beautiful property: the first ciphertext block is C_0 = E_FEK(IV_k XOR 0) = E_FEK(IV_k). Therefore:

IV_k = AES-ECB-Decrypt(FEK, C_0)

We can read IV_k straight out of the ciphertext of any zero-fill sector, with nothing more than the FEK. Validate by decrypting the whole sector and checking it is all zeros; if it is, you have a correct IV_k for some specific k.

Do this for a handful of adjacent zero-fill sectors and the pattern is immediate. Bytes 0, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 15 never change. Bytes [1:3] as a little-endian u16 increment by 2 per sector. Bytes [9:11] as a little-endian u16 also increment by 2 per sector. Back-derive IV_0 by subtracting 2k from both counters, and you have a key that decrypts the entire file.

For cluster-aligned files with no zero-fill tail, the tool falls back to a known-plaintext recovery against well-known file magics (see file_magics.py).

Caveat

This formula was derived empirically on Windows 11 24H2 with NTFS 64 KB clusters. The pattern is so arithmetically clean that I expect it to hold universally, but it has not been cross-validated against other Windows versions, other cluster sizes, or files encrypted by pre-Vista EFS. If you have access to such a sample and can confirm or refute the formula, please open an issue. Cross-version validation is the single most valuable contribution this project could receive.

Crypto chain

password
  |
  +-- DPAPI master key  (recovered via dpapick3)
        |
        +-- CAPI1 RSA private-key blob  (from Crypto\RSA\<SID>\ on the old profile)
              |
              +-- PEM  (PKCS#1 RSA 2048, exported from the CAPI1 blob)
                    |
                    +-- LE-reversed PKCS#1 v1.5 unwrap of the 256-byte Encrypted FEK
                    |   pulled from the file's $EFS metadata stream
                          |
                          +-- 48-byte FEK struct
                                +-- 16B header: keylen*2, alg_id=0x6610 (CALG_AES_256), reserved
                                +-- 32B AES-256 FEK
                                      |
                                      +-- AES-256-CBC per-sector decrypt of raw $DATA
                                          with derive_iv(iv0, k) per 512-byte sector

Install

Clone the repo and install in editable mode with pip:

git clone https://github.com/AeyeOps/efs-offline-recovery.git
cd efs-offline-recovery
pip install -e .

Requirements

  • Python 3.11 or later
  • dissect.ntfs >= 3.0
  • cryptography >= 42
  • Windows in practice - \\.\X: raw volume syntax is a Windows-ism. The library code is technically cross-platform if you wire it up to a raw image file handle instead of a volume device, but the shipped CLI expects Windows.
  • An elevated shell with SeBackupPrivilege for raw volume access.

The package installs an efs-decrypt console script as its entry point.

Quickstart

The 60-second path, assuming you already have the PEM key and you know which volume, thumbprint, and subtree you want to decrypt:

REM Launch an ELEVATED cmd.exe (Run as administrator)

efs-decrypt decrypt ^
    --pem C:\recovery\efs_private_key.pem ^
    --volume \\.\X: ^
    --thumbprint 0123456789ABCDEF0123456789ABCDEF01234567 ^
    --source Users\olduser\Documents\encrypted ^
    --out C:\recovery\out

Sample output:

2026-04-11 14:02:11 INFO efs_offline_recovery: opening \\.\X:
  OK [zerofill]     142,318 B  Users\olduser\Documents\encrypted\notes.txt
  OK [zerofill]   1,047,552 B  Users\olduser\Documents\encrypted\plan.md
  OK [zerofill]      88,174 B  Users\olduser\Documents\encrypted\photos\sunrise.webp
  COPY (not encrypted): Users\olduser\Documents\encrypted\readme.txt
  OK [zerofill]     512,000 B  Users\olduser\Documents\encrypted\archive\report.pdf
  ...
Summary: ok=6707 fail=0 copy=78 empty=1 skip=0

You must run the command in an elevated shell. efs-decrypt will fail immediately when it cannot open \\.\<volume>: for raw reads. There is no workaround: raw volume access requires admin on Windows.

CLI reference

All subcommands share these top-level flags:

Flag Description
--version Print version and exit.
-v, --verbose Enable debug logging (default: info).

efs-decrypt decrypt

efs-decrypt decrypt --pem PEM --volume VOLUME --thumbprint HEX --source PATH --out DIR

Recursively decrypts an NTFS subtree to an output directory, mirroring its layout.

Required flags:

  • --pem PATH - path to the PEM-encoded RSA private key (unencrypted).
  • --volume PATH - raw volume path, e.g. \\.\X:.
  • --thumbprint HEX - hex SHA-1 thumbprint of the certificate whose private key is in --pem. Must match a DDF entry in each file's $EFS stream.
  • --source PATH - NTFS-relative path of the subtree to decrypt, e.g. Users\olduser\Documents\encrypted.
  • --out DIR - output directory (mirrors the source subtree layout; created if missing).

Exit code: 0 if every file decrypted cleanly, 1 if any file failed.

Example:

efs-decrypt decrypt --pem key.pem --volume \\.\X: --thumbprint 0123...4567 --source Users\me\Docs --out out

efs-decrypt inspect

efs-decrypt inspect --volume VOLUME --file PATH

Parses and dumps the $EFS metadata stream of a single file, listing every DDF entry with its SID, thumbprint, container GUID, provider, and FEK blob offset. This is the first thing to run when you do not yet know which thumbprint to pass to decrypt.

Required flags:

  • --volume PATH - raw volume path, e.g. \\.\X:.
  • --file PATH - NTFS-relative path of the file to inspect.

Exit code: 0 on success, 2 if the file has no $EFS attribute (i.e. is not EFS-encrypted).

Example:

efs-decrypt inspect --volume \\.\X: --file Users\olduser\Documents\encrypted\notes.txt

Sample output:

$EFS stream for: Users\olduser\Documents\encrypted\notes.txt
  length        : 688
  version       : 2
  file_id       : 0123456789abcdef0123456789abcdef
  ddf_offset    : 0x00000054
  drf_offset    : 0x00000000
  ddf_entries   : 1
  [DDF 0]
    sid           : S-1-5-21-...
    thumbprint    : 0123456789ABCDEF0123456789ABCDEF01234567
    container     : 00000000-1111-2222-3333-444444444444
    provider      : Microsoft Enhanced Cryptographic Provider v1.0
    display_name  : <unknown>
    fek_offset    : 0x000001ae (256 bytes)

efs-decrypt recover-fek

efs-decrypt recover-fek --pem PEM --volume VOLUME --file PATH --thumbprint HEX

Unwraps and prints the raw 32-byte AES-256 FEK of a single file, as hex. Useful for debugging IV recovery in isolation, or for feeding the FEK into other tools.

Required flags:

  • --pem PATH - path to the PEM-encoded RSA private key (unencrypted).
  • --volume PATH - raw volume path, e.g. \\.\X:.
  • --file PATH - NTFS-relative path of the file.
  • --thumbprint HEX - hex SHA-1 thumbprint of the certificate. If no DDF entry matches, the tool will warn and fall back to the first DDF entry.

Exit code: 0 on success, 2 if the file is not encrypted.

Example:

efs-decrypt recover-fek --pem key.pem --volume \\.\X: --file Users\me\Docs\notes.txt --thumbprint 0123...4567

Sample output:

3f1a2b7c9d4e5f6071829304a5b6c7d8e9fa0b1c2d3e4f506172839405a6b7c8

How to get the inputs

The preconditions section lists what you need. Here is how to actually get each piece. This repo does not reimplement any of these steps - it depends on mature upstream tools.

  • The user's password. If the user remembers it, you are done. If not, and you have a credential history file, dpapick3 can sometimes recover it. If you have the NTLM or MSCache hash, crack it with hashcat. Without the password, you cannot open the DPAPI master keys, and the entire crypto chain stops dead.
  • The DPAPI master keys. Copy the old profile's entire AppData\Roaming\Microsoft\Protect\ tree and feed it to dpapick3 with the password. You want one .key file per GUID in there.
  • The RSA private key. Copy the old profile's AppData\Roaming\Microsoft\Crypto\RSA\<SID>\ directory. dpapick3's probes.certificate.PrivateKeyBlob will take the CAPI1 key blobs, unwrap them with the DPAPI master keys you just recovered, and give you a PEM. That is the PEM you pass to efs-decrypt --pem.
  • The target certificate thumbprint. Run efs-decrypt inspect on one encrypted file. It will print every DDF entry. The thumbprint you want is the one whose container GUID and provider match the CAPI1 key blob you just exported. In practice there is usually only one DDF per file, and it is obvious.

Limitations

  • V1 EFS metadata only. The V2 metadata layout (with the intermediate FMK keywrap layer) is not yet implemented. Files written by newer Windows builds that use V2 DDFs will not decrypt.
  • RSA keys only. Only RSA private keys with CALG_AES_256 (0x6610) FEKs are supported. ECDH / ECC EFS keys are not handled. Non-AES-256 FEKs are rejected at unwrap time.
  • PKCS#1 v1.5 padding only. RSA-OAEP is not supported. CAPI1 EFS keys always use PKCS#1 v1.5, so this matches reality for the targeted scenario, but it is a hard constraint.
  • IV formula unvalidated across versions. The per-sector IV derivation was recovered on Windows 11 24H2 with 64 KB clusters. It is expected to hold elsewhere but has not been proven.
  • file_magics coverage is narrow. The .webp known-plaintext fallback is fully exercised. Headers for .png, .jpg, .pdf, and .zip are present but less tested. For cluster-aligned files of other formats, the zero-fill path is preferred.
  • Throughput. The decryptor constructs a fresh Cipher() for every 512-byte sector. On AES-NI hardware this caps throughput around 60 MB/s, compared to roughly 1 GB/s for a single-shot AES decrypt. A fast path (one ECB decrypt across all sectors, manual CBC XOR) is not yet implemented. For most recovery jobs the total volume is small enough that this does not matter.

Related work

  • libfsefs (libyal project) - partial EFS implementation focused on metadata parsing. Does not implement the per-sector IV derivation.
  • ReactOS NTFS / EFS drivers - partial reimplementation of the Windows NTFS and EFS paths, useful as a reference for the on-disk structures.
  • MS-EFSR - the Microsoft open specification for the EFS remote protocol. Documents the $EFS metadata format but not the sector-level encryption construction.
  • Passware Kit Forensic and ElcomSoft Advanced EFS Data Recovery: commercial forensic tools with EFS recovery features. Closed source.
  • dpapick3 by Tijl Deneut - the DPAPI recovery library that sits upstream of this tool in the crypto chain. If you do not have a PEM yet, start there.

Security considerations

This tool does not weaken EFS in any way. The security boundary of EFS is the DPAPI master key, which is protected by the user's password (and, on modern Windows, by TPM-backed LSA isolation and Credential Guard). Publishing the per-sector IV derivation formula does not help an attacker who does not already have the FEK - and any attacker who already has the FEK can decrypt the file through legitimate Windows APIs (or a four-line Python script) without needing this tool at all.

Per Kerckhoffs's principle, a cryptosystem's security should depend only on the secrecy of its key, not the secrecy of its construction. BitLocker is the canonical counter-example to "secret construction = more secure": its AES-XTS construction, key schedule, and on-disk layout are all fully public and standardized, and no serious cryptographer considers that publication a weakness. EFS's per-sector IV derivation was never documented, but it was never a secret either - it is a deterministic arithmetic function of a single per-file value, and reverse-engineering it is a few hours of work given any zero-filled sector. Documenting it now just saves the next person those hours.

Contributing

Bug reports, reproductions of the IV formula on other Windows versions, V2 metadata support, and additional file magics are all welcome. File issues on the GitHub repo: https://github.com/AeyeOps/efs-offline-recovery/issues.

The most-wanted contributions, in priority order:

  1. Cross-version IV validation. Confirm or refute the per-sector IV formula on Windows versions other than Windows 11 24H2, and on cluster sizes other than 64 KB.
  2. V2 $EFS metadata support. Implement the FMK keywrap layer so files written by newer Windows builds can be decrypted.
  3. Additional file magics. Expand file_magics.py for more common file types to broaden the known-plaintext fallback.
  4. Performance fast path. Replace the per-sector Cipher() construction with a single bulk ECB decrypt plus manual CBC XOR of adjacent blocks.
  5. Cross-platform volume access. Accept a raw image file as an alternative to a \\.\<volume>: device, so the tool can run on Linux against a dd'd NTFS image.

Coding style: PEP 8, type hints everywhere, dataclasses for return values, module-level docstrings that explain the why, not just the what.

License

MIT - see LICENSE.

About

Built by AeyeOps - Integrate AI Into Every Operation.

For the full incident case study, technical deep dive, and methodology, see AeyeOps-EFS-Offline-Recovery-Whitepaper.docx.

About

Offline recovery of Windows EFS-encrypted files when Windows refuses. Companion whitepaper documents the IV derivation formula.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages