Files
HackBrowserData/browser/keydump.go
T
Roger b901f7dff0 refactor(browser): split installation and profile abstractions (#603)
* refactor(browser): split installation and profile abstractions

A Chromium installation shares one master key across its profiles, but
modeling each profile as its own Browser re-derived the key per profile.
Browser now represents one installation holding its profiles and derives
the key once; new types.Profile/ExtractResult/CountResult carry per-profile
results.

* style: gofumpt safari_test.go

* test(chromium): rename shadowed loop var to path
2026-05-31 16:37:23 +08:00

107 lines
3.5 KiB
Go

package browser
import (
"runtime"
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
"github.com/moond4rk/hackbrowserdata/log"
)
// BuildDump exports per-installation master keys. Each Browser is one installation,
// so this is a straight one-Vault-per-installation map: ExportKeys is invoked once
// per installation. Installations without KeyManager (Firefox/Safari) are skipped.
// Partial results (e.g. V10 retrieved, V20 failed) keep the usable tiers rather than
// discarding the vault — a Chrome 127+ profile mixes v10 + v20 ciphertexts and a
// v20-only failure must not erase a usable v10 key.
func BuildDump(browsers []Browser) keyretriever.Dump {
dump := keyretriever.NewDump()
for _, b := range browsers {
km, ok := b.(KeyManager)
if !ok {
continue
}
keys, err := km.ExportKeys()
if err != nil {
status := "partial"
if !keys.HasAny() {
status = "failed"
}
log.Warnf("dump-keys: %s %s: %v", b.BrowserName(), status, err)
}
if !keys.HasAny() {
continue
}
dump.Vaults = append(dump.Vaults, keyretriever.Vault{
Browser: b.BrowserName(),
UserDataDir: b.UserDataDir(),
Profiles: profileNames(b),
Keys: keys,
})
}
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
}
// ApplyDump installs master keys from dump onto matching installations, replacing
// each installation's default platform-native retrievers with StaticProviders
// backed by the Dump's bytes. Matching is by (BrowserName, UserDataDir) — the same
// key BuildDump emits. When exact match fails (commonly a cross-host path mismatch:
// Windows backslash vs POSIX, or a relocated User Data dir via -p), falls back to
// the sole vault for that browser name when one exists. Installations without a
// matching vault are warned and left untouched; non-KeyManager installations
// (Firefox/Safari) are skipped silently.
func ApplyDump(browsers []Browser, dump keyretriever.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)
}
vaultIndex := make(map[string]*keyretriever.Vault, len(dump.Vaults))
vaultsByBrowser := make(map[string][]*keyretriever.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 {
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
}
}
if !found {
log.Warnf("apply-keys: %s no matching vault in dump", b.BrowserName())
continue
}
km.SetKeyRetrievers(keyretriever.Retrievers{
V10: maybeStaticProvider(v.Keys.V10),
V11: maybeStaticProvider(v.Keys.V11),
V20: maybeStaticProvider(v.Keys.V20),
})
}
}
// maybeStaticProvider wraps non-empty key bytes as a StaticProvider; an empty/nil key returns nil
// to preserve the "tier not applicable" signal NewMasterKeys expects.
func maybeStaticProvider(key []byte) keyretriever.KeyRetriever {
if len(key) == 0 {
return nil
}
return keyretriever.NewStaticProvider(key)
}