Offline recovery of Windows EFS-encrypted files when Windows refuses to decrypt them - even with the correct keys.
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
.pemfile - 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.
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:
- You have physical (or block-level) access to the drive and can open it raw as
\\.\X:. - You have administrator rights and
SeBackupPrivilegeon the machine running the tool. - You have the user's password, or a viable path to recover it.
- The DPAPI blobs under the old profile's
AppData\Roaming\Microsoft\Protect\are intact. - The
SystemCertificates/Crypto\RSA\<SID>\key store on the old profile is intact. - 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.
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.
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:
- 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.
- 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.
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:
- Open
\\.\X:and walk the NTFS MFT viadissect.ntfsto locate the target file's MFT entry. - Extract the
$LOGGED_UTILITY_STREAM:$EFSattribute (attribute type0x100) - this is the per-file EFS metadata stream that Windows writes alongside every encrypted file. - Parse the V1
$EFSstructure: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. - Find the DDF entry whose thumbprint matches the certificate that issued the PEM key you hold.
- RSA-unwrap the encrypted FEK. This is where a subtle detail bites you: CAPI1 stores RSA
ciphertext in little-endian byte order, but
cryptography'sprivkey.decryptexpects big-endian. The unwrap is thereforeprivkey.decrypt(fek_blob[::-1], PKCS1v15()). - 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). - Read the raw
$DATAciphertext 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. - Recover the per-file IV_0 (see the next section - this is the hard part).
- Decrypt each 512-byte sector under AES-256-CBC, with a per-sector IV derived from IV_0.
- Truncate the concatenated plaintext to the real file size read from the
$STANDARD_INFORMATIONattribute, 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:.
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.
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).
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.
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
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 .- Python 3.11 or later
dissect.ntfs>= 3.0cryptography>= 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
SeBackupPrivilegefor raw volume access.
The package installs an efs-decrypt console script as its entry point.
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\outSample 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.
All subcommands share these top-level flags:
| Flag | Description |
|---|---|
--version |
Print version and exit. |
-v, --verbose |
Enable debug logging (default: info). |
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$EFSstream.--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 outefs-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.txtSample 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 --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...4567Sample output:
3f1a2b7c9d4e5f6071829304a5b6c7d8e9fa0b1c2d3e4f506172839405a6b7c8
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,
dpapick3can sometimes recover it. If you have the NTLM or MSCache hash, crack it withhashcat. 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 todpapick3with the password. You want one.keyfile per GUID in there. - The RSA private key. Copy the old profile's
AppData\Roaming\Microsoft\Crypto\RSA\<SID>\directory.dpapick3'sprobes.certificate.PrivateKeyBlobwill 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 toefs-decrypt --pem. - The target certificate thumbprint. Run
efs-decrypt inspecton 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.
- 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_magicscoverage is narrow. The.webpknown-plaintext fallback is fully exercised. Headers for.png,.jpg,.pdf, and.zipare 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.
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
$EFSmetadata 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.
dpapick3by 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.
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.
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:
- 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.
- V2
$EFSmetadata support. Implement the FMK keywrap layer so files written by newer Windows builds can be decrypted. - Additional file magics. Expand
file_magics.pyfor more common file types to broaden the known-plaintext fallback. - Performance fast path. Replace the per-sector
Cipher()construction with a single bulk ECB decrypt plus manual CBC XOR of adjacent blocks. - 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.
MIT - see LICENSE.
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.