mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-06-04 19:48:01 +02:00
refactor: extract master-key code into masterkey package (#604)
This commit is contained in:
@@ -25,7 +25,7 @@ HackBrowserData/
|
||||
│ └── firefox/ # Firefox engine: extraction, NSS key derivation
|
||||
├── types/ # Data model: Category enum, Entry structs, BrowserData
|
||||
├── crypto/ # Encryption primitives, cipher version detection
|
||||
│ └── keyretriever/ # Platform-specific master key retrieval (Keychain/DPAPI/D-Bus)
|
||||
├── masterkey/ # Platform-specific master key retrieval (Keychain/DPAPI/D-Bus)
|
||||
├── filemanager/ # Temp file session, locked file handling (Windows)
|
||||
├── output/ # Output Writer: CSV, JSON, CookieEditor formatters
|
||||
├── log/ # Logging with level filtering
|
||||
@@ -83,10 +83,10 @@ There are two entry points, one for extraction and one for discovery:
|
||||
|
||||
```
|
||||
DiscoverBrowsersWithKeys(opts) // used by `dump` — ready to Extract
|
||||
→ pickFromConfigs(configs, opts) // shared discovery core
|
||||
→ discoverFromConfigs(configs, opts) // shared discovery core
|
||||
→ platformBrowsers() // build-tagged list for this OS
|
||||
→ filter by name / profile path
|
||||
→ newBrowsers(cfg) // dispatch to chromium/firefox/safari.NewBrowsers
|
||||
→ newBrowser(cfg) // dispatch to chromium/firefox/safari.NewBrowser
|
||||
→ discoverProfiles() // scan profile subdirectories
|
||||
→ resolveSourcePaths() // stat candidates, first match wins
|
||||
→ newCredentialInjector(opts) // build-tagged: returns a browserInjector
|
||||
@@ -94,20 +94,15 @@ DiscoverBrowsersWithKeys(opts) // used by `dump` — ready to
|
||||
inject(b) // type-assert retrieverSetter / keychainPasswordSetter
|
||||
|
||||
DiscoverBrowsers(opts) // used by `list` / `list --detail`
|
||||
→ pickFromConfigs(configs, opts) // same shared discovery core, NO injection
|
||||
→ discoverFromConfigs(configs, opts) // same shared discovery core, NO injection
|
||||
```
|
||||
|
||||
`DiscoverBrowsersWithKeys` does discovery + decryption setup in one call; the returned
|
||||
browsers are ready for `b.Extract`. `DiscoverBrowsers` skips injection
|
||||
entirely, so list-style commands never trigger the macOS Keychain password
|
||||
prompt — they have no use for the credential. Both entry points share the
|
||||
same `pickFromConfigs` core, so filtering/profile-path/glob semantics stay
|
||||
consistent.
|
||||
`DiscoverBrowsersWithKeys` does discovery + decryption setup in one call; the returned browsers are ready for `b.Extract`. `DiscoverBrowsers` skips injection entirely, so list-style commands never trigger the macOS Keychain password prompt — they have no use for the credential. Both entry points share the same `discoverFromConfigs` core, so filtering/profile-path/glob semantics stay consistent.
|
||||
|
||||
Key design decisions:
|
||||
|
||||
- **One KeyRetriever chain per process** — built lazily inside `newCredentialInjector` and reused across every Chromium browser and every profile to prevent repeated keychain prompts on macOS.
|
||||
- **Discovery is decoupled from injection** — `pickFromConfigs` is injection-free; `DiscoverBrowsers` stops after it, `DiscoverBrowsersWithKeys` continues into injection.
|
||||
- **One Retriever chain per process** — built lazily inside `newCredentialInjector` and reused across every Chromium browser and every profile to prevent repeated keychain prompts on macOS.
|
||||
- **Discovery is decoupled from injection** — `discoverFromConfigs` is injection-free; `DiscoverBrowsers` stops after it, `DiscoverBrowsersWithKeys` continues into injection.
|
||||
- **Profile discovery differs by engine**: Chromium looks for `Preferences` files in subdirectories; Firefox accepts any subdirectory containing known source files.
|
||||
- **Flat layout fallback** — Opera-style browsers that store data directly in UserDataDir (no profile subdirectories) are handled by falling back to the base directory.
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@ Chromium-based browsers encrypt sensitive data (passwords, cookies, credit cards
|
||||
| Windows | `Local State` JSON (DPAPI-encrypted) | Raw AES-256 key |
|
||||
| Linux | GNOME Keyring / KDE Wallet via D-Bus | Password string → PBKDF2 → AES-128 |
|
||||
|
||||
Each platform may have multiple retrieval strategies. The `KeyRetriever` interface and `ChainRetriever` pattern abstract over these strategies, trying each in priority order until one succeeds.
|
||||
Each platform may have multiple retrieval strategies. The `Retriever` interface and `ChainRetriever` pattern abstract over these strategies, trying each in priority order until one succeeds.
|
||||
|
||||
For Chromium encryption details (cipher versions, AES-CBC/GCM), see [RFC-003](003-chromium-encryption.md). Firefox manages its own keys via `key4.db` — see [RFC-005](005-firefox-encryption.md).
|
||||
|
||||
## 2. KeyRetriever Interface
|
||||
## 2. Retriever Interface
|
||||
|
||||
The interface takes a single `Hints` struct so caller intent is explicit rather than positional:
|
||||
|
||||
@@ -115,14 +115,14 @@ Unlike macOS/Linux, DPAPI gives the **final AES-256 key directly**. No intermedi
|
||||
|
||||
### 4.4 Dual-Tier Retrievers (V10 + V20)
|
||||
|
||||
Windows populates two slots of the `keyretriever.Retrievers` struct — V10 (legacy DPAPI) and V20 (Chrome 127+ App-Bound Encryption) — which run independently rather than as a first-success chain. V11 stays nil on Windows (Chromium does not emit v11 prefix there).
|
||||
Windows populates two slots of the `masterkey.Retrievers` struct — V10 (legacy DPAPI) and V20 (Chrome 127+ App-Bound Encryption) — which run independently rather than as a first-success chain. V11 stays nil on Windows (Chromium does not emit v11 prefix there).
|
||||
|
||||
| Slot | Retriever | Source field | Mechanism |
|
||||
|------|-----------|--------------|-----------|
|
||||
| V10 | `DPAPIRetriever` | `os_crypt.encrypted_key` | `CryptUnprotectData` (Crypt32.dll) |
|
||||
| V20 | `ABERetriever` | `os_crypt.app_bound_encrypted_key` | IElevator via reflective injection (see [RFC-010](010-chrome-abe-integration.md)) |
|
||||
|
||||
`browser/browser_windows.go::newCredentialInjector` calls `keyretriever.DefaultRetrievers()` and wires the resulting struct through `Browser.SetKeyRetrievers(r)`. At extract time `keyretriever.NewMasterKeys` runs each slot independently — a failure on one tier does not prevent the other from succeeding, because mixed-tier Chrome profiles (upgraded from pre-127) need partial success to be useful.
|
||||
`browser/browser_windows.go::newCredentialInjector` calls `masterkey.DefaultRetrievers()` and wires the resulting struct through `Browser.SetRetrievers(r)`. At extract time `masterkey.NewMasterKeys` runs each slot independently — a failure on one tier does not prevent the other from succeeding, because mixed-tier Chrome profiles (upgraded from pre-127) need partial success to be useful.
|
||||
|
||||
**Why not a ChainRetriever?** `ChainRetriever` has first-success semantics: once ABE returns a key, DPAPI is never called. That semantics is wrong for orthogonal tiers — it was the root cause of issue #578, where upgraded profiles' v10-encrypted passwords silently failed because only the v20 key was retrieved. `NewMasterKeys` evaluates each tier independently and returns an `errors.Join` of per-tier failures; log severity is a caller-side decision. `browser/chromium::getMasterKeys` currently logs all tier errors uniformly at `Warnf` — the distinction between "partial" and "total" failure was judged low-value for a short-lived CLI where all warn lines are visible in the default output.
|
||||
|
||||
@@ -132,7 +132,7 @@ Windows populates two slots of the `keyretriever.Retrievers` struct — V10 (leg
|
||||
|
||||
### 5.1 Dual-Tier Retrievers (V10 + V11)
|
||||
|
||||
Linux populates two slots of the `keyretriever.Retrievers` struct — one per cipher prefix that Chromium emits on this platform:
|
||||
Linux populates two slots of the `masterkey.Retrievers` struct — one per cipher prefix that Chromium emits on this platform:
|
||||
|
||||
| Slot | Prefix | Retriever | Mechanism | Chromium name |
|
||||
|------|--------|-----------|-----------|---------------|
|
||||
@@ -182,7 +182,7 @@ The authoritative mapping lives in the `KeychainLabel` field of each entry in `p
|
||||
|
||||
## 7. Safari Credential Extraction
|
||||
|
||||
Safari is **not** a consumer of the `KeyRetriever` interface. It has its own credential-extraction path in `browser/safari/extract_password.go`, which uses [keychainbreaker](https://github.com/moond4rk/keychainbreaker) directly to list `InternetPassword` records from `login.keychain-db`.
|
||||
Safari is **not** a consumer of the `Retriever` interface. It has its own credential-extraction path in `browser/safari/extract_password.go`, which uses [keychainbreaker](https://github.com/moond4rk/keychainbreaker) directly to list `InternetPassword` records from `login.keychain-db`.
|
||||
|
||||
This is a deliberate architectural choice, not an oversight. The following sections explain why.
|
||||
|
||||
@@ -196,21 +196,21 @@ This is a deliberate architectural choice, not an oversight. The following secti
|
||||
| Failure mode | Hard fail (no key → cannot decrypt) | Soft fail (degrade to metadata-only) |
|
||||
| Caching benefit | High (multi-profile, multi-browser) | None (single browser, single call) |
|
||||
|
||||
Forcing Safari through the `KeyRetriever` interface would require returning a different type than `[]byte`, contradicting the interface's documented purpose as the *master-key* abstraction. Forcing it through a parallel "InternetPassword chain" would be over-engineering for a single consumer that has no fallback strategies worth chaining.
|
||||
Forcing Safari through the `Retriever` interface would require returning a different type than `[]byte`, contradicting the interface's documented purpose as the *master-key* abstraction. Forcing it through a parallel "InternetPassword chain" would be over-engineering for a single consumer that has no fallback strategies worth chaining.
|
||||
|
||||
Note the "failure mode" row in particular: Chromium *must* have a master key or extraction fails entirely, so it needs a chain of escalating strategies. Safari can degrade gracefully — if the keychain cannot be unlocked, metadata-only export (URLs and usernames, no plaintext passwords) is still useful output, so a single "try keychainbreaker, warn on failure" is sufficient.
|
||||
|
||||
### 7.2 The General Rule
|
||||
|
||||
> **Each browser package owns its own credential-acquisition strategy. `crypto/keyretriever` exists only to share retrieval logic across the Chromium variant family. New browser implementations should follow Safari's and Firefox's example — own your credential code.**
|
||||
> **Each browser package owns its own credential-acquisition strategy. `masterkey` exists only to share retrieval logic across the Chromium variant family. New browser implementations should follow Safari's and Firefox's example — own your credential code.**
|
||||
|
||||
Evidence the rule is already in force:
|
||||
|
||||
- **Firefox** (`browser/firefox/firefox.go`) does not import `keyretriever` or `keychainbreaker`. It derives keys from `key4.db` via internal NSS PBE. See RFC-005.
|
||||
- **Firefox** (`browser/firefox/firefox.go`) does not import `masterkey` or `keychainbreaker`. It derives keys from `key4.db` via internal NSS PBE. See RFC-005.
|
||||
- **Safari** (`browser/safari/extract_password.go`) uses `keychainbreaker` directly for `InternetPassword` records.
|
||||
- **Chromium variants** all go through `crypto/keyretriever` because they share exactly one chain and benefit from the shared `sync.Once` caching.
|
||||
- **Chromium variants** all go through `masterkey` because they share exactly one chain and benefit from the shared `sync.Once` caching.
|
||||
|
||||
Future contributors adding a new macOS browser that reads credentials from the Keychain should add their access logic to that browser's package, not extend `keyretriever`. Only extend `keyretriever` if the new browser is a Chromium variant that fits the existing master-key chain.
|
||||
Future contributors adding a new macOS browser that reads credentials from the Keychain should add their access logic to that browser's package, not extend `masterkey`. Only extend `masterkey` if the new browser is a Chromium variant that fits the existing master-key chain.
|
||||
|
||||
### 7.3 Where the `--keychain-pw` Password Goes
|
||||
|
||||
@@ -218,7 +218,7 @@ The macOS login password is resolved once at startup by `browser/browser_darwin.
|
||||
|
||||
| Consumer | Capability interface | Defined in | Payload |
|
||||
|---|---|---|---|
|
||||
| Chromium browsers | `keyRetrieversSetter` | `browser/browser.go` | `keyretriever.Retrievers` struct (V10 / V11 / V20 slots; unused tiers nil) |
|
||||
| Chromium browsers | `keyRetrieversSetter` | `browser/browser.go` | `masterkey.Retrievers` struct (V10 / V11 / V20 slots; unused tiers nil) |
|
||||
| Safari | `keychainPasswordSetter` | `browser/browser_darwin.go` | raw `string` |
|
||||
|
||||
The two setters are **intentionally not unified**. They carry different abstractions — one hands the browser a pre-assembled retrieval chain, the other hands the browser a credential token to unlock its own access path. Unifying them would create a leaky polymorphic interface with no real shared semantics. Note that `keychainPasswordSetter` is defined in the darwin-only file because Safari (its only implementer) is darwin-only.
|
||||
|
||||
@@ -14,7 +14,7 @@ This RFC documents how HackBrowserData integrates ABE support end-to-end while k
|
||||
Related RFCs:
|
||||
|
||||
- [RFC-003](003-chromium-encryption.md) — cipher versions (v10, v11, v20)
|
||||
- [RFC-006](006-key-retrieval-mechanisms.md) — `KeyRetriever` / `ChainRetriever`
|
||||
- [RFC-006](006-key-retrieval-mechanisms.md) — `Retriever` / `ChainRetriever`
|
||||
- [RFC-009](009-windows-locked-file-bypass.md) — other Windows-specific handling
|
||||
|
||||
### 1.1 Compatibility contract
|
||||
@@ -46,7 +46,7 @@ End-to-end flow when `hack-browser-data.exe` encounters a v20 Chromium cookie on
|
||||
|
||||
```
|
||||
browser/chromium.Extract()
|
||||
→ keyretriever.Chain [ABERetriever, DPAPIRetriever]
|
||||
→ masterkey.Chain [ABERetriever, DPAPIRetriever]
|
||||
→ ABERetriever.RetrieveKey():
|
||||
reads Local State → extracts APPB-prefixed blob
|
||||
resolves browser exe via registry App Paths
|
||||
@@ -191,7 +191,7 @@ Go consumes the same constants via **`go tool cgo -godefs`** (a development-time
|
||||
|
||||
### 5.3 Retriever wiring & v20 routing
|
||||
|
||||
`keyretriever.DefaultRetrievers()` on Windows returns a `Retrievers` struct with `V10 = &DPAPIRetriever{}` and `V20 = &ABERetriever{}`. The two tiers are wired independently — not in a ChainRetriever — because a single Chrome profile upgraded from pre-127 can carry mixed v10+v20 ciphertexts, and both keys must be available for `decryptValue` to route each ciphertext to its matching tier (see [RFC-006](006-key-retrieval-mechanisms.md) §4.4 and issue #578). `ABERetriever.RetrieveKey`:
|
||||
`masterkey.DefaultRetrievers()` on Windows returns a `Retrievers` struct with `V10 = &DPAPIRetriever{}` and `V20 = &ABERetriever{}`. The two tiers are wired independently — not in a ChainRetriever — because a single Chrome profile upgraded from pre-127 can carry mixed v10+v20 ciphertexts, and both keys must be available for `decryptValue` to route each ciphertext to its matching tier (see [RFC-006](006-key-retrieval-mechanisms.md) §4.4 and issue #578). `ABERetriever.RetrieveKey`:
|
||||
|
||||
1. Reads `Local State` → extracts `os_crypt.app_bound_encrypted_key` → strips `APPB` prefix. If the field is missing, `ABERetriever` returns `(nil, nil)`, `V20` remains empty, and the independently-wired `V10` DPAPI tier still runs.
|
||||
2. Resolves browser executable via `utils/winutil/browser_path_windows.go` (registry App Paths → hardcoded fallback).
|
||||
@@ -321,5 +321,5 @@ Edit `crypto/windows/abe_native/com_iid.c` (add the entry), `utils/winutil/brows
|
||||
| RFC | Relation |
|
||||
|---|---|
|
||||
| [RFC-003 Chromium Encryption](003-chromium-encryption.md) | v10/v11/v20 cipher format reference; v20 now implemented on Windows per this RFC |
|
||||
| [RFC-006 Key Retrieval](006-key-retrieval-mechanisms.md) | `keyretriever.Retrievers` taxonomy; Windows populates V10 (DPAPI) + V20 (ABE) as independent tier slots |
|
||||
| [RFC-006 Key Retrieval](006-key-retrieval-mechanisms.md) | `masterkey.Retrievers` taxonomy; Windows populates V10 (DPAPI) + V20 (ABE) as independent tier slots |
|
||||
| [RFC-009 Windows Locked Files](009-windows-locked-file-bypass.md) | Sibling Windows-specific workaround (handle duplication for locked DBs) |
|
||||
|
||||
Reference in New Issue
Block a user