diff --git a/rfcs/001-project-architecture.md b/rfcs/001-project-architecture.md index 80f8641..165fc61 100644 --- a/rfcs/001-project-architecture.md +++ b/rfcs/001-project-architecture.md @@ -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. diff --git a/rfcs/006-key-retrieval-mechanisms.md b/rfcs/006-key-retrieval-mechanisms.md index 4dbff88..3aad882 100644 --- a/rfcs/006-key-retrieval-mechanisms.md +++ b/rfcs/006-key-retrieval-mechanisms.md @@ -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. diff --git a/rfcs/010-chrome-abe-integration.md b/rfcs/010-chrome-abe-integration.md index 56a8a05..ff6780e 100644 --- a/rfcs/010-chrome-abe-integration.md +++ b/rfcs/010-chrome-abe-integration.md @@ -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) |