Files
HackBrowserData/cmd/hack-browser-data/restore.go
T
Roger bf96ba8c80 feat(restore): cross-platform restore via dump engine rebuild (#606) (#611)
* feat(restore): cross-platform restore via dump engine rebuild (#606)

Restore previously required the dump's origin OS, overlaying keys onto locally-discovered browsers. It now rebuilds Chromium engines from the dump's vaults (v2 adds engine kind), so copied data or an archive zip decrypts on any OS.

* fix(restore): polish help text, drop dead check, dedup dump kinds

pflag treats backticked words in flag usage as the value placeholder,
so --data-zip rendered as "--data-zip archive" in help output.
2026-06-12 20:53:00 +08:00

121 lines
3.9 KiB
Go

package main
import (
"fmt"
"io"
"os"
"github.com/spf13/cobra"
"github.com/moond4rk/hackbrowserdata/browser"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/masterkey"
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
)
func restoreCmd() *cobra.Command {
var (
keysPath string
dataDir string
dataZip string
browserName string
category string
outputFormat string
outputDir string
compress bool
)
cmd := &cobra.Command{
Use: "restore",
Short: "Decrypt copied profile data using exported master keys",
Example: ` hack-browser-data restore --keys keys.json --data-zip data.zip
hack-browser-data restore --keys keys.json --data-dir ./data -b chrome -c cookie
hack-browser-data restore --keys keys.json --data-dir ./chrome-userdata -b chrome
ssh origin "hack-browser-data dumpkeys" | hack-browser-data restore --keys - --data-zip data.zip`,
RunE: func(cmd *cobra.Command, args []string) error {
resolvedDir, cleanup, err := resolveDataDir(dataDir, dataZip)
if err != nil {
return err
}
defer cleanup()
browsers, err := loadRestoreBrowsers(keysPath, resolvedDir, browserName)
if err != nil {
return err
}
if len(browsers) == 0 {
log.Warnf("no browsers to restore from the supplied keys and data")
return nil
}
categories, err := parseCategories(category)
if err != nil {
return err
}
return extractAndWrite(browsers, categories, outputDir, outputFormat, compress)
},
}
cmd.Flags().StringVar(&keysPath, "keys", "", "keys file from dumpkeys (use - for stdin)")
cmd.Flags().StringVar(&dataDir, "data-dir", "", "copied profile data dir (archive layout, or one browser's User Data with -b)")
cmd.Flags().StringVar(&dataZip, "data-zip", "", "zip produced by the archive command (alternative to --data-dir)")
cmd.Flags().StringVarP(&browserName, "browser", "b", "", "restore only this browser (optional; must match a vault in --keys)")
cmd.Flags().StringVarP(&category, "category", "c", "all", "data categories (comma-separated): all|"+categoryNames())
cmd.Flags().StringVarP(&outputFormat, "format", "f", "json", "output format: csv|json|cookie-editor")
cmd.Flags().StringVarP(&outputDir, "dir", "d", "results", "output directory")
cmd.Flags().BoolVar(&compress, "zip", false, "compress output to zip")
_ = cmd.MarkFlagRequired("keys")
cmd.MarkFlagsMutuallyExclusive("data-dir", "data-zip")
return cmd
}
func loadRestoreBrowsers(keysPath, dataDir, browserName string) ([]browser.Browser, error) {
if keysPath == "" {
return nil, fmt.Errorf("requires --keys <file> (or - for stdin)")
}
var r io.Reader = os.Stdin
if keysPath != "-" {
f, err := os.Open(keysPath)
if err != nil {
return nil, fmt.Errorf("open keys file %q: %w", keysPath, err)
}
defer f.Close()
r = f
}
dump, err := masterkey.ReadJSON(r)
if err != nil {
return nil, fmt.Errorf("read keys file %q: %w", keysPath, err)
}
return browser.BuildFromDump(dump, dataDir, browserName)
}
// resolveDataDir returns the directory restore reads from: --data-dir as-is, or --data-zip extracted
// into a temp dir (removed by the returned cleanup). Exactly one of the two must be set.
func resolveDataDir(dataDir, dataZip string) (string, func(), error) {
noop := func() {}
if (dataDir == "") == (dataZip == "") {
return "", noop, fmt.Errorf("exactly one of --data-dir or --data-zip is required")
}
if dataDir != "" {
return dataDir, noop, nil
}
tmp, err := os.MkdirTemp("", "hbd-restore-*")
if err != nil {
return "", noop, fmt.Errorf("create temp dir: %w", err)
}
if err := fileutil.Unzip(dataZip, tmp); err != nil {
removeTempDir(tmp)
return "", noop, fmt.Errorf("extract %s: %w", dataZip, err)
}
return tmp, func() { removeTempDir(tmp) }, nil
}
func removeTempDir(dir string) {
if err := os.RemoveAll(dir); err != nil {
log.Warnf("restore: remove temp dir %s: %v", dir, err)
}
}