mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-05-19 18:58:03 +02:00
refactor: remove dead code and rename V2 files (#541)
* refactor: remove V1 dead code and rename V2 files
- Delete extractor/ package (V1 Extractor interface and registry)
- Delete browserdata/ package (V1 orchestrator, outputter, 9 sub-packages)
- Delete V1 browser implementations (chromium.go, chromium_{platform}.go, firefox.go)
- Delete types/types.go (V1 DataType enum) and utils/byteutil/
- Remove gocsv and go-sqlmock dependencies, demote x/text to indirect
- Upgrade keychainbreaker v0.1.0 → v0.2.5
- Rename chromium_new.go → chromium.go, firefox_new.go → firefox.go
* refactor: remove unused V1 utility functions
Remove functions no longer called by V2 code:
- fileutil: IsDirExists, CopyDir, BrowserName, ReadFile, CopyFile,
Filename, ParentDir, ParentBaseDir, BaseDir
- typeutil: Keys, IntToBool
This commit is contained in:
+201
-140
@@ -1,183 +1,244 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/browserdata"
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||
"github.com/moond4rk/hackbrowserdata/filemanager"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
type Chromium struct {
|
||||
name string
|
||||
storage string
|
||||
profilePath string
|
||||
masterKey []byte
|
||||
dataTypes []types.DataType
|
||||
Paths map[types.DataType]string
|
||||
// Browser represents a single Chromium profile ready for extraction.
|
||||
type Browser struct {
|
||||
cfg types.BrowserConfig
|
||||
profileDir string // absolute path to profile directory
|
||||
sources map[types.Category][]sourcePath // Category → candidate paths (priority order)
|
||||
extractors map[types.Category]categoryExtractor // Category → custom extract function override
|
||||
sourcePaths map[types.Category]resolvedPath // Category → discovered absolute path
|
||||
}
|
||||
|
||||
// New create instance of Chromium browser, fill item's path if item is existed.
|
||||
func New(name, storage, profilePath string, dataTypes []types.DataType) ([]*Chromium, error) {
|
||||
c := &Chromium{
|
||||
name: name,
|
||||
storage: storage,
|
||||
profilePath: profilePath,
|
||||
dataTypes: dataTypes,
|
||||
// NewBrowsers discovers Chromium profiles under cfg.UserDataDir and returns
|
||||
// one Browser per profile. Uses ReadDir to find profile directories,
|
||||
// then Stat to check which data sources exist in each profile.
|
||||
func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
|
||||
sources := sourcesForKind(cfg.Kind)
|
||||
extractors := extractorsForKind(cfg.Kind)
|
||||
|
||||
profileDirs := discoverProfiles(cfg.UserDataDir, sources)
|
||||
if len(profileDirs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
multiDataTypePaths, err := c.userDataTypePaths(c.profilePath, c.dataTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chromiumList := make([]*Chromium, 0, len(multiDataTypePaths))
|
||||
for user, itemPaths := range multiDataTypePaths {
|
||||
chromiumList = append(chromiumList, &Chromium{
|
||||
name: fileutil.BrowserName(name, user),
|
||||
dataTypes: typeutil.Keys(itemPaths),
|
||||
Paths: itemPaths,
|
||||
storage: storage,
|
||||
|
||||
var browsers []*Browser
|
||||
for _, profileDir := range profileDirs {
|
||||
sourcePaths := resolveSourcePaths(sources, profileDir)
|
||||
if len(sourcePaths) == 0 {
|
||||
continue
|
||||
}
|
||||
browsers = append(browsers, &Browser{
|
||||
cfg: cfg,
|
||||
profileDir: profileDir,
|
||||
sources: sources,
|
||||
extractors: extractors,
|
||||
sourcePaths: sourcePaths,
|
||||
})
|
||||
}
|
||||
return chromiumList, nil
|
||||
return browsers, nil
|
||||
}
|
||||
|
||||
func (c *Chromium) Name() string {
|
||||
return c.name
|
||||
func (b *Browser) BrowserName() string { return b.cfg.Name }
|
||||
func (b *Browser) ProfileName() string {
|
||||
if b.profileDir == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Base(b.profileDir)
|
||||
}
|
||||
|
||||
func (c *Chromium) BrowsingData(isFullExport bool) (*browserdata.BrowserData, error) {
|
||||
// delete chromiumKey from dataTypes, doesn't need to export key
|
||||
var dataTypes []types.DataType
|
||||
for _, dt := range c.dataTypes {
|
||||
if dt != types.ChromiumKey {
|
||||
dataTypes = append(dataTypes, dt)
|
||||
}
|
||||
}
|
||||
|
||||
if !isFullExport {
|
||||
dataTypes = types.FilterSensitiveItems(c.dataTypes)
|
||||
}
|
||||
|
||||
data := browserdata.New(dataTypes)
|
||||
|
||||
if err := c.copyItemToLocal(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
masterKey, err := c.GetMasterKey()
|
||||
// Extract copies browser files to a temp directory, retrieves the master key,
|
||||
// and extracts data for the requested categories.
|
||||
func (b *Browser) Extract(categories []types.Category) (*types.BrowserData, error) {
|
||||
session, err := filemanager.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer session.Cleanup()
|
||||
|
||||
c.masterKey = masterKey
|
||||
if err := data.Recovery(c.masterKey); err != nil {
|
||||
return nil, err
|
||||
tempPaths := b.acquireFiles(session, categories)
|
||||
|
||||
masterKey, err := b.getMasterKey(session)
|
||||
if err != nil {
|
||||
log.Debugf("get master key for %s: %v", b.BrowserName()+"/"+b.ProfileName(), err)
|
||||
}
|
||||
|
||||
data := &types.BrowserData{}
|
||||
for _, cat := range categories {
|
||||
path, ok := tempPaths[cat]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
b.extractCategory(data, cat, masterKey, path)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *Chromium) copyItemToLocal() error {
|
||||
for i, path := range c.Paths {
|
||||
filename := i.TempFilename()
|
||||
var err error
|
||||
switch {
|
||||
case fileutil.IsDirExists(path):
|
||||
if i == types.ChromiumLocalStorage {
|
||||
err = fileutil.CopyDir(path, filename, "lock")
|
||||
}
|
||||
if i == types.ChromiumSessionStorage {
|
||||
err = fileutil.CopyDir(path, filename, "lock")
|
||||
}
|
||||
default:
|
||||
err = fileutil.CopyFile(path, filename)
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("copy item to local, path %s, filename %s err %v", path, filename, err)
|
||||
// acquireFiles copies source files to the session temp directory.
|
||||
func (b *Browser) acquireFiles(session *filemanager.Session, categories []types.Category) map[types.Category]string {
|
||||
tempPaths := make(map[types.Category]string)
|
||||
for _, cat := range categories {
|
||||
rp, ok := b.sourcePaths[cat]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
dst := filepath.Join(session.TempDir(), cat.String())
|
||||
if err := session.Acquire(rp.absPath, dst, rp.isDir); err != nil {
|
||||
log.Debugf("acquire %s: %v", cat, err)
|
||||
continue
|
||||
}
|
||||
tempPaths[cat] = dst
|
||||
}
|
||||
return nil
|
||||
return tempPaths
|
||||
}
|
||||
|
||||
// userDataTypePaths return a map of user to item path, map[profile 1][item's name & path key pair]
|
||||
func (c *Chromium) userDataTypePaths(profilePath string, items []types.DataType) (map[string]map[types.DataType]string, error) {
|
||||
multiItemPaths := make(map[string]map[types.DataType]string)
|
||||
parentDir := fileutil.ParentDir(profilePath)
|
||||
err := filepath.Walk(parentDir, chromiumWalkFunc(items, multiItemPaths))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// getMasterKey retrieves the Chromium master encryption key.
|
||||
//
|
||||
// On Windows, the key is read from the Local State file and decrypted via DPAPI.
|
||||
// On macOS, the key is derived from Keychain (Local State is not needed).
|
||||
// On Linux, the key is derived from D-Bus Secret Service or a fallback password.
|
||||
//
|
||||
// The retriever is always called regardless of whether Local State exists,
|
||||
// because macOS/Linux retrievers don't need it.
|
||||
func (b *Browser) getMasterKey(session *filemanager.Session) ([]byte, error) {
|
||||
// Try to locate and copy Local State (needed on Windows, ignored on macOS/Linux).
|
||||
// Multi-profile layout: Local State is in the parent of profileDir.
|
||||
// Flat layout (Opera): Local State is alongside data files in profileDir.
|
||||
var localStateDst string
|
||||
for _, dir := range []string{filepath.Dir(b.profileDir), b.profileDir} {
|
||||
candidate := filepath.Join(dir, "Local State")
|
||||
if fileutil.IsFileExists(candidate) {
|
||||
localStateDst = filepath.Join(session.TempDir(), "Local State")
|
||||
if err := session.Acquire(candidate, localStateDst, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
var keyPath string
|
||||
var dir string
|
||||
for userDir, profiles := range multiItemPaths {
|
||||
for _, profile := range profiles {
|
||||
if strings.HasSuffix(profile, types.ChromiumKey.Filename()) {
|
||||
keyPath = profile
|
||||
dir = userDir
|
||||
|
||||
retriever := keyretriever.DefaultRetriever(b.cfg.KeychainPassword)
|
||||
return retriever.RetrieveKey(b.cfg.Storage, localStateDst)
|
||||
}
|
||||
|
||||
// extractCategory calls the appropriate extract function for a category.
|
||||
// If a custom extractor is registered for this category (via extractorsForKind),
|
||||
// it is used instead of the default switch logic.
|
||||
func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, masterKey []byte, path string) {
|
||||
if ext, ok := b.extractors[cat]; ok {
|
||||
if err := ext.extract(masterKey, path, data); err != nil {
|
||||
log.Debugf("extract %s for %s: %v", cat, b.BrowserName()+"/"+b.ProfileName(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
switch cat {
|
||||
case types.Password:
|
||||
data.Passwords, err = extractPasswords(masterKey, path)
|
||||
case types.Cookie:
|
||||
data.Cookies, err = extractCookies(masterKey, path)
|
||||
case types.History:
|
||||
data.Histories, err = extractHistories(path)
|
||||
case types.Download:
|
||||
data.Downloads, err = extractDownloads(path)
|
||||
case types.Bookmark:
|
||||
data.Bookmarks, err = extractBookmarks(path)
|
||||
case types.CreditCard:
|
||||
data.CreditCards, err = extractCreditCards(masterKey, path)
|
||||
case types.Extension:
|
||||
data.Extensions, err = extractExtensions(path)
|
||||
case types.LocalStorage:
|
||||
data.LocalStorage, err = extractLocalStorage(path)
|
||||
case types.SessionStorage:
|
||||
data.SessionStorage, err = extractSessionStorage(path)
|
||||
}
|
||||
if err != nil {
|
||||
log.Debugf("extract %s for %s: %v", cat, b.BrowserName()+"/"+b.ProfileName(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// discoverProfiles lists subdirectories of userDataDir that contain at least
|
||||
// one known data source. Each such directory is a browser profile.
|
||||
func discoverProfiles(userDataDir string, sources map[types.Category][]sourcePath) []string {
|
||||
entries, err := os.ReadDir(userDataDir)
|
||||
if err != nil {
|
||||
log.Debugf("read user data dir %s: %v", userDataDir, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var profiles []string
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() || isSkippedDir(e.Name()) {
|
||||
continue
|
||||
}
|
||||
dir := filepath.Join(userDataDir, e.Name())
|
||||
if hasAnySource(sources, dir) {
|
||||
profiles = append(profiles, dir)
|
||||
}
|
||||
}
|
||||
|
||||
// Flat layout fallback (older Opera): data files directly in userDataDir
|
||||
if len(profiles) == 0 && hasAnySource(sources, userDataDir) {
|
||||
profiles = append(profiles, userDataDir)
|
||||
}
|
||||
return profiles
|
||||
}
|
||||
|
||||
// hasAnySource checks if dir contains at least one source file or directory.
|
||||
func hasAnySource(sources map[types.Category][]sourcePath, dir string) bool {
|
||||
for _, candidates := range sources {
|
||||
for _, sp := range candidates {
|
||||
abs := filepath.Join(dir, sp.rel)
|
||||
if _, err := os.Stat(abs); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// resolvedPath holds the absolute path and type for a discovered source.
|
||||
type resolvedPath struct {
|
||||
absPath string
|
||||
isDir bool
|
||||
}
|
||||
|
||||
// resolveSourcePaths checks which sources actually exist in profileDir.
|
||||
// Candidates are tried in priority order; the first existing path wins.
|
||||
func resolveSourcePaths(sources map[types.Category][]sourcePath, profileDir string) map[types.Category]resolvedPath {
|
||||
resolved := make(map[types.Category]resolvedPath)
|
||||
for cat, candidates := range sources {
|
||||
for _, sp := range candidates {
|
||||
abs := filepath.Join(profileDir, sp.rel)
|
||||
info, err := os.Stat(abs)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if sp.isDir == info.IsDir() {
|
||||
resolved[cat] = resolvedPath{abs, sp.isDir}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
t := make(map[string]map[types.DataType]string)
|
||||
for userDir, v := range multiItemPaths {
|
||||
if userDir == dir {
|
||||
continue
|
||||
}
|
||||
t[userDir] = v
|
||||
t[userDir][types.ChromiumKey] = keyPath
|
||||
fillLocalStoragePath(t[userDir], types.ChromiumLocalStorage)
|
||||
}
|
||||
return t, nil
|
||||
return resolved
|
||||
}
|
||||
|
||||
// chromiumWalkFunc return a filepath.WalkFunc to find item's path
|
||||
func chromiumWalkFunc(items []types.DataType, multiItemPaths map[string]map[types.DataType]string) filepath.WalkFunc {
|
||||
return func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
log.Warnf("skipping walk chromium path permission error, path %s, err %v", path, err)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
for _, v := range items {
|
||||
if info.Name() != v.Filename() {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(path, "System Profile") {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(path, "Snapshot") {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(path, "def") {
|
||||
continue
|
||||
}
|
||||
profileFolder := fileutil.ParentBaseDir(path)
|
||||
if strings.Contains(filepath.ToSlash(path), "/Network/Cookies") {
|
||||
profileFolder = fileutil.BaseDir(strings.ReplaceAll(filepath.ToSlash(path), "/Network/Cookies", ""))
|
||||
}
|
||||
if _, exist := multiItemPaths[profileFolder]; exist {
|
||||
multiItemPaths[profileFolder][v] = path
|
||||
} else {
|
||||
multiItemPaths[profileFolder] = map[types.DataType]string{v: path}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func fillLocalStoragePath(itemPaths map[types.DataType]string, storage types.DataType) {
|
||||
if p, ok := itemPaths[types.ChromiumHistory]; ok {
|
||||
lsp := filepath.Join(filepath.Dir(p), storage.Filename())
|
||||
if fileutil.IsDirExists(lsp) {
|
||||
itemPaths[types.ChromiumLocalStorage] = lsp
|
||||
}
|
||||
// isSkippedDir returns true for directory names that should never be
|
||||
// treated as browser profiles.
|
||||
func isSkippedDir(name string) bool {
|
||||
switch name {
|
||||
case "System Profile", "Guest Profile", "Snapshot":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
//go:build darwin
|
||||
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
)
|
||||
|
||||
var (
|
||||
errWrongSecurityCommand = errors.New("wrong security command")
|
||||
errCouldNotFindInKeychain = errors.New("could not be find in keychain")
|
||||
)
|
||||
|
||||
func (c *Chromium) GetMasterKey() ([]byte, error) {
|
||||
// don't need chromium key file for macOS
|
||||
defer os.Remove(types.ChromiumKey.TempFilename())
|
||||
|
||||
// Try get the master key via gcoredump(CVE-2025-24204)
|
||||
secret, err := keyretriever.DecryptKeychain(c.storage)
|
||||
if err == nil && secret != "" {
|
||||
log.Debugf("get master key via gcoredump(CVE-2025-24204) success, browser %s", c.name)
|
||||
if key, err := c.parseSecret([]byte(secret)); err == nil {
|
||||
return key, nil
|
||||
}
|
||||
} else {
|
||||
log.Warnf("get master key via gcoredump(CVE-2025-24204) failed: %v, skipping...", err)
|
||||
}
|
||||
|
||||
// Get the master key from the keychain
|
||||
// $ security find-generic-password -wa 'Chrome'
|
||||
var (
|
||||
stdout, stderr bytes.Buffer
|
||||
)
|
||||
cmd := exec.Command("security", "find-generic-password", "-wa", strings.TrimSpace(c.storage)) //nolint:gosec
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("run security command failed: %w, message %s", err, stderr.String())
|
||||
}
|
||||
|
||||
if stderr.Len() > 0 {
|
||||
if strings.Contains(stderr.String(), "could not be found") {
|
||||
return nil, errCouldNotFindInKeychain
|
||||
}
|
||||
return nil, errors.New(stderr.String())
|
||||
}
|
||||
|
||||
return c.parseSecret(stdout.Bytes())
|
||||
}
|
||||
|
||||
func (c *Chromium) parseSecret(secret []byte) ([]byte, error) {
|
||||
secret = bytes.TrimSpace(secret)
|
||||
if len(secret) == 0 {
|
||||
return nil, errWrongSecurityCommand
|
||||
}
|
||||
|
||||
salt := []byte("saltysalt")
|
||||
// @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157
|
||||
key := crypto.PBKDF2Key(secret, salt, 1003, 16, sha1.New)
|
||||
if key == nil {
|
||||
return nil, errWrongSecurityCommand
|
||||
}
|
||||
c.masterKey = key
|
||||
log.Debugf("get master key success, browser %s", c.name)
|
||||
return key, nil
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
//go:build linux
|
||||
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
keyring "github.com/ppacher/go-dbus-keyring"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
)
|
||||
|
||||
func (c *Chromium) GetMasterKey() ([]byte, error) {
|
||||
// what is d-bus @https://dbus.freedesktop.org/
|
||||
// don't need chromium key file for Linux
|
||||
defer os.Remove(types.ChromiumKey.TempFilename())
|
||||
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
svc, err := keyring.GetSecretService(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
session, err := svc.OpenSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err := session.Close(); err != nil {
|
||||
log.Errorf("close dbus session error: %v", err)
|
||||
}
|
||||
}()
|
||||
collections, err := svc.GetAllCollections()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var secret []byte
|
||||
for _, col := range collections {
|
||||
items, err := col.GetAllItems()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, i := range items {
|
||||
label, err := i.GetLabel()
|
||||
if err != nil {
|
||||
log.Warnf("get label from dbus: %v", err)
|
||||
continue
|
||||
}
|
||||
if label == c.storage {
|
||||
se, err := i.GetSecret(session.Path())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get storage from dbus: %w", err)
|
||||
}
|
||||
secret = se.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(secret) == 0 {
|
||||
// set default secret @https://source.chromium.org/chromium/chromium/src/+/main:components/os_crypt/os_crypt_linux.cc;l=100
|
||||
secret = []byte("peanuts")
|
||||
}
|
||||
salt := []byte("saltysalt")
|
||||
// @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_linux.cc
|
||||
key := crypto.PBKDF2Key(secret, salt, 1, 16, sha1.New)
|
||||
c.masterKey = key
|
||||
log.Debugf("get master key success, browser %s", c.name)
|
||||
return key, nil
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||
"github.com/moond4rk/hackbrowserdata/filemanager"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
|
||||
)
|
||||
|
||||
// Browser represents a single Chromium profile ready for extraction.
|
||||
type Browser struct {
|
||||
cfg types.BrowserConfig
|
||||
profileDir string // absolute path to profile directory
|
||||
sources map[types.Category][]sourcePath // Category → candidate paths (priority order)
|
||||
extractors map[types.Category]categoryExtractor // Category → custom extract function override
|
||||
sourcePaths map[types.Category]resolvedPath // Category → discovered absolute path
|
||||
}
|
||||
|
||||
// NewBrowsers discovers Chromium profiles under cfg.UserDataDir and returns
|
||||
// one Browser per profile. Uses ReadDir to find profile directories,
|
||||
// then Stat to check which data sources exist in each profile.
|
||||
func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
|
||||
sources := sourcesForKind(cfg.Kind)
|
||||
extractors := extractorsForKind(cfg.Kind)
|
||||
|
||||
profileDirs := discoverProfiles(cfg.UserDataDir, sources)
|
||||
if len(profileDirs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var browsers []*Browser
|
||||
for _, profileDir := range profileDirs {
|
||||
sourcePaths := resolveSourcePaths(sources, profileDir)
|
||||
if len(sourcePaths) == 0 {
|
||||
continue
|
||||
}
|
||||
browsers = append(browsers, &Browser{
|
||||
cfg: cfg,
|
||||
profileDir: profileDir,
|
||||
sources: sources,
|
||||
extractors: extractors,
|
||||
sourcePaths: sourcePaths,
|
||||
})
|
||||
}
|
||||
return browsers, nil
|
||||
}
|
||||
|
||||
func (b *Browser) BrowserName() string { return b.cfg.Name }
|
||||
func (b *Browser) ProfileName() string {
|
||||
if b.profileDir == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Base(b.profileDir)
|
||||
}
|
||||
|
||||
// Extract copies browser files to a temp directory, retrieves the master key,
|
||||
// and extracts data for the requested categories.
|
||||
func (b *Browser) Extract(categories []types.Category) (*types.BrowserData, error) {
|
||||
session, err := filemanager.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer session.Cleanup()
|
||||
|
||||
tempPaths := b.acquireFiles(session, categories)
|
||||
|
||||
masterKey, err := b.getMasterKey(session)
|
||||
if err != nil {
|
||||
log.Debugf("get master key for %s: %v", b.BrowserName()+"/"+b.ProfileName(), err)
|
||||
}
|
||||
|
||||
data := &types.BrowserData{}
|
||||
for _, cat := range categories {
|
||||
path, ok := tempPaths[cat]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
b.extractCategory(data, cat, masterKey, path)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// acquireFiles copies source files to the session temp directory.
|
||||
func (b *Browser) acquireFiles(session *filemanager.Session, categories []types.Category) map[types.Category]string {
|
||||
tempPaths := make(map[types.Category]string)
|
||||
for _, cat := range categories {
|
||||
rp, ok := b.sourcePaths[cat]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
dst := filepath.Join(session.TempDir(), cat.String())
|
||||
if err := session.Acquire(rp.absPath, dst, rp.isDir); err != nil {
|
||||
log.Debugf("acquire %s: %v", cat, err)
|
||||
continue
|
||||
}
|
||||
tempPaths[cat] = dst
|
||||
}
|
||||
return tempPaths
|
||||
}
|
||||
|
||||
// getMasterKey retrieves the Chromium master encryption key.
|
||||
//
|
||||
// On Windows, the key is read from the Local State file and decrypted via DPAPI.
|
||||
// On macOS, the key is derived from Keychain (Local State is not needed).
|
||||
// On Linux, the key is derived from D-Bus Secret Service or a fallback password.
|
||||
//
|
||||
// The retriever is always called regardless of whether Local State exists,
|
||||
// because macOS/Linux retrievers don't need it.
|
||||
func (b *Browser) getMasterKey(session *filemanager.Session) ([]byte, error) {
|
||||
// Try to locate and copy Local State (needed on Windows, ignored on macOS/Linux).
|
||||
// Multi-profile layout: Local State is in the parent of profileDir.
|
||||
// Flat layout (Opera): Local State is alongside data files in profileDir.
|
||||
var localStateDst string
|
||||
for _, dir := range []string{filepath.Dir(b.profileDir), b.profileDir} {
|
||||
candidate := filepath.Join(dir, "Local State")
|
||||
if fileutil.IsFileExists(candidate) {
|
||||
localStateDst = filepath.Join(session.TempDir(), "Local State")
|
||||
if err := session.Acquire(candidate, localStateDst, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
retriever := keyretriever.DefaultRetriever(b.cfg.KeychainPassword)
|
||||
return retriever.RetrieveKey(b.cfg.Storage, localStateDst)
|
||||
}
|
||||
|
||||
// extractCategory calls the appropriate extract function for a category.
|
||||
// If a custom extractor is registered for this category (via extractorsForKind),
|
||||
// it is used instead of the default switch logic.
|
||||
func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, masterKey []byte, path string) {
|
||||
if ext, ok := b.extractors[cat]; ok {
|
||||
if err := ext.extract(masterKey, path, data); err != nil {
|
||||
log.Debugf("extract %s for %s: %v", cat, b.BrowserName()+"/"+b.ProfileName(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
switch cat {
|
||||
case types.Password:
|
||||
data.Passwords, err = extractPasswords(masterKey, path)
|
||||
case types.Cookie:
|
||||
data.Cookies, err = extractCookies(masterKey, path)
|
||||
case types.History:
|
||||
data.Histories, err = extractHistories(path)
|
||||
case types.Download:
|
||||
data.Downloads, err = extractDownloads(path)
|
||||
case types.Bookmark:
|
||||
data.Bookmarks, err = extractBookmarks(path)
|
||||
case types.CreditCard:
|
||||
data.CreditCards, err = extractCreditCards(masterKey, path)
|
||||
case types.Extension:
|
||||
data.Extensions, err = extractExtensions(path)
|
||||
case types.LocalStorage:
|
||||
data.LocalStorage, err = extractLocalStorage(path)
|
||||
case types.SessionStorage:
|
||||
data.SessionStorage, err = extractSessionStorage(path)
|
||||
}
|
||||
if err != nil {
|
||||
log.Debugf("extract %s for %s: %v", cat, b.BrowserName()+"/"+b.ProfileName(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// discoverProfiles lists subdirectories of userDataDir that contain at least
|
||||
// one known data source. Each such directory is a browser profile.
|
||||
func discoverProfiles(userDataDir string, sources map[types.Category][]sourcePath) []string {
|
||||
entries, err := os.ReadDir(userDataDir)
|
||||
if err != nil {
|
||||
log.Debugf("read user data dir %s: %v", userDataDir, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var profiles []string
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() || isSkippedDir(e.Name()) {
|
||||
continue
|
||||
}
|
||||
dir := filepath.Join(userDataDir, e.Name())
|
||||
if hasAnySource(sources, dir) {
|
||||
profiles = append(profiles, dir)
|
||||
}
|
||||
}
|
||||
|
||||
// Flat layout fallback (older Opera): data files directly in userDataDir
|
||||
if len(profiles) == 0 && hasAnySource(sources, userDataDir) {
|
||||
profiles = append(profiles, userDataDir)
|
||||
}
|
||||
return profiles
|
||||
}
|
||||
|
||||
// hasAnySource checks if dir contains at least one source file or directory.
|
||||
func hasAnySource(sources map[types.Category][]sourcePath, dir string) bool {
|
||||
for _, candidates := range sources {
|
||||
for _, sp := range candidates {
|
||||
abs := filepath.Join(dir, sp.rel)
|
||||
if _, err := os.Stat(abs); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// resolvedPath holds the absolute path and type for a discovered source.
|
||||
type resolvedPath struct {
|
||||
absPath string
|
||||
isDir bool
|
||||
}
|
||||
|
||||
// resolveSourcePaths checks which sources actually exist in profileDir.
|
||||
// Candidates are tried in priority order; the first existing path wins.
|
||||
func resolveSourcePaths(sources map[types.Category][]sourcePath, profileDir string) map[types.Category]resolvedPath {
|
||||
resolved := make(map[types.Category]resolvedPath)
|
||||
for cat, candidates := range sources {
|
||||
for _, sp := range candidates {
|
||||
abs := filepath.Join(profileDir, sp.rel)
|
||||
info, err := os.Stat(abs)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if sp.isDir == info.IsDir() {
|
||||
resolved[cat] = resolvedPath{abs, sp.isDir}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
// isSkippedDir returns true for directory names that should never be
|
||||
// treated as browser profiles.
|
||||
func isSkippedDir(name string) bool {
|
||||
switch name {
|
||||
case "System Profile", "Guest Profile", "Snapshot":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
//go:build windows
|
||||
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
|
||||
)
|
||||
|
||||
var errDecodeMasterKeyFailed = errors.New("decode master key failed")
|
||||
|
||||
func (c *Chromium) GetMasterKey() ([]byte, error) {
|
||||
b, err := fileutil.ReadFile(types.ChromiumKey.TempFilename())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(types.ChromiumKey.TempFilename())
|
||||
|
||||
encryptedKey := gjson.Get(b, "os_crypt.encrypted_key")
|
||||
if !encryptedKey.Exists() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
key, err := base64.StdEncoding.DecodeString(encryptedKey.String())
|
||||
if err != nil {
|
||||
return nil, errDecodeMasterKeyFailed
|
||||
}
|
||||
c.masterKey, err = crypto.DecryptWithDPAPI(key[5:])
|
||||
if err != nil {
|
||||
log.Errorf("decrypt master key failed, err %v", err)
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("get master key success, browser %s", c.name)
|
||||
return c.masterKey, nil
|
||||
}
|
||||
+204
-84
@@ -3,115 +3,235 @@ package firefox
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
_ "modernc.org/sqlite" // sqlite3 driver TODO: replace with chooseable driver
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/browserdata"
|
||||
"github.com/moond4rk/hackbrowserdata/filemanager"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
type Firefox struct {
|
||||
name string
|
||||
storage string
|
||||
profilePath string
|
||||
masterKey []byte
|
||||
items []types.DataType
|
||||
itemPaths map[types.DataType]string
|
||||
// Browser represents a single Firefox profile ready for extraction.
|
||||
type Browser struct {
|
||||
cfg types.BrowserConfig
|
||||
profileDir string // absolute path to profile directory
|
||||
sources map[types.Category][]sourcePath // Category → candidate paths (priority order)
|
||||
sourcePaths map[types.Category]resolvedPath // Category → discovered absolute path
|
||||
}
|
||||
|
||||
var ErrProfilePathNotFound = errors.New("profile path not found")
|
||||
// NewBrowsers discovers Firefox profiles under cfg.UserDataDir and returns
|
||||
// one Browser per profile. Firefox profile directories have random names
|
||||
// (e.g. "97nszz88.default-release"); any subdirectory containing known
|
||||
// data files is treated as a valid profile.
|
||||
func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
|
||||
profileDirs := discoverProfiles(cfg.UserDataDir, firefoxSources)
|
||||
if len(profileDirs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// New returns new Firefox instances.
|
||||
func New(profilePath string, items []types.DataType) ([]*Firefox, error) {
|
||||
multiItemPaths := make(map[string]map[types.DataType]string)
|
||||
// ignore walk dir error since it can be produced by a single entry
|
||||
_ = filepath.WalkDir(profilePath, firefoxWalkFunc(items, multiItemPaths))
|
||||
|
||||
firefoxList := make([]*Firefox, 0, len(multiItemPaths))
|
||||
for name, itemPaths := range multiItemPaths {
|
||||
firefoxList = append(firefoxList, &Firefox{
|
||||
name: fmt.Sprintf("firefox-%s", name),
|
||||
items: typeutil.Keys(itemPaths),
|
||||
itemPaths: itemPaths,
|
||||
var browsers []*Browser
|
||||
for _, profileDir := range profileDirs {
|
||||
sourcePaths := resolveSourcePaths(firefoxSources, profileDir)
|
||||
if len(sourcePaths) == 0 {
|
||||
continue
|
||||
}
|
||||
browsers = append(browsers, &Browser{
|
||||
cfg: cfg,
|
||||
profileDir: profileDir,
|
||||
sources: firefoxSources,
|
||||
sourcePaths: sourcePaths,
|
||||
})
|
||||
}
|
||||
|
||||
return firefoxList, nil
|
||||
return browsers, nil
|
||||
}
|
||||
|
||||
func (f *Firefox) copyItemToLocal() error {
|
||||
for i, path := range f.itemPaths {
|
||||
filename := i.TempFilename()
|
||||
if err := fileutil.CopyFile(path, filename); err != nil {
|
||||
return err
|
||||
}
|
||||
func (b *Browser) BrowserName() string { return b.cfg.Name }
|
||||
func (b *Browser) ProfileName() string {
|
||||
if b.profileDir == "" {
|
||||
return ""
|
||||
}
|
||||
return nil
|
||||
return filepath.Base(b.profileDir)
|
||||
}
|
||||
|
||||
func firefoxWalkFunc(items []types.DataType, multiItemPaths map[string]map[types.DataType]string) fs.WalkDirFunc {
|
||||
return func(path string, info fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
log.Warnf("skipping walk firefox path %s permission error: %v", path, err)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
for _, v := range items {
|
||||
if info.Name() == v.Filename() {
|
||||
parentBaseDir := fileutil.ParentBaseDir(path)
|
||||
if _, exist := multiItemPaths[parentBaseDir]; exist {
|
||||
multiItemPaths[parentBaseDir][v] = path
|
||||
} else {
|
||||
multiItemPaths[parentBaseDir] = map[types.DataType]string{v: path}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetMasterKey returns master key of Firefox. from key4.db
|
||||
func (f *Firefox) GetMasterKey() ([]byte, error) {
|
||||
tempFilename := types.FirefoxKey4.TempFilename()
|
||||
defer os.Remove(tempFilename)
|
||||
|
||||
loginsPath := types.FirefoxPassword.TempFilename()
|
||||
return retrieveMasterKey(tempFilename, loginsPath)
|
||||
}
|
||||
|
||||
func (f *Firefox) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f *Firefox) BrowsingData(isFullExport bool) (*browserdata.BrowserData, error) {
|
||||
dataTypes := f.items
|
||||
if !isFullExport {
|
||||
dataTypes = types.FilterSensitiveItems(f.items)
|
||||
}
|
||||
|
||||
data := browserdata.New(dataTypes)
|
||||
|
||||
if err := f.copyItemToLocal(); err != nil {
|
||||
// Extract copies browser files to a temp directory, retrieves the master key,
|
||||
// and extracts data for the requested categories.
|
||||
func (b *Browser) Extract(categories []types.Category) (*types.BrowserData, error) {
|
||||
session, err := filemanager.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer session.Cleanup()
|
||||
|
||||
masterKey, err := f.GetMasterKey()
|
||||
tempPaths := b.acquireFiles(session, categories)
|
||||
|
||||
masterKey, err := b.getMasterKey(session, tempPaths)
|
||||
if err != nil {
|
||||
log.Debugf("get master key for %s: %v", b.BrowserName()+"/"+b.ProfileName(), err)
|
||||
}
|
||||
|
||||
data := &types.BrowserData{}
|
||||
for _, cat := range categories {
|
||||
path, ok := tempPaths[cat]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
b.extractCategory(data, cat, masterKey, path)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// acquireFiles copies source files to the session temp directory.
|
||||
func (b *Browser) acquireFiles(session *filemanager.Session, categories []types.Category) map[types.Category]string {
|
||||
tempPaths := make(map[types.Category]string)
|
||||
for _, cat := range categories {
|
||||
rp, ok := b.sourcePaths[cat]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
dst := filepath.Join(session.TempDir(), cat.String())
|
||||
if err := session.Acquire(rp.absPath, dst, rp.isDir); err != nil {
|
||||
log.Debugf("acquire %s: %v", cat, err)
|
||||
continue
|
||||
}
|
||||
tempPaths[cat] = dst
|
||||
}
|
||||
return tempPaths
|
||||
}
|
||||
|
||||
// getMasterKey retrieves the Firefox master encryption key from key4.db.
|
||||
// The key is derived via NSS ASN1 PBE decryption (platform-agnostic).
|
||||
// If logins.json was already acquired by acquireFiles, the derived key
|
||||
// is validated by attempting to decrypt an actual login entry.
|
||||
func (b *Browser) getMasterKey(session *filemanager.Session, tempPaths map[types.Category]string) ([]byte, error) {
|
||||
key4Src := filepath.Join(b.profileDir, "key4.db")
|
||||
if !fileutil.IsFileExists(key4Src) {
|
||||
return nil, nil
|
||||
}
|
||||
key4Dst := filepath.Join(session.TempDir(), "key4.db")
|
||||
if err := session.Acquire(key4Src, key4Dst, false); err != nil {
|
||||
return nil, fmt.Errorf("acquire key4.db: %w", err)
|
||||
}
|
||||
|
||||
// logins.json is already acquired by acquireFiles as the Password source;
|
||||
// reuse it for master key validation if available.
|
||||
loginsPath := tempPaths[types.Password]
|
||||
return retrieveMasterKey(key4Dst, loginsPath)
|
||||
}
|
||||
|
||||
// retrieveMasterKey reads key4.db and derives the master key using NSS.
|
||||
// If loginsPath is non-empty, the derived key is validated against actual
|
||||
// login data to ensure the correct candidate is selected.
|
||||
func retrieveMasterKey(key4Path, loginsPath string) ([]byte, error) {
|
||||
k4, err := readKey4DB(key4Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f.masterKey = masterKey
|
||||
if err := data.Recovery(f.masterKey); err != nil {
|
||||
keys, err := k4.deriveKeys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
if len(keys) == 0 {
|
||||
return nil, errors.New("no valid master key candidates in key4.db")
|
||||
}
|
||||
|
||||
// No logins to validate against — return the first derived key.
|
||||
if loginsPath == "" {
|
||||
return keys[0], nil
|
||||
}
|
||||
|
||||
// Validate against actual login data.
|
||||
if key := validateKeyWithLogins(keys, loginsPath); key != nil {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("derived %d key(s) but none could decrypt logins", len(keys))
|
||||
}
|
||||
|
||||
// extractCategory calls the appropriate extract function for a category.
|
||||
func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, masterKey []byte, path string) {
|
||||
var err error
|
||||
switch cat {
|
||||
case types.Password:
|
||||
data.Passwords, err = extractPasswords(masterKey, path)
|
||||
case types.Cookie:
|
||||
data.Cookies, err = extractCookies(path)
|
||||
case types.History:
|
||||
data.Histories, err = extractHistories(path)
|
||||
case types.Download:
|
||||
data.Downloads, err = extractDownloads(path)
|
||||
case types.Bookmark:
|
||||
data.Bookmarks, err = extractBookmarks(path)
|
||||
case types.Extension:
|
||||
data.Extensions, err = extractExtensions(path)
|
||||
case types.LocalStorage:
|
||||
data.LocalStorage, err = extractLocalStorage(path)
|
||||
case types.CreditCard, types.SessionStorage:
|
||||
// Firefox does not support CreditCard or SessionStorage extraction.
|
||||
}
|
||||
if err != nil {
|
||||
log.Debugf("extract %s for %s: %v", cat, b.BrowserName()+"/"+b.ProfileName(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// resolvedPath holds the absolute path and type for a discovered source.
|
||||
type resolvedPath struct {
|
||||
absPath string
|
||||
isDir bool
|
||||
}
|
||||
|
||||
// discoverProfiles lists subdirectories of userDataDir that contain at least
|
||||
// one known data source. Each such directory is a Firefox profile.
|
||||
func discoverProfiles(userDataDir string, sources map[types.Category][]sourcePath) []string {
|
||||
entries, err := os.ReadDir(userDataDir)
|
||||
if err != nil {
|
||||
log.Debugf("read user data dir %s: %v", userDataDir, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var profiles []string
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
dir := filepath.Join(userDataDir, e.Name())
|
||||
if hasAnySource(sources, dir) {
|
||||
profiles = append(profiles, dir)
|
||||
}
|
||||
}
|
||||
return profiles
|
||||
}
|
||||
|
||||
// hasAnySource checks if dir contains at least one source file or directory.
|
||||
func hasAnySource(sources map[types.Category][]sourcePath, dir string) bool {
|
||||
for _, candidates := range sources {
|
||||
for _, sp := range candidates {
|
||||
abs := filepath.Join(dir, sp.rel)
|
||||
if _, err := os.Stat(abs); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// resolveSourcePaths checks which sources actually exist in profileDir.
|
||||
// Candidates are tried in priority order; the first existing path wins.
|
||||
func resolveSourcePaths(sources map[types.Category][]sourcePath, profileDir string) map[types.Category]resolvedPath {
|
||||
resolved := make(map[types.Category]resolvedPath)
|
||||
for cat, candidates := range sources {
|
||||
for _, sp := range candidates {
|
||||
abs := filepath.Join(profileDir, sp.rel)
|
||||
info, err := os.Stat(abs)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if sp.isDir == info.IsDir() {
|
||||
resolved[cat] = resolvedPath{abs, sp.isDir}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
package firefox
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/filemanager"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
|
||||
)
|
||||
|
||||
// Browser represents a single Firefox profile ready for extraction.
|
||||
type Browser struct {
|
||||
cfg types.BrowserConfig
|
||||
profileDir string // absolute path to profile directory
|
||||
sources map[types.Category][]sourcePath // Category → candidate paths (priority order)
|
||||
sourcePaths map[types.Category]resolvedPath // Category → discovered absolute path
|
||||
}
|
||||
|
||||
// NewBrowsers discovers Firefox profiles under cfg.UserDataDir and returns
|
||||
// one Browser per profile. Firefox profile directories have random names
|
||||
// (e.g. "97nszz88.default-release"); any subdirectory containing known
|
||||
// data files is treated as a valid profile.
|
||||
func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
|
||||
profileDirs := discoverProfiles(cfg.UserDataDir, firefoxSources)
|
||||
if len(profileDirs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var browsers []*Browser
|
||||
for _, profileDir := range profileDirs {
|
||||
sourcePaths := resolveSourcePaths(firefoxSources, profileDir)
|
||||
if len(sourcePaths) == 0 {
|
||||
continue
|
||||
}
|
||||
browsers = append(browsers, &Browser{
|
||||
cfg: cfg,
|
||||
profileDir: profileDir,
|
||||
sources: firefoxSources,
|
||||
sourcePaths: sourcePaths,
|
||||
})
|
||||
}
|
||||
return browsers, nil
|
||||
}
|
||||
|
||||
func (b *Browser) BrowserName() string { return b.cfg.Name }
|
||||
func (b *Browser) ProfileName() string {
|
||||
if b.profileDir == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Base(b.profileDir)
|
||||
}
|
||||
|
||||
// Extract copies browser files to a temp directory, retrieves the master key,
|
||||
// and extracts data for the requested categories.
|
||||
func (b *Browser) Extract(categories []types.Category) (*types.BrowserData, error) {
|
||||
session, err := filemanager.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer session.Cleanup()
|
||||
|
||||
tempPaths := b.acquireFiles(session, categories)
|
||||
|
||||
masterKey, err := b.getMasterKey(session, tempPaths)
|
||||
if err != nil {
|
||||
log.Debugf("get master key for %s: %v", b.BrowserName()+"/"+b.ProfileName(), err)
|
||||
}
|
||||
|
||||
data := &types.BrowserData{}
|
||||
for _, cat := range categories {
|
||||
path, ok := tempPaths[cat]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
b.extractCategory(data, cat, masterKey, path)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// acquireFiles copies source files to the session temp directory.
|
||||
func (b *Browser) acquireFiles(session *filemanager.Session, categories []types.Category) map[types.Category]string {
|
||||
tempPaths := make(map[types.Category]string)
|
||||
for _, cat := range categories {
|
||||
rp, ok := b.sourcePaths[cat]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
dst := filepath.Join(session.TempDir(), cat.String())
|
||||
if err := session.Acquire(rp.absPath, dst, rp.isDir); err != nil {
|
||||
log.Debugf("acquire %s: %v", cat, err)
|
||||
continue
|
||||
}
|
||||
tempPaths[cat] = dst
|
||||
}
|
||||
return tempPaths
|
||||
}
|
||||
|
||||
// getMasterKey retrieves the Firefox master encryption key from key4.db.
|
||||
// The key is derived via NSS ASN1 PBE decryption (platform-agnostic).
|
||||
// If logins.json was already acquired by acquireFiles, the derived key
|
||||
// is validated by attempting to decrypt an actual login entry.
|
||||
func (b *Browser) getMasterKey(session *filemanager.Session, tempPaths map[types.Category]string) ([]byte, error) {
|
||||
key4Src := filepath.Join(b.profileDir, "key4.db")
|
||||
if !fileutil.IsFileExists(key4Src) {
|
||||
return nil, nil
|
||||
}
|
||||
key4Dst := filepath.Join(session.TempDir(), "key4.db")
|
||||
if err := session.Acquire(key4Src, key4Dst, false); err != nil {
|
||||
return nil, fmt.Errorf("acquire key4.db: %w", err)
|
||||
}
|
||||
|
||||
// logins.json is already acquired by acquireFiles as the Password source;
|
||||
// reuse it for master key validation if available.
|
||||
loginsPath := tempPaths[types.Password]
|
||||
return retrieveMasterKey(key4Dst, loginsPath)
|
||||
}
|
||||
|
||||
// retrieveMasterKey reads key4.db and derives the master key using NSS.
|
||||
// If loginsPath is non-empty, the derived key is validated against actual
|
||||
// login data to ensure the correct candidate is selected.
|
||||
func retrieveMasterKey(key4Path, loginsPath string) ([]byte, error) {
|
||||
k4, err := readKey4DB(key4Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keys, err := k4.deriveKeys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil, errors.New("no valid master key candidates in key4.db")
|
||||
}
|
||||
|
||||
// No logins to validate against — return the first derived key.
|
||||
if loginsPath == "" {
|
||||
return keys[0], nil
|
||||
}
|
||||
|
||||
// Validate against actual login data.
|
||||
if key := validateKeyWithLogins(keys, loginsPath); key != nil {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("derived %d key(s) but none could decrypt logins", len(keys))
|
||||
}
|
||||
|
||||
// extractCategory calls the appropriate extract function for a category.
|
||||
func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, masterKey []byte, path string) {
|
||||
var err error
|
||||
switch cat {
|
||||
case types.Password:
|
||||
data.Passwords, err = extractPasswords(masterKey, path)
|
||||
case types.Cookie:
|
||||
data.Cookies, err = extractCookies(path)
|
||||
case types.History:
|
||||
data.Histories, err = extractHistories(path)
|
||||
case types.Download:
|
||||
data.Downloads, err = extractDownloads(path)
|
||||
case types.Bookmark:
|
||||
data.Bookmarks, err = extractBookmarks(path)
|
||||
case types.Extension:
|
||||
data.Extensions, err = extractExtensions(path)
|
||||
case types.LocalStorage:
|
||||
data.LocalStorage, err = extractLocalStorage(path)
|
||||
case types.CreditCard, types.SessionStorage:
|
||||
// Firefox does not support CreditCard or SessionStorage extraction.
|
||||
}
|
||||
if err != nil {
|
||||
log.Debugf("extract %s for %s: %v", cat, b.BrowserName()+"/"+b.ProfileName(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// resolvedPath holds the absolute path and type for a discovered source.
|
||||
type resolvedPath struct {
|
||||
absPath string
|
||||
isDir bool
|
||||
}
|
||||
|
||||
// discoverProfiles lists subdirectories of userDataDir that contain at least
|
||||
// one known data source. Each such directory is a Firefox profile.
|
||||
func discoverProfiles(userDataDir string, sources map[types.Category][]sourcePath) []string {
|
||||
entries, err := os.ReadDir(userDataDir)
|
||||
if err != nil {
|
||||
log.Debugf("read user data dir %s: %v", userDataDir, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var profiles []string
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
dir := filepath.Join(userDataDir, e.Name())
|
||||
if hasAnySource(sources, dir) {
|
||||
profiles = append(profiles, dir)
|
||||
}
|
||||
}
|
||||
return profiles
|
||||
}
|
||||
|
||||
// hasAnySource checks if dir contains at least one source file or directory.
|
||||
func hasAnySource(sources map[types.Category][]sourcePath, dir string) bool {
|
||||
for _, candidates := range sources {
|
||||
for _, sp := range candidates {
|
||||
abs := filepath.Join(dir, sp.rel)
|
||||
if _, err := os.Stat(abs); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// resolveSourcePaths checks which sources actually exist in profileDir.
|
||||
// Candidates are tried in priority order; the first existing path wins.
|
||||
func resolveSourcePaths(sources map[types.Category][]sourcePath, profileDir string) map[types.Category]resolvedPath {
|
||||
resolved := make(map[types.Category]resolvedPath)
|
||||
for cat, candidates := range sources {
|
||||
for _, sp := range candidates {
|
||||
abs := filepath.Join(profileDir, sp.rel)
|
||||
info, err := os.Stat(abs)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if sp.isDir == info.IsDir() {
|
||||
resolved[cat] = resolvedPath{abs, sp.isDir}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
package bookmark
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
_ "modernc.org/sqlite" // import sqlite3 driver
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumBookmark, func() extractor.Extractor {
|
||||
return new(ChromiumBookmark)
|
||||
})
|
||||
extractor.RegisterExtractor(types.FirefoxBookmark, func() extractor.Extractor {
|
||||
return new(FirefoxBookmark)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumBookmark []bookmark
|
||||
|
||||
type bookmark struct {
|
||||
ID int64
|
||||
Name string
|
||||
Type string
|
||||
URL string
|
||||
DateAdded time.Time
|
||||
}
|
||||
|
||||
func (c *ChromiumBookmark) Extract(_ []byte) error {
|
||||
bookmarks, err := fileutil.ReadFile(types.ChromiumBookmark.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.ChromiumBookmark.TempFilename())
|
||||
r := gjson.Parse(bookmarks)
|
||||
if r.Exists() {
|
||||
roots := r.Get("roots")
|
||||
roots.ForEach(func(key, value gjson.Result) bool {
|
||||
getBookmarkChildren(value, c)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(*c, func(i, j int) bool {
|
||||
return (*c)[i].DateAdded.After((*c)[j].DateAdded)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
bookmarkID = "id"
|
||||
bookmarkAdded = "date_added"
|
||||
bookmarkURL = "url"
|
||||
bookmarkName = "name"
|
||||
bookmarkType = "type"
|
||||
bookmarkChildren = "children"
|
||||
)
|
||||
|
||||
func getBookmarkChildren(value gjson.Result, w *ChromiumBookmark) (children gjson.Result) {
|
||||
nodeType := value.Get(bookmarkType)
|
||||
children = value.Get(bookmarkChildren)
|
||||
|
||||
bm := bookmark{
|
||||
ID: value.Get(bookmarkID).Int(),
|
||||
Name: value.Get(bookmarkName).String(),
|
||||
URL: value.Get(bookmarkURL).String(),
|
||||
DateAdded: typeutil.TimeEpoch(value.Get(bookmarkAdded).Int()),
|
||||
}
|
||||
if nodeType.Exists() {
|
||||
bm.Type = nodeType.String()
|
||||
*w = append(*w, bm)
|
||||
if children.Exists() && children.IsArray() {
|
||||
for _, v := range children.Array() {
|
||||
children = getBookmarkChildren(v, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
func (c *ChromiumBookmark) Name() string {
|
||||
return "bookmark"
|
||||
}
|
||||
|
||||
func (c *ChromiumBookmark) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
type FirefoxBookmark []bookmark
|
||||
|
||||
const (
|
||||
queryFirefoxBookMark = `SELECT id, url, type, dateAdded, title FROM (SELECT * FROM moz_bookmarks INNER JOIN moz_places ON moz_bookmarks.fk=moz_places.id)`
|
||||
closeJournalMode = `PRAGMA journal_mode=off`
|
||||
)
|
||||
|
||||
func (f *FirefoxBookmark) Extract(_ []byte) error {
|
||||
db, err := sql.Open("sqlite", types.FirefoxBookmark.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.FirefoxBookmark.TempFilename())
|
||||
defer db.Close()
|
||||
_, err = db.Exec(closeJournalMode)
|
||||
if err != nil {
|
||||
log.Debugf("close journal mode error: %v", err)
|
||||
}
|
||||
rows, err := db.Query(queryFirefoxBookMark)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, bt, dateAdded int64
|
||||
url string
|
||||
title sql.NullString
|
||||
)
|
||||
if err = rows.Scan(&id, &url, &bt, &dateAdded, &title); err != nil {
|
||||
log.Debugf("scan bookmark error: %v", err)
|
||||
}
|
||||
*f = append(*f, bookmark{
|
||||
ID: id,
|
||||
Name: title.String,
|
||||
Type: linkType(bt),
|
||||
URL: url,
|
||||
DateAdded: typeutil.TimeStamp(dateAdded / 1000000),
|
||||
})
|
||||
}
|
||||
sort.Slice(*f, func(i, j int) bool {
|
||||
return (*f)[i].DateAdded.After((*f)[j].DateAdded)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FirefoxBookmark) Name() string {
|
||||
return "bookmark"
|
||||
}
|
||||
|
||||
func (f *FirefoxBookmark) Len() int {
|
||||
return len(*f)
|
||||
}
|
||||
|
||||
func linkType(a int64) string {
|
||||
switch a {
|
||||
case 1:
|
||||
return "url"
|
||||
default:
|
||||
return "folder"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package browserdata
|
||||
|
||||
import "github.com/moond4rk/hackbrowserdata/types"
|
||||
|
||||
// Data holds all extracted data from one browser profile.
|
||||
// Each field is a slice that may be nil (not supported) or empty (no data found).
|
||||
// This struct will replace the current BrowserData once the refactoring is complete.
|
||||
type Data struct {
|
||||
Passwords []types.LoginEntry `json:"passwords,omitempty"`
|
||||
Cookies []types.CookieEntry `json:"cookies,omitempty"`
|
||||
Bookmarks []types.BookmarkEntry `json:"bookmarks,omitempty"`
|
||||
Histories []types.HistoryEntry `json:"histories,omitempty"`
|
||||
Downloads []types.DownloadEntry `json:"downloads,omitempty"`
|
||||
CreditCards []types.CreditCardEntry `json:"credit_cards,omitempty"`
|
||||
Extensions []types.ExtensionEntry `json:"extensions,omitempty"`
|
||||
LocalStorage []types.StorageEntry `json:"local_storage,omitempty"`
|
||||
SessionStorage []types.StorageEntry `json:"session_storage,omitempty"`
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package browserdata
|
||||
|
||||
import (
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
|
||||
)
|
||||
|
||||
type BrowserData struct {
|
||||
extractors map[types.DataType]extractor.Extractor
|
||||
}
|
||||
|
||||
func New(items []types.DataType) *BrowserData {
|
||||
bd := &BrowserData{
|
||||
extractors: make(map[types.DataType]extractor.Extractor),
|
||||
}
|
||||
bd.addExtractors(items)
|
||||
return bd
|
||||
}
|
||||
|
||||
func (d *BrowserData) Recovery(masterKey []byte) error {
|
||||
for _, source := range d.extractors {
|
||||
if err := source.Extract(masterKey); err != nil {
|
||||
log.Debugf("parse %s error: %v", source.Name(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *BrowserData) Output(dir, browserName, flag string) {
|
||||
output := newOutPutter(flag)
|
||||
|
||||
for _, source := range d.extractors {
|
||||
if source.Len() == 0 {
|
||||
// if the length of the export data is 0, then it is not necessary to output
|
||||
continue
|
||||
}
|
||||
filename := fileutil.Filename(browserName, source.Name(), output.Ext())
|
||||
|
||||
f, err := output.CreateFile(dir, filename)
|
||||
if err != nil {
|
||||
log.Debugf("create file %s error: %v", filename, err)
|
||||
continue
|
||||
}
|
||||
if err := output.Write(source, f); err != nil {
|
||||
log.Debugf("write to file %s error: %v", filename, err)
|
||||
continue
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
log.Debugf("close file %s error: %v", filename, err)
|
||||
continue
|
||||
}
|
||||
log.Warnf("export success: %s", filename)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *BrowserData) addExtractors(items []types.DataType) {
|
||||
for _, itemType := range items {
|
||||
if source := extractor.CreateExtractor(itemType); source != nil {
|
||||
d.extractors[itemType] = source
|
||||
} else {
|
||||
log.Debugf("source not found: %s", itemType)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package cookie
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
// import sqlite3 driver
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumCookie, func() extractor.Extractor {
|
||||
return new(ChromiumCookie)
|
||||
})
|
||||
extractor.RegisterExtractor(types.FirefoxCookie, func() extractor.Extractor {
|
||||
return new(FirefoxCookie)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumCookie []cookie
|
||||
|
||||
type cookie struct {
|
||||
Host string
|
||||
Path string
|
||||
KeyName string
|
||||
encryptValue []byte
|
||||
Value string
|
||||
IsSecure bool
|
||||
IsHTTPOnly bool
|
||||
HasExpire bool
|
||||
IsPersistent bool
|
||||
CreateDate time.Time
|
||||
ExpireDate time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
queryChromiumCookie = `SELECT name, encrypted_value, host_key, path, creation_utc, expires_utc, is_secure, is_httponly, has_expires, is_persistent FROM cookies`
|
||||
)
|
||||
|
||||
func (c *ChromiumCookie) Extract(masterKey []byte) error {
|
||||
db, err := sql.Open("sqlite", types.ChromiumCookie.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.ChromiumCookie.TempFilename())
|
||||
defer db.Close()
|
||||
rows, err := db.Query(queryChromiumCookie)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
key, host, path string
|
||||
isSecure, isHTTPOnly, hasExpire, isPersistent int
|
||||
createDate, expireDate int64
|
||||
value, encryptValue []byte
|
||||
)
|
||||
if err = rows.Scan(&key, &encryptValue, &host, &path, &createDate, &expireDate, &isSecure, &isHTTPOnly, &hasExpire, &isPersistent); err != nil {
|
||||
log.Debugf("scan chromium cookie error: %v", err)
|
||||
}
|
||||
|
||||
cookie := cookie{
|
||||
KeyName: key,
|
||||
Host: host,
|
||||
Path: path,
|
||||
encryptValue: encryptValue,
|
||||
IsSecure: typeutil.IntToBool(isSecure),
|
||||
IsHTTPOnly: typeutil.IntToBool(isHTTPOnly),
|
||||
HasExpire: typeutil.IntToBool(hasExpire),
|
||||
IsPersistent: typeutil.IntToBool(isPersistent),
|
||||
CreateDate: typeutil.TimeEpoch(createDate),
|
||||
ExpireDate: typeutil.TimeEpoch(expireDate),
|
||||
}
|
||||
|
||||
if len(encryptValue) > 0 {
|
||||
value, err = crypto.DecryptWithDPAPI(encryptValue)
|
||||
if err != nil {
|
||||
value, err = crypto.DecryptWithChromium(masterKey, encryptValue)
|
||||
if err != nil {
|
||||
log.Debugf("decrypt chromium cookie error: %v", err)
|
||||
} else if len(value) > 32 {
|
||||
// https://gist.github.com/kosh04/36cf6023fb75b516451ce933b9db2207?permalink_comment_id=5291243#gistcomment-5291243
|
||||
value = value[32:]
|
||||
}
|
||||
}
|
||||
}
|
||||
cookie.Value = string(value)
|
||||
*c = append(*c, cookie)
|
||||
}
|
||||
sort.Slice(*c, func(i, j int) bool {
|
||||
return (*c)[i].CreateDate.After((*c)[j].CreateDate)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChromiumCookie) Name() string {
|
||||
return "cookie"
|
||||
}
|
||||
|
||||
func (c *ChromiumCookie) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
type FirefoxCookie []cookie
|
||||
|
||||
const (
|
||||
queryFirefoxCookie = `SELECT name, value, host, path, creationTime, expiry, isSecure, isHttpOnly FROM moz_cookies`
|
||||
)
|
||||
|
||||
func (f *FirefoxCookie) Extract(_ []byte) error {
|
||||
db, err := sql.Open("sqlite", types.FirefoxCookie.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.FirefoxCookie.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(queryFirefoxCookie)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
name, value, host, path string
|
||||
isSecure, isHTTPOnly int
|
||||
creationTime, expiry int64
|
||||
)
|
||||
if err = rows.Scan(&name, &value, &host, &path, &creationTime, &expiry, &isSecure, &isHTTPOnly); err != nil {
|
||||
log.Debugf("scan firefox cookie error: %v", err)
|
||||
}
|
||||
*f = append(*f, cookie{
|
||||
KeyName: name,
|
||||
Host: host,
|
||||
Path: path,
|
||||
IsSecure: typeutil.IntToBool(isSecure),
|
||||
IsHTTPOnly: typeutil.IntToBool(isHTTPOnly),
|
||||
CreateDate: typeutil.TimeStamp(creationTime / 1000000),
|
||||
ExpireDate: typeutil.TimeStamp(expiry),
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(*f, func(i, j int) bool {
|
||||
return (*f)[i].CreateDate.After((*f)[j].CreateDate)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FirefoxCookie) Name() string {
|
||||
return "cookie"
|
||||
}
|
||||
|
||||
func (f *FirefoxCookie) Len() int {
|
||||
return len(*f)
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package creditcard
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
|
||||
// import sqlite3 driver
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumCreditCard, func() extractor.Extractor {
|
||||
return new(ChromiumCreditCard)
|
||||
})
|
||||
extractor.RegisterExtractor(types.YandexCreditCard, func() extractor.Extractor {
|
||||
return new(YandexCreditCard)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumCreditCard []card
|
||||
|
||||
type card struct {
|
||||
GUID string
|
||||
Name string
|
||||
ExpirationYear string
|
||||
ExpirationMonth string
|
||||
CardNumber string
|
||||
Address string
|
||||
NickName string
|
||||
}
|
||||
|
||||
const (
|
||||
queryChromiumCredit = `SELECT guid, name_on_card, expiration_month, expiration_year, card_number_encrypted, billing_address_id, nickname FROM credit_cards`
|
||||
)
|
||||
|
||||
func (c *ChromiumCreditCard) Extract(masterKey []byte) error {
|
||||
db, err := sql.Open("sqlite", types.ChromiumCreditCard.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.ChromiumCreditCard.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(queryChromiumCredit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
name, month, year, guid, address, nickname string
|
||||
value, encryptValue []byte
|
||||
)
|
||||
if err := rows.Scan(&guid, &name, &month, &year, &encryptValue, &address, &nickname); err != nil {
|
||||
log.Debugf("scan chromium credit card error: %v", err)
|
||||
}
|
||||
ccInfo := card{
|
||||
GUID: guid,
|
||||
Name: name,
|
||||
ExpirationMonth: month,
|
||||
ExpirationYear: year,
|
||||
Address: address,
|
||||
NickName: nickname,
|
||||
}
|
||||
if len(encryptValue) > 0 {
|
||||
if len(masterKey) == 0 {
|
||||
value, err = crypto.DecryptWithDPAPI(encryptValue)
|
||||
} else {
|
||||
value, err = crypto.DecryptWithChromium(masterKey, encryptValue)
|
||||
}
|
||||
if err != nil {
|
||||
log.Debugf("decrypt chromium credit card error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
ccInfo.CardNumber = string(value)
|
||||
*c = append(*c, ccInfo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChromiumCreditCard) Name() string {
|
||||
return "creditcard"
|
||||
}
|
||||
|
||||
func (c *ChromiumCreditCard) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
type YandexCreditCard []card
|
||||
|
||||
func (c *YandexCreditCard) Extract(masterKey []byte) error {
|
||||
db, err := sql.Open("sqlite", types.YandexCreditCard.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.YandexCreditCard.TempFilename())
|
||||
defer db.Close()
|
||||
rows, err := db.Query(queryChromiumCredit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
name, month, year, guid, address, nickname string
|
||||
value, encryptValue []byte
|
||||
)
|
||||
if err := rows.Scan(&guid, &name, &month, &year, &encryptValue, &address, &nickname); err != nil {
|
||||
log.Debugf("scan chromium credit card error: %v", err)
|
||||
}
|
||||
ccInfo := card{
|
||||
GUID: guid,
|
||||
Name: name,
|
||||
ExpirationMonth: month,
|
||||
ExpirationYear: year,
|
||||
Address: address,
|
||||
NickName: nickname,
|
||||
}
|
||||
if len(encryptValue) > 0 {
|
||||
if len(masterKey) == 0 {
|
||||
value, err = crypto.DecryptWithDPAPI(encryptValue)
|
||||
} else {
|
||||
value, err = crypto.DecryptWithChromium(masterKey, encryptValue)
|
||||
}
|
||||
if err != nil {
|
||||
log.Debugf("decrypt chromium credit card error: %v", err)
|
||||
}
|
||||
}
|
||||
ccInfo.CardNumber = string(value)
|
||||
*c = append(*c, ccInfo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *YandexCreditCard) Name() string {
|
||||
return "creditcard"
|
||||
}
|
||||
|
||||
func (c *YandexCreditCard) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
_ "modernc.org/sqlite" // import sqlite3 driver
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumDownload, func() extractor.Extractor {
|
||||
return new(ChromiumDownload)
|
||||
})
|
||||
extractor.RegisterExtractor(types.FirefoxDownload, func() extractor.Extractor {
|
||||
return new(FirefoxDownload)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumDownload []download
|
||||
|
||||
type download struct {
|
||||
TargetPath string
|
||||
URL string
|
||||
TotalBytes int64
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
MimeType string
|
||||
}
|
||||
|
||||
const (
|
||||
queryChromiumDownload = `SELECT target_path, tab_url, total_bytes, start_time, end_time, mime_type FROM downloads`
|
||||
)
|
||||
|
||||
func (c *ChromiumDownload) Extract(_ []byte) error {
|
||||
db, err := sql.Open("sqlite", types.ChromiumDownload.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.ChromiumDownload.TempFilename())
|
||||
defer db.Close()
|
||||
rows, err := db.Query(queryChromiumDownload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
targetPath, tabURL, mimeType string
|
||||
totalBytes, startTime, endTime int64
|
||||
)
|
||||
if err := rows.Scan(&targetPath, &tabURL, &totalBytes, &startTime, &endTime, &mimeType); err != nil {
|
||||
log.Warnf("scan chromium download error: %v", err)
|
||||
}
|
||||
data := download{
|
||||
TargetPath: targetPath,
|
||||
URL: tabURL,
|
||||
TotalBytes: totalBytes,
|
||||
StartTime: typeutil.TimeEpoch(startTime),
|
||||
EndTime: typeutil.TimeEpoch(endTime),
|
||||
MimeType: mimeType,
|
||||
}
|
||||
*c = append(*c, data)
|
||||
}
|
||||
sort.Slice(*c, func(i, j int) bool {
|
||||
return (*c)[i].TotalBytes > (*c)[j].TotalBytes
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChromiumDownload) Name() string {
|
||||
return "download"
|
||||
}
|
||||
|
||||
func (c *ChromiumDownload) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
type FirefoxDownload []download
|
||||
|
||||
const (
|
||||
queryFirefoxDownload = `SELECT place_id, GROUP_CONCAT(content), url, dateAdded FROM (SELECT * FROM moz_annos INNER JOIN moz_places ON moz_annos.place_id=moz_places.id) t GROUP BY place_id`
|
||||
closeJournalMode = `PRAGMA journal_mode=off`
|
||||
)
|
||||
|
||||
func (f *FirefoxDownload) Extract(_ []byte) error {
|
||||
db, err := sql.Open("sqlite", types.FirefoxDownload.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.FirefoxDownload.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(closeJournalMode)
|
||||
if err != nil {
|
||||
log.Debugf("close journal mode error: %v", err)
|
||||
}
|
||||
rows, err := db.Query(queryFirefoxDownload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
content, url string
|
||||
placeID, dateAdded int64
|
||||
)
|
||||
if err = rows.Scan(&placeID, &content, &url, &dateAdded); err != nil {
|
||||
log.Warnf("scan firefox download error: %v", err)
|
||||
}
|
||||
contentList := strings.Split(content, ",{")
|
||||
if len(contentList) > 1 {
|
||||
path := contentList[0]
|
||||
json := "{" + contentList[1]
|
||||
endTime := gjson.Get(json, "endTime")
|
||||
fileSize := gjson.Get(json, "fileSize")
|
||||
*f = append(*f, download{
|
||||
TargetPath: path,
|
||||
URL: url,
|
||||
TotalBytes: fileSize.Int(),
|
||||
StartTime: typeutil.TimeStamp(dateAdded / 1000000),
|
||||
EndTime: typeutil.TimeStamp(endTime.Int() / 1000),
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.Slice(*f, func(i, j int) bool {
|
||||
return (*f)[i].TotalBytes < (*f)[j].TotalBytes
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FirefoxDownload) Name() string {
|
||||
return "download"
|
||||
}
|
||||
|
||||
func (f *FirefoxDownload) Len() int {
|
||||
return len(*f)
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumExtension, func() extractor.Extractor {
|
||||
return new(ChromiumExtension)
|
||||
})
|
||||
extractor.RegisterExtractor(types.FirefoxExtension, func() extractor.Extractor {
|
||||
return new(FirefoxExtension)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumExtension []*extension
|
||||
|
||||
type extension struct {
|
||||
ID string
|
||||
URL string
|
||||
Enabled bool
|
||||
Name string
|
||||
Description string
|
||||
Version string
|
||||
HomepageURL string
|
||||
}
|
||||
|
||||
func (c *ChromiumExtension) Extract(_ []byte) error {
|
||||
extensionFile, err := fileutil.ReadFile(types.ChromiumExtension.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.ChromiumExtension.TempFilename())
|
||||
|
||||
result, err := parseChromiumExtensions(extensionFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*c = result
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseChromiumExtensions(content string) ([]*extension, error) {
|
||||
settingKeys := []string{
|
||||
"settings.extensions",
|
||||
"settings.settings",
|
||||
"extensions.settings",
|
||||
}
|
||||
var settings gjson.Result
|
||||
for _, key := range settingKeys {
|
||||
settings = gjson.Parse(content).Get(key)
|
||||
if settings.Exists() {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !settings.Exists() {
|
||||
return nil, fmt.Errorf("cannot find extensions in settings")
|
||||
}
|
||||
var c []*extension
|
||||
|
||||
settings.ForEach(func(id, ext gjson.Result) bool {
|
||||
location := ext.Get("location")
|
||||
if !location.Exists() {
|
||||
return true
|
||||
}
|
||||
switch location.Int() {
|
||||
case 5, 10: // https://source.chromium.org/chromium/chromium/src/+/main:extensions/common/mojom/manifest.mojom
|
||||
return true
|
||||
}
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:extensions/browser/disable_reason.h
|
||||
enabled := !ext.Get("disable_reasons").Exists()
|
||||
b := ext.Get("manifest")
|
||||
if !b.Exists() {
|
||||
c = append(c, &extension{
|
||||
ID: id.String(),
|
||||
Enabled: enabled,
|
||||
Name: ext.Get("path").String(),
|
||||
})
|
||||
return true
|
||||
}
|
||||
c = append(c, &extension{
|
||||
ID: id.String(),
|
||||
URL: getChromiumExtURL(id.String(), b.Get("update_url").String()),
|
||||
Enabled: enabled,
|
||||
Name: b.Get("name").String(),
|
||||
Description: b.Get("description").String(),
|
||||
Version: b.Get("version").String(),
|
||||
HomepageURL: b.Get("homepage_url").String(),
|
||||
})
|
||||
return true
|
||||
})
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func getChromiumExtURL(id, updateURL string) string {
|
||||
if strings.HasSuffix(updateURL, "clients2.google.com/service/update2/crx") {
|
||||
return "https://chrome.google.com/webstore/detail/" + id
|
||||
} else if strings.HasSuffix(updateURL, "edge.microsoft.com/extensionwebstorebase/v1/crx") {
|
||||
return "https://microsoftedge.microsoft.com/addons/detail/" + id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *ChromiumExtension) Name() string {
|
||||
return "extension"
|
||||
}
|
||||
|
||||
func (c *ChromiumExtension) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
type FirefoxExtension []*extension
|
||||
|
||||
var lang = language.Und
|
||||
|
||||
func (f *FirefoxExtension) Extract(_ []byte) error {
|
||||
s, err := fileutil.ReadFile(types.FirefoxExtension.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Remove(types.FirefoxExtension.TempFilename())
|
||||
j := gjson.Parse(s)
|
||||
for _, v := range j.Get("addons").Array() {
|
||||
// https://searchfox.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIDatabase.jsm#157
|
||||
if v.Get("location").String() != "app-profile" {
|
||||
continue
|
||||
}
|
||||
|
||||
if lang != language.Und {
|
||||
locale := findFirefoxLocale(v.Get("locales").Array(), lang)
|
||||
*f = append(*f, &extension{
|
||||
ID: v.Get("id").String(),
|
||||
Enabled: v.Get("active").Bool(),
|
||||
Name: locale.Get("name").String(),
|
||||
Description: locale.Get("description").String(),
|
||||
Version: v.Get("version").String(),
|
||||
HomepageURL: locale.Get("homepageURL").String(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
*f = append(*f, &extension{
|
||||
ID: v.Get("id").String(),
|
||||
Enabled: v.Get("active").Bool(),
|
||||
Name: v.Get("defaultLocale.name").String(),
|
||||
Description: v.Get("defaultLocale.description").String(),
|
||||
Version: v.Get("version").String(),
|
||||
HomepageURL: v.Get("defaultLocale.homepageURL").String(),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findFirefoxLocale(locales []gjson.Result, targetLang language.Tag) gjson.Result {
|
||||
tags := make([]language.Tag, 0, len(locales))
|
||||
indices := make([]int, 0, len(locales))
|
||||
for i, locale := range locales {
|
||||
for _, tagStr := range locale.Get("locales").Array() {
|
||||
tag, _ := language.Parse(tagStr.String())
|
||||
if tag == language.Und {
|
||||
continue
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
indices = append(indices, i)
|
||||
}
|
||||
}
|
||||
_, tagIndex, _ := language.NewMatcher(tags).Match(targetLang)
|
||||
return locales[indices[tagIndex]]
|
||||
}
|
||||
|
||||
func (f *FirefoxExtension) Name() string {
|
||||
return "extension"
|
||||
}
|
||||
|
||||
func (f *FirefoxExtension) Len() int {
|
||||
return len(*f)
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
// import sqlite3 driver
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumHistory, func() extractor.Extractor {
|
||||
return new(ChromiumHistory)
|
||||
})
|
||||
extractor.RegisterExtractor(types.FirefoxHistory, func() extractor.Extractor {
|
||||
return new(FirefoxHistory)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumHistory []history
|
||||
|
||||
type history struct {
|
||||
Title string
|
||||
URL string
|
||||
VisitCount int
|
||||
LastVisitTime time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
queryChromiumHistory = `SELECT url, title, visit_count, last_visit_time FROM urls`
|
||||
)
|
||||
|
||||
func (c *ChromiumHistory) Extract(_ []byte) error {
|
||||
db, err := sql.Open("sqlite", types.ChromiumHistory.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.ChromiumHistory.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(queryChromiumHistory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
url, title string
|
||||
visitCount int
|
||||
lastVisitTime int64
|
||||
)
|
||||
if err := rows.Scan(&url, &title, &visitCount, &lastVisitTime); err != nil {
|
||||
log.Warnf("scan chromium history error: %v", err)
|
||||
}
|
||||
data := history{
|
||||
URL: url,
|
||||
Title: title,
|
||||
VisitCount: visitCount,
|
||||
LastVisitTime: typeutil.TimeEpoch(lastVisitTime),
|
||||
}
|
||||
*c = append(*c, data)
|
||||
}
|
||||
sort.Slice(*c, func(i, j int) bool {
|
||||
return (*c)[i].VisitCount > (*c)[j].VisitCount
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChromiumHistory) Name() string {
|
||||
return "history"
|
||||
}
|
||||
|
||||
func (c *ChromiumHistory) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
type FirefoxHistory []history
|
||||
|
||||
const (
|
||||
queryFirefoxHistory = `SELECT id, url, COALESCE(last_visit_date, 0), COALESCE(title, ''), visit_count FROM moz_places`
|
||||
closeJournalMode = `PRAGMA journal_mode=off`
|
||||
)
|
||||
|
||||
func (f *FirefoxHistory) Extract(_ []byte) error {
|
||||
db, err := sql.Open("sqlite", types.FirefoxHistory.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.FirefoxHistory.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(closeJournalMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
rows, err := db.Query(queryFirefoxHistory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, visitDate int64
|
||||
url, title string
|
||||
visitCount int
|
||||
)
|
||||
if err = rows.Scan(&id, &url, &visitDate, &title, &visitCount); err != nil {
|
||||
log.Debugf("scan firefox history error: %v", err)
|
||||
}
|
||||
*f = append(*f, history{
|
||||
Title: title,
|
||||
URL: url,
|
||||
VisitCount: visitCount,
|
||||
LastVisitTime: typeutil.TimeStamp(visitDate / 1000000),
|
||||
})
|
||||
}
|
||||
sort.Slice(*f, func(i, j int) bool {
|
||||
return (*f)[i].VisitCount < (*f)[j].VisitCount
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FirefoxHistory) Name() string {
|
||||
return "history"
|
||||
}
|
||||
|
||||
func (f *FirefoxHistory) Len() int {
|
||||
return len(*f)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Package browserdata is responsible for initializing all the necessary
|
||||
// components that handle different types of browser data extraction.
|
||||
// This file, imports.go, is specifically used to import various data
|
||||
// handler packages to ensure their initialization logic is executed.
|
||||
// These imports are crucial as they trigger the `init()` functions
|
||||
// within each package, which typically handle registration of their
|
||||
// specific data handlers to a central registry.
|
||||
package browserdata
|
||||
|
||||
import (
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/bookmark"
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/cookie"
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/creditcard"
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/download"
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/extension"
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/history"
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/localstorage"
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/password"
|
||||
_ "github.com/moond4rk/hackbrowserdata/browserdata/sessionstorage"
|
||||
)
|
||||
@@ -1,234 +0,0 @@
|
||||
package localstorage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumLocalStorage, func() extractor.Extractor {
|
||||
return new(ChromiumLocalStorage)
|
||||
})
|
||||
extractor.RegisterExtractor(types.FirefoxLocalStorage, func() extractor.Extractor {
|
||||
return new(FirefoxLocalStorage)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumLocalStorage []storage
|
||||
|
||||
type storage struct {
|
||||
IsMeta bool
|
||||
URL string
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
const maxLocalStorageValueLength = 1024 * 2
|
||||
|
||||
const (
|
||||
chromiumLocalStorageVersionKey = "VERSION"
|
||||
chromiumLocalStorageMetaPrefix = "META:"
|
||||
chromiumLocalStorageMetaAccessKey = "METAACCESS:"
|
||||
chromiumLocalStorageDataPrefix = '_'
|
||||
chromiumStringUTF16Format = 0
|
||||
chromiumStringLatin1Format = 1
|
||||
)
|
||||
|
||||
func (c *ChromiumLocalStorage) Extract(_ []byte) error {
|
||||
entries, err := extractChromiumLocalStorage(types.ChromiumLocalStorage.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(types.ChromiumLocalStorage.TempFilename())
|
||||
*c = append(*c, entries...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChromiumLocalStorage) Name() string {
|
||||
return "localStorage"
|
||||
}
|
||||
|
||||
func (c *ChromiumLocalStorage) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
func extractChromiumLocalStorage(path string) (ChromiumLocalStorage, error) {
|
||||
db, err := leveldb.OpenFile(path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var entries ChromiumLocalStorage
|
||||
iter := db.NewIterator(nil, nil)
|
||||
defer iter.Release()
|
||||
|
||||
for iter.Next() {
|
||||
entry, ok := parseChromiumLocalStorageEntry(iter.Key(), iter.Value())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return entries, iter.Error()
|
||||
}
|
||||
|
||||
func parseChromiumLocalStorageEntry(key, value []byte) (storage, bool) {
|
||||
switch {
|
||||
case bytes.Equal(key, []byte(chromiumLocalStorageVersionKey)):
|
||||
return storage{}, false
|
||||
case bytes.HasPrefix(key, []byte(chromiumLocalStorageMetaAccessKey)):
|
||||
return storage{
|
||||
IsMeta: true,
|
||||
URL: string(bytes.TrimPrefix(key, []byte(chromiumLocalStorageMetaAccessKey))),
|
||||
Value: fmt.Sprintf("meta data, value bytes is %v", value),
|
||||
}, true
|
||||
case bytes.HasPrefix(key, []byte(chromiumLocalStorageMetaPrefix)):
|
||||
return storage{
|
||||
IsMeta: true,
|
||||
URL: string(bytes.TrimPrefix(key, []byte(chromiumLocalStorageMetaPrefix))),
|
||||
Value: fmt.Sprintf("meta data, value bytes is %v", value),
|
||||
}, true
|
||||
case len(key) > 0 && key[0] == chromiumLocalStorageDataPrefix:
|
||||
return parseChromiumLocalStorageDataEntry(key[1:], value), true
|
||||
default:
|
||||
return storage{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func parseChromiumLocalStorageDataEntry(key, value []byte) storage {
|
||||
entry := storage{
|
||||
Value: decodeChromiumLocalStorageValue(value),
|
||||
}
|
||||
|
||||
separator := bytes.IndexByte(key, 0)
|
||||
if separator < 0 {
|
||||
entry.Key = "unsupported chromium localStorage key encoding: missing origin separator"
|
||||
return entry
|
||||
}
|
||||
|
||||
entry.URL = string(key[:separator])
|
||||
scriptKey, err := decodeChromiumString(key[separator+1:])
|
||||
if err != nil {
|
||||
entry.Key = fmt.Sprintf("unsupported chromium localStorage key encoding: %v", err)
|
||||
return entry
|
||||
}
|
||||
entry.Key = scriptKey
|
||||
return entry
|
||||
}
|
||||
|
||||
func convertUTF16toUTF8(source []byte, endian unicode.Endianness) ([]byte, error) {
|
||||
r, _, err := transform.Bytes(unicode.UTF16(endian, unicode.IgnoreBOM).NewDecoder(), source)
|
||||
return r, err
|
||||
}
|
||||
|
||||
func decodeChromiumString(b []byte) (string, error) {
|
||||
if len(b) == 0 {
|
||||
return "", fmt.Errorf("empty chromium string")
|
||||
}
|
||||
|
||||
switch b[0] {
|
||||
case chromiumStringLatin1Format:
|
||||
return string(b[1:]), nil
|
||||
case chromiumStringUTF16Format:
|
||||
if len(b) == 1 {
|
||||
return "", nil
|
||||
}
|
||||
if (len(b)-1)%2 != 0 {
|
||||
return "", fmt.Errorf("invalid UTF-16 byte length %d", len(b)-1)
|
||||
}
|
||||
value, err := convertUTF16toUTF8(b[1:], unicode.LittleEndian)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(value), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown chromium string format 0x%02x", b[0])
|
||||
}
|
||||
}
|
||||
|
||||
func decodeChromiumLocalStorageValue(value []byte) string {
|
||||
if len(value) >= maxLocalStorageValueLength {
|
||||
return fmt.Sprintf(
|
||||
"value is too long, length is %d, supported max length is %d",
|
||||
len(value),
|
||||
maxLocalStorageValueLength,
|
||||
)
|
||||
}
|
||||
|
||||
decoded, err := decodeChromiumString(value)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("unsupported chromium localStorage value encoding: %v", err)
|
||||
}
|
||||
return decoded
|
||||
}
|
||||
|
||||
type FirefoxLocalStorage []storage
|
||||
|
||||
const (
|
||||
queryLocalStorage = `SELECT originKey, key, value FROM webappsstore2`
|
||||
closeJournalMode = `PRAGMA journal_mode=off`
|
||||
)
|
||||
|
||||
func (f *FirefoxLocalStorage) Extract(_ []byte) error {
|
||||
db, err := sql.Open("sqlite", types.FirefoxLocalStorage.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.FirefoxLocalStorage.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(closeJournalMode)
|
||||
if err != nil {
|
||||
log.Debugf("close journal mode error: %v", err)
|
||||
}
|
||||
rows, err := db.Query(queryLocalStorage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var originKey, key, value string
|
||||
if err = rows.Scan(&originKey, &key, &value); err != nil {
|
||||
log.Debugf("scan firefox local storage error: %v", err)
|
||||
}
|
||||
s := new(storage)
|
||||
s.fillFirefox(originKey, key, value)
|
||||
*f = append(*f, *s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *storage) fillFirefox(originKey, key, value string) {
|
||||
// originKey = moc.buhtig.:https:443
|
||||
p := strings.Split(originKey, ":")
|
||||
h := typeutil.Reverse([]byte(p[0]))
|
||||
if bytes.HasPrefix(h, []byte(".")) {
|
||||
h = h[1:]
|
||||
}
|
||||
if len(p) == 3 {
|
||||
s.URL = fmt.Sprintf("%s://%s:%s", p[1], string(h), p[2])
|
||||
}
|
||||
s.Key = key
|
||||
s.Value = value
|
||||
}
|
||||
|
||||
func (f *FirefoxLocalStorage) Name() string {
|
||||
return "localStorage"
|
||||
}
|
||||
|
||||
func (f *FirefoxLocalStorage) Len() int {
|
||||
return len(*f)
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
package localstorage
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
func TestDecodeChromiumString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "latin1",
|
||||
input: encodeChromiumLatin1("abc123"),
|
||||
want: "abc123",
|
||||
},
|
||||
{
|
||||
name: "utf16le",
|
||||
input: encodeChromiumUTF16("飞连"),
|
||||
want: "飞连",
|
||||
},
|
||||
{
|
||||
name: "unknown format",
|
||||
input: []byte{2, 'x'},
|
||||
wantErr: "unknown chromium string format",
|
||||
},
|
||||
{
|
||||
name: "invalid utf16 byte length",
|
||||
input: []byte{chromiumStringUTF16Format, 0x61},
|
||||
wantErr: "invalid UTF-16 byte length",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := decodeChromiumString(tc.input)
|
||||
if tc.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseChromiumLocalStorageEntry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key []byte
|
||||
value []byte
|
||||
wantParsed bool
|
||||
wantMeta bool
|
||||
wantURL string
|
||||
wantKey string
|
||||
wantValue string
|
||||
wantContains string
|
||||
}{
|
||||
{
|
||||
name: "skip version key",
|
||||
key: []byte(chromiumLocalStorageVersionKey),
|
||||
wantParsed: false,
|
||||
},
|
||||
{
|
||||
name: "meta key",
|
||||
key: []byte(chromiumLocalStorageMetaPrefix + "https://example.com"),
|
||||
value: []byte{0x08, 0x96, 0x01},
|
||||
wantParsed: true,
|
||||
wantMeta: true,
|
||||
wantURL: "https://example.com",
|
||||
wantValue: "meta data, value bytes is [8 150 1]",
|
||||
},
|
||||
{
|
||||
name: "meta access key",
|
||||
key: []byte(chromiumLocalStorageMetaAccessKey + "https://example.com"),
|
||||
value: []byte{0x10, 0x20},
|
||||
wantParsed: true,
|
||||
wantMeta: true,
|
||||
wantURL: "https://example.com",
|
||||
wantValue: "meta data, value bytes is [16 32]",
|
||||
},
|
||||
{
|
||||
name: "latin1 business key",
|
||||
key: append([]byte("_https://example.com\x00"), encodeChromiumLatin1("token")...),
|
||||
value: encodeChromiumLatin1("abc123"),
|
||||
wantParsed: true,
|
||||
wantURL: "https://example.com",
|
||||
wantKey: "token",
|
||||
wantValue: "abc123",
|
||||
},
|
||||
{
|
||||
name: "utf16 business key",
|
||||
key: append([]byte("_https://example.com\x00"), encodeChromiumUTF16("飞连")...),
|
||||
value: encodeChromiumUTF16("终端安全"),
|
||||
wantParsed: true,
|
||||
wantURL: "https://example.com",
|
||||
wantKey: "飞连",
|
||||
wantValue: "终端安全",
|
||||
},
|
||||
{
|
||||
name: "unsupported business key format",
|
||||
key: append([]byte("_https://example.com\x00"), []byte{2, 'x'}...),
|
||||
value: encodeChromiumLatin1("abc123"),
|
||||
wantParsed: true,
|
||||
wantURL: "https://example.com",
|
||||
wantContains: "unsupported chromium localStorage key encoding",
|
||||
wantValue: "abc123",
|
||||
},
|
||||
{
|
||||
name: "missing origin separator",
|
||||
key: append([]byte("_https://example.com"), encodeChromiumLatin1("token")...),
|
||||
value: encodeChromiumLatin1("abc123"),
|
||||
wantParsed: true,
|
||||
wantContains: "missing origin separator",
|
||||
wantValue: "abc123",
|
||||
},
|
||||
{
|
||||
name: "unsupported value format",
|
||||
key: append([]byte("_https://example.com\x00"), encodeChromiumLatin1("token")...),
|
||||
value: []byte{2, 'x'},
|
||||
wantParsed: true,
|
||||
wantURL: "https://example.com",
|
||||
wantKey: "token",
|
||||
wantValue: "unsupported chromium localStorage value encoding: unknown chromium string format 0x02",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, parsed := parseChromiumLocalStorageEntry(tc.key, tc.value)
|
||||
|
||||
assert.Equal(t, tc.wantParsed, parsed)
|
||||
assert.Equal(t, tc.wantMeta, got.IsMeta)
|
||||
assert.Equal(t, tc.wantURL, got.URL)
|
||||
assert.Equal(t, tc.wantValue, got.Value)
|
||||
if tc.wantContains != "" {
|
||||
assert.Contains(t, got.Key, tc.wantContains)
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tc.wantKey, got.Key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractChromiumLocalStorage(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
db, err := leveldb.OpenFile(dir, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
testEntries := map[string][]byte{
|
||||
chromiumLocalStorageVersionKey: []byte("1"),
|
||||
chromiumLocalStorageMetaPrefix + "https://example.com": {0x08, 0x96, 0x01},
|
||||
chromiumLocalStorageMetaAccessKey + "https://example.com": {0x10, 0x20},
|
||||
string(append([]byte("_https://example.com\x00"), encodeChromiumLatin1("token")...)): encodeChromiumLatin1("abc123"),
|
||||
string(append([]byte("_https://example.com\x00"), encodeChromiumUTF16("飞连")...)): encodeChromiumUTF16("终端安全"),
|
||||
}
|
||||
|
||||
for key, value := range testEntries {
|
||||
require.NoError(t, db.Put([]byte(key), value, nil))
|
||||
}
|
||||
require.NoError(t, db.Close())
|
||||
|
||||
got, err := extractChromiumLocalStorage(dir)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 4)
|
||||
|
||||
metaCount := 0
|
||||
valuesByKey := make(map[string]string)
|
||||
for _, entry := range got {
|
||||
if entry.IsMeta {
|
||||
metaCount++
|
||||
assert.Equal(t, "https://example.com", entry.URL)
|
||||
assert.Contains(t, entry.Value, "meta data, value bytes is")
|
||||
continue
|
||||
}
|
||||
valuesByKey[entry.Key] = entry.Value
|
||||
assert.Equal(t, "https://example.com", entry.URL)
|
||||
}
|
||||
|
||||
assert.Equal(t, 2, metaCount)
|
||||
assert.Equal(t, "abc123", valuesByKey["token"])
|
||||
assert.Equal(t, "终端安全", valuesByKey["飞连"])
|
||||
}
|
||||
|
||||
func encodeChromiumLatin1(s string) []byte {
|
||||
return append([]byte{chromiumStringLatin1Format}, []byte(s)...)
|
||||
}
|
||||
|
||||
func encodeChromiumUTF16(s string) []byte {
|
||||
encoded := utf16.Encode([]rune(s))
|
||||
result := make([]byte, 1, 1+len(encoded)*2)
|
||||
result[0] = chromiumStringUTF16Format
|
||||
for _, r := range encoded {
|
||||
var raw [2]byte
|
||||
binary.LittleEndian.PutUint16(raw[:], r)
|
||||
result = append(result, raw[:]...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package browserdata
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gocarina/gocsv"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
)
|
||||
|
||||
type outPutter struct {
|
||||
json bool
|
||||
csv bool
|
||||
}
|
||||
|
||||
func newOutPutter(flag string) *outPutter {
|
||||
o := &outPutter{}
|
||||
if flag == "json" {
|
||||
o.json = true
|
||||
} else {
|
||||
o.csv = true
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *outPutter) Write(data extractor.Extractor, writer io.Writer) error {
|
||||
switch o.json {
|
||||
case true:
|
||||
encoder := json.NewEncoder(writer)
|
||||
encoder.SetIndent("", " ")
|
||||
encoder.SetEscapeHTML(false)
|
||||
return encoder.Encode(data)
|
||||
default:
|
||||
gocsv.SetCSVWriter(func(w io.Writer) *gocsv.SafeCSVWriter {
|
||||
writer := csv.NewWriter(transform.NewWriter(w, unicode.UTF8BOM.NewEncoder()))
|
||||
writer.Comma = ','
|
||||
return gocsv.NewSafeCSVWriter(writer)
|
||||
})
|
||||
return gocsv.Marshal(data, writer)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *outPutter) CreateFile(dir, filename string) (*os.File, error) {
|
||||
if filename == "" {
|
||||
return nil, errors.New("empty filename")
|
||||
}
|
||||
|
||||
if dir != "" {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
err := os.MkdirAll(dir, 0o750)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var file *os.File
|
||||
var err error
|
||||
p := filepath.Join(dir, filename)
|
||||
file, err = os.OpenFile(filepath.Clean(p), os.O_TRUNC|os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (o *outPutter) Ext() string {
|
||||
if o.json {
|
||||
return "json"
|
||||
}
|
||||
return "csv"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package browserdata
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewOutPutter(t *testing.T) {
|
||||
t.Parallel()
|
||||
out := newOutPutter("json")
|
||||
if out == nil {
|
||||
t.Error("New() returned nil")
|
||||
}
|
||||
f, err := out.CreateFile("results", "test.json")
|
||||
if err != nil {
|
||||
t.Error("CreateFile() returned an error", err)
|
||||
}
|
||||
defer os.RemoveAll("results")
|
||||
err = out.Write(nil, f)
|
||||
if err != nil {
|
||||
t.Error("Write() returned an error", err)
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
_ "modernc.org/sqlite" // import sqlite3 driver
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumPassword, func() extractor.Extractor {
|
||||
return new(ChromiumPassword)
|
||||
})
|
||||
extractor.RegisterExtractor(types.YandexPassword, func() extractor.Extractor {
|
||||
return new(YandexPassword)
|
||||
})
|
||||
extractor.RegisterExtractor(types.FirefoxPassword, func() extractor.Extractor {
|
||||
return new(FirefoxPassword)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumPassword []loginData
|
||||
|
||||
type loginData struct {
|
||||
UserName string
|
||||
encryptPass []byte
|
||||
encryptUser []byte
|
||||
Password string
|
||||
LoginURL string
|
||||
CreateDate time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
queryChromiumLogin = `SELECT origin_url, username_value, password_value, date_created FROM logins`
|
||||
)
|
||||
|
||||
func (c *ChromiumPassword) Extract(masterKey []byte) error {
|
||||
db, err := sql.Open("sqlite", types.ChromiumPassword.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.ChromiumPassword.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(queryChromiumLogin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
url, username string
|
||||
pwd, password []byte
|
||||
create int64
|
||||
)
|
||||
if err := rows.Scan(&url, &username, &pwd, &create); err != nil {
|
||||
log.Debugf("scan chromium password error: %v", err)
|
||||
}
|
||||
login := loginData{
|
||||
UserName: username,
|
||||
encryptPass: pwd,
|
||||
LoginURL: url,
|
||||
}
|
||||
|
||||
if len(pwd) > 0 {
|
||||
password, err = crypto.DecryptWithDPAPI(pwd)
|
||||
if err != nil {
|
||||
password, err = crypto.DecryptWithChromium(masterKey, pwd)
|
||||
if err != nil {
|
||||
log.Debugf("decrypt chromium password error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if create > time.Now().Unix() {
|
||||
login.CreateDate = typeutil.TimeEpoch(create)
|
||||
} else {
|
||||
login.CreateDate = typeutil.TimeStamp(create)
|
||||
}
|
||||
login.Password = string(password)
|
||||
*c = append(*c, login)
|
||||
}
|
||||
// sort with create date
|
||||
sort.Slice(*c, func(i, j int) bool {
|
||||
return (*c)[i].CreateDate.After((*c)[j].CreateDate)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChromiumPassword) Name() string {
|
||||
return "password"
|
||||
}
|
||||
|
||||
func (c *ChromiumPassword) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
type YandexPassword []loginData
|
||||
|
||||
const (
|
||||
queryYandexLogin = `SELECT action_url, username_value, password_value, date_created FROM logins`
|
||||
)
|
||||
|
||||
func (c *YandexPassword) Extract(masterKey []byte) error {
|
||||
db, err := sql.Open("sqlite", types.YandexPassword.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.YandexPassword.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(queryYandexLogin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
url, username string
|
||||
pwd, password []byte
|
||||
create int64
|
||||
)
|
||||
if err := rows.Scan(&url, &username, &pwd, &create); err != nil {
|
||||
log.Debugf("scan yandex password error: %v", err)
|
||||
}
|
||||
login := loginData{
|
||||
UserName: username,
|
||||
encryptPass: pwd,
|
||||
LoginURL: url,
|
||||
}
|
||||
|
||||
if len(pwd) > 0 {
|
||||
if len(masterKey) == 0 {
|
||||
password, err = crypto.DecryptWithDPAPI(pwd)
|
||||
} else {
|
||||
password, err = crypto.DecryptWithChromium(masterKey, pwd)
|
||||
}
|
||||
if err != nil {
|
||||
log.Debugf("decrypt yandex password error: %v", err)
|
||||
}
|
||||
}
|
||||
if create > time.Now().Unix() {
|
||||
login.CreateDate = typeutil.TimeEpoch(create)
|
||||
} else {
|
||||
login.CreateDate = typeutil.TimeStamp(create)
|
||||
}
|
||||
login.Password = string(password)
|
||||
*c = append(*c, login)
|
||||
}
|
||||
// sort with create date
|
||||
sort.Slice(*c, func(i, j int) bool {
|
||||
return (*c)[i].CreateDate.After((*c)[j].CreateDate)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *YandexPassword) Name() string {
|
||||
return "password"
|
||||
}
|
||||
|
||||
func (c *YandexPassword) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
type FirefoxPassword []loginData
|
||||
|
||||
func (f *FirefoxPassword) Extract(globalSalt []byte) error {
|
||||
logins, err := getFirefoxLoginData()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range logins {
|
||||
userPBE, err := crypto.NewASN1PBE(v.encryptUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pwdPBE, err := crypto.NewASN1PBE(v.encryptPass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user, err := userPBE.Decrypt(globalSalt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pwd, err := pwdPBE.Decrypt(globalSalt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*f = append(*f, loginData{
|
||||
LoginURL: v.LoginURL,
|
||||
UserName: string(user),
|
||||
Password: string(pwd),
|
||||
CreateDate: v.CreateDate,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(*f, func(i, j int) bool {
|
||||
return (*f)[i].CreateDate.After((*f)[j].CreateDate)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFirefoxLoginData() ([]loginData, error) {
|
||||
s, err := os.ReadFile(types.FirefoxPassword.TempFilename())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(types.FirefoxPassword.TempFilename())
|
||||
loginsJSON := gjson.GetBytes(s, "logins")
|
||||
var logins []loginData
|
||||
if loginsJSON.Exists() {
|
||||
for _, v := range loginsJSON.Array() {
|
||||
var (
|
||||
m loginData
|
||||
user []byte
|
||||
pass []byte
|
||||
)
|
||||
// Use formSubmitURL if available, otherwise fallback to hostname
|
||||
m.LoginURL = v.Get("formSubmitURL").String()
|
||||
if m.LoginURL == "" {
|
||||
m.LoginURL = v.Get("hostname").String()
|
||||
}
|
||||
user, err = base64.StdEncoding.DecodeString(v.Get("encryptedUsername").String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pass, err = base64.StdEncoding.DecodeString(v.Get("encryptedPassword").String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.encryptUser = user
|
||||
m.encryptPass = pass
|
||||
m.CreateDate = typeutil.TimeStamp(v.Get("timeCreated").Int() / 1000)
|
||||
logins = append(logins, m)
|
||||
}
|
||||
}
|
||||
return logins, nil
|
||||
}
|
||||
|
||||
func (f *FirefoxPassword) Name() string {
|
||||
return "password"
|
||||
}
|
||||
|
||||
func (f *FirefoxPassword) Len() int {
|
||||
return len(*f)
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
package sessionstorage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/extractor"
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/byteutil"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extractor.RegisterExtractor(types.ChromiumSessionStorage, func() extractor.Extractor {
|
||||
return new(ChromiumSessionStorage)
|
||||
})
|
||||
extractor.RegisterExtractor(types.FirefoxSessionStorage, func() extractor.Extractor {
|
||||
return new(FirefoxSessionStorage)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromiumSessionStorage []session
|
||||
|
||||
type session struct {
|
||||
IsMeta bool
|
||||
URL string
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
const maxLocalStorageValueLength = 1024 * 2
|
||||
|
||||
func (c *ChromiumSessionStorage) Extract(_ []byte) error {
|
||||
db, err := leveldb.OpenFile(types.ChromiumSessionStorage.TempFilename(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(types.ChromiumSessionStorage.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
iter := db.NewIterator(nil, nil)
|
||||
for iter.Next() {
|
||||
key := iter.Key()
|
||||
value := iter.Value()
|
||||
s := new(session)
|
||||
s.fillKey(key)
|
||||
// don't all value upper than 2KB
|
||||
if len(value) < maxLocalStorageValueLength {
|
||||
s.fillValue(value)
|
||||
} else {
|
||||
s.Value = fmt.Sprintf("value is too long, length is %d, supported max length is %d", len(value), maxLocalStorageValueLength)
|
||||
}
|
||||
if s.IsMeta {
|
||||
s.Value = fmt.Sprintf("meta data, value bytes is %v", value)
|
||||
}
|
||||
*c = append(*c, *s)
|
||||
}
|
||||
iter.Release()
|
||||
err = iter.Error()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ChromiumSessionStorage) Name() string {
|
||||
return "sessionStorage"
|
||||
}
|
||||
|
||||
func (c *ChromiumSessionStorage) Len() int {
|
||||
return len(*c)
|
||||
}
|
||||
|
||||
func (s *session) fillKey(b []byte) {
|
||||
keys := bytes.Split(b, []byte("-"))
|
||||
if len(keys) == 1 && bytes.HasPrefix(keys[0], []byte("META:")) {
|
||||
s.IsMeta = true
|
||||
s.fillMetaHeader(keys[0])
|
||||
}
|
||||
if len(keys) == 2 && bytes.HasPrefix(keys[0], []byte("_")) {
|
||||
s.fillHeader(keys[0], keys[1])
|
||||
}
|
||||
if len(keys) == 3 {
|
||||
if string(keys[0]) == "map" {
|
||||
s.Key = string(keys[2])
|
||||
} else if string(keys[0]) == "namespace" {
|
||||
s.URL = string(keys[2])
|
||||
s.Key = string(keys[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *session) fillMetaHeader(b []byte) {
|
||||
s.URL = string(bytes.Trim(b, "META:"))
|
||||
}
|
||||
|
||||
func (s *session) fillHeader(url, key []byte) {
|
||||
s.URL = string(bytes.Trim(url, "_"))
|
||||
s.Key = string(bytes.Trim(key, "\x01"))
|
||||
}
|
||||
|
||||
func convertUTF16toUTF8(source []byte, endian unicode.Endianness) ([]byte, error) {
|
||||
r, _, err := transform.Bytes(unicode.UTF16(endian, unicode.IgnoreBOM).NewDecoder(), source)
|
||||
return r, err
|
||||
}
|
||||
|
||||
// fillValue fills value of the storage
|
||||
// TODO: support unicode charter
|
||||
func (s *session) fillValue(b []byte) {
|
||||
value := bytes.Map(byteutil.OnSplitUTF8Func, b)
|
||||
s.Value = string(value)
|
||||
}
|
||||
|
||||
type FirefoxSessionStorage []session
|
||||
|
||||
const (
|
||||
querySessionStorage = `SELECT originKey, key, value FROM webappsstore2`
|
||||
closeJournalMode = `PRAGMA journal_mode=off`
|
||||
)
|
||||
|
||||
func (f *FirefoxSessionStorage) Extract(_ []byte) error {
|
||||
db, err := sql.Open("sqlite", types.FirefoxSessionStorage.TempFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(types.FirefoxSessionStorage.TempFilename())
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(closeJournalMode)
|
||||
if err != nil {
|
||||
log.Debugf("close journal mode error: %v", err)
|
||||
}
|
||||
rows, err := db.Query(querySessionStorage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var originKey, key, value string
|
||||
if err = rows.Scan(&originKey, &key, &value); err != nil {
|
||||
log.Debugf("scan session storage error: %v", err)
|
||||
}
|
||||
s := new(session)
|
||||
s.fillFirefox(originKey, key, value)
|
||||
*f = append(*f, *s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) fillFirefox(originKey, key, value string) {
|
||||
// originKey = moc.buhtig.:https:443
|
||||
p := strings.Split(originKey, ":")
|
||||
h := typeutil.Reverse([]byte(p[0]))
|
||||
if bytes.HasPrefix(h, []byte(".")) {
|
||||
h = h[1:]
|
||||
}
|
||||
if len(p) == 3 {
|
||||
s.URL = fmt.Sprintf("%s://%s:%s", p[1], string(h), p[2])
|
||||
}
|
||||
s.Key = key
|
||||
s.Value = value
|
||||
}
|
||||
|
||||
func (f *FirefoxSessionStorage) Name() string {
|
||||
return "sessionStorage"
|
||||
}
|
||||
|
||||
func (f *FirefoxSessionStorage) Len() int {
|
||||
return len(*f)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package extractor
|
||||
|
||||
// Extractor is an interface for extracting data from browser data files
|
||||
type Extractor interface {
|
||||
Extract(masterKey []byte) error
|
||||
|
||||
Name() string
|
||||
|
||||
Len() int
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package extractor
|
||||
|
||||
import (
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
)
|
||||
|
||||
var extractorRegistry = make(map[types.DataType]func() Extractor)
|
||||
|
||||
// RegisterExtractor is used to register the data source
|
||||
func RegisterExtractor(dataType types.DataType, factoryFunc func() Extractor) {
|
||||
extractorRegistry[dataType] = factoryFunc
|
||||
}
|
||||
|
||||
// CreateExtractor is used to create the data source
|
||||
func CreateExtractor(dataType types.DataType) Extractor {
|
||||
if factoryFunc, ok := extractorRegistry[dataType]; ok {
|
||||
return factoryFunc()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -3,10 +3,8 @@ module github.com/moond4rk/hackbrowserdata
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
|
||||
github.com/godbus/dbus/v5 v5.2.2
|
||||
github.com/moond4rk/keychainbreaker v0.1.0
|
||||
github.com/moond4rk/keychainbreaker v0.2.5
|
||||
github.com/otiai10/copy v1.14.1
|
||||
github.com/ppacher/go-dbus-keyring v1.0.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
@@ -14,7 +12,6 @@ require (
|
||||
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/text v0.19.0
|
||||
modernc.org/sqlite v1.31.1
|
||||
)
|
||||
|
||||
@@ -35,6 +32,8 @@ require (
|
||||
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/text v0.19.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||
modernc.org/libc v1.55.3 // indirect
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
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=
|
||||
@@ -7,8 +5,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
@@ -22,11 +18,10 @@ 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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||
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/moond4rk/keychainbreaker v0.1.0 h1:9hkE70c4jxaTHStZ3kny4GEJ/srcvt2DZe0vUg3m8V0=
|
||||
github.com/moond4rk/keychainbreaker v0.1.0/go.mod h1:VVx2VXwL2EGhuU2WBD67w66JCKKqLFXGJg91y3FY4f0=
|
||||
github.com/moond4rk/keychainbreaker v0.2.5 h1:1f2qmgpt1sl+mXA8DTW9nnVhzo4oGO08bnkXu70DL04=
|
||||
github.com/moond4rk/keychainbreaker v0.2.5/go.mod h1:VVx2VXwL2EGhuU2WBD67w66JCKKqLFXGJg91y3FY4f0=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -61,8 +56,8 @@ github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AO
|
||||
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/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
|
||||
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/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
@@ -74,6 +69,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
|
||||
-221
@@ -1,221 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type DataType int
|
||||
|
||||
const (
|
||||
ChromiumKey DataType = iota
|
||||
ChromiumPassword
|
||||
ChromiumCookie
|
||||
ChromiumBookmark
|
||||
ChromiumHistory
|
||||
ChromiumDownload
|
||||
ChromiumCreditCard
|
||||
ChromiumLocalStorage
|
||||
ChromiumSessionStorage
|
||||
ChromiumExtension
|
||||
|
||||
YandexPassword
|
||||
YandexCreditCard
|
||||
|
||||
FirefoxKey4
|
||||
FirefoxPassword
|
||||
FirefoxCookie
|
||||
FirefoxBookmark
|
||||
FirefoxHistory
|
||||
FirefoxDownload
|
||||
FirefoxCreditCard
|
||||
FirefoxLocalStorage
|
||||
FirefoxSessionStorage
|
||||
FirefoxExtension
|
||||
)
|
||||
|
||||
var itemFileNames = map[DataType]string{
|
||||
ChromiumKey: fileChromiumKey,
|
||||
ChromiumPassword: fileChromiumPassword,
|
||||
ChromiumCookie: fileChromiumCookie,
|
||||
ChromiumBookmark: fileChromiumBookmark,
|
||||
ChromiumDownload: fileChromiumDownload,
|
||||
ChromiumLocalStorage: fileChromiumLocalStorage,
|
||||
ChromiumSessionStorage: fileChromiumSessionStorage,
|
||||
ChromiumCreditCard: fileChromiumCredit,
|
||||
ChromiumExtension: fileChromiumExtension,
|
||||
ChromiumHistory: fileChromiumHistory,
|
||||
YandexPassword: fileYandexPassword,
|
||||
YandexCreditCard: fileYandexCredit,
|
||||
FirefoxKey4: fileFirefoxKey4,
|
||||
FirefoxPassword: fileFirefoxPassword,
|
||||
FirefoxCookie: fileFirefoxCookie,
|
||||
FirefoxBookmark: fileFirefoxData,
|
||||
FirefoxDownload: fileFirefoxData,
|
||||
FirefoxLocalStorage: fileFirefoxLocalStorage,
|
||||
FirefoxHistory: fileFirefoxData,
|
||||
FirefoxExtension: fileFirefoxExtension,
|
||||
FirefoxSessionStorage: UnsupportedItem,
|
||||
FirefoxCreditCard: UnsupportedItem,
|
||||
}
|
||||
|
||||
func (i DataType) String() string {
|
||||
switch i {
|
||||
case ChromiumKey:
|
||||
return "ChromiumKey"
|
||||
case ChromiumPassword:
|
||||
return "ChromiumPassword"
|
||||
case ChromiumCookie:
|
||||
return "ChromiumCookie"
|
||||
case ChromiumBookmark:
|
||||
return "ChromiumBookmark"
|
||||
case ChromiumHistory:
|
||||
return "ChromiumHistory"
|
||||
case ChromiumDownload:
|
||||
return "ChromiumDownload"
|
||||
case ChromiumCreditCard:
|
||||
return "ChromiumCreditCard"
|
||||
case ChromiumLocalStorage:
|
||||
return "ChromiumLocalStorage"
|
||||
case ChromiumSessionStorage:
|
||||
return "ChromiumSessionStorage"
|
||||
case ChromiumExtension:
|
||||
return "ChromiumExtension"
|
||||
case YandexPassword:
|
||||
return "YandexPassword"
|
||||
case YandexCreditCard:
|
||||
return "YandexCreditCard"
|
||||
case FirefoxKey4:
|
||||
return "FirefoxKey4"
|
||||
case FirefoxPassword:
|
||||
return "FirefoxPassword"
|
||||
case FirefoxCookie:
|
||||
return "FirefoxCookie"
|
||||
case FirefoxBookmark:
|
||||
return "FirefoxBookmark"
|
||||
case FirefoxHistory:
|
||||
return "FirefoxHistory"
|
||||
case FirefoxDownload:
|
||||
return "FirefoxDownload"
|
||||
case FirefoxCreditCard:
|
||||
return "FirefoxCreditCard"
|
||||
case FirefoxLocalStorage:
|
||||
return "FirefoxLocalStorage"
|
||||
case FirefoxSessionStorage:
|
||||
return "FirefoxSessionStorage"
|
||||
case FirefoxExtension:
|
||||
return "FirefoxExtension"
|
||||
default:
|
||||
return "UnsupportedItem"
|
||||
}
|
||||
}
|
||||
|
||||
// Filename returns the filename for the item, defined by browser
|
||||
// chromium local storage is a folder, so it returns the file name of the folder
|
||||
func (i DataType) Filename() string {
|
||||
if fileName, ok := itemFileNames[i]; ok {
|
||||
return fileName
|
||||
}
|
||||
return UnsupportedItem
|
||||
}
|
||||
|
||||
// TempFilename returns the temp filename for the item with suffix
|
||||
// eg: chromiumKey_0.temp
|
||||
func (i DataType) TempFilename() string {
|
||||
const tempSuffix = "temp"
|
||||
tempFile := fmt.Sprintf("%s_%d.%s", i.Filename(), i, tempSuffix)
|
||||
return filepath.Join(os.TempDir(), tempFile)
|
||||
}
|
||||
|
||||
// IsSensitive returns whether the item is sensitive data
|
||||
// password, cookie, credit card, master key is unlimited
|
||||
func (i DataType) IsSensitive() bool {
|
||||
switch i {
|
||||
case ChromiumKey, ChromiumCookie, ChromiumPassword, ChromiumCreditCard,
|
||||
FirefoxKey4, FirefoxPassword, FirefoxCookie, FirefoxCreditCard,
|
||||
YandexPassword, YandexCreditCard:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// FilterSensitiveItems returns the sensitive items
|
||||
func FilterSensitiveItems(items []DataType) []DataType {
|
||||
var filtered []DataType
|
||||
for _, item := range items {
|
||||
if item.IsSensitive() {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// DefaultFirefoxTypes returns the default items for the firefox browser
|
||||
var DefaultFirefoxTypes = []DataType{
|
||||
FirefoxKey4,
|
||||
FirefoxPassword,
|
||||
FirefoxCookie,
|
||||
FirefoxBookmark,
|
||||
FirefoxHistory,
|
||||
FirefoxDownload,
|
||||
FirefoxCreditCard,
|
||||
FirefoxLocalStorage,
|
||||
FirefoxSessionStorage,
|
||||
FirefoxExtension,
|
||||
}
|
||||
|
||||
// DefaultYandexTypes returns the default items for the yandex browser
|
||||
var DefaultYandexTypes = []DataType{
|
||||
ChromiumKey,
|
||||
ChromiumCookie,
|
||||
ChromiumBookmark,
|
||||
ChromiumHistory,
|
||||
ChromiumDownload,
|
||||
ChromiumExtension,
|
||||
YandexPassword,
|
||||
ChromiumLocalStorage,
|
||||
ChromiumSessionStorage,
|
||||
YandexCreditCard,
|
||||
}
|
||||
|
||||
// DefaultChromiumTypes returns the default items for the chromium browser
|
||||
var DefaultChromiumTypes = []DataType{
|
||||
ChromiumKey,
|
||||
ChromiumPassword,
|
||||
ChromiumCookie,
|
||||
ChromiumBookmark,
|
||||
ChromiumHistory,
|
||||
ChromiumDownload,
|
||||
ChromiumCreditCard,
|
||||
ChromiumLocalStorage,
|
||||
ChromiumSessionStorage,
|
||||
ChromiumExtension,
|
||||
}
|
||||
|
||||
// item's default filename
|
||||
const (
|
||||
fileChromiumKey = "Local State"
|
||||
fileChromiumCredit = "Web Data"
|
||||
fileChromiumPassword = "Login Data"
|
||||
fileChromiumHistory = "History"
|
||||
fileChromiumDownload = "History"
|
||||
fileChromiumCookie = "Cookies"
|
||||
fileChromiumBookmark = "Bookmarks"
|
||||
fileChromiumLocalStorage = "Local Storage/leveldb"
|
||||
fileChromiumSessionStorage = "Session Storage"
|
||||
fileChromiumExtension = "Secure Preferences" // TODO: add more extension files and folders, eg: Preferences
|
||||
|
||||
fileYandexPassword = "Ya Passman Data"
|
||||
fileYandexCredit = "Ya Credit Cards"
|
||||
|
||||
fileFirefoxKey4 = "key4.db"
|
||||
fileFirefoxCookie = "cookies.sqlite"
|
||||
fileFirefoxPassword = "logins.json"
|
||||
fileFirefoxData = "places.sqlite"
|
||||
fileFirefoxLocalStorage = "webappsstore.sqlite"
|
||||
fileFirefoxExtension = "extensions.json"
|
||||
|
||||
UnsupportedItem = "unsupported item"
|
||||
)
|
||||
@@ -1,130 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDataType_FileName(t *testing.T) {
|
||||
for _, item := range DefaultChromiumTypes {
|
||||
assert.Equal(t, item.Filename(), item.filename())
|
||||
}
|
||||
for _, item := range DefaultFirefoxTypes {
|
||||
assert.Equal(t, item.Filename(), item.filename())
|
||||
}
|
||||
for _, item := range DefaultYandexTypes {
|
||||
assert.Equal(t, item.Filename(), item.filename())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataType_TempFilename(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
|
||||
testCases := []struct {
|
||||
item DataType
|
||||
expected string
|
||||
}{
|
||||
{ChromiumKey, "Local State"},
|
||||
{ChromiumPassword, "Login Data"},
|
||||
{ChromiumLocalStorage, "Local Storage/leveldb"},
|
||||
{FirefoxSessionStorage, "unsupported item"},
|
||||
{FirefoxLocalStorage, "webappsstore.sqlite"},
|
||||
{YandexPassword, "Ya Passman Data"},
|
||||
{YandexCreditCard, "Ya Credit Cards"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
expectedPrefix := tc.expected + "_" + strconv.Itoa(int(tc.item)) + ".temp"
|
||||
actualPath := filepath.ToSlash(tc.item.TempFilename())
|
||||
asserts.Contains(actualPath, expectedPrefix, "TempFilename should contain the correct prefix for "+tc.expected)
|
||||
asserts.Contains(actualPath, filepath.ToSlash(os.TempDir()), "TempFilename should be in the system temp directory for "+tc.expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataType_IsSensitive(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
testCases := []struct {
|
||||
item DataType
|
||||
expected bool
|
||||
}{
|
||||
{ChromiumKey, true},
|
||||
{ChromiumPassword, true},
|
||||
{ChromiumBookmark, false},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
asserts.Equal(tc.expected, tc.item.IsSensitive(), fmt.Sprintf("IsSensitive for %v should be %v", tc.item, tc.expected))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterSensitiveItems(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
testCases := []struct {
|
||||
items []DataType
|
||||
expected int
|
||||
}{
|
||||
{[]DataType{ChromiumKey, ChromiumBookmark, ChromiumPassword}, 2},
|
||||
{[]DataType{ChromiumBookmark, ChromiumHistory}, 0},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
filteredItems := FilterSensitiveItems(tc.items)
|
||||
asserts.Len(filteredItems, tc.expected, "FilterSensitiveItems should return the correct number of sensitive items")
|
||||
for _, item := range filteredItems {
|
||||
asserts.True(item.IsSensitive(), "Filtered items should be sensitive")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i DataType) filename() string {
|
||||
switch i {
|
||||
case ChromiumKey:
|
||||
return fileChromiumKey
|
||||
case ChromiumPassword:
|
||||
return fileChromiumPassword
|
||||
case ChromiumCookie:
|
||||
return fileChromiumCookie
|
||||
case ChromiumBookmark:
|
||||
return fileChromiumBookmark
|
||||
case ChromiumDownload:
|
||||
return fileChromiumDownload
|
||||
case ChromiumLocalStorage:
|
||||
return fileChromiumLocalStorage
|
||||
case ChromiumSessionStorage:
|
||||
return fileChromiumSessionStorage
|
||||
case ChromiumCreditCard:
|
||||
return fileChromiumCredit
|
||||
case ChromiumExtension:
|
||||
return fileChromiumExtension
|
||||
case ChromiumHistory:
|
||||
return fileChromiumHistory
|
||||
case YandexPassword:
|
||||
return fileYandexPassword
|
||||
case YandexCreditCard:
|
||||
return fileYandexCredit
|
||||
case FirefoxKey4:
|
||||
return fileFirefoxKey4
|
||||
case FirefoxPassword:
|
||||
return fileFirefoxPassword
|
||||
case FirefoxCookie:
|
||||
return fileFirefoxCookie
|
||||
case FirefoxBookmark:
|
||||
return fileFirefoxData
|
||||
case FirefoxDownload:
|
||||
return fileFirefoxData
|
||||
case FirefoxLocalStorage:
|
||||
return fileFirefoxLocalStorage
|
||||
case FirefoxHistory:
|
||||
return fileFirefoxData
|
||||
case FirefoxExtension:
|
||||
return fileFirefoxExtension
|
||||
case FirefoxCreditCard:
|
||||
return UnsupportedItem
|
||||
default:
|
||||
return UnsupportedItem
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package byteutil
|
||||
|
||||
var OnSplitUTF8Func = func(r rune) rune {
|
||||
if r == 0x00 || r == 0x01 {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}
|
||||
@@ -6,9 +6,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
cp "github.com/otiai10/copy"
|
||||
)
|
||||
|
||||
// IsFileExists checks if the file exists in the provided path
|
||||
@@ -23,72 +20,6 @@ func IsFileExists(filename string) bool {
|
||||
return !info.IsDir()
|
||||
}
|
||||
|
||||
// IsDirExists checks if the folder exists
|
||||
func IsDirExists(folder string) bool {
|
||||
info, err := os.Stat(folder)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return info.IsDir()
|
||||
}
|
||||
|
||||
// ReadFile reads the file from the provided path
|
||||
func ReadFile(filename string) (string, error) {
|
||||
s, err := os.ReadFile(filename)
|
||||
return string(s), err
|
||||
}
|
||||
|
||||
// CopyDir copies the directory from the source to the destination
|
||||
// skip the file if you don't want to copy
|
||||
func CopyDir(src, dst, skip string) error {
|
||||
s := cp.Options{Skip: func(info os.FileInfo, src, dst string) (bool, error) {
|
||||
return strings.HasSuffix(strings.ToLower(src), skip), nil
|
||||
}}
|
||||
return cp.Copy(src, dst, s)
|
||||
}
|
||||
|
||||
// CopyFile copies the file from the source to the destination
|
||||
func CopyFile(src, dst string) error {
|
||||
s, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(dst, s, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filename returns the filename from the provided path
|
||||
func Filename(browser, dataType, ext string) string {
|
||||
replace := strings.NewReplacer(" ", "_", ".", "_", "-", "_")
|
||||
return strings.ToLower(fmt.Sprintf("%s_%s.%s", replace.Replace(browser), dataType, ext))
|
||||
}
|
||||
|
||||
func BrowserName(browser, user string) string {
|
||||
replace := strings.NewReplacer(" ", "_", ".", "_", "-", "_", "Profile", "user")
|
||||
return strings.ToLower(fmt.Sprintf("%s_%s", replace.Replace(browser), replace.Replace(user)))
|
||||
}
|
||||
|
||||
// ParentDir returns the parent directory of the provided path
|
||||
func ParentDir(p string) string {
|
||||
return filepath.Dir(filepath.Clean(p))
|
||||
}
|
||||
|
||||
// BaseDir returns the base directory of the provided path
|
||||
func BaseDir(p string) string {
|
||||
return filepath.Base(p)
|
||||
}
|
||||
|
||||
// ParentBaseDir returns the parent base directory of the provided path
|
||||
func ParentBaseDir(p string) string {
|
||||
return BaseDir(ParentDir(p))
|
||||
}
|
||||
|
||||
// CompressDir compresses the directory into a zip file
|
||||
func CompressDir(dir string) error {
|
||||
files, err := os.ReadDir(dir)
|
||||
|
||||
@@ -4,30 +4,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Keys returns a slice of the keys of the map. based with go 1.18 generics
|
||||
func Keys[K comparable, V any](m map[K]V) []K {
|
||||
r := make([]K, 0, len(m))
|
||||
for k := range m {
|
||||
r = append(r, k)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Signed is a constraint that permits any signed integer type.
|
||||
// If future releases of Go add new predeclared signed integer types,
|
||||
// this constraint will be modified to include them.
|
||||
type Signed interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64
|
||||
}
|
||||
|
||||
func IntToBool[T Signed](a T) bool {
|
||||
switch a {
|
||||
case 0, -1:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func Reverse[T any](s []T) []T {
|
||||
h := make([]T, len(s))
|
||||
for i := 0; i < len(s); i++ {
|
||||
|
||||
Reference in New Issue
Block a user