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.
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),
|
||||
}
|
||||
}
|
||||
|
||||
func kindToDump(k types.BrowserKind) (string, error) {
|
||||
switch k {
|
||||
case types.Chromium, types.ChromiumYandex, types.ChromiumOpera:
|
||||
return k.String(), nil
|
||||
default:
|
||||
return "", fmt.Errorf("engine kind %s is not exportable", k)
|
||||
}
|
||||
}
|
||||
|
||||
// dumpableKinds are the engine kinds a vault may carry; kindFromDump reverses BrowserKind.String()
|
||||
// over exactly these, keeping the wire vocabulary single-sourced in the types enum.
|
||||
var dumpableKinds = []types.BrowserKind{types.Chromium, types.ChromiumYandex, types.ChromiumOpera}
|
||||
|
||||
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