mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-07-04 21:37:47 +02:00
bf96ba8c80
* feat(restore): cross-platform restore via dump engine rebuild (#606) Restore previously required the dump's origin OS, overlaying keys onto locally-discovered browsers. It now rebuilds Chromium engines from the dump's vaults (v2 adds engine kind), so copied data or an archive zip decrypts on any OS. * fix(restore): polish help text, drop dead check, dedup dump kinds pflag treats backticked words in flag usage as the value placeholder, so --data-zip rendered as "--data-zip archive" in help output.
198 lines
5.8 KiB
Go
198 lines
5.8 KiB
Go
package browser
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/moond4rk/hackbrowserdata/log"
|
|
"github.com/moond4rk/hackbrowserdata/masterkey"
|
|
"github.com/moond4rk/hackbrowserdata/types"
|
|
)
|
|
|
|
// BuildDump exports one Vault per installation (Firefox/Safari, lacking KeyManager, are skipped).
|
|
// Partial results are kept — a Chrome 127+ profile mixes v10+v20, so a v20-only failure must not
|
|
// discard a usable v10 key.
|
|
func BuildDump(browsers []Browser) masterkey.Dump {
|
|
dump := masterkey.NewDump()
|
|
for _, b := range browsers {
|
|
km, ok := b.(KeyManager)
|
|
if !ok {
|
|
continue
|
|
}
|
|
mk, err := km.ExportKeys()
|
|
if err != nil {
|
|
status := "partial"
|
|
if !mk.HasAny() {
|
|
status = "failed"
|
|
}
|
|
log.Warnf("dump-keys: %s %s: %v", b.BrowserName(), status, err)
|
|
}
|
|
if !mk.HasAny() {
|
|
continue
|
|
}
|
|
kind, err := kindToDump(km.Kind())
|
|
if err != nil {
|
|
log.Warnf("dump-keys: %s: %v", b.BrowserName(), err)
|
|
continue
|
|
}
|
|
dump.Vaults = append(dump.Vaults, masterkey.Vault{
|
|
Browser: km.BrowserKey(),
|
|
Kind: kind,
|
|
UserDataDir: b.UserDataDir(),
|
|
Profiles: profileNames(b),
|
|
Keys: mk,
|
|
})
|
|
}
|
|
return dump
|
|
}
|
|
|
|
func profileNames(b Browser) []string {
|
|
profiles := b.Profiles()
|
|
names := make([]string, 0, len(profiles))
|
|
for _, p := range profiles {
|
|
names = append(names, p.Name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
// BuildFromDump reconstructs Chromium engines straight from a dump's vaults, rooted at copied data
|
|
// instead of the local platform table — this is what lets an analyst host decrypt a browser its OS
|
|
// never installs. filter is a browser key ("" or "all" = every vault); a filter matching no vault is
|
|
// an error rather than silent empty output.
|
|
//
|
|
// Data layout is resolved two ways. When dataDir holds per-key subdirs (the archive layout), each
|
|
// vault is rooted at dataDir/<key>. Otherwise dataDir is treated as one browser's User Data (a
|
|
// hand-copied folder), which is unambiguous only for a single vault — so filter must pick one.
|
|
func BuildFromDump(dump masterkey.Dump, dataDir, filter string) ([]Browser, error) {
|
|
filter = strings.ToLower(filter)
|
|
if filter == "all" {
|
|
filter = ""
|
|
}
|
|
|
|
var selected []masterkey.Vault
|
|
for _, v := range dump.Vaults {
|
|
if filter != "" && !strings.EqualFold(v.Browser, filter) {
|
|
continue
|
|
}
|
|
selected = append(selected, v)
|
|
}
|
|
if filter != "" && len(selected) == 0 {
|
|
return nil, fmt.Errorf("no vault for browser %q in keys (have: %s)", filter, vaultKeys(dump))
|
|
}
|
|
|
|
if !dirExists(dataDir) {
|
|
return nil, fmt.Errorf("data dir %q does not exist", dataDir)
|
|
}
|
|
|
|
archiveLayout := isArchiveLayout(dataDir, selected)
|
|
if !archiveLayout && len(selected) > 1 {
|
|
return nil, fmt.Errorf("--data-dir %q has no per-browser subdir but keys has %d browsers; "+
|
|
"point it at the archive root, or use -b <browser> for one browser's User Data (have: %s)",
|
|
dataDir, len(selected), vaultKeys(dump))
|
|
}
|
|
|
|
var browsers []Browser
|
|
for _, v := range selected {
|
|
root := dataDir
|
|
if archiveLayout {
|
|
root = filepath.Join(dataDir, strings.ToLower(v.Browser))
|
|
if !dirExists(root) {
|
|
log.Warnf("restore: %s has no data under %s, skipping", v.Browser, root)
|
|
continue
|
|
}
|
|
}
|
|
kind, err := kindFromDump(v.Kind)
|
|
if err != nil {
|
|
log.Warnf("restore: %s: %v", v.Browser, err)
|
|
continue
|
|
}
|
|
cfg := types.BrowserConfig{
|
|
Key: strings.ToLower(v.Browser),
|
|
Name: v.Browser,
|
|
Kind: kind,
|
|
UserDataDir: root,
|
|
}
|
|
b, err := newBrowser(cfg)
|
|
if err != nil {
|
|
log.Errorf("restore: build %s: %v", v.Browser, err)
|
|
continue
|
|
}
|
|
if b == nil {
|
|
log.Warnf("restore: %s found no profiles under %s", v.Browser, root)
|
|
continue
|
|
}
|
|
if km, ok := b.(KeyManager); ok {
|
|
km.SetRetrievers(retrieversFromKeys(v.Keys))
|
|
}
|
|
browsers = append(browsers, b)
|
|
}
|
|
return browsers, nil
|
|
}
|
|
|
|
// isArchiveLayout reports whether dataDir uses the archive layout — one per-browser subdir named by
|
|
// the vault key — rather than a raw single-browser User Data copy.
|
|
func isArchiveLayout(dataDir string, vaults []masterkey.Vault) bool {
|
|
for _, v := range vaults {
|
|
if dirExists(filepath.Join(dataDir, strings.ToLower(v.Browser))) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func vaultKeys(dump masterkey.Dump) string {
|
|
keys := make([]string, 0, len(dump.Vaults))
|
|
for _, v := range dump.Vaults {
|
|
keys = append(keys, strings.ToLower(v.Browser))
|
|
}
|
|
return strings.Join(keys, ", ")
|
|
}
|
|
|
|
func dirExists(path string) bool {
|
|
info, err := os.Stat(path)
|
|
return err == nil && info.IsDir()
|
|
}
|
|
|
|
// maybeStaticRetriever wraps non-empty key bytes as a StaticRetriever; an empty/nil key returns nil
|
|
// to preserve the "tier not applicable" signal NewMasterKeys expects.
|
|
func maybeStaticRetriever(key []byte) masterkey.Retriever {
|
|
if len(key) == 0 {
|
|
return nil
|
|
}
|
|
return masterkey.NewStaticRetriever(key)
|
|
}
|
|
|
|
// retrieversFromKeys maps a vault's per-tier key bytes to static retrievers; an absent tier stays nil
|
|
// so NewMasterKeys keeps treating it as "not applicable".
|
|
func retrieversFromKeys(mk masterkey.MasterKeys) masterkey.Retrievers {
|
|
return masterkey.Retrievers{
|
|
V10: maybeStaticRetriever(mk.V10),
|
|
V11: maybeStaticRetriever(mk.V11),
|
|
V20: maybeStaticRetriever(mk.V20),
|
|
}
|
|
}
|
|
|
|
// dumpableKinds are the engine kinds a vault may carry; kindToDump/kindFromDump translate to and from
|
|
// the wire form via BrowserKind.String(), keeping the vocabulary single-sourced in the types enum.
|
|
var dumpableKinds = []types.BrowserKind{types.Chromium, types.ChromiumYandex, types.ChromiumOpera}
|
|
|
|
func kindToDump(k types.BrowserKind) (string, error) {
|
|
for _, dk := range dumpableKinds {
|
|
if k == dk {
|
|
return k.String(), nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("engine kind %s is not exportable", k)
|
|
}
|
|
|
|
func kindFromDump(s string) (types.BrowserKind, error) {
|
|
for _, k := range dumpableKinds {
|
|
if k.String() == s {
|
|
return k, nil
|
|
}
|
|
}
|
|
return 0, fmt.Errorf("unknown engine kind %q", s)
|
|
}
|