mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-07-04 21:37:47 +02:00
docs: cross-host decryption guide and comment cleanup (#614)
* docs(readme): document cross-host decryption workflow * docs: drop RFC citations and what-comments
This commit is contained in:
@@ -11,7 +11,7 @@ HackBrowserData is a CLI security research tool that extracts and decrypts brows
|
||||
Key constraints:
|
||||
|
||||
- **Go 1.20** — the module must build with Go 1.20 to maintain Windows 7 support. Features from Go 1.21+ (`log/slog`, `slices`, `maps`, `cmp`) must not be used.
|
||||
- **Supported engines**: Chromium (including Yandex and Opera variants) and Firefox.
|
||||
- **Supported engines**: Chromium (including Yandex and Opera variants), Firefox, and Safari.
|
||||
- **Supported platforms**: Windows (DPAPI), macOS (Keychain), Linux (D-Bus Secret Service).
|
||||
- **No root-level library API** — the CLI calls `browser.DiscoverBrowsersWithKeys()` directly; there is no importable `pkg/` surface.
|
||||
|
||||
@@ -19,10 +19,11 @@ Key constraints:
|
||||
|
||||
```
|
||||
HackBrowserData/
|
||||
├── cmd/hack-browser-data/ # CLI entrypoint: cobra root, dump, list, version
|
||||
├── cmd/hack-browser-data/ # CLI entrypoint: cobra root, dump, dumpkeys, archive, restore, list, version
|
||||
├── browser/ # Browser interface, DiscoverBrowsersWithKeys(), platform browser lists
|
||||
│ ├── chromium/ # Chromium engine: extraction, decryption, profile discovery
|
||||
│ └── firefox/ # Firefox engine: extraction, NSS key derivation
|
||||
│ ├── firefox/ # Firefox engine: extraction, NSS key derivation
|
||||
│ └── safari/ # Safari engine: Keychain, Bookmark, History, Downloads (macOS only)
|
||||
├── types/ # Data model: Category enum, Entry structs, BrowserData
|
||||
├── crypto/ # Encryption primitives, cipher version detection
|
||||
├── masterkey/ # Platform-specific master key retrieval (Keychain/DPAPI/D-Bus)
|
||||
@@ -59,7 +60,7 @@ Each category has a corresponding Entry struct with `json` and `csv` struct tags
|
||||
|
||||
### 3.3 BrowserData Container
|
||||
|
||||
`BrowserData` is the result container returned by `Extract()`. It holds typed slices — one per category. The container is populated field-by-field during extraction. The output layer uses `makeExtractor[T]()` generics to pull the correct slice for serialization.
|
||||
`BrowserData` is the per-profile data container holding typed slices — one per category, populated field-by-field during extraction. `Extract()` returns `[]ExtractResult`, where each element pairs a `Profile` identity with a `*BrowserData`. The output layer uses `makeExtractor[T]()` generics to pull the correct slice for serialization.
|
||||
|
||||
## 4. Browser Interface & Registration
|
||||
|
||||
@@ -91,7 +92,7 @@ DiscoverBrowsersWithKeys(opts) // used by `dump` — ready to
|
||||
→ resolveSourcePaths() // stat candidates, first match wins
|
||||
→ newCredentialInjector(opts) // build-tagged: returns a browserInjector
|
||||
→ for each browser: // closure captures retriever + keychain pw lazily
|
||||
inject(b) // type-assert retrieverSetter / keychainPasswordSetter
|
||||
inject(b) // type-assert KeyManager / KeychainPasswordReceiver
|
||||
|
||||
DiscoverBrowsers(opts) // used by `list` / `list --detail`
|
||||
→ discoverFromConfigs(configs, opts) // same shared discovery core, NO injection
|
||||
@@ -118,13 +119,13 @@ Adding a new browser is a config-only change in `platformBrowsers()`; this secti
|
||||
|
||||
## 5. Extract() Orchestration
|
||||
|
||||
Both Chromium and Firefox engines follow the same extraction pattern:
|
||||
Both Chromium and Firefox engines follow the same per-profile extraction pattern (Firefox runs it inside each `profile.extract()` call; for Firefox the master key comes from `key4.db` rather than a platform API):
|
||||
|
||||
```
|
||||
Extract(categories)
|
||||
Extract(categories) // per-profile: one invocation per profile
|
||||
1. NewSession() → create isolated temp directory
|
||||
2. acquireFiles(session) → copy source files to temp dir (with dedup and WAL/SHM)
|
||||
3. getMasterKey(session) → platform-specific key retrieval
|
||||
3. getMasterKey(session) → platform-specific key retrieval (Firefox: key4.db)
|
||||
4. for each category:
|
||||
extractCategory(data, cat, masterKey, path)
|
||||
5. defer session.Cleanup() → remove temp directory
|
||||
@@ -146,7 +147,7 @@ The extraction loop maximizes data recovery. Each category is extracted independ
|
||||
|
||||
### 5.2 Custom Extractors
|
||||
|
||||
The `categoryExtractor` interface allows browser-specific extraction logic. Yandex and Opera use custom extractors for passwords and extensions respectively, while all other categories fall through to the default Chromium implementation.
|
||||
The `categoryExtractor` interface allows browser-specific extraction logic. Yandex uses custom extractors for passwords and credit cards; Opera uses a custom extractor for extensions. All other categories fall through to the default Chromium implementation.
|
||||
|
||||
## 6. Dependency Constraints
|
||||
|
||||
@@ -160,7 +161,7 @@ The module is pinned to `go 1.20` in `go.mod`. This is enforced by a CI lint che
|
||||
| `github.com/spf13/cobra` | v1.10.2 | CLI framework |
|
||||
| `github.com/moond4rk/keychainbreaker` | v0.2.5 | macOS keychain decryption |
|
||||
| `github.com/godbus/dbus/v5` | v5.2.2 | Linux D-Bus Secret Service |
|
||||
| `golang.org/x/sys` | v0.27.0 | Windows syscalls (DPAPI, DuplicateHandle) |
|
||||
| `golang.org/x/sys` | v0.30.0 | Windows syscalls (DPAPI, DuplicateHandle) |
|
||||
|
||||
## Related RFCs
|
||||
|
||||
|
||||
@@ -33,13 +33,13 @@ Yandex overrides two file names from the standard Chromium layout:
|
||||
| Password | `Login Data` | `Ya Passman Data` |
|
||||
| CreditCard | `Web Data` | `Ya Credit Cards` |
|
||||
|
||||
Yandex also uses `action_url` instead of `origin_url` in its password SQL query.
|
||||
Yandex's password query selects extra columns (`username_element`, `password_element`, `signon_realm`) beyond the standard four; these columns are used to construct the per-row AAD for decryption. The URL column is `origin_url`, same as standard Chromium.
|
||||
|
||||
**Important limitation**: Yandex passwords and cookies currently cannot be decrypted because Yandex uses its own proprietary encryption algorithm. Only non-encrypted categories (bookmarks, history, downloads, extensions, storage) produce useful results.
|
||||
Yandex passwords and credit cards use Yandex's proprietary two-layer encryption (see RFC-012) and are fully supported. Cookie decryption follows standard Chromium v10/v20 paths.
|
||||
|
||||
### 2.2 Opera
|
||||
|
||||
Opera differs from standard Chromium in two ways:
|
||||
Opera differs from standard Chromium in three ways:
|
||||
|
||||
- **Extension key**: Opera stores extension settings under `extensions.opsettings` in Secure Preferences, instead of the standard `extensions.settings`.
|
||||
- **Windows path**: Opera uses `AppData/Roaming` rather than `AppData/Local`, unlike most Chromium browsers.
|
||||
@@ -106,8 +106,8 @@ No encrypted fields. Shares the same `History` SQLite database as browsing histo
|
||||
### 4.6 Credit Cards (Web Data -- SQLite)
|
||||
|
||||
```sql
|
||||
SELECT guid, name_on_card, expiration_month, expiration_year,
|
||||
card_number_encrypted, nickname, billing_address_id FROM credit_cards
|
||||
SELECT COALESCE(guid, ''), name_on_card, expiration_month, expiration_year,
|
||||
card_number_encrypted, COALESCE(nickname, ''), COALESCE(billing_address_id, '') FROM credit_cards
|
||||
```
|
||||
|
||||
The `card_number_encrypted` column contains encrypted bytes.
|
||||
|
||||
@@ -18,6 +18,7 @@ Every encrypted value begins with a 3-byte prefix that identifies the cipher ver
|
||||
|--------|---------|---------|
|
||||
| `v10` | CipherV10 | Chrome 80+ standard encryption (AES-GCM on Windows, AES-CBC on macOS/Linux) |
|
||||
| `v11` | CipherV11 | Linux-only: AES-CBC variant where the key comes from libsecret / kwallet. Same algorithm and parameters as `v10` — only the key source differs |
|
||||
| `v12` | CipherV12 | Chromium SecretPortal/Flatpak (xdg-desktop-portal) — recognized by the version detector so a clear error can be returned; not yet implemented |
|
||||
| `v20` | CipherV20 | Chrome 127+ App-Bound Encryption |
|
||||
| (none) | CipherDPAPI | Pre-Chrome 80 raw DPAPI encryption (Windows only, no prefix) |
|
||||
|
||||
@@ -92,20 +93,15 @@ Decryption uses AES-128-CBC with a fixed IV of 16 space bytes (`0x20`) and PKCS5
|
||||
|
||||
## 6. v20 App-Bound Encryption (Chrome 127+)
|
||||
|
||||
Chrome 127 introduced App-Bound Encryption on Windows, identified by the `v20` prefix. This scheme binds the encryption key to the Chrome application identity, making it harder for external tools to decrypt. After decryption, the payload contains a 32-byte application header before the actual plaintext:
|
||||
Chrome 127 introduced App-Bound Encryption on Windows, identified by the `v20` prefix. This scheme binds the encryption key to the Chrome application identity. The key is a 32-byte AES-256 key retrieved via reflective injection into the browser process (`ABERetriever`). Ciphertext layout:
|
||||
|
||||
```
|
||||
| v20 | nonce | AES-GCM payload |
|
||||
| v20 | nonce | AES-GCM ciphertext + auth tag |
|
||||
|-------|--------|-------------------------------------|
|
||||
| 3B | 12B | remaining bytes |
|
||||
|
||||
After decryption:
|
||||
| app-bound header | plaintext |
|
||||
|------------------|------------------------------------|
|
||||
| 32B | remaining bytes |
|
||||
```
|
||||
|
||||
**Current status**: v20 decryption is not yet implemented. Encountering a `v20`-prefixed value returns an error. This primarily affects recent Chrome installations on Windows.
|
||||
Decryption uses `DecryptChromiumGCM` with the ABE-retrieved key. Note: `DecryptChromiumGCM` strips only the version prefix (3B) and nonce (12B) before passing to AES-GCM; it does not strip any post-decrypt header from the result.
|
||||
|
||||
## 7. Decryption Flow
|
||||
|
||||
@@ -113,8 +109,9 @@ The high-level decryption path for any encrypted Chromium value:
|
||||
|
||||
1. **Detect version** -- inspect the first 3 bytes of the ciphertext
|
||||
2. **Route by version**:
|
||||
- `v10` / `v11` -- strip prefix, call platform-specific decryption (AES-CBC on macOS/Linux, AES-GCM on Windows). On Linux, a failed decryption retries once with `kEmptyKey` to recover legacy crbug.com/40055416 data
|
||||
- `v20` -- not yet supported, return error
|
||||
- `v10` / `v11` -- strip prefix, call platform-specific decryption (AES-CBC on macOS/Linux, AES-GCM on Windows). On macOS/Linux, a failed AES-CBC decryption retries once with `kEmptyKey` to recover legacy crbug.com/40055416 data
|
||||
- `v12` -- SecretPortal/Flatpak — recognized, returns known-gap error (not yet implemented)
|
||||
- `v20` -- AES-256-GCM with 32-byte ABE key (retrieved via Windows reflective injection)
|
||||
- DPAPI (no prefix) -- call Windows `CryptUnprotectData` directly (Windows only; returns error on other platforms)
|
||||
3. **Return plaintext** -- the decrypted bytes are interpreted as a UTF-8 string
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ Firefox uses inconsistent timestamp units across data types. All are Unix epoch-
|
||||
| Cookies (`expiry`) | Seconds | direct |
|
||||
| History (`last_visit_date`) | Microseconds | / 1,000,000 |
|
||||
| Downloads (`dateAdded`) | Microseconds | / 1,000,000 |
|
||||
| Downloads (`endTime`) | Milliseconds | / 1,000 |
|
||||
| Bookmarks (`dateAdded`) | Microseconds | / 1,000,000 |
|
||||
| Passwords (`timeCreated`) | Milliseconds | / 1,000 |
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ key = dk[:24], iv = dk[32:40] // 3DES key + IV
|
||||
|
||||
### 3.2 passwordCheckPBE Key Derivation
|
||||
|
||||
Uses standard PBKDF2 with SHA-256 and parameters embedded in the ASN1 structure (entry salt, iteration count, key size). The IV is reconstructed by prepending the ASN.1 OCTET STRING header (`0x04 0x0E`) to the 14-byte IV value from the parsed structure, yielding a 16-byte AES IV.
|
||||
Uses PBKDF2-SHA-256 with parameters embedded in the ASN1 structure (entry salt, iteration count, key size). The PBKDF2 password is `SHA1(globalSalt)` (a 20-byte digest), not `globalSalt` itself. The IV is reconstructed by prepending the ASN.1 OCTET STRING header (`0x04 0x0E`) to the 14-byte IV value from the parsed structure, yielding a 16-byte AES IV.
|
||||
|
||||
## 4. Password Decryption
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ Windows populates two slots of the `masterkey.Retrievers` struct — V10 (legacy
|
||||
|
||||
`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.
|
||||
**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.(Browser).masterKeys` 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.
|
||||
|
||||
**Non-ABE Chromium forks** (Opera, Vivaldi, Yandex, 360, QQ, Sogou) omit `WindowsABE` in `platformBrowsers()` (default false). The caller leaves `Hints.WindowsABEKey` empty, and `ABERetriever` returns `(nil, nil)` for empty `WindowsABEKey`, which `NewMasterKeys` treats silently as "not applicable" — so attempting ABE on these forks is a no-op, not a failure. Their V10 DPAPI key continues to work unchanged.
|
||||
|
||||
@@ -178,7 +178,7 @@ The authoritative mapping lives in the `KeychainLabel` field of each entry in `p
|
||||
| Windows | V10 = DPAPIRetriever; V20 = ABERetriever (Chrome 127+) | No | AES-256 |
|
||||
| Linux | V10 = PosixRetriever ("peanuts" kV10Key); V11 = DBusRetriever (keyring kV11Key) | 1 iteration | AES-128 |
|
||||
|
||||
\* Only included when `--keychain-pw` is provided.
|
||||
\* Only included when a non-empty password resolves — either via `--keychain-pw` flag or an interactive TTY prompt.
|
||||
|
||||
## 7. Safari Credential Extraction
|
||||
|
||||
@@ -218,10 +218,10 @@ 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` | `masterkey.Retrievers` struct (V10 / V11 / V20 slots; unused tiers nil) |
|
||||
| Safari | `keychainPasswordSetter` | `browser/browser_darwin.go` | raw `string` |
|
||||
| Chromium browsers | `KeyManager` | `browser/browser.go` | `masterkey.Retrievers` struct (V10 / V11 / V20 slots; unused tiers nil) |
|
||||
| Safari | `KeychainPasswordReceiver` | `browser/browser.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.
|
||||
The two interfaces 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.
|
||||
|
||||
`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.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## 1. Command Structure
|
||||
|
||||
The CLI is built on [cobra](https://github.com/spf13/cobra) with three subcommands: `dump`, `list`, and `version`.
|
||||
The CLI is built on [cobra](https://github.com/spf13/cobra) with six subcommands: `dump`, `dumpkeys`, `archive`, `restore`, `list`, and `version`.
|
||||
|
||||
### 1.1 Root Command
|
||||
|
||||
@@ -22,7 +22,7 @@ The primary command. Extracts, decrypts, and writes browser data to files.
|
||||
|------|-------|---------|-------------|
|
||||
| `--browser` | `-b` | `"all"` | Target browser |
|
||||
| `--category` | `-c` | `"all"` | Data categories (comma-separated) |
|
||||
| `--format` | `-f` | `"csv"` | Output format: csv, json, cookie-editor |
|
||||
| `--format` | `-f` | `"json"` | Output format: csv, json, cookie-editor |
|
||||
| `--dir` | `-d` | `"results"` | Output directory |
|
||||
| `--profile-path` | `-p` | | Custom profile directory |
|
||||
| `--keychain-pw` | | | macOS keychain password |
|
||||
@@ -38,7 +38,7 @@ Lists all detected browsers and profiles via `text/tabwriter`.
|
||||
|
||||
**Basic mode** (default) — three columns: Browser, Profile, Path.
|
||||
|
||||
**Detail mode** (`--detail`) — adds a column for every category showing entry counts. This actually calls `Extract()` on each browser to count entries.
|
||||
**Detail mode** (`--detail`) — adds a column for every category showing entry counts. This calls `CountEntries()` on each browser (not `Extract()`) — no decryption is performed.
|
||||
|
||||
### 1.4 version Command
|
||||
|
||||
@@ -125,7 +125,7 @@ CLI: hack-browser-data dump -b chrome -c password,cookie -f csv -d results
|
||||
→ parseCategories("password,cookie") → []Category
|
||||
→ NewWriter("results", "csv") → *Writer
|
||||
→ for each browser:
|
||||
Extract(categories) → *BrowserData
|
||||
Extract(categories) → []ExtractResult
|
||||
Writer.Add(browser, profile, data)
|
||||
→ Writer.Write()
|
||||
→ aggregate by category → format rows → write files
|
||||
|
||||
@@ -27,8 +27,10 @@ Acquire(src, dst, isDir)
|
||||
├── isDir=true → copyDir(src, dst, skip="lock")
|
||||
│
|
||||
└── isDir=false → copyFile(src, dst)
|
||||
├── success → copy -wal and -shm companions if present
|
||||
└── failure + Windows → copyLocked(src, dst) fallback
|
||||
├── success ──┐
|
||||
└── failure + Windows → copyLocked(src, dst)
|
||||
└── success ──┐
|
||||
copy -wal and -shm companions if present
|
||||
```
|
||||
|
||||
### SQLite Companion Files
|
||||
|
||||
@@ -43,6 +43,7 @@ Each entry in the result table:
|
||||
|
||||
| Field | Size | Description |
|
||||
|-------|------|-------------|
|
||||
| Object | `uintptr` | Kernel object pointer |
|
||||
| UniqueProcessID | `uintptr` | Owning process PID |
|
||||
| HandleValue | `uintptr` | Handle value in the owning process |
|
||||
| GrantedAccess | `uint32` | Access mask |
|
||||
@@ -76,13 +77,13 @@ Suffix: google\chrome\...\network\cookies
|
||||
Once we have a duplicated handle to the locked file:
|
||||
|
||||
```
|
||||
| DuplicateHandle (read access) |
|
||||
| DuplicateHandle(DUPLICATE_SAME_ACCESS) |
|
||||
|-------------------------------------------------|
|
||||
↓
|
||||
| CreateFileMappingW(handle, PAGE_READONLY) |
|
||||
|-------------------------------------------------|
|
||||
↓
|
||||
| MapViewOfFile(mapping, FILE_MAP_READ, fileSize) |
|
||||
| MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0) |
|
||||
|-------------------------------------------------|
|
||||
↓
|
||||
| byte slice from kernel file cache |
|
||||
@@ -95,7 +96,7 @@ Once we have a duplicated handle to the locked file:
|
||||
|
||||
Memory-mapped I/O reads from the OS kernel's **file cache**, which includes data Chrome has written but not yet checkpointed to disk. This produces a more complete snapshot than a raw `ReadFile`.
|
||||
|
||||
**Fallback**: if `CreateFileMappingW` fails (e.g., the file is empty or zero-length), falls back to `Seek(0)` + `ReadFile` on the duplicated handle.
|
||||
**Fallback**: if `CreateFileMappingW` fails for any reason, falls back to `Seek(0)` + `ReadFile` on the duplicated handle.
|
||||
|
||||
## 4. Why This Works
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ End-to-end flow when `hack-browser-data.exe` encounters a v20 Chromium cookie on
|
||||
|
||||
```
|
||||
browser/chromium.Extract()
|
||||
→ masterkey.Chain [ABERetriever, DPAPIRetriever]
|
||||
→ masterkey.Retrievers{V10: &DPAPIRetriever{}, V20: &ABERetriever{}}
|
||||
→ ABERetriever.RetrieveKey():
|
||||
reads Local State → extracts APPB-prefixed blob
|
||||
resolves browser exe via registry App Paths
|
||||
@@ -92,7 +92,7 @@ DoExtractKey → see §4.2
|
||||
2. `ReadProcessMemory` for the 12-byte diagnostic header, then 32-byte key when `status == ready`.
|
||||
3. `TerminateProcess(browser)` — the target was a throwaway from the start.
|
||||
|
||||
The returned key flows back up to `crypto.DecryptChromiumV20` (cross-platform AES-256-GCM; see §5.3) and then to the usual cookie/password extraction pipeline.
|
||||
The returned key flows back up to `crypto.DecryptChromiumGCM` (cross-platform AES-256-GCM; see §5.3) and then to the usual cookie/password extraction pipeline.
|
||||
|
||||
## 4. C payload — `crypto/windows/abe_native/`
|
||||
|
||||
@@ -163,12 +163,11 @@ Validity relies on Windows **KnownDlls + session-consistent ASLR** — `kernel32
|
||||
|
||||
### 5.1 Injector package — `utils/injector/`
|
||||
|
||||
Three files collaborate:
|
||||
Four files collaborate:
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `reflective_windows.go` | `Reflective.Inject(exePath, payload, env) ([]byte, error)` — the orchestrator |
|
||||
| `winapi_windows.go` | Package-level `windows.LazyProc` handles + `callBoolErr` helper. Centralizes `VirtualAllocEx` / `CreateRemoteThread` / NtFlushIC / import-address lookups. `ReadProcessMemory` / `WriteProcessMemory` use `x/sys/windows` typed wrappers directly. |
|
||||
| `reflective_windows.go` | `Reflective.Inject(exePath, payload, env) ([]byte, error)` — the orchestrator. Win32 calls (`VirtualAllocEx`, `CreateRemoteThread`, `NtFlushIC`, import-address lookups) delegate to `utils/winapi/` via `CallBoolErr`. |
|
||||
| `errors_windows.go` | `formatABEError(scratchResult) string` — renders the C-side diag channel into human-readable strings via two lookup maps (`ABE_ERR_*` names + known HRESULT names like `E_ACCESSDENIED`). |
|
||||
| `pe_windows.go` | `FindExportFileOffset(dllBytes, "Bootstrap")` — raw-file offset via `debug/pe`. |
|
||||
| `arch_windows.go` | Architecture validation (amd64-only today). |
|
||||
@@ -185,7 +184,7 @@ _Static_assert(offsetof(struct BootstrapScratch, hresult) == 0x2C, "hresult offs
|
||||
_Static_assert(offsetof(struct BootstrapScratch, shared) == 0x40, "shared offset");
|
||||
```
|
||||
|
||||
Go consumes the same constants via **`go tool cgo -godefs`** (a development-time tool, not a runtime dependency). `make gen-layout` regenerates `crypto/windows/abe_native/bootstrap/layout.go` from `bootstrap_layout.h` using `CC="zig cc"` for bit-identical results across host OSes. `make gen-layout-verify` is wired into CI to fail if the committed `layout.go` is stale.
|
||||
Go consumes the same constants via **`go tool cgo -godefs`** (a development-time tool, not a runtime dependency). `make gen-layout` regenerates `crypto/windows/abe_native/bootstrap/layout.go` from `bootstrap_layout.h` using `CC="zig cc"` for bit-identical results across host OSes. `make gen-layout-verify` can be run locally to verify the committed `layout.go` matches the current header.
|
||||
|
||||
**Why `cgo -godefs` rather than runtime `import "C"`**: we only need constants shared, not FFI to C functions. Runtime CGO would force the whole project into `CGO_ENABLED=1`, losing the "non-Windows contributor needs no C toolchain" guarantee. `cgo -godefs` bakes the values into a pure-Go file that commits to git; the project stays `CGO_ENABLED=0`.
|
||||
|
||||
@@ -201,12 +200,12 @@ Go consumes the same constants via **`go tool cgo -godefs`** (a development-time
|
||||
|
||||
On extraction success, logs at `Info` level (`abe: retrieved <browser> master key via reflective injection`).
|
||||
|
||||
**v20 decryption** is cross-platform by design: `browser/chromium/decrypt.go` routes `CipherV20` → `crypto.DecryptChromiumV20` (defined in `crypto/crypto.go`, uses `AESGCMDecrypt`). This lets Linux/macOS CI exercise the same decryption path as Windows — only the key-source side is platform-gated.
|
||||
**v20 decryption** is cross-platform by design: `browser/chromium/decrypt.go` routes `CipherV20` → `crypto.DecryptChromiumGCM` (defined in `crypto/crypto.go`, uses `AESGCMDecrypt`). This lets Linux/macOS CI exercise the same decryption path as Windows — only the key-source side is platform-gated.
|
||||
|
||||
## 6. Build chain
|
||||
|
||||
- **Default build** (any host, no zig): `go build ./cmd/hack-browser-data/` succeeds; ABE is stubbed out. Legacy v10/v11 cookies still decrypt via DPAPI.
|
||||
- **Windows release with ABE**: `make build-windows` = `make payload` (zig cc → `crypto/abe_extractor_amd64.bin`) + `GOOS=windows go build -tags abe_embed`. The `abe_embed` tag activates `//go:embed` on the compiled binary.
|
||||
- **Windows release with ABE**: `make build-windows` = `make payload` (zig cc → `crypto/windows/payload/abe_extractor_amd64.bin`) + `GOOS=windows go build -tags abe_embed`. The `abe_embed` tag activates `//go:embed` on the compiled binary.
|
||||
- **Layout regen**: `make gen-layout` after any change to `bootstrap_layout.h`.
|
||||
- **`go.mod` unchanged** — no new dependencies. `zig` is the only external toolchain, and only when actually rebuilding the payload.
|
||||
|
||||
@@ -226,7 +225,7 @@ All ABE-specific Go code is behind `//go:build windows` (plus `&& abe_embed` for
|
||||
**No payload bytes ever touch disk on the target machine.**
|
||||
|
||||
- Payload DLL exists only as:
|
||||
1. Build artifact on the developer machine (`crypto/abe_extractor_amd64.bin`, git-ignored)
|
||||
1. Build artifact on the developer machine (`crypto/windows/payload/abe_extractor_amd64.bin`, git-ignored)
|
||||
2. `.rdata` section of `hack-browser-data.exe` (`//go:embed`)
|
||||
3. Go `[]byte` in our process memory (one `copy()` for import patching)
|
||||
4. `VirtualAllocEx`'d region in the target browser during injection; released on `TerminateProcess`
|
||||
|
||||
@@ -62,6 +62,7 @@ Safari uses two different casings for the same profile UUID across the container
|
||||
| Cookie | `Container/Cookies/Cookies.binarycookies`, then `~/Library/Cookies/Cookies.binarycookies` | BinaryCookies |
|
||||
| Bookmark | `~/Library/Safari/Bookmarks.plist` | plist |
|
||||
| Download | `~/Library/Safari/Downloads.plist` | plist |
|
||||
| Extension | `Container/Safari/AppExtensions/Extensions.plist`, `Container/Safari/WebExtensions/Extensions.plist` | plist |
|
||||
| LocalStorage | `Container/WebKit/WebsiteData/Default/` | WebKit Origins dir |
|
||||
| Password | macOS Keychain | — |
|
||||
|
||||
@@ -87,9 +88,13 @@ Passwords live in the user-scope Keychain, not on a per-profile basis — only t
|
||||
### 4.1 History (History.db — SQLite)
|
||||
|
||||
```sql
|
||||
SELECT url, title, visit_count, visit_time
|
||||
FROM history_items
|
||||
LEFT JOIN history_visits ON history_items.id = history_visits.history_item
|
||||
SELECT hi.url, COALESCE(hv.title, ''), hi.visit_count, COALESCE(hv.visit_time, 0)
|
||||
FROM history_items hi
|
||||
LEFT JOIN history_visits hv ON hv.id = (
|
||||
SELECT hv2.id FROM history_visits hv2
|
||||
WHERE hv2.history_item = hi.id
|
||||
ORDER BY hv2.visit_time DESC LIMIT 1
|
||||
)
|
||||
```
|
||||
|
||||
Schema notes:
|
||||
@@ -99,7 +104,7 @@ Schema notes:
|
||||
|
||||
### 4.2 Cookies (Cookies.binarycookies — binary)
|
||||
|
||||
Apple's proprietary BinaryCookies format — not SQLite, not a documented format. Parsed by the [go-binarycookies](https://github.com/moond4rk/go-binarycookies) library.
|
||||
Apple's proprietary BinaryCookies format — not SQLite, not a documented format. Parsed by the [binarycookies](https://github.com/moond4rk/binarycookies) library.
|
||||
|
||||
High-level layout:
|
||||
|
||||
@@ -122,7 +127,7 @@ A nested dictionary tree with a `WebBookmarkType` discriminator at each node:
|
||||
| `WebBookmarkTypeList` | Folder | `Children` (array) |
|
||||
| `WebBookmarkTypeLeaf` | URL entry | `URLString`, `URIDictionary.title` |
|
||||
|
||||
The extractor walks the tree recursively, collecting leaf nodes into a flat list. Folder names are not preserved (only URL + title pairs are exported).
|
||||
The extractor walks the tree recursively, collecting leaf nodes into a flat list. Folder names are preserved in the `Folder` field of each `BookmarkEntry`.
|
||||
|
||||
### 4.4 Downloads (Downloads.plist — property list)
|
||||
|
||||
@@ -132,7 +137,7 @@ A flat structure with a `DownloadHistory` array. Relevant keys per entry:
|
||||
|-----|---------|
|
||||
| `DownloadEntryURL` | Source URL |
|
||||
| `DownloadEntryPath` | Local filesystem path |
|
||||
| `DownloadEntryBytesReceivedSoFar` | Bytes downloaded |
|
||||
| `DownloadEntryProgressTotalToLoad` | Total bytes to download |
|
||||
| `DownloadEntryProfileUUIDStringKey` | Owning profile's uppercase UUID, or `"DefaultProfile"` |
|
||||
|
||||
The extractor filters by the caller-provided owner UUID so each profile reports its own downloads. MIME type and start/end times are not stored by Safari — `MimeType` is always empty in the output.
|
||||
@@ -241,7 +246,7 @@ The only encrypted category is passwords. Because they are not stored in Safari'
|
||||
- **Full Disk Access (TCC)** is required to read the sandboxed container. Without it, cookies / history / downloads / localStorage reads fail silently with permission errors at stat or open time. Legacy paths under `~/Library/Safari/` sometimes remain readable without FDA, but are mostly empty on modern systems.
|
||||
- **Live-file safety** follows a live-vs-temp split:
|
||||
- **Live reads** (`SafariTabs.db` during profile discovery in `profiles.go`) use `?mode=ro&immutable=1`, which disables WAL replay and locking so the extractor cannot disturb a running Safari — it sees a consistent snapshot of the main DB as of read time, at the cost of missing any pending WAL content.
|
||||
- **Temp-copy reads** (`History.db`, `localstorage.sqlite3`, etc. via `filemanager.Session.Acquire`) use `?mode=ro` only. `Session.Acquire` copies the `-wal` / `-shm` sidecars alongside the main DB, so SQLite can replay uncommitted transactions on the copy — surfacing entries Safari has written to WAL but not yet checkpointed. Any `-shm` writes SQLite performs during replay land on the ephemeral copy and are deleted with the session.
|
||||
- **Temp-copy reads** (via `filemanager.Session.Acquire`) vary by file: `localstorage.sqlite3` uses `?mode=ro` so SQLite can replay the copied `-wal` sidecar; `History.db` opens with `PRAGMA journal_mode=off` (WAL replay not needed for read-only history queries). `Session.Acquire` copies the `-wal` / `-shm` sidecars alongside the main DB. Any `-shm` writes SQLite performs during replay land on the ephemeral copy and are deleted with the session.
|
||||
- **Multi-profile availability**: requires Safari 17 (macOS 14 Sonoma) or newer. Older Safari versions have only the default profile; discovery degrades cleanly via the ReadDir fallback described in §2.1.
|
||||
- **File acquisition**: all per-profile files are copied into a `filemanager.Session` temp directory before extraction, except the discovery-time `SafariTabs.db` read which opens the live file directly. See [RFC-008](008-file-acquisition-and-platform-quirks.md) for the general pattern.
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ Deferred to a follow-up RFC / PR:
|
||||
### 3.1 `meta.local_encryptor_data`
|
||||
|
||||
```
|
||||
[protobuf preamble bytes...] "v10" [12B nonce] [68B plaintext + 16B GCM tag]
|
||||
[protobuf preamble bytes...] "v10" [12B nonce] [68B ciphertext + 16B GCM tag]
|
||||
```
|
||||
|
||||
The 68-byte plaintext (decrypted with the Chromium master key, empty AAD) has the shape:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# RFC-013: CLI Redesign — Flat-Verb Surface & Cross-Host Restore
|
||||
|
||||
**Author**: moonD4rk
|
||||
**Status**: Accepted — `archive` (#607) implemented; cross-platform `restore` (#606) pending
|
||||
**Status**: Implemented — `archive` (#610); cross-platform `restore` (#611)
|
||||
**Created**: 2026-06-03
|
||||
**Revised**: 2026-06-06 (subdir-convention archive, dual-mode restore, Local State, delivery order)
|
||||
|
||||
@@ -126,4 +126,4 @@ Working backwards from the chosen surface:
|
||||
| [RFC-003](003-chromium-encryption.md) | Cipher version dispatch (v10/v11/v20) consumed by restore |
|
||||
| [RFC-006](006-key-retrieval-mechanisms.md) | Master-key retrieval the cross-host split externalizes |
|
||||
| [RFC-001](001-project-architecture.md) | Browser interface and Extract() orchestration |
|
||||
| [RFC-008](008-file-acquisition-and-platform-quirks.md) | Locked-file session and CompressDir used by archive |
|
||||
| [RFC-008](008-file-acquisition-and-platform-quirks.md) | Locked-file session and ZipDir used by archive |
|
||||
|
||||
Reference in New Issue
Block a user