diff --git a/cmd/hack-browser-data/dump.go b/cmd/hack-browser-data/dump.go index 84e87b4..9886bc4 100644 --- a/cmd/hack-browser-data/dump.go +++ b/cmd/hack-browser-data/dump.go @@ -2,18 +2,13 @@ package main import ( "fmt" - "os" - "path/filepath" "strings" "github.com/spf13/cobra" "github.com/moond4rk/hackbrowserdata/browser" - "github.com/moond4rk/hackbrowserdata/crypto/keyretriever" "github.com/moond4rk/hackbrowserdata/log" - "github.com/moond4rk/hackbrowserdata/output" "github.com/moond4rk/hackbrowserdata/types" - "github.com/moond4rk/hackbrowserdata/utils/fileutil" ) func dumpCmd() *cobra.Command { @@ -24,7 +19,6 @@ func dumpCmd() *cobra.Command { outputDir string profilePath string keychainPw string - keysPath string compress bool ) @@ -35,10 +29,13 @@ func dumpCmd() *cobra.Command { hack-browser-data dump -b chrome -c password,cookie hack-browser-data dump -b chrome -f json -d output hack-browser-data dump -f cookie-editor - hack-browser-data dump --keys dump.json -b chrome -p /path/to/copied/User\ Data hack-browser-data dump --zip`, RunE: func(cmd *cobra.Command, args []string) error { - browsers, err := selectBrowsers(browserName, profilePath, keychainPw, keysPath) + browsers, err := browser.PickBrowsers(browser.PickOptions{ + Name: browserName, + ProfilePath: profilePath, + KeychainPassword: keychainPw, + }) if err != nil { return err } @@ -46,37 +43,11 @@ func dumpCmd() *cobra.Command { log.Warnf("no browsers found") return nil } - categories, err := parseCategories(category) if err != nil { return err } - - w, err := output.NewWriter(outputDir, outputFormat) - if err != nil { - return err - } - - for _, b := range browsers { - log.Infof("Extracting %s/%s...", b.BrowserName(), b.ProfileName()) - data, extractErr := b.Extract(categories) - if extractErr != nil { - log.Errorf("extract %s/%s: %v", b.BrowserName(), b.ProfileName(), extractErr) - } - w.Add(b.BrowserName(), b.ProfileName(), data) - } - - if err := w.Write(); err != nil { - return err - } - - if compress { - if err := fileutil.CompressDir(outputDir); err != nil { - return fmt.Errorf("compress: %w", err) - } - log.Infof("Compressed: %s/%s.zip", outputDir, filepath.Base(outputDir)) - } - return nil + return extractAndWrite(browsers, categories, outputDir, outputFormat, compress) }, } @@ -86,69 +57,11 @@ func dumpCmd() *cobra.Command { cmd.Flags().StringVarP(&outputDir, "dir", "d", "results", "output directory") cmd.Flags().StringVarP(&profilePath, "profile-path", "p", "", "custom profile dir path, get with chrome://version") cmd.Flags().StringVar(&keychainPw, "keychain-pw", "", "macOS keychain password") - cmd.Flags().StringVar(&keysPath, "keys", "", "import master keys from JSON file (from `keys export`), skipping platform retrieval") cmd.Flags().BoolVar(&compress, "zip", false, "compress output to zip") return cmd } -// selectBrowsers returns wired-up browsers for either platform-native key retrieval (default) or -// dump-based key injection (when keysPath is non-empty). The dump path uses DiscoverBrowsers so it -// never triggers a keychain prompt or platform retrievers. -func selectBrowsers(browserName, profilePath, keychainPw, keysPath string) ([]browser.Browser, error) { - if keysPath == "" { - return browser.PickBrowsers(browser.PickOptions{ - Name: browserName, - ProfilePath: profilePath, - KeychainPassword: keychainPw, - }) - } - - // Require -p and a single -b to prevent dumped keys from being applied to local profile data, - // which would decrypt to garbage. -b all is rejected because pickFromConfigs ignores -p in that case. - if profilePath == "" { - return nil, fmt.Errorf("--keys requires -p ") - } - name := strings.ToLower(browserName) - if name == "" || name == "all" { - return nil, fmt.Errorf(`--keys requires -b (single, not "all")`) - } - - if keychainPw != "" { - log.Warnf("--keychain-pw is ignored when --keys is set") - } - - browsers, err := browser.DiscoverBrowsers(browser.PickOptions{ - Name: browserName, - ProfilePath: profilePath, - }) - if err != nil { - return nil, err - } - - f, err := os.Open(keysPath) - if err != nil { - return nil, fmt.Errorf("open keys file %q: %w", keysPath, err) - } - defer f.Close() - - dump, err := keyretriever.ReadJSON(f) - if err != nil { - return nil, fmt.Errorf("read keys file %q: %w", keysPath, err) - } - - browser.ApplyDump(browsers, dump) - - for _, b := range browsers { - if _, ok := b.(browser.KeychainPasswordReceiver); ok { - log.Infof("Safari has no portable master key; run `dump -b safari` separately for full extraction") - break - } - } - - return browsers, nil -} - // parseCategories converts a comma-separated string into a Category slice. // "all" returns all categories. func parseCategories(s string) ([]types.Category, error) { diff --git a/cmd/hack-browser-data/extract.go b/cmd/hack-browser-data/extract.go new file mode 100644 index 0000000..49c28e6 --- /dev/null +++ b/cmd/hack-browser-data/extract.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + "path/filepath" + + "github.com/moond4rk/hackbrowserdata/browser" + "github.com/moond4rk/hackbrowserdata/log" + "github.com/moond4rk/hackbrowserdata/output" + "github.com/moond4rk/hackbrowserdata/types" + "github.com/moond4rk/hackbrowserdata/utils/fileutil" +) + +func extractAndWrite(browsers []browser.Browser, categories []types.Category, outputDir, outputFormat string, compress bool) error { + w, err := output.NewWriter(outputDir, outputFormat) + if err != nil { + return err + } + for _, b := range browsers { + log.Infof("Extracting %s/%s...", b.BrowserName(), b.ProfileName()) + data, extractErr := b.Extract(categories) + if extractErr != nil { + log.Errorf("extract %s/%s: %v", b.BrowserName(), b.ProfileName(), extractErr) + } + w.Add(b.BrowserName(), b.ProfileName(), data) + } + if err := w.Write(); err != nil { + return err + } + if compress { + if err := fileutil.CompressDir(outputDir); err != nil { + return fmt.Errorf("compress: %w", err) + } + log.Infof("Compressed: %s/%s.zip", outputDir, filepath.Base(outputDir)) + } + return nil +} diff --git a/cmd/hack-browser-data/keys.go b/cmd/hack-browser-data/keys.go index 66b4eea..e397ebf 100644 --- a/cmd/hack-browser-data/keys.go +++ b/cmd/hack-browser-data/keys.go @@ -2,11 +2,14 @@ package main import ( "fmt" + "io" "os" + "strings" "github.com/spf13/cobra" "github.com/moond4rk/hackbrowserdata/browser" + "github.com/moond4rk/hackbrowserdata/crypto/keyretriever" "github.com/moond4rk/hackbrowserdata/log" ) @@ -15,7 +18,7 @@ func keysCmd() *cobra.Command { Use: "keys", Short: "Manage cross-host master keys", } - cmd.AddCommand(keysExportCmd()) + cmd.AddCommand(keysExportCmd(), keysImportCmd()) return cmd } @@ -61,3 +64,98 @@ func keysExportCmd() *cobra.Command { return cmd } + +func keysImportCmd() *cobra.Command { + var ( + keysPath string + browserName string + category string + outputFormat string + outputDir string + profilePath string + compress bool + ) + + 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`, + RunE: func(cmd *cobra.Command, args []string) error { + browsers, err := loadAndApplyKeys(browserName, profilePath, keysPath) + if err != nil { + return err + } + if len(browsers) == 0 { + log.Warnf("no browsers found") + return nil + } + categories, err := parseCategories(category) + if err != nil { + return err + } + return extractAndWrite(browsers, categories, outputDir, outputFormat, compress) + }, + } + + cmd.Flags().StringVarP(&keysPath, "input", "i", "", "input keys file (use - for stdin)") + cmd.Flags().StringVarP(&browserName, "browser", "b", "", "target browser (single, required): "+browser.Names()) + 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().StringVarP(&profilePath, "profile-path", "p", "", "copied profile dir path (required)") + cmd.Flags().BoolVar(&compress, "zip", false, "compress output to zip") + + _ = cmd.MarkFlagRequired("input") + _ = cmd.MarkFlagRequired("browser") + _ = cmd.MarkFlagRequired("profile-path") + + return cmd +} + +func loadAndApplyKeys(browserName, profilePath, keysPath string) ([]browser.Browser, error) { + if profilePath == "" { + return nil, fmt.Errorf("requires -p ") + } + name := strings.ToLower(browserName) + if name == "" || name == "all" { + return nil, fmt.Errorf(`requires -b (single, not "all")`) + } + if keysPath == "" { + return nil, fmt.Errorf("requires -i (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 := keyretriever.ReadJSON(r) + if err != nil { + return nil, fmt.Errorf("read keys file %q: %w", keysPath, err) + } + + browsers, err := browser.DiscoverBrowsers(browser.PickOptions{ + Name: browserName, + ProfilePath: profilePath, + }) + if err != nil { + return nil, err + } + + browser.ApplyDump(browsers, dump) + + for _, b := range browsers { + if _, ok := b.(browser.KeychainPasswordReceiver); ok { + log.Infof("Safari has no portable master key; run `dump -b safari` separately for full extraction") + break + } + } + + return browsers, nil +}