mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-07-04 21:37:47 +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
|
package chromium
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"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/log"
|
||||||
"github.com/moond4rk/hackbrowserdata/types"
|
"github.com/moond4rk/hackbrowserdata/types"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
|
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Chromium struct {
|
// Browser represents a single Chromium profile ready for extraction.
|
||||||
name string
|
type Browser struct {
|
||||||
storage string
|
cfg types.BrowserConfig
|
||||||
profilePath string
|
profileDir string // absolute path to profile directory
|
||||||
masterKey []byte
|
sources map[types.Category][]sourcePath // Category → candidate paths (priority order)
|
||||||
dataTypes []types.DataType
|
extractors map[types.Category]categoryExtractor // Category → custom extract function override
|
||||||
Paths map[types.DataType]string
|
sourcePaths map[types.Category]resolvedPath // Category → discovered absolute path
|
||||||
}
|
}
|
||||||
|
|
||||||
// New create instance of Chromium browser, fill item's path if item is existed.
|
// NewBrowsers discovers Chromium profiles under cfg.UserDataDir and returns
|
||||||
func New(name, storage, profilePath string, dataTypes []types.DataType) ([]*Chromium, error) {
|
// one Browser per profile. Uses ReadDir to find profile directories,
|
||||||
c := &Chromium{
|
// then Stat to check which data sources exist in each profile.
|
||||||
name: name,
|
func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
|
||||||
storage: storage,
|
sources := sourcesForKind(cfg.Kind)
|
||||||
profilePath: profilePath,
|
extractors := extractorsForKind(cfg.Kind)
|
||||||
dataTypes: dataTypes,
|
|
||||||
|
profileDirs := discoverProfiles(cfg.UserDataDir, sources)
|
||||||
|
if len(profileDirs) == 0 {
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
multiDataTypePaths, err := c.userDataTypePaths(c.profilePath, c.dataTypes)
|
|
||||||
if err != nil {
|
var browsers []*Browser
|
||||||
return nil, err
|
for _, profileDir := range profileDirs {
|
||||||
}
|
sourcePaths := resolveSourcePaths(sources, profileDir)
|
||||||
chromiumList := make([]*Chromium, 0, len(multiDataTypePaths))
|
if len(sourcePaths) == 0 {
|
||||||
for user, itemPaths := range multiDataTypePaths {
|
continue
|
||||||
chromiumList = append(chromiumList, &Chromium{
|
}
|
||||||
name: fileutil.BrowserName(name, user),
|
browsers = append(browsers, &Browser{
|
||||||
dataTypes: typeutil.Keys(itemPaths),
|
cfg: cfg,
|
||||||
Paths: itemPaths,
|
profileDir: profileDir,
|
||||||
storage: storage,
|
sources: sources,
|
||||||
|
extractors: extractors,
|
||||||
|
sourcePaths: sourcePaths,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return chromiumList, nil
|
return browsers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chromium) Name() string {
|
func (b *Browser) BrowserName() string { return b.cfg.Name }
|
||||||
return c.name
|
func (b *Browser) ProfileName() string {
|
||||||
|
if b.profileDir == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return filepath.Base(b.profileDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chromium) BrowsingData(isFullExport bool) (*browserdata.BrowserData, error) {
|
// Extract copies browser files to a temp directory, retrieves the master key,
|
||||||
// delete chromiumKey from dataTypes, doesn't need to export key
|
// and extracts data for the requested categories.
|
||||||
var dataTypes []types.DataType
|
func (b *Browser) Extract(categories []types.Category) (*types.BrowserData, error) {
|
||||||
for _, dt := range c.dataTypes {
|
session, err := filemanager.NewSession()
|
||||||
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()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer session.Cleanup()
|
||||||
|
|
||||||
c.masterKey = masterKey
|
tempPaths := b.acquireFiles(session, categories)
|
||||||
if err := data.Recovery(c.masterKey); err != nil {
|
|
||||||
return nil, err
|
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
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chromium) copyItemToLocal() error {
|
// acquireFiles copies source files to the session temp directory.
|
||||||
for i, path := range c.Paths {
|
func (b *Browser) acquireFiles(session *filemanager.Session, categories []types.Category) map[types.Category]string {
|
||||||
filename := i.TempFilename()
|
tempPaths := make(map[types.Category]string)
|
||||||
var err error
|
for _, cat := range categories {
|
||||||
switch {
|
rp, ok := b.sourcePaths[cat]
|
||||||
case fileutil.IsDirExists(path):
|
if !ok {
|
||||||
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)
|
|
||||||
continue
|
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]
|
// getMasterKey retrieves the Chromium master encryption key.
|
||||||
func (c *Chromium) userDataTypePaths(profilePath string, items []types.DataType) (map[string]map[types.DataType]string, error) {
|
//
|
||||||
multiItemPaths := make(map[string]map[types.DataType]string)
|
// On Windows, the key is read from the Local State file and decrypted via DPAPI.
|
||||||
parentDir := fileutil.ParentDir(profilePath)
|
// On macOS, the key is derived from Keychain (Local State is not needed).
|
||||||
err := filepath.Walk(parentDir, chromiumWalkFunc(items, multiItemPaths))
|
// On Linux, the key is derived from D-Bus Secret Service or a fallback password.
|
||||||
if err != nil {
|
//
|
||||||
return nil, err
|
// 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
|
retriever := keyretriever.DefaultRetriever(b.cfg.KeychainPassword)
|
||||||
for userDir, profiles := range multiItemPaths {
|
return retriever.RetrieveKey(b.cfg.Storage, localStateDst)
|
||||||
for _, profile := range profiles {
|
}
|
||||||
if strings.HasSuffix(profile, types.ChromiumKey.Filename()) {
|
|
||||||
keyPath = profile
|
// extractCategory calls the appropriate extract function for a category.
|
||||||
dir = userDir
|
// 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
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t := make(map[string]map[types.DataType]string)
|
return resolved
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// chromiumWalkFunc return a filepath.WalkFunc to find item's path
|
// isSkippedDir returns true for directory names that should never be
|
||||||
func chromiumWalkFunc(items []types.DataType, multiItemPaths map[string]map[types.DataType]string) filepath.WalkFunc {
|
// treated as browser profiles.
|
||||||
return func(path string, info fs.FileInfo, err error) error {
|
func isSkippedDir(name string) bool {
|
||||||
if err != nil {
|
switch name {
|
||||||
if os.IsPermission(err) {
|
case "System Profile", "Guest Profile", "Snapshot":
|
||||||
log.Warnf("skipping walk chromium path permission error, path %s, err %v", path, err)
|
return true
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
_ "modernc.org/sqlite" // sqlite3 driver TODO: replace with chooseable driver
|
"github.com/moond4rk/hackbrowserdata/filemanager"
|
||||||
|
|
||||||
"github.com/moond4rk/hackbrowserdata/browserdata"
|
|
||||||
"github.com/moond4rk/hackbrowserdata/log"
|
"github.com/moond4rk/hackbrowserdata/log"
|
||||||
"github.com/moond4rk/hackbrowserdata/types"
|
"github.com/moond4rk/hackbrowserdata/types"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
|
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Firefox struct {
|
// Browser represents a single Firefox profile ready for extraction.
|
||||||
name string
|
type Browser struct {
|
||||||
storage string
|
cfg types.BrowserConfig
|
||||||
profilePath string
|
profileDir string // absolute path to profile directory
|
||||||
masterKey []byte
|
sources map[types.Category][]sourcePath // Category → candidate paths (priority order)
|
||||||
items []types.DataType
|
sourcePaths map[types.Category]resolvedPath // Category → discovered absolute path
|
||||||
itemPaths map[types.DataType]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
var browsers []*Browser
|
||||||
func New(profilePath string, items []types.DataType) ([]*Firefox, error) {
|
for _, profileDir := range profileDirs {
|
||||||
multiItemPaths := make(map[string]map[types.DataType]string)
|
sourcePaths := resolveSourcePaths(firefoxSources, profileDir)
|
||||||
// ignore walk dir error since it can be produced by a single entry
|
if len(sourcePaths) == 0 {
|
||||||
_ = filepath.WalkDir(profilePath, firefoxWalkFunc(items, multiItemPaths))
|
continue
|
||||||
|
}
|
||||||
firefoxList := make([]*Firefox, 0, len(multiItemPaths))
|
browsers = append(browsers, &Browser{
|
||||||
for name, itemPaths := range multiItemPaths {
|
cfg: cfg,
|
||||||
firefoxList = append(firefoxList, &Firefox{
|
profileDir: profileDir,
|
||||||
name: fmt.Sprintf("firefox-%s", name),
|
sources: firefoxSources,
|
||||||
items: typeutil.Keys(itemPaths),
|
sourcePaths: sourcePaths,
|
||||||
itemPaths: itemPaths,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return browsers, nil
|
||||||
return firefoxList, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Firefox) copyItemToLocal() error {
|
func (b *Browser) BrowserName() string { return b.cfg.Name }
|
||||||
for i, path := range f.itemPaths {
|
func (b *Browser) ProfileName() string {
|
||||||
filename := i.TempFilename()
|
if b.profileDir == "" {
|
||||||
if err := fileutil.CopyFile(path, filename); err != nil {
|
return ""
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return filepath.Base(b.profileDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
func firefoxWalkFunc(items []types.DataType, multiItemPaths map[string]map[types.DataType]string) fs.WalkDirFunc {
|
// Extract copies browser files to a temp directory, retrieves the master key,
|
||||||
return func(path string, info fs.DirEntry, err error) error {
|
// and extracts data for the requested categories.
|
||||||
if err != nil {
|
func (b *Browser) Extract(categories []types.Category) (*types.BrowserData, error) {
|
||||||
if os.IsPermission(err) {
|
session, err := filemanager.NewSession()
|
||||||
log.Warnf("skipping walk firefox path %s permission error: %v", path, err)
|
if err != nil {
|
||||||
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 {
|
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
f.masterKey = masterKey
|
keys, err := k4.deriveKeys()
|
||||||
if err := data.Recovery(f.masterKey); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
go 1.20
|
||||||
|
|
||||||
require (
|
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/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/otiai10/copy v1.14.1
|
||||||
github.com/ppacher/go-dbus-keyring v1.0.1
|
github.com/ppacher/go-dbus-keyring v1.0.1
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
@@ -14,7 +12,6 @@ require (
|
|||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
github.com/urfave/cli/v2 v2.27.7
|
github.com/urfave/cli/v2 v2.27.7
|
||||||
golang.org/x/sys v0.27.0
|
golang.org/x/sys v0.27.0
|
||||||
golang.org/x/text v0.19.0
|
|
||||||
modernc.org/sqlite v1.31.1
|
modernc.org/sqlite v1.31.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,6 +32,8 @@ require (
|
|||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
|
golang.org/x/text v0.19.0 // indirect
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||||
modernc.org/libc v1.55.3 // 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 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@@ -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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
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/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.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
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/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/moond4rk/keychainbreaker v0.1.0 h1:9hkE70c4jxaTHStZ3kny4GEJ/srcvt2DZe0vUg3m8V0=
|
github.com/moond4rk/keychainbreaker v0.2.5 h1:1f2qmgpt1sl+mXA8DTW9nnVhzo4oGO08bnkXu70DL04=
|
||||||
github.com/moond4rk/keychainbreaker v0.1.0/go.mod h1:VVx2VXwL2EGhuU2WBD67w66JCKKqLFXGJg91y3FY4f0=
|
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 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
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=
|
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 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
|
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
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 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
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 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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=
|
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"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
cp "github.com/otiai10/copy"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsFileExists checks if the file exists in the provided path
|
// IsFileExists checks if the file exists in the provided path
|
||||||
@@ -23,72 +20,6 @@ func IsFileExists(filename string) bool {
|
|||||||
return !info.IsDir()
|
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
|
// CompressDir compresses the directory into a zip file
|
||||||
func CompressDir(dir string) error {
|
func CompressDir(dir string) error {
|
||||||
files, err := os.ReadDir(dir)
|
files, err := os.ReadDir(dir)
|
||||||
|
|||||||
@@ -4,30 +4,6 @@ import (
|
|||||||
"time"
|
"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 {
|
func Reverse[T any](s []T) []T {
|
||||||
h := make([]T, len(s))
|
h := make([]T, len(s))
|
||||||
for i := 0; i < len(s); i++ {
|
for i := 0; i < len(s); i++ {
|
||||||
|
|||||||
Reference in New Issue
Block a user