mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-07-04 21:37:47 +02:00
* 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.
This commit is contained in:
+132
-34
@@ -1,10 +1,14 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"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).
|
||||
@@ -28,8 +32,14 @@ func BuildDump(browsers []Browser) masterkey.Dump {
|
||||
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: b.BrowserName(),
|
||||
Browser: km.BrowserKey(),
|
||||
Kind: kind,
|
||||
UserDataDir: b.UserDataDir(),
|
||||
Profiles: profileNames(b),
|
||||
Keys: mk,
|
||||
@@ -47,46 +57,102 @@ func profileNames(b Browser) []string {
|
||||
return names
|
||||
}
|
||||
|
||||
// ApplyDump overlays StaticRetrievers from dump onto matching installations (Firefox/Safari skipped).
|
||||
// Match is by (BrowserName, UserDataDir); on miss — commonly a cross-host path mismatch (Windows vs
|
||||
// POSIX, or a relocated dir via -p) — it falls back to the sole vault for that browser name. No match
|
||||
// → warn and leave the platform retrievers in place.
|
||||
func ApplyDump(browsers []Browser, dump masterkey.Dump) {
|
||||
if dump.Host.OS != "" && dump.Host.OS != runtime.GOOS {
|
||||
log.Infof("apply-keys: dump created on %s/%s; current host is %s/%s",
|
||||
dump.Host.OS, dump.Host.Arch, runtime.GOOS, runtime.GOARCH)
|
||||
// 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 = ""
|
||||
}
|
||||
vaultIndex := make(map[string]*masterkey.Vault, len(dump.Vaults))
|
||||
vaultsByBrowser := make(map[string][]*masterkey.Vault)
|
||||
for i := range dump.Vaults {
|
||||
v := &dump.Vaults[i]
|
||||
vaultIndex[v.Browser+"|"+v.UserDataDir] = v
|
||||
vaultsByBrowser[v.Browser] = append(vaultsByBrowser[v.Browser], v)
|
||||
}
|
||||
for _, b := range browsers {
|
||||
km, ok := b.(KeyManager)
|
||||
if !ok {
|
||||
|
||||
var selected []masterkey.Vault
|
||||
for _, v := range dump.Vaults {
|
||||
if filter != "" && !strings.EqualFold(v.Browser, filter) {
|
||||
continue
|
||||
}
|
||||
v, found := vaultIndex[b.BrowserName()+"|"+b.UserDataDir()]
|
||||
if !found {
|
||||
if candidates := vaultsByBrowser[b.BrowserName()]; len(candidates) == 1 {
|
||||
v = candidates[0]
|
||||
log.Infof("apply-keys: %s using sole vault for browser (dump path %q != local %q)",
|
||||
b.BrowserName(), v.UserDataDir, b.UserDataDir())
|
||||
found = true
|
||||
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
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
log.Warnf("apply-keys: %s no matching vault in dump", b.BrowserName())
|
||||
kind, err := kindFromDump(v.Kind)
|
||||
if err != nil {
|
||||
log.Warnf("restore: %s: %v", v.Browser, err)
|
||||
continue
|
||||
}
|
||||
km.SetRetrievers(masterkey.Retrievers{
|
||||
V10: maybeStaticRetriever(v.Keys.V10),
|
||||
V11: maybeStaticRetriever(v.Keys.V11),
|
||||
V20: maybeStaticRetriever(v.Keys.V20),
|
||||
})
|
||||
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
|
||||
@@ -97,3 +163,35 @@ func maybeStaticRetriever(key []byte) masterkey.Retriever {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user