feat: cli migrate to cobra with subcommands (#550)

* feat: migrate CLI to cobra with dump/list/version subcommands (#546)
* fix: remove residual duckduckgo references and add README/LICENSE to release archives
* fix: address PR review feedback from Copilot
This commit is contained in:
Roger
2026-04-05 14:25:51 +08:00
committed by GitHub
parent 068b82178f
commit 4af2ded428
15 changed files with 418 additions and 112 deletions
+1
View File
@@ -83,6 +83,7 @@ linters:
min-len: 2 min-len: 2
min-occurrences: 3 min-occurrences: 3
ignore-string-values: ignore-string-values:
- "all"
- "csv" - "csv"
- "json" - "json"
gocritic: gocritic:
+9 -3
View File
@@ -6,7 +6,7 @@ before:
builds: builds:
- id: "hack-browser-data" - id: "hack-browser-data"
main: ./cmd/hack-browser-data/main.go main: ./cmd/hack-browser-data/
binary: hack-browser-data binary: hack-browser-data
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
@@ -23,11 +23,17 @@ builds:
- -trimpath - -trimpath
ldflags: ldflags:
- -s -w - -s -w
- -X main.version={{.Version}}
- -X main.commit={{.ShortCommit}}
- -X main.buildDate={{.Date}}
archives: archives:
- id: "archive" - id: "archive"
format: zip formats:
builds: ["hack-browser-data"] - zip
files:
- README.md
- LICENSE
name_template: >- name_template: >-
hack-browser-data- hack-browser-data-
{{- if eq .Os "darwin" }}osx {{- if eq .Os "darwin" }}osx
+25 -10
View File
@@ -16,19 +16,30 @@ import (
type Browser interface { type Browser interface {
BrowserName() string BrowserName() string
ProfileName() string ProfileName() string
ProfileDir() string
Extract(categories []types.Category) (*types.BrowserData, error) Extract(categories []types.Category) (*types.BrowserData, error)
} }
// PickBrowsers returns browsers matching the given name. // PickOptions configures which browsers to pick.
// When name is "all", all known browsers are tried. type PickOptions struct {
// profilePath overrides the default user data directory (only when targeting a specific browser). Name string // browser name filter: "all"|"chrome"|"firefox"|...
func PickBrowsers(name, profilePath string) ([]Browser, error) { ProfilePath string // custom profile directory override
return pickFromConfigs(platformBrowsers(), name, profilePath) KeychainPassword string // macOS keychain password (ignored on other platforms)
}
// PickBrowsers returns browsers matching the given options.
// When Name is "all", all known browsers are tried.
// ProfilePath overrides the default user data directory (only when targeting a specific browser).
func PickBrowsers(opts PickOptions) ([]Browser, error) {
return pickFromConfigs(platformBrowsers(), opts)
} }
// pickFromConfigs is the testable core of PickBrowsers. // pickFromConfigs is the testable core of PickBrowsers.
func pickFromConfigs(configs []types.BrowserConfig, name, profilePath string) ([]Browser, error) { func pickFromConfigs(configs []types.BrowserConfig, opts PickOptions) ([]Browser, error) {
name = strings.ToLower(name) name := strings.ToLower(opts.Name)
if name == "" {
name = "all"
}
var browsers []Browser var browsers []Browser
for _, cfg := range configs { for _, cfg := range configs {
@@ -36,14 +47,18 @@ func pickFromConfigs(configs []types.BrowserConfig, name, profilePath string) ([
continue continue
} }
if profilePath != "" && name != "all" { if opts.ProfilePath != "" && name != "all" {
if cfg.Kind == types.KindFirefox { if cfg.Kind == types.KindFirefox {
cfg.UserDataDir = filepath.Dir(filepath.Clean(profilePath)) cfg.UserDataDir = filepath.Dir(filepath.Clean(opts.ProfilePath))
} else { } else {
cfg.UserDataDir = profilePath cfg.UserDataDir = opts.ProfilePath
} }
} }
if opts.KeychainPassword != "" {
cfg.KeychainPassword = opts.KeychainPassword
}
bs, err := newBrowsers(cfg) bs, err := newBrowsers(cfg)
if err != nil { if err != nil {
log.Errorf("browser %s: %v", cfg.Name, err) log.Errorf("browser %s: %v", cfg.Name, err)
+9 -3
View File
@@ -27,6 +27,7 @@ func TestListBrowsers(t *testing.T) {
func TestPickFromConfigs_NameFilter(t *testing.T) { func TestPickFromConfigs_NameFilter(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
mkFile(t, dir, "Default", "Preferences")
mkFile(t, dir, "Default", "Login Data") mkFile(t, dir, "Default", "Login Data")
mkFile(t, dir, "Default", "History") mkFile(t, dir, "Default", "History")
@@ -67,7 +68,7 @@ func TestPickFromConfigs_NameFilter(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
browsers, err := pickFromConfigs(configs, tt.pickName, "") browsers, err := pickFromConfigs(configs, PickOptions{Name: tt.pickName})
require.NoError(t, err) require.NoError(t, err)
assertBrowsers(t, browsers, tt.wantNames, tt.wantProfiles) assertBrowsers(t, browsers, tt.wantNames, tt.wantProfiles)
}) })
@@ -76,8 +77,10 @@ func TestPickFromConfigs_NameFilter(t *testing.T) {
func TestPickFromConfigs_BrowserKind(t *testing.T) { func TestPickFromConfigs_BrowserKind(t *testing.T) {
chromeDir := t.TempDir() chromeDir := t.TempDir()
mkFile(t, chromeDir, "Default", "Preferences")
mkFile(t, chromeDir, "Default", "Login Data") mkFile(t, chromeDir, "Default", "Login Data")
mkFile(t, chromeDir, "Default", "History") mkFile(t, chromeDir, "Default", "History")
mkFile(t, chromeDir, "Profile 1", "Preferences")
mkFile(t, chromeDir, "Profile 1", "Login Data") mkFile(t, chromeDir, "Profile 1", "Login Data")
mkFile(t, chromeDir, "Profile 1", "History") mkFile(t, chromeDir, "Profile 1", "History")
@@ -86,6 +89,7 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) {
mkFile(t, firefoxDir, "abc123.default-release", "places.sqlite") mkFile(t, firefoxDir, "abc123.default-release", "places.sqlite")
yandexDir := t.TempDir() yandexDir := t.TempDir()
mkFile(t, yandexDir, "Default", "Preferences")
mkFile(t, yandexDir, "Default", "Ya Passman Data") mkFile(t, yandexDir, "Default", "Ya Passman Data")
mkFile(t, yandexDir, "Default", "History") mkFile(t, yandexDir, "Default", "History")
@@ -129,7 +133,7 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
browsers, err := pickFromConfigs(tt.configs, "all", "") browsers, err := pickFromConfigs(tt.configs, PickOptions{Name: "all"})
require.NoError(t, err) require.NoError(t, err)
assertBrowsers(t, browsers, tt.wantNames, tt.wantProfiles) assertBrowsers(t, browsers, tt.wantNames, tt.wantProfiles)
}) })
@@ -138,8 +142,10 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) {
func TestPickFromConfigs_ProfilePath(t *testing.T) { func TestPickFromConfigs_ProfilePath(t *testing.T) {
chromeDir := t.TempDir() chromeDir := t.TempDir()
mkFile(t, chromeDir, "Default", "Preferences")
mkFile(t, chromeDir, "Default", "Login Data") mkFile(t, chromeDir, "Default", "Login Data")
mkFile(t, chromeDir, "Default", "History") mkFile(t, chromeDir, "Default", "History")
mkFile(t, chromeDir, "Profile 1", "Preferences")
mkFile(t, chromeDir, "Profile 1", "Login Data") mkFile(t, chromeDir, "Profile 1", "Login Data")
mkFile(t, chromeDir, "Profile 1", "History") mkFile(t, chromeDir, "Profile 1", "History")
@@ -189,7 +195,7 @@ func TestPickFromConfigs_ProfilePath(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
browsers, err := pickFromConfigs(tt.configs, tt.pickName, tt.profilePath) browsers, err := pickFromConfigs(tt.configs, PickOptions{Name: tt.pickName, ProfilePath: tt.profilePath})
require.NoError(t, err) require.NoError(t, err)
assertBrowsers(t, browsers, tt.wantNames, tt.wantProfiles) assertBrowsers(t, browsers, tt.wantNames, tt.wantProfiles)
}) })
+7 -1
View File
@@ -68,6 +68,12 @@ func platformBrowsers() []types.BrowserConfig {
Kind: types.KindChromiumYandex, Kind: types.KindChromiumYandex,
UserDataDir: homeDir + "/AppData/Local/Yandex/YandexBrowser/User Data", UserDataDir: homeDir + "/AppData/Local/Yandex/YandexBrowser/User Data",
}, },
{
Key: "360x",
Name: speed360XName,
Kind: types.KindChromium,
UserDataDir: homeDir + "/AppData/Local/360ChromeX/Chrome/User Data",
},
{ {
Key: "360", Key: "360",
Name: speed360Name, Name: speed360Name,
@@ -90,7 +96,7 @@ func platformBrowsers() []types.BrowserConfig {
Key: "sogou", Key: "sogou",
Name: sogouName, Name: sogouName,
Kind: types.KindChromium, Kind: types.KindChromium,
UserDataDir: homeDir + "/AppData/Roaming/SogouExplorer/Webkit", UserDataDir: homeDir + "/AppData/Local/Sogou/SogouExplorer/User Data",
}, },
{ {
Key: "firefox", Key: "firefox",
+29 -4
View File
@@ -57,6 +57,7 @@ func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
} }
func (b *Browser) BrowserName() string { return b.cfg.Name } func (b *Browser) BrowserName() string { return b.cfg.Name }
func (b *Browser) ProfileDir() string { return b.profileDir }
func (b *Browser) ProfileName() string { func (b *Browser) ProfileName() string {
if b.profileDir == "" { if b.profileDir == "" {
return "" return ""
@@ -173,8 +174,9 @@ func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, m
} }
} }
// discoverProfiles lists subdirectories of userDataDir that contain at least // discoverProfiles lists subdirectories of userDataDir that are valid
// one known data source. Each such directory is a browser profile. // Chromium profile directories. A directory is considered a profile if it
// contains a "Preferences" file, which Chromium creates for every profile.
func discoverProfiles(userDataDir string, sources map[types.Category][]sourcePath) []string { func discoverProfiles(userDataDir string, sources map[types.Category][]sourcePath) []string {
entries, err := os.ReadDir(userDataDir) entries, err := os.ReadDir(userDataDir)
if err != nil { if err != nil {
@@ -188,18 +190,41 @@ func discoverProfiles(userDataDir string, sources map[types.Category][]sourcePat
continue continue
} }
dir := filepath.Join(userDataDir, e.Name()) dir := filepath.Join(userDataDir, e.Name())
if hasAnySource(sources, dir) { if isProfileDir(dir) {
profiles = append(profiles, dir) profiles = append(profiles, dir)
} }
} }
// Flat layout fallback (older Opera): data files directly in userDataDir // Flat layout fallback (older Opera): data files directly in userDataDir.
// Opera stores data alongside Local State in userDataDir itself, so check
// for any known source file instead of Preferences.
if len(profiles) == 0 && hasAnySource(sources, userDataDir) { if len(profiles) == 0 && hasAnySource(sources, userDataDir) {
profiles = append(profiles, userDataDir) profiles = append(profiles, userDataDir)
} }
return profiles return profiles
} }
// profileMarkers are filenames that identify a directory as a Chromium profile.
// Chromium creates a per-profile preferences file on first use; checking for
// its existence filters out non-profile subdirectories (Crashpad, ShaderCache, etc.).
//
// - "Preferences" — standard Chromium and all major forks (Chrome, Edge, Brave, …)
// - "Preferences_02" — Tencent-based browsers (QQ Browser, Sogou Explorer)
var profileMarkers = []string{
"Preferences",
"Preferences_02",
}
// isProfileDir reports whether dir is a valid Chromium profile directory.
func isProfileDir(dir string) bool {
for _, name := range profileMarkers {
if _, err := os.Stat(filepath.Join(dir, name)); err == nil {
return true
}
}
return false
}
// hasAnySource checks if dir contains at least one source file or directory. // hasAnySource checks if dir contains at least one source file or directory.
func hasAnySource(sources map[types.Category][]sourcePath, dir string) bool { func hasAnySource(sources map[types.Category][]sourcePath, dir string) bool {
for _, candidates := range sources { for _, candidates := range sources {
+7
View File
@@ -45,6 +45,7 @@ func buildFixtures() {
fixture.chrome = filepath.Join(fixture.root, "chrome") fixture.chrome = filepath.Join(fixture.root, "chrome")
mkFile(fixture.chrome, "Local State") mkFile(fixture.chrome, "Local State")
for _, p := range []string{"Default", "Profile 1", "Profile 3"} { for _, p := range []string{"Default", "Profile 1", "Profile 3"} {
mkFile(fixture.chrome, p, "Preferences")
mkFile(fixture.chrome, p, "Login Data") mkFile(fixture.chrome, p, "Login Data")
mkFile(fixture.chrome, p, "History") mkFile(fixture.chrome, p, "History")
mkFile(fixture.chrome, p, "Bookmarks") mkFile(fixture.chrome, p, "Bookmarks")
@@ -60,6 +61,7 @@ func buildFixtures() {
fixture.opera = filepath.Join(fixture.root, "opera") fixture.opera = filepath.Join(fixture.root, "opera")
mkFile(fixture.opera, "Local State") mkFile(fixture.opera, "Local State")
mkFile(fixture.opera, "Default", "Preferences")
mkFile(fixture.opera, "Default", "Login Data") mkFile(fixture.opera, "Default", "Login Data")
mkFile(fixture.opera, "Default", "History") mkFile(fixture.opera, "Default", "History")
mkFile(fixture.opera, "Default", "Bookmarks") mkFile(fixture.opera, "Default", "Bookmarks")
@@ -73,6 +75,7 @@ func buildFixtures() {
fixture.yandex = filepath.Join(fixture.root, "yandex") fixture.yandex = filepath.Join(fixture.root, "yandex")
mkFile(fixture.yandex, "Local State") mkFile(fixture.yandex, "Local State")
mkFile(fixture.yandex, "Default", "Preferences")
mkFile(fixture.yandex, "Default", "Ya Passman Data") mkFile(fixture.yandex, "Default", "Ya Passman Data")
mkFile(fixture.yandex, "Default", "Ya Credit Cards") mkFile(fixture.yandex, "Default", "Ya Credit Cards")
mkFile(fixture.yandex, "Default", "History") mkFile(fixture.yandex, "Default", "History")
@@ -80,14 +83,17 @@ func buildFixtures() {
mkFile(fixture.yandex, "Default", "Bookmarks") mkFile(fixture.yandex, "Default", "Bookmarks")
fixture.oldCookies = filepath.Join(fixture.root, "old-cookies") fixture.oldCookies = filepath.Join(fixture.root, "old-cookies")
mkFile(fixture.oldCookies, "Default", "Preferences")
mkFile(fixture.oldCookies, "Default", "History") mkFile(fixture.oldCookies, "Default", "History")
mkFile(fixture.oldCookies, "Default", "Cookies") mkFile(fixture.oldCookies, "Default", "Cookies")
fixture.bothCookies = filepath.Join(fixture.root, "both-cookies") fixture.bothCookies = filepath.Join(fixture.root, "both-cookies")
mkFile(fixture.bothCookies, "Default", "Preferences")
mkFile(fixture.bothCookies, "Default", "Cookies") mkFile(fixture.bothCookies, "Default", "Cookies")
mkFile(fixture.bothCookies, "Default", "Network", "Cookies") mkFile(fixture.bothCookies, "Default", "Network", "Cookies")
fixture.leveldb = filepath.Join(fixture.root, "leveldb") fixture.leveldb = filepath.Join(fixture.root, "leveldb")
mkFile(fixture.leveldb, "Default", "Preferences")
mkFile(fixture.leveldb, "Default", "History") mkFile(fixture.leveldb, "Default", "History")
mkDir(fixture.leveldb, "Default", "Local Storage", "leveldb") mkDir(fixture.leveldb, "Default", "Local Storage", "leveldb")
mkFile(fixture.leveldb, "Default", "Local Storage", "leveldb", "000001.ldb") mkFile(fixture.leveldb, "Default", "Local Storage", "leveldb", "000001.ldb")
@@ -95,6 +101,7 @@ func buildFixtures() {
mkFile(fixture.leveldb, "Default", "Session Storage", "000001.ldb") mkFile(fixture.leveldb, "Default", "Session Storage", "000001.ldb")
fixture.leveldbOnly = filepath.Join(fixture.root, "leveldb-only") fixture.leveldbOnly = filepath.Join(fixture.root, "leveldb-only")
mkFile(fixture.leveldbOnly, "Default", "Preferences")
mkDir(fixture.leveldbOnly, "Default", "Local Storage", "leveldb") mkDir(fixture.leveldbOnly, "Default", "Local Storage", "leveldb")
mkDir(fixture.leveldbOnly, "Default", "Session Storage") mkDir(fixture.leveldbOnly, "Default", "Session Storage")
+2 -1
View File
@@ -19,7 +19,8 @@ const (
coccocName = "CocCoc" coccocName = "CocCoc"
yandexName = "Yandex" yandexName = "Yandex"
firefoxName = "Firefox" firefoxName = "Firefox"
speed360Name = "360speed" speed360Name = "360 Speed"
speed360XName = "360 Speed X"
qqBrowserName = "QQ" qqBrowserName = "QQ"
dcBrowserName = "DC" dcBrowserName = "DC"
sogouName = "Sogou" sogouName = "Sogou"
+1
View File
@@ -47,6 +47,7 @@ func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
} }
func (b *Browser) BrowserName() string { return b.cfg.Name } func (b *Browser) BrowserName() string { return b.cfg.Name }
func (b *Browser) ProfileDir() string { return b.profileDir }
func (b *Browser) ProfileName() string { func (b *Browser) ProfileName() string {
if b.profileDir == "" { if b.profileDir == "" {
return "" return ""
+130
View File
@@ -0,0 +1,130 @@
package main
import (
"fmt"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"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 dumpCmd() *cobra.Command {
var (
browserName string
category string
outputFormat string
outputDir string
profilePath string
keychainPw string
compress bool
)
cmd := &cobra.Command{
Use: "dump",
Short: "Extract and decrypt browser data (default command)",
Example: ` hack-browser-data dump
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 --zip`,
RunE: func(cmd *cobra.Command, args []string) error {
browsers, err := browser.PickBrowsers(browser.PickOptions{
Name: browserName,
ProfilePath: profilePath,
KeychainPassword: keychainPw,
})
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
}
w, err := output.NewWriter(outputDir, outputFormat)
if err != nil {
return err
}
for _, b := range browsers {
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.Warnf("compressed: %s/%s.zip", outputDir, filepath.Base(outputDir))
}
return nil
},
}
cmd.Flags().StringVarP(&browserName, "browser", "b", "all", "target browser: all|"+browser.Names())
cmd.Flags().StringVarP(&category, "category", "c", "all", "data categories (comma-separated): all|"+categoryNames())
cmd.Flags().StringVarP(&outputFormat, "format", "f", "csv", "output format: csv|json|cookie-editor")
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().BoolVar(&compress, "zip", false, "compress output to zip")
return cmd
}
// parseCategories converts a comma-separated string into a Category slice.
// "all" returns all categories.
func parseCategories(s string) ([]types.Category, error) {
s = strings.TrimSpace(s)
if strings.EqualFold(s, "all") {
return types.AllCategories, nil
}
categoryMap := make(map[string]types.Category, len(types.AllCategories))
for _, c := range types.AllCategories {
categoryMap[c.String()] = c
}
var categories []types.Category
for _, name := range strings.Split(s, ",") {
name = strings.TrimSpace(strings.ToLower(name))
if name == "" {
continue
}
c, ok := categoryMap[name]
if !ok {
return nil, fmt.Errorf("unknown category: %q, available: all|%s", name, categoryNames())
}
categories = append(categories, c)
}
if len(categories) == 0 {
return nil, fmt.Errorf("no categories specified")
}
return categories, nil
}
func categoryNames() string {
names := make([]string, len(types.AllCategories))
for i, c := range types.AllCategories {
names[i] = c.String()
}
return strings.Join(names, ",")
}
+97
View File
@@ -0,0 +1,97 @@
package main
import (
"fmt"
"io"
"text/tabwriter"
"github.com/spf13/cobra"
"github.com/moond4rk/hackbrowserdata/browser"
"github.com/moond4rk/hackbrowserdata/types"
)
func listCmd() *cobra.Command {
var detail bool
cmd := &cobra.Command{
Use: "list",
Short: "List detected browsers and profiles",
Example: ` hack-browser-data list
hack-browser-data list --detail`,
RunE: func(cmd *cobra.Command, args []string) error {
browsers, err := browser.PickBrowsers(browser.PickOptions{Name: "all"})
if err != nil {
return err
}
if len(browsers) == 0 {
cmd.Println("No browsers found.")
return nil
}
if detail {
return printDetail(cmd.OutOrStdout(), browsers)
}
return printBasic(cmd.OutOrStdout(), browsers)
},
}
cmd.Flags().BoolVar(&detail, "detail", false, "show per-category entry counts")
return cmd
}
func printBasic(out io.Writer, browsers []browser.Browser) error {
w := tabwriter.NewWriter(out, 0, 0, 3, ' ', 0)
fmt.Fprintln(w, "Browser\tProfile\tPath")
for _, b := range browsers {
fmt.Fprintf(w, "%s\t%s\t%s\n", b.BrowserName(), b.ProfileName(), b.ProfileDir())
}
return w.Flush()
}
func printDetail(out io.Writer, browsers []browser.Browser) error {
// Build header: Browser Profile Password Cookie ...
w := tabwriter.NewWriter(out, 0, 0, 3, ' ', 0)
fmt.Fprint(w, "Browser\tProfile")
for _, c := range types.AllCategories {
fmt.Fprintf(w, "\t%s", c.String())
}
fmt.Fprintln(w)
for _, b := range browsers {
data, _ := b.Extract(types.AllCategories)
fmt.Fprintf(w, "%s\t%s", b.BrowserName(), b.ProfileName())
for _, c := range types.AllCategories {
fmt.Fprintf(w, "\t%d", countEntries(data, c))
}
fmt.Fprintln(w)
}
return w.Flush()
}
func countEntries(data *types.BrowserData, c types.Category) int {
if data == nil {
return 0
}
switch c {
case types.Password:
return len(data.Passwords)
case types.Cookie:
return len(data.Cookies)
case types.Bookmark:
return len(data.Bookmarks)
case types.History:
return len(data.Histories)
case types.Download:
return len(data.Downloads)
case types.CreditCard:
return len(data.CreditCards)
case types.Extension:
return len(data.Extensions)
case types.LocalStorage:
return len(data.LocalStorage)
case types.SessionStorage:
return len(data.SessionStorage)
default:
return 0
}
}
+37 -79
View File
@@ -3,95 +3,53 @@ package main
import ( import (
"os" "os"
"github.com/urfave/cli/v2" "github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/moond4rk/hackbrowserdata/browser"
"github.com/moond4rk/hackbrowserdata/log" "github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/output"
"github.com/moond4rk/hackbrowserdata/types"
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
) )
var ( var verbose bool
browserName string
outputDir string
outputFormat string
verbose bool
compress bool
profilePath string
isFullExport bool
)
func main() { func rootCmd() *cobra.Command {
Execute() root := &cobra.Command{
} Use: "hack-browser-data",
Short: "A CLI tool for decrypting and exporting browser data",
Long: `hack-browser-data decrypts and exports browser data from Chromium-based
browsers and Firefox on Windows, macOS, and Linux.
func Execute() { GitHub: https://github.com/moonD4rk/HackBrowserData`,
app := &cli.App{ PersistentPreRun: func(cmd *cobra.Command, args []string) {
Name: "hack-browser-data",
Usage: "Export passwords|bookmarks|cookies|history|credit cards|download history|localStorage|extensions from browser",
UsageText: "[hack-browser-data -b chrome -f json --dir results --zip]\nExport all browsing data (passwords/cookies/history/bookmarks) from browser\nGithub Link: https://github.com/moonD4rk/HackBrowserData",
Version: "0.5.0",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "verbose", Aliases: []string{"vv"}, Destination: &verbose, Value: false, Usage: "verbose"},
&cli.BoolFlag{Name: "compress", Aliases: []string{"zip"}, Destination: &compress, Value: false, Usage: "compress result to zip"},
&cli.StringFlag{Name: "browser", Aliases: []string{"b"}, Destination: &browserName, Value: "all", Usage: "available browsers: all|" + browser.Names()},
&cli.StringFlag{Name: "results-dir", Aliases: []string{"dir"}, Destination: &outputDir, Value: "results", Usage: "export dir"},
&cli.StringFlag{Name: "format", Aliases: []string{"f"}, Destination: &outputFormat, Value: "csv", Usage: "output format: csv|json"},
&cli.StringFlag{Name: "profile-path", Aliases: []string{"p"}, Destination: &profilePath, Value: "", Usage: "custom profile dir path, get with chrome://version"},
&cli.BoolFlag{Name: "full-export", Aliases: []string{"full"}, Destination: &isFullExport, Value: true, Usage: "is export full browsing data"},
},
HideHelpCommand: true,
Action: func(c *cli.Context) error {
if verbose { if verbose {
log.SetVerbose() log.SetVerbose()
} }
browsers, err := browser.PickBrowsers(browserName, profilePath)
if err != nil {
log.Errorf("pick browsers: %v", err)
return err
}
if len(browsers) == 0 {
log.Warnf("no browsers found")
return nil
}
categories := types.AllCategories
if !isFullExport {
categories = types.NonSensitiveCategories()
}
w, err := output.NewWriter(outputDir, outputFormat)
if err != nil {
log.Errorf("create output writer: %v", err)
return err
}
for _, b := range browsers {
data, err := b.Extract(categories)
if err != nil {
log.Errorf("extract %s/%s: %v", b.BrowserName(), b.ProfileName(), err)
}
w.Add(b.BrowserName(), b.ProfileName(), data)
}
if err := w.Write(); err != nil {
log.Errorf("write output: %v", err)
return err
}
if compress {
if err = fileutil.CompressDir(outputDir); err != nil {
log.Errorf("compress error %v", err)
}
log.Debug("compress success")
}
return nil
}, },
} }
err := app.Run(os.Args)
if err != nil { root.CompletionOptions.HiddenDefaultCmd = true
log.Fatalf("run app error %v", err)
root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable debug logging")
dump := dumpCmd()
root.AddCommand(dump, listCmd(), versionCmd())
// Default to dump when no subcommand is given.
// Copy dump flags to root so that `hack-browser-data -b chrome`
// works the same as `hack-browser-data dump -b chrome`.
root.RunE = func(cmd *cobra.Command, args []string) error {
return dump.RunE(dump, args)
}
dump.Flags().VisitAll(func(f *pflag.Flag) {
if root.Flags().Lookup(f.Name) == nil {
root.Flags().AddFlag(f)
}
})
return root
}
func main() {
if err := rootCmd().Execute(); err != nil {
os.Exit(1)
} }
} }
+53
View File
@@ -0,0 +1,53 @@
package main
import (
"fmt"
"runtime/debug"
"github.com/spf13/cobra"
)
var (
version = "dev"
commit = "none"
buildDate = "unknown"
)
func versionCmd() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Print version information",
Run: func(cmd *cobra.Command, _ []string) {
resolveVersionFromBuildInfo()
fmt.Fprintf(cmd.OutOrStdout(), "hack-browser-data %s\n commit: %s\n built: %s\n",
version, commit, buildDate)
},
}
}
func resolveVersionFromBuildInfo() {
if version != "dev" {
return
}
info, ok := debug.ReadBuildInfo()
if !ok {
return
}
if info.Main.Version != "" && info.Main.Version != "(devel)" {
version = info.Main.Version
}
for _, s := range info.Settings {
switch s.Key {
case "vcs.revision":
if len(s.Value) > 8 {
commit = s.Value[:8]
} else if s.Value != "" {
commit = s.Value
}
case "vcs.time":
if s.Value != "" {
buildDate = s.Value
}
}
}
}
+3 -4
View File
@@ -7,30 +7,29 @@ require (
github.com/moond4rk/keychainbreaker v0.2.5 github.com/moond4rk/keychainbreaker v0.2.5
github.com/otiai10/copy v1.14.1 github.com/otiai10/copy v1.14.1
github.com/ppacher/go-dbus-keyring v1.0.1 github.com/ppacher/go-dbus-keyring v1.0.1
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/syndtr/goleveldb v1.0.0 github.com/syndtr/goleveldb v1.0.0
github.com/tidwall/gjson v1.18.0 github.com/tidwall/gjson v1.18.0
github.com/urfave/cli/v2 v2.27.7
golang.org/x/sys v0.27.0 golang.org/x/sys v0.27.0
modernc.org/sqlite v1.31.1 modernc.org/sqlite v1.31.1
) )
require ( require (
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/otiai10/mint v1.6.3 // indirect github.com/otiai10/mint v1.6.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/pflag v1.0.9 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/pretty v1.2.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/sync v0.8.0 // indirect golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.19.0 // indirect golang.org/x/text v0.19.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
+8 -7
View File
@@ -1,5 +1,4 @@
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@@ -18,6 +17,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/moond4rk/keychainbreaker v0.2.5 h1:1f2qmgpt1sl+mXA8DTW9nnVhzo4oGO08bnkXu70DL04= github.com/moond4rk/keychainbreaker v0.2.5 h1:1f2qmgpt1sl+mXA8DTW9nnVhzo4oGO08bnkXu70DL04=
@@ -39,8 +40,11 @@ github.com/ppacher/go-dbus-keyring v1.0.1 h1:dM4dMfP5w9MxY+foFHCQiN7izEGpFdKr3tZ
github.com/ppacher/go-dbus-keyring v1.0.1/go.mod h1:JEmkRwBVPBFkOHedAsoZALWmhNJxR/R/ykkFpbEHtGE= github.com/ppacher/go-dbus-keyring v1.0.1/go.mod h1:JEmkRwBVPBFkOHedAsoZALWmhNJxR/R/ykkFpbEHtGE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
@@ -51,10 +55,7 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=