mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-05-19 18:58:03 +02:00
feat: add Safari password extraction from macOS Keychain (#568)
This commit is contained in:
@@ -65,43 +65,61 @@ Each category has a corresponding Entry struct with `json` and `csv` struct tags
|
||||
|
||||
### 4.1 BrowserKind
|
||||
|
||||
Four engine kinds determine source paths and extractors:
|
||||
Each config declares an engine kind that determines source paths and extraction logic. Kinds fall into three engine families:
|
||||
|
||||
| Kind | Description |
|
||||
|------|-------------|
|
||||
| `Chromium` | Standard Chromium layout |
|
||||
| `ChromiumYandex` | Yandex variant: different file names and SQL queries |
|
||||
| `ChromiumOpera` | Opera variant: different extension key, Roaming path on Windows |
|
||||
| `Firefox` | Firefox: NSS encryption, SQLite + JSON files |
|
||||
- **Chromium** (`Chromium`, `ChromiumYandex`, `ChromiumOpera`) — the standard Chromium layout plus two variants that override file names or storage paths for Yandex and Opera forks. See RFC-003.
|
||||
- **Firefox** — NSS-based key derivation from `key4.db`, SQLite + JSON source files. See RFC-005.
|
||||
- **Safari** — macOS only, with direct Keychain-based credential extraction. See RFC-006 §7.
|
||||
|
||||
See `types/category.go` for the authoritative enum definition.
|
||||
|
||||
### 4.2 BrowserConfig
|
||||
|
||||
`BrowserConfig` is the declarative, platform-specific browser definition containing: Key (CLI matching), Name (display), Kind (engine), Storage (keychain label), UserDataDir (data path).
|
||||
|
||||
### 4.3 PickBrowsers() Flow
|
||||
### 4.3 Browser Selection Flow
|
||||
|
||||
There are two entry points, one for extraction and one for discovery:
|
||||
|
||||
```
|
||||
PickBrowsers(opts)
|
||||
→ platformBrowsers() // build-tagged: returns []BrowserConfig for this OS
|
||||
→ pickFromConfigs(configs, opts) // filter by name, apply profile-path/keychain overrides
|
||||
→ newBrowsers(cfg) // dispatch by Kind to chromium.NewBrowsers or firefox.NewBrowsers
|
||||
→ discoverProfiles() // scan for profile subdirectories
|
||||
→ resolveSourcePaths() // stat each candidate path, first match wins
|
||||
PickBrowsers(opts) // used by `dump` — ready to Extract
|
||||
→ pickFromConfigs(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
|
||||
→ discoverProfiles() // scan profile subdirectories
|
||||
→ resolveSourcePaths() // stat candidates, first match wins
|
||||
→ newPlatformInjector(opts) // build-tagged: returns a func(Browser)
|
||||
→ for each browser: // closure captures retriever + keychain pw lazily
|
||||
inject(b) // type-assert retrieverSetter / keychainPasswordSetter
|
||||
|
||||
DiscoverBrowsers(opts) // used by `list` / `list --detail`
|
||||
→ pickFromConfigs(configs, opts) // same shared discovery core, NO injection
|
||||
```
|
||||
|
||||
`PickBrowsers` 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.
|
||||
|
||||
Key design decisions:
|
||||
|
||||
- **One KeyRetriever per browser** — created once and shared across all profiles to prevent repeated keychain prompts on macOS.
|
||||
- **One KeyRetriever chain per process** — built lazily inside `newPlatformInjector` 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, `PickBrowsers` 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.
|
||||
|
||||
### 4.4 Platform Browser Lists
|
||||
|
||||
Browser configs are defined per-platform via build tags:
|
||||
Browser configs are defined per-platform via build tags in `platformBrowsers()` (`browser/browser_{darwin,linux,windows}.go`). The supported set groups by engine family:
|
||||
|
||||
- **macOS** — 12 browsers (Chrome, Edge, Chromium, Chrome Beta, Opera, OperaGX, Vivaldi, CocCoc, Brave, Yandex, Arc, Firefox)
|
||||
- **Windows** — 16 browsers (all macOS minus Arc, plus 360 Speed, 360 Speed X, QQ, DC, Sogou)
|
||||
- **Linux** — 8 browsers (Chrome, Edge, Chromium, Chrome Beta, Opera, Vivaldi, Brave, Firefox)
|
||||
- **Chromium-based** — the largest family, covering mainstream browsers (Chrome, Edge, Brave, Vivaldi, Opera, Chromium) across all three platforms plus regional variants and forks. Windows carries the longest list because of China-region Chromium forks (360, QQ, Sogou, DC, …) and MSIX-packaged browsers with dynamic install paths (Arc, DuckDuckGo).
|
||||
- **Firefox** — all three platforms, via internal NSS key derivation (RFC-005).
|
||||
- **Safari** — macOS only, via direct Keychain `InternetPassword` extraction (RFC-006 §7).
|
||||
|
||||
Adding a new browser is a config-only change in `platformBrowsers()`; this section does not need updates for new variants within an existing family.
|
||||
|
||||
## 5. Extract() Orchestration
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ The return value is the **ready-to-use decryption key** — either the raw AES k
|
||||
|
||||
`ChainRetriever` wraps multiple retrievers and tries them in order. The first successful result wins. If all fail, errors from every retriever are combined into a single error.
|
||||
|
||||
**Caching**: the retriever is created once per browser and shared across all profiles. macOS retrievers use `sync.Once` internally, so multi-profile browsers only trigger one keychain prompt or memory dump.
|
||||
**Caching**: the retriever chain is created once per process inside `newPlatformInjector` (see `browser/browser_{darwin,linux,windows}.go`) and shared across every Chromium browser and every profile. macOS retrievers additionally use `sync.Once` internally, so multi-profile browsers only trigger one keychain prompt or memory dump.
|
||||
|
||||
## 3. macOS Key Retrieval
|
||||
|
||||
@@ -69,17 +69,9 @@ All macOS strategies produce a raw password string from the keychain. This is de
|
||||
|
||||
### 3.4 Storage Labels
|
||||
|
||||
| Browser | Keychain Account |
|
||||
|---------|-----------------|
|
||||
| Chrome / Chrome Beta | `"Chrome"` |
|
||||
| Edge | `"Microsoft Edge"` |
|
||||
| Chromium | `"Chromium"` |
|
||||
| Opera / OperaGX | `"Opera"` |
|
||||
| Vivaldi | `"Vivaldi"` |
|
||||
| Brave | `"Brave"` |
|
||||
| Yandex | `"Yandex"` |
|
||||
| Arc | `"Arc"` |
|
||||
| CocCoc | `"CocCoc"` |
|
||||
Each browser identifies its Keychain entry with a short account string — typically the browser's base name (`"Chrome"`, `"Brave"`, `"Arc"`). Edge uses `"Microsoft Edge"`. Related variants share labels rather than defining their own: Chrome Beta aliases onto `"Chrome"`, Opera GX aliases onto `"Opera"`.
|
||||
|
||||
The authoritative mapping lives in the `Storage` field of each entry in `platformBrowsers()` (`browser/browser_darwin.go`).
|
||||
|
||||
## 4. Windows Key Retrieval
|
||||
|
||||
@@ -148,11 +140,9 @@ A single iteration makes PBKDF2 essentially a keyed HMAC — no real key-stretch
|
||||
|
||||
### 5.4 Storage Labels
|
||||
|
||||
| Browser | D-Bus Label |
|
||||
|---------|-------------|
|
||||
| Chrome / Chrome Beta / Vivaldi | `"Chrome Safe Storage"` |
|
||||
| Chromium / Edge / Opera | `"Chromium Safe Storage"` |
|
||||
| Brave | `"Brave Safe Storage"` |
|
||||
Linux D-Bus labels follow a `"<name> Safe Storage"` convention, but many browsers alias onto a small shared set rather than defining their own. The three distinct labels are `"Chrome Safe Storage"`, `"Chromium Safe Storage"`, and `"Brave Safe Storage"` — everything else maps onto one of these.
|
||||
|
||||
The authoritative mapping lives in the `Storage` field of each entry in `platformBrowsers()` (`browser/browser_linux.go`).
|
||||
|
||||
## 6. Platform Summary
|
||||
|
||||
@@ -164,6 +154,51 @@ A single iteration makes PBKDF2 essentially a keyed HMAC — no real key-stretch
|
||||
|
||||
\* Only included when `--keychain-pw` is provided.
|
||||
|
||||
## 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`.
|
||||
|
||||
This is a deliberate architectural choice, not an oversight. The following sections explain why.
|
||||
|
||||
### 7.1 Why Safari Does Not Share the Chromium Chain
|
||||
|
||||
| Aspect | Chromium chain | Safari direct access |
|
||||
|---|---|---|
|
||||
| Output | A 16-byte AES-128 key | A list of `InternetPassword` records |
|
||||
| Use case | Decrypt Login Data DB | Records *are* the credentials |
|
||||
| Number of consumers | 10+ Chromium variants | 1 (Safari only) |
|
||||
| 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.
|
||||
|
||||
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.**
|
||||
|
||||
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.
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
### 7.3 Where the `--keychain-pw` Password Goes
|
||||
|
||||
The macOS login password is resolved once at startup by `browser/browser_darwin.go::resolveKeychainPassword`, then delivered to both consumers from within a single platform-specific closure, `newPlatformInjector` (defined per platform in `browser/browser_{darwin,linux,windows}.go`). The closure captures both the retriever chain and the raw password, and applies whichever capability interface each Browser happens to satisfy:
|
||||
|
||||
| Consumer | Capability interface | Defined in | Payload |
|
||||
|---|---|---|---|
|
||||
| Chromium browsers | `retrieverSetter` | `browser/browser.go` | `keyretriever.KeyRetriever` chain |
|
||||
| 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.
|
||||
|
||||
`resolveKeychainPassword` additionally performs an early `TryUnlock` against `keychainbreaker` before the chain is built, so a bad password surfaces as a startup warning rather than a mid-extraction failure. The small cost of opening the keychain twice (once for validation, once inside `KeychainPasswordRetriever`) buys meaningful UX.
|
||||
|
||||
## References
|
||||
|
||||
- **macOS**: [os_crypt_mac.mm](https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157)
|
||||
|
||||
Reference in New Issue
Block a user