mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-06-10 20:07:46 +02:00
refactor(cli): flatten keys export/import into dumpkeys/restore (#608)
* refactor(cli): flatten keys export/import into dumpkeys/restore The keys noun-group clashed with the flat dump/list verbs; unify on flat verbs and drop the keys parent. Pure rename, no behavior change. * docs(rfc): add RFC-013 CLI redesign & cross-host restore The accepted design doc for the flat-verb CLI redesign and cross-platform restore.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/browser"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
)
|
||||
|
||||
func dumpKeysCmd() *cobra.Command {
|
||||
var (
|
||||
browserName string
|
||||
outputPath string
|
||||
keychainPw string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "dumpkeys",
|
||||
Short: "Export Chromium master keys as JSON for cross-host decryption",
|
||||
Example: ` hack-browser-data dumpkeys -o keys.json
|
||||
hack-browser-data dumpkeys -b chrome`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
browsers, err := browser.DiscoverBrowsersWithKeys(browser.DiscoverOptions{
|
||||
Name: browserName,
|
||||
KeychainPassword: keychainPw,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dump := browser.BuildDump(browsers)
|
||||
log.Infof("Exported keys for %d vault(s)", len(dump.Vaults))
|
||||
|
||||
if outputPath == "" {
|
||||
return dump.WriteJSON(os.Stdout)
|
||||
}
|
||||
f, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create %s: %w", outputPath, err)
|
||||
}
|
||||
defer f.Close()
|
||||
return dump.WriteJSON(f)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&browserName, "browser", "b", "all", "target browser: all|"+browser.Names())
|
||||
cmd.Flags().StringVarP(&outputPath, "output", "o", "", "output file (default: stdout)")
|
||||
cmd.Flags().StringVar(&keychainPw, "keychain-pw", "", "macOS keychain password")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -31,7 +31,7 @@ GitHub: https://github.com/moonD4rk/HackBrowserData`,
|
||||
root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable debug logging")
|
||||
|
||||
dump := dumpCmd()
|
||||
root.AddCommand(dump, listCmd(), keysCmd(), versionCmd())
|
||||
root.AddCommand(dump, dumpKeysCmd(), restoreCmd(), listCmd(), versionCmd())
|
||||
|
||||
// Default to dump when no subcommand is given.
|
||||
// Copy dump flags to root so that `hack-browser-data -b chrome`
|
||||
|
||||
@@ -13,59 +13,7 @@ import (
|
||||
"github.com/moond4rk/hackbrowserdata/masterkey"
|
||||
)
|
||||
|
||||
func keysCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "keys",
|
||||
Short: "Manage cross-host master keys",
|
||||
}
|
||||
cmd.AddCommand(keysExportCmd(), keysImportCmd())
|
||||
return cmd
|
||||
}
|
||||
|
||||
func keysExportCmd() *cobra.Command {
|
||||
var (
|
||||
browserName string
|
||||
outputPath string
|
||||
keychainPw string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "export",
|
||||
Short: "Export Chromium master keys as JSON for cross-host decryption",
|
||||
Example: ` hack-browser-data keys export -o dump.json
|
||||
hack-browser-data keys export -b chrome`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
browsers, err := browser.DiscoverBrowsersWithKeys(browser.DiscoverOptions{
|
||||
Name: browserName,
|
||||
KeychainPassword: keychainPw,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dump := browser.BuildDump(browsers)
|
||||
log.Infof("Exported keys for %d vault(s)", len(dump.Vaults))
|
||||
|
||||
if outputPath == "" {
|
||||
return dump.WriteJSON(os.Stdout)
|
||||
}
|
||||
f, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create %s: %w", outputPath, err)
|
||||
}
|
||||
defer f.Close()
|
||||
return dump.WriteJSON(f)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&browserName, "browser", "b", "all", "target browser: all|"+browser.Names())
|
||||
cmd.Flags().StringVarP(&outputPath, "output", "o", "", "output file (default: stdout)")
|
||||
cmd.Flags().StringVar(&keychainPw, "keychain-pw", "", "macOS keychain password")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func keysImportCmd() *cobra.Command {
|
||||
func restoreCmd() *cobra.Command {
|
||||
var (
|
||||
keysPath string
|
||||
browserName string
|
||||
@@ -77,11 +25,11 @@ func keysImportCmd() *cobra.Command {
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "import",
|
||||
Short: "Import master keys from JSON and decrypt a copied profile",
|
||||
Example: ` hack-browser-data keys import -i dump.json -b chrome -p /path/to/copied/User\ Data
|
||||
hack-browser-data keys import -i dump.json -b edge -p /path -c cookie -f csv
|
||||
ssh origin "hack-browser-data keys export" | hack-browser-data keys import -i - -b chrome -p /path`,
|
||||
Use: "restore",
|
||||
Short: "Decrypt a copied profile using exported master keys",
|
||||
Example: ` hack-browser-data restore -i keys.json -b chrome -p /path/to/copied/User\ Data
|
||||
hack-browser-data restore -i keys.json -b edge -p /path -c cookie -f csv
|
||||
ssh origin "hack-browser-data dumpkeys" | hack-browser-data restore -i - -b chrome -p /path`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
browsers, err := loadAndApplyKeys(browserName, profilePath, keysPath)
|
||||
if err != nil {
|
||||
@@ -0,0 +1,121 @@
|
||||
# RFC-013: CLI Redesign — Flat-Verb Surface & Cross-Host Restore
|
||||
|
||||
**Author**: moonD4rk
|
||||
**Status**: Accepted — implementation pending
|
||||
**Created**: 2026-06-03
|
||||
|
||||
## 1. Summary
|
||||
|
||||
The command-line surface has accreted two grammars: flat task-verbs (`dump`, `list`) alongside a noun-grouped `keys export` / `keys import` family. This RFC redesigns the whole surface around one grammar — flat verbs — and specifies the cross-host workflow end to end: export master keys on the origin host, archive the minimal data files, and restore (decrypt) them offline on an analyst host of any platform. It also records the structural problem the redesign exists to solve: the set of browsers a command can act on is platform-specific, yet cross-host restore must work for browsers that were never built for the analyst's OS. Breaking changes are accepted (pre-1.0); the method is CLI-first, deriving the internal data model backwards from the chosen surface.
|
||||
|
||||
## 2. Motivation
|
||||
|
||||
### 2.1 Two grammars in one tool
|
||||
|
||||
`dump` and `list` are flat task-verbs — the verb *is* the action. `keys export` / `keys import` is a noun-then-action grammar that implies a `keys` resource with sub-operations. Mixing the two in one small CLI reads as inconsistent: there is no `data export` or `browser list` to make the noun grammar systematic, so `keys` stands alone as a special case. The aim is a tool like kubectl — predictable because it is uniformly `verb resource` — but without kubectl's complexity. For a tool this size, the matching "simple and consistent" choice is uniformly flat verbs.
|
||||
|
||||
### 2.2 The cross-host workflow is half-automated
|
||||
|
||||
Cross-host decryption (export keys on the origin, decrypt a copied profile on the analyst host) shipped in #599–#605, but only the *keys* half is automated. The analyst still copies the origin's profile data by hand — awkward because the live SQLite files are locked on Windows, the full `User Data` tree is huge (caches), and only a handful of small files actually matter for decryption (#607).
|
||||
|
||||
### 2.3 Restore is bound to the local platform browser table (#606)
|
||||
|
||||
The consumer side reuses `DiscoverBrowsers`, which iterates `platformBrowsers()` — the browser table for the *analyst's* OS, selected by build tag (`browser_{darwin,windows,linux}.go`). A Windows-only fork such as Sogou / QQ / 360 lives only in the Windows table. On macOS, `restore -b sogou` matches nothing and aborts with `no browsers found`, even with a valid `keys.json` and the data supplied explicitly. This is the crux: the browsers a command may act on are platform-specific, but cross-host restore must transcend that — given a key and the data, it should decrypt any Chromium profile regardless of whether that browser exists on the analyst platform.
|
||||
|
||||
## 3. Proposed CLI surface
|
||||
|
||||
Six flat verbs, one grammar:
|
||||
|
||||
```
|
||||
hack-browser-data [flags] # default → dump (also Windows double-click)
|
||||
dump -b -c -f -d -p --zip # local: decrypt this host's browsers → data
|
||||
dumpkeys -b -o [--keychain-pw] # origin: master keys → keys.json (stdout default)
|
||||
archive -b -c -o # origin: minimal decryption-relevant files → zip
|
||||
restore --keys K (--data-dir D | --data-zip Z) [-b] -c -f -d # analyst: keys.json + data → decrypted
|
||||
list [--detail]
|
||||
version
|
||||
```
|
||||
|
||||
Workflows:
|
||||
|
||||
```
|
||||
local : hbd dump -b chrome -c cookie,password
|
||||
cross-host: origin> hbd dumpkeys -o keys.json
|
||||
origin> hbd archive -b chrome -o data.zip
|
||||
analyst> hbd restore --keys keys.json --data-zip data.zip -c cookie
|
||||
```
|
||||
|
||||
The `keys` parent command is removed: `keys export` becomes `dumpkeys`, `keys import` becomes `restore`, and a new `archive` fills the missing data-transport step. `dump` / `list` / `version` keep their current behavior; `dump` stays the default when no subcommand is given (which also covers the Windows double-click case).
|
||||
|
||||
## 4. The browser-universe model
|
||||
|
||||
The resolution to §2.3 is a single rule: **the set of browsers a command may act on — its "universe" — matches the nature of that command.**
|
||||
|
||||
| Command | Browser universe | `-b sogou` on macOS |
|
||||
|---------|------------------|---------------------|
|
||||
| `dump` / `dumpkeys` / `archive` / `list` | the local `platformBrowsers()` table (what this OS installs) | correctly fails — Sogou is not on macOS |
|
||||
| `restore` | the `keys.json` itself (whatever the origin exported) | succeeds — the dump contains a Sogou vault |
|
||||
|
||||
`dump`, `dumpkeys`, `archive`, and `list` act on browsers *installed on this host*, so the platform table is the right source and `-b`'s vocabulary is the local set. `restore` acts on *transported artifacts that may have come from any platform*, so its universe is the `keys.json`, and `-b` validates against the dump's vaults, not the local table. Stated plainly: **the browsers you can restore are exactly the browsers in your `keys.json`.** This turns the platform difference from a bug into a property, and it is what makes #606 dissolve rather than be patched.
|
||||
|
||||
## 5. Cross-host artifacts and the restore command
|
||||
|
||||
The cross-host producer emits two independent, composable artifacts; the consumer takes both.
|
||||
|
||||
- `dumpkeys` writes `keys.json` — the portable master keys (stdout by default for `ssh origin hbd dumpkeys | …` pipelines; `-o` for a 0600 file).
|
||||
- `archive` writes `data.zip` — only the decryption-relevant files for the requested `-c` categories (`Login Data`, `Cookies`, `Web Data`, `History`, … plus `Local State` and `Preferences`), read through the existing locked-file bypass, preserving the relative `User Data` layout so the zip's internal root *is* the `User Data` dir.
|
||||
- `restore` takes `--keys keys.json` and the data via two explicit flags, `--data-dir <dir>` or `--data-zip <zip>` (mutually exclusive, exactly one required). A zip is extracted to a temporary directory; a directory is used as-is. Because the archive preserves layout, `unzip data.zip -d X && restore --data-dir X` equals `restore --data-zip data.zip`.
|
||||
|
||||
`restore` is a **separate verb**, not a `dump --keys` mode. Folding it into `dump` would force one command to carry two mutually-exclusive input modes (`-b` for local discovery xor `--keys/--data` for transported artifacts) and dead flags (a `--keychain-pw` that silently does nothing once keys are supplied — a friction the earlier `dump --keys` design already hit). One verb, one job keeps each command's flags and help self-contained. `restore -b` is an **optional filter** over the dump's vaults, not a required selector, because the dump self-describes what each vault is (§4, §6).
|
||||
|
||||
## 6. The cross-platform identity problem (#606): implementation options
|
||||
|
||||
Grounding facts:
|
||||
|
||||
- Every browser in the tables resolves to one of three engine kinds. All Windows-only forks (`360`, `360x`, `qq`, `sogou`, `dc`, `arc`, …) are `types.Chromium`; only Opera is `ChromiumOpera` and Yandex is `ChromiumYandex`. **Three kinds cover every fork.**
|
||||
- The extraction logic (`sourcesForKind` / `extractorsForKind`) carries no build tags — it is OS-independent. A Sogou profile decrypts through the generic Chromium path with no Sogou-specific code.
|
||||
- So restore needs only the **engine kind** (one of three) plus the data path and the keys. Everything else in `BrowserConfig` (display name, keychain label, ABE flag, default install path) is either a label or is used solely for *local* key derivation and discovery — all irrelevant once static keys and an explicit data path are supplied.
|
||||
|
||||
Two ways to give restore the kind for a browser absent from the analyst's table:
|
||||
|
||||
**Option A — self-describing dump (chosen).** The `keys.json` vault carries the kind. `restore` reads it and constructs a generic engine of that kind rooted at the supplied data path; it never consults `platformBrowsers()`. Minimal, and maximally robust: even a fork this build has never heard of by name still restores as long as its kind is one of the three.
|
||||
|
||||
**Option B — global, OS-independent browser registry.** Split the three per-OS tables into one full-fork registry (carrying kind) plus per-OS views that add paths and ABE flags. `-b`, help text, and `list` would then recognize every fork on any OS. This is a larger refactor and is not required for restore (restore always has a `keys.json` to serve as its universe); it is worth doing only if cross-platform `-b` / `list` awareness is a goal in its own right.
|
||||
|
||||
**Decision: Option A.** The `keys.json` vault carries the engine kind, and `restore` constructs from it without ever consulting `platformBrowsers()`. Option B above is the considered, rejected alternative.
|
||||
|
||||
This crystallizes the principle that lets cross-platform decryption and the current local mode coexist: **one engine constructor (`chromium.NewBrowser`), two config sources.** Local commands feed it configs from the per-OS `platformBrowsers()` table — unchanged; `restore` feeds it configs synthesized from the keys.json vaults. The cross-platform capability is an additive second source confined to the restore path, so the local mode is left untouched.
|
||||
|
||||
## 7. Downstream architecture implications (derived from the surface)
|
||||
|
||||
Working backwards from the chosen surface:
|
||||
|
||||
- **keydump struct** (`masterkey/dump.go`): the vault carries the engine kind so restore can construct without the local table. The `Browser` field becomes the canonical key (it was the display name), a `Kind` string field is added (values `chromium` / `chromium-yandex` / `chromium-opera`), and `DumpVersion` goes "1"→"2". `UserDataDir` and `Profiles` remain as informational fields. The keys stay `V10` / `V11` / `V20` (Chromium-only; Firefox keys are out of scope, §9).
|
||||
- **`browser/keydump.go`**: `BuildDump` records the kind; the overlay `ApplyDump` (which mutates locally-discovered browsers) is replaced by a construct-from-dump path that synthesizes a `BrowserConfig` from each vault and builds the engine directly — no `platformBrowsers()` dependency. This is the mechanical form of §4.
|
||||
- **`archive`** reuses the per-category source-path resolution already used by extraction, plus the existing locked-file session and the zip helper.
|
||||
- **cmd layer**: drop the `keys` parent; add `dumpkeys`, `archive`, `restore` as siblings of `dump` / `list` / `version`.
|
||||
- **Cross-cutting (orthogonal to the taxonomy)**: a Chromium-import password CSV format (`name,url,username,password,note`, #602) and category-aware credential prompting so a no-decryption request never asks for a password (#570).
|
||||
|
||||
## 8. Decisions (2026-06-03)
|
||||
|
||||
1. The browser-universe model (§4) is adopted: `restore`'s `-b` validates against the dump, not the local table.
|
||||
2. #606 implementation: **Option A** — self-describing dump (§6).
|
||||
3. keydump vault identity: **option 1A** — `Browser` becomes the canonical key and a `Kind` field is added (§7).
|
||||
4. Verb names are final: `archive` and `restore`.
|
||||
|
||||
## 9. Non-goals / deferred
|
||||
|
||||
- Firefox / Safari key export (Firefox keys are per-profile NSS; Safari has no portable key).
|
||||
- A single self-describing bundle fusing keys + data into one file (the composable two-artifact model is chosen for now).
|
||||
- Encrypted or signed dump artifacts.
|
||||
- The global browser registry (§6 Option B), unless adopted for #606.
|
||||
|
||||
## Related RFCs
|
||||
|
||||
| RFC | Topic |
|
||||
|-----|-------|
|
||||
| [RFC-007](007-cli-and-output-design.md) | The CLI and output design this RFC revises |
|
||||
| [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 |
|
||||
Reference in New Issue
Block a user