This article is for educational purposes only. Only run these techniques against your own browser data. Credit card numbers are sensitive information — handle them with care.
The Problem
I had 5 credit/debit cards saved in Microsoft Edge's autofill system — accumulated over years of online shopping. I needed to export them as CSV for a personal finance dashboard I was building.
Simple, right? Wrong.
The Conventional Way
Edge lets you export passwords as CSV (edge://settings/passwords → Export) but there is no such button for payment methods. You can view your cards at edge://settings/payments, but you have to click each one → Edit → manually copy the number.
For 5 cards, that's tedious. For 50, it's impractical. And there's no programmatic API — this is intentionally locked down for security.
The Data Storage
Edge is Chromium-based, so it stores autofill data in a SQLite database at:
%LOCALAPPDATA%\Microsoft\Edge\User Data\Default\Web Data
The credit_cards table has columns like name_on_card, expiration_month, expiration_year, and — the critical one — card_number_encrypted.
Let's peek inside:
SELECT guid, name_on_card, expiration_month, expiration_year,
hex(card_number_encrypted), length(card_number_encrypted)
FROM credit_cards;
39d22812... | Yash Desai | 12/2028 | prefix=763232 | len=67
ccbc5e9b... | (none) | 2/2031 | prefix=763232 | len=67
df1b288b... | (none) | 12/2029 | prefix=763230 | len=47
ebcab4a3... | Krunal | 9/2029 | prefix=763232 | len=67
aa7d0770... | (none) | 4/2031 | prefix=763232 | len=67
Notice the hex prefixes:
-
763230= ASCII"v20"— v20 encryption (47 bytes) -
763232= ASCII"v22"— v22 encryption (67 bytes)
Edge uses AES-GCM encryption, and the encrypted blob format is:
- 3 bytes: version prefix (
v10,v11,v20, orv22) - 12 bytes: AES-GCM nonce (IV)
- N bytes: ciphertext (card number)
- 16 bytes: GCM authentication tag
The Encryption Key
There are two generations of key management in Chromium-based browsers:
v10/v11 (Legacy)
The master key is stored in Local State under os_crypt.encrypted_key:
{
"os_crypt": {
"encrypted_key": "RFBQQUkAAA...base64..."
}
}
The base64-decoded blob starts with DPAPI (5 bytes), followed by DPAPI-encrypted data. Decrypt it with CryptUnprotectData:
import win32crypt, binascii, json
with open("Local State") as f:
local_state = json.load(f)
encrypted_key = binascii.a2b_base64(local_state["os_crypt"]["encrypted_key"])
assert encrypted_key[:5] == b"DPAPI"
master_key = win32crypt.CryptUnprotectData(encrypted_key[5:], None, None, None, 0)[1]
This returns a 32-byte AES key.
v20+ (App-Bound Encryption)
Newer Edge versions (and Chrome v127+) use App-Bound Encryption. The key lives under os_crypt.app_bound_encrypted_key:
{
"os_crypt": {
"app_bound_encrypted_key": "QVBQQgEAAADQjJ3fARXR..." // starts with "APPB"
}
}
The decryption requires two DPAPI passes:
-
SYSTEM-level DPAPI — impersonating
lsass.exe - User-level DPAPI — standard user context
This is why it needs Administrator privileges.
The Tooling Stack
| Tool | Purpose |
|---|---|
| Python 3.11 | Scripting |
pycryptodome |
AES-GCM / ChaCha20-Poly1305 |
PythonForWindows |
Windows token manipulation, DPAPI |
pywin32 |
Win32 API wrappers |
sqlite3 (stdlib) |
Reading Edge's Web Data DB |
| DB Browser for SQLite | Quick DB inspection (GUI) |
The Experiment
Phase 1: Naive SQLite Dump
I started simple — just query the credit_cards table and dump to CSV.
import sqlite3
conn = sqlite3.connect("Web Data")
cursor = conn.cursor()
cursor.execute("SELECT * FROM credit_cards")
# ...write to CSV
Result: CSV with metadata (names, expiry) but card_number_encrypted was binary garbage — AES ciphertext.
Phase 2: v10 Key Extraction
Tried the legacy DPAPI approach:
# Decrypt DPAPI-wrapped key from Local State
master_key = win32crypt.CryptUnprotectData(encrypted_blob)[1]
# Decrypt each card
nonce = blob[3:15]
ct = blob[15:-16]
tag = blob[-16:]
cipher = AES.new(master_key, AES.MODE_GCM, nonce=nonce)
card_number = cipher.decrypt_and_verify(ct, tag)
Result: MAC check failed on all cards. Our Edge instance uses App-Bound Encryption, not legacy DPAPI.
Phase 3: App-Bound Encryption (First Attempt)
Found the hieuhp01/BrowserDatabaseDecryption repo on GitHub with a promising script. It handles v20 App-Bound Encryption by:
- Impersonating
lsass.exe(SYSTEM token) for the first DPAPI pass - Standard user DPAPI for the second pass
- Taking the last 32 bytes of the decrypted blob as the AES key
from contextlib import contextmanager
import windows, windows.crypto, windows.generated_def as gdef
@contextmanager
def impersonate_lsass():
original_token = windows.current_thread.token
windows.current_process.token.enable_privilege("SeDebugPrivilege")
proc = next(p for p in windows.system.processes if p.name == "lsass.exe")
lsass_token = proc.token
impersonation_token = lsass_token.duplicate(
type=gdef.TokenImpersonation,
impersonation_level=gdef.SecurityImpersonation
)
windows.current_thread.token = impersonation_token
yield
windows.current_thread.token = original_token
The problem: This needs Administrator rights AND the SeDebugPrivilege privilege. When you run the script non-elevated, it fails immediately.
The fix: Write a PowerShell wrapper that runs the Python script via Start-Process -Verb RunAs.
# run_elevated.ps1
$venvPy = "C:\path\to\.venv\Scripts\python.exe"
$script = "C:\path\to\decrypt_card_number_edge.py"
& $venvPy $script 2>&1 | Add-Content "C:\path\to\result.txt"
Then invoke it:
Start-Process -Verb RunAs -Wait -FilePath "powershell" -ArgumentList "-ExecutionPolicy Bypass -File run_elevated.ps1"
Gotcha: Start-Process -Verb RunAs doesn't support -RedirectStandardOutput. You must redirect inside the elevated script itself.
Phase 4: Partial Success — The v20 Card
Running the script elevated, we got:
Name on Card Expiry Card Number
=======================================================
Yash Desai 12/2028 error: MAC check failed
(no name) 2/2031 error: MAC check failed
(no name) 12/2029 4748469484762006
Krunal 9/2029 error: MAC check failed
(no name) 4/2031 error: MAC check failed
1 out of 5 decrypted. The working card had a v20 prefix (47 bytes). The 4 failures were v22 (67 bytes).
Phase 5: The v22 Mystery
I dumped the app_bound_encrypted_key key blob to understand why v22 cards fail:
Key blob (72 bytes):
20000000 02 433a5c50726f6772616d...
^^
flag=2
The decrypted key blob structure:
-
20 00 00 00(4 bytes) = DWORD length prefix (32) -
02(1 byte) = flag byte -
C:\Program Files\Microsoft\Edge \0(32 bytes) = path string -
00 00 00 00(4 bytes) = padding -
ce7d3d72...49f75(32 bytes) = encrypted/derived key material
The flag byte was 2, not 1. According to StackOverflow research:
| Flag | Algorithm | Key Source |
|---|---|---|
| 1 | AES-GCM | Hard-coded in elevation_service.exe
|
| 2 | ChaCha20-Poly1305 | Hard-coded in elevation_service.exe
|
| 3 | AES-GCM | CNG (Microsoft Key Storage Provider) |
Flag 2 means ChaCha20-Poly1305 — a completely different cipher. I updated the script to try both:
from Crypto.Cipher import AES, ChaCha20_Poly1305
def decrypt_chacha20(blob, key):
nonce = blob[3:15]
ct = blob[15:-16]
tag = blob[-16:]
cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
return cipher.decrypt_and_verify(ct, tag).decode("utf-8")
Result: Still MAC check failed on all 4 v22 cards. Something else is going on.
Phase 6: Diagnosis
After extensive debugging, here's what I concluded:
- The v20 card (4748469484762006) was saved locally on the device and used the simpler AES-GCM path.
-
The 4 v22 cards likely fall into one of these categories:
- Synced from Microsoft Account — encrypted with a cloud-derived key, not the local app-bound key
-
Encrypted with an older key version — before a key rotation, or with a different
elevation_service.exe -
Use key material from
elevation_service.exe— the actual master key for flag 2 is embedded in Edge's binary, accessible only via reverse-engineering the PE file
The elevation_service.exe (signed by Microsoft) contains hard-coded key material that's XORed or combined with the blob data to produce the final encryption key. Extracting that requires:
- Reverse-engineering a Microsoft signed binary
- Understanding the exact byte offsets and XOR masks
- Keeping up with updates (every Edge release can change this)
That's a rabbit hole I didn't go down — it's active cat-and-mouse territory.
The Final Results
CSV: C:\Users\...\edge_payment_methods_decrypted.csv
Name on Card Expiry Card Number Method
======================================================================
Yash Desai 12/2028 FAILED ChaCha20: MAC check failed
(no name) 2/2031 FAILED ChaCha20: MAC check failed
(no name) 12/2029 4748469484762006 AES-GCM ✅
Krunal 9/2029 FAILED ChaCha20: MAC check failed
(no name) 4/2031 FAILED ChaCha20: MAC check failed
Decrypted card (masked for safety): 474846******2006
Key Takeaways
- Edge has no built-in export for payment methods — only passwords.
- Two encryption generations exist simultaneously: legacy DPAPI (v10) and App-Bound (v20/v22).
-
App-Bound Encryption requires SYSTEM-level DPAPI — needs Admin rights and
SeDebugPrivilege. - v20 and v22 use different ciphers: AES-GCM vs ChaCha20-Poly1305 — but even the right cipher isn't enough if the key derivation differs.
- Microsoft Account-synced cards likely use a separate key chain inaccessible from local-only extraction.
- The encryption is designed to be hard to export — this is a security feature, not a bug. Payment data is PCI-DSS regulated, and browser vendors are adding stronger protections each release.
Tools Referenced
- hieuhp01/BrowserDatabaseDecryption — Python scripts for Chrome/Edge/Firefox credit card decryption
- PythonForWindows — Windows API bindings for Python
- pycryptodome — Cryptographic routines (AES-GCM, ChaCha20-Poly1305)
- DB Browser for SQLite — GUI tool for inspecting SQLite databases
For the Brave: What's Next?
If you want to fully crack v22, you'd need to:
- Extract the hard-coded key from
elevation_service.exeusing a disassembler (Ghidra, IDA Pro) - Find the XOR mask / key derivation function for flag 2
- Apply it to the blob data to derive the per-card AES/ChaCha20 key
- Decrypt the individual card blobs
This is doable but non-trivial — and every Edge update may shuffle the offsets. It's an ongoing arms race between forensics researchers and browser security teams.
By Yash Desai | AI Infrastructure & Fullstack Engineering | yashddesai.com
Top comments (0)