mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-06-06 19:53:53 +02:00
feat(keys): add --keys flag to dump for cross-host decryption (#600)
* feat(keys): add --keys flag to dump for cross-host decryption Consumer side of the cross-host key workflow (pairs with #599). ApplyDump wires StaticProviders from a dump.json into matching browsers, so dump --keys f.json -p /copied/data decrypts without native retrievers. * fix(keys): guard --keys against misuse + hint Safari Without -p, dumped keys would be applied to local profile data and decrypt to garbage; -b all hits the same path because pickFromConfigs ignores -p when name == "all". Require both. * chore(keys): address PR #600 Copilot review - example: add -b chrome (without it, --keys + default -b all errors out) - use %q for keysPath in error wrap, matching surrounding style
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
package browser
|
package browser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||||
"github.com/moond4rk/hackbrowserdata/log"
|
"github.com/moond4rk/hackbrowserdata/log"
|
||||||
)
|
)
|
||||||
@@ -69,3 +71,56 @@ func groupByInstallation(browsers []Browser) (map[string]*installGroup, []string
|
|||||||
}
|
}
|
||||||
return groups, order
|
return groups, order
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ApplyDump installs master keys from dump onto matching browsers, replacing each browser's default
|
||||||
|
// platform-native retrievers with StaticProviders backed by the Dump's bytes. Matching is by
|
||||||
|
// (BrowserName, UserDataDir) — the same key BuildDump groups by. 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. Browsers without a matching vault
|
||||||
|
// are warned and left untouched; non-KeyManager browsers (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/%s using sole vault for browser (dump path %q != local %q)",
|
||||||
|
b.BrowserName(), b.ProfileName(), v.UserDataDir, b.UserDataDir())
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
log.Warnf("apply-keys: %s/%s no matching vault in dump", b.BrowserName(), b.ProfileName())
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|||||||
+128
-4
@@ -36,12 +36,15 @@ func (m *mockBrowser) CountEntries(_ []types.Category) (map[types.Category]int,
|
|||||||
|
|
||||||
type mockChromiumBrowser struct {
|
type mockChromiumBrowser struct {
|
||||||
mockBrowser
|
mockBrowser
|
||||||
keys keyretriever.MasterKeys
|
keys keyretriever.MasterKeys
|
||||||
exportErr error
|
exportErr error
|
||||||
calls int
|
calls int
|
||||||
|
receivedRetrievers keyretriever.Retrievers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockChromiumBrowser) SetKeyRetrievers(_ keyretriever.Retrievers) {}
|
func (m *mockChromiumBrowser) SetKeyRetrievers(r keyretriever.Retrievers) {
|
||||||
|
m.receivedRetrievers = r
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockChromiumBrowser) ExportKeys() (keyretriever.MasterKeys, error) {
|
func (m *mockChromiumBrowser) ExportKeys() (keyretriever.MasterKeys, error) {
|
||||||
m.calls++
|
m.calls++
|
||||||
@@ -196,6 +199,127 @@ func TestBuildDump_PartialKeys(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyDump_Match(t *testing.T) {
|
||||||
|
b := &mockChromiumBrowser{
|
||||||
|
mockBrowser: mockBrowser{name: chromeName, profile: testProfileDefault, userDataDir: testUDD},
|
||||||
|
}
|
||||||
|
dump := keyretriever.Dump{
|
||||||
|
Vaults: []keyretriever.Vault{
|
||||||
|
{Browser: chromeName, UserDataDir: testUDD, Keys: keyretriever.MasterKeys{V10: []byte("v10-from-dump")}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ApplyDump([]Browser{b}, dump)
|
||||||
|
|
||||||
|
if b.receivedRetrievers.V10 == nil {
|
||||||
|
t.Fatal("V10 retriever should be set from matching vault")
|
||||||
|
}
|
||||||
|
got, err := b.receivedRetrievers.V10.RetrieveKey(keyretriever.Hints{})
|
||||||
|
if err != nil || string(got) != "v10-from-dump" {
|
||||||
|
t.Errorf("V10.RetrieveKey() = %q, err = %v, want %q", got, err, "v10-from-dump")
|
||||||
|
}
|
||||||
|
if b.receivedRetrievers.V11 != nil {
|
||||||
|
t.Errorf("V11 should be nil (tier not in dump), got %v", b.receivedRetrievers.V11)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyDump_MissingVault(t *testing.T) {
|
||||||
|
b := &mockChromiumBrowser{
|
||||||
|
mockBrowser: mockBrowser{name: chromeName, profile: testProfileDefault, userDataDir: testUDD},
|
||||||
|
}
|
||||||
|
dump := keyretriever.Dump{
|
||||||
|
Vaults: []keyretriever.Vault{
|
||||||
|
{Browser: testEdgeName, UserDataDir: "/edge", Keys: keyretriever.MasterKeys{V10: []byte("v10")}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ApplyDump([]Browser{b}, dump)
|
||||||
|
|
||||||
|
if b.receivedRetrievers.V10 != nil {
|
||||||
|
t.Errorf("V10 should remain nil when no matching vault, got %v", b.receivedRetrievers.V10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyDump_NonKeyManagerSkipped(t *testing.T) {
|
||||||
|
firefox := &mockBrowser{name: firefoxName, profile: "default-release", userDataDir: "/ff"}
|
||||||
|
dump := keyretriever.Dump{
|
||||||
|
Vaults: []keyretriever.Vault{
|
||||||
|
{Browser: firefoxName, UserDataDir: "/ff", Keys: keyretriever.MasterKeys{V10: []byte("v10")}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// firefox does not implement KeyManager; ApplyDump must not panic and must not attempt injection.
|
||||||
|
ApplyDump([]Browser{firefox}, dump)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyDump_RoundTrip(t *testing.T) {
|
||||||
|
src := &mockChromiumBrowser{
|
||||||
|
mockBrowser: mockBrowser{name: chromeName, profile: testProfileDefault, userDataDir: testUDD},
|
||||||
|
keys: keyretriever.MasterKeys{V10: []byte("v10-rt"), V20: []byte("v20-rt")},
|
||||||
|
}
|
||||||
|
dump := BuildDump([]Browser{src})
|
||||||
|
|
||||||
|
dst := &mockChromiumBrowser{
|
||||||
|
mockBrowser: mockBrowser{name: chromeName, profile: testProfileDefault, userDataDir: testUDD},
|
||||||
|
}
|
||||||
|
ApplyDump([]Browser{dst}, dump)
|
||||||
|
|
||||||
|
v10, _ := dst.receivedRetrievers.V10.RetrieveKey(keyretriever.Hints{})
|
||||||
|
if string(v10) != "v10-rt" {
|
||||||
|
t.Errorf("V10 round-trip: got %q, want v10-rt", v10)
|
||||||
|
}
|
||||||
|
v20, _ := dst.receivedRetrievers.V20.RetrieveKey(keyretriever.Hints{})
|
||||||
|
if string(v20) != "v20-rt" {
|
||||||
|
t.Errorf("V20 round-trip: got %q, want v20-rt", v20)
|
||||||
|
}
|
||||||
|
if dst.receivedRetrievers.V11 != nil {
|
||||||
|
t.Errorf("V11 should be nil (not in source keys), got %v", dst.receivedRetrievers.V11)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyDump_FallbackOnPathMismatch(t *testing.T) {
|
||||||
|
// Cross-host scenario: dump was created on Windows but is applied on Linux/macOS where the
|
||||||
|
// UserDataDir literally differs. With a single vault for the browser, ApplyDump should still
|
||||||
|
// inject — otherwise the primary cross-host use case fails silently.
|
||||||
|
b := &mockChromiumBrowser{
|
||||||
|
mockBrowser: mockBrowser{name: chromeName, profile: testProfileDefault, userDataDir: "/local/chrome"},
|
||||||
|
}
|
||||||
|
dump := keyretriever.Dump{
|
||||||
|
Vaults: []keyretriever.Vault{
|
||||||
|
{
|
||||||
|
Browser: chromeName,
|
||||||
|
UserDataDir: `C:\Users\foo\AppData\Local\Google\Chrome\User Data`,
|
||||||
|
Keys: keyretriever.MasterKeys{V10: []byte("v10-fallback")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ApplyDump([]Browser{b}, dump)
|
||||||
|
|
||||||
|
if b.receivedRetrievers.V10 == nil {
|
||||||
|
t.Fatal("V10 retriever should be set via single-vault fallback")
|
||||||
|
}
|
||||||
|
got, err := b.receivedRetrievers.V10.RetrieveKey(keyretriever.Hints{})
|
||||||
|
if err != nil || string(got) != "v10-fallback" {
|
||||||
|
t.Errorf("V10.RetrieveKey() = %q, err = %v, want %q", got, err, "v10-fallback")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyDump_NoFallbackWhenAmbiguous(t *testing.T) {
|
||||||
|
// Two Chrome vaults in the dump and no exact path match — ApplyDump must not guess which
|
||||||
|
// installation the local browser corresponds to.
|
||||||
|
b := &mockChromiumBrowser{
|
||||||
|
mockBrowser: mockBrowser{name: chromeName, profile: testProfileDefault, userDataDir: "/local/chrome"},
|
||||||
|
}
|
||||||
|
dump := keyretriever.Dump{
|
||||||
|
Vaults: []keyretriever.Vault{
|
||||||
|
{Browser: chromeName, UserDataDir: "/path/a", Keys: keyretriever.MasterKeys{V10: []byte("a")}},
|
||||||
|
{Browser: chromeName, UserDataDir: "/path/b", Keys: keyretriever.MasterKeys{V10: []byte("b")}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ApplyDump([]Browser{b}, dump)
|
||||||
|
|
||||||
|
if b.receivedRetrievers.V10 != nil {
|
||||||
|
t.Errorf("V10 should remain nil when fallback is ambiguous, got %v", b.receivedRetrievers.V10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildDump_GroupingOrderIndependent(t *testing.T) {
|
func TestBuildDump_GroupingOrderIndependent(t *testing.T) {
|
||||||
for _, name := range []string{"p1 first", "p2 first"} {
|
for _, name := range []string{"p1 first", "p2 first"} {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/moond4rk/hackbrowserdata/browser"
|
"github.com/moond4rk/hackbrowserdata/browser"
|
||||||
|
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||||
"github.com/moond4rk/hackbrowserdata/log"
|
"github.com/moond4rk/hackbrowserdata/log"
|
||||||
"github.com/moond4rk/hackbrowserdata/output"
|
"github.com/moond4rk/hackbrowserdata/output"
|
||||||
"github.com/moond4rk/hackbrowserdata/types"
|
"github.com/moond4rk/hackbrowserdata/types"
|
||||||
@@ -22,6 +24,7 @@ func dumpCmd() *cobra.Command {
|
|||||||
outputDir string
|
outputDir string
|
||||||
profilePath string
|
profilePath string
|
||||||
keychainPw string
|
keychainPw string
|
||||||
|
keysPath string
|
||||||
compress bool
|
compress bool
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,13 +35,10 @@ func dumpCmd() *cobra.Command {
|
|||||||
hack-browser-data dump -b chrome -c password,cookie
|
hack-browser-data dump -b chrome -c password,cookie
|
||||||
hack-browser-data dump -b chrome -f json -d output
|
hack-browser-data dump -b chrome -f json -d output
|
||||||
hack-browser-data dump -f cookie-editor
|
hack-browser-data dump -f cookie-editor
|
||||||
|
hack-browser-data dump --keys dump.json -b chrome -p /path/to/copied/User\ Data
|
||||||
hack-browser-data dump --zip`,
|
hack-browser-data dump --zip`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
browsers, err := browser.PickBrowsers(browser.PickOptions{
|
browsers, err := selectBrowsers(browserName, profilePath, keychainPw, keysPath)
|
||||||
Name: browserName,
|
|
||||||
ProfilePath: profilePath,
|
|
||||||
KeychainPassword: keychainPw,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -86,11 +86,69 @@ func dumpCmd() *cobra.Command {
|
|||||||
cmd.Flags().StringVarP(&outputDir, "dir", "d", "results", "output directory")
|
cmd.Flags().StringVarP(&outputDir, "dir", "d", "results", "output directory")
|
||||||
cmd.Flags().StringVarP(&profilePath, "profile-path", "p", "", "custom profile dir path, get with chrome://version")
|
cmd.Flags().StringVarP(&profilePath, "profile-path", "p", "", "custom profile dir path, get with chrome://version")
|
||||||
cmd.Flags().StringVar(&keychainPw, "keychain-pw", "", "macOS keychain password")
|
cmd.Flags().StringVar(&keychainPw, "keychain-pw", "", "macOS keychain password")
|
||||||
|
cmd.Flags().StringVar(&keysPath, "keys", "", "import master keys from JSON file (from `keys export`), skipping platform retrieval")
|
||||||
cmd.Flags().BoolVar(&compress, "zip", false, "compress output to zip")
|
cmd.Flags().BoolVar(&compress, "zip", false, "compress output to zip")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// selectBrowsers returns wired-up browsers for either platform-native key retrieval (default) or
|
||||||
|
// dump-based key injection (when keysPath is non-empty). The dump path uses DiscoverBrowsers so it
|
||||||
|
// never triggers a keychain prompt or platform retrievers.
|
||||||
|
func selectBrowsers(browserName, profilePath, keychainPw, keysPath string) ([]browser.Browser, error) {
|
||||||
|
if keysPath == "" {
|
||||||
|
return browser.PickBrowsers(browser.PickOptions{
|
||||||
|
Name: browserName,
|
||||||
|
ProfilePath: profilePath,
|
||||||
|
KeychainPassword: keychainPw,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require -p and a single -b to prevent dumped keys from being applied to local profile data,
|
||||||
|
// which would decrypt to garbage. -b all is rejected because pickFromConfigs ignores -p in that case.
|
||||||
|
if profilePath == "" {
|
||||||
|
return nil, fmt.Errorf("--keys requires -p <copied-profile-dir>")
|
||||||
|
}
|
||||||
|
name := strings.ToLower(browserName)
|
||||||
|
if name == "" || name == "all" {
|
||||||
|
return nil, fmt.Errorf(`--keys requires -b <browser> (single, not "all")`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if keychainPw != "" {
|
||||||
|
log.Warnf("--keychain-pw is ignored when --keys is set")
|
||||||
|
}
|
||||||
|
|
||||||
|
browsers, err := browser.DiscoverBrowsers(browser.PickOptions{
|
||||||
|
Name: browserName,
|
||||||
|
ProfilePath: profilePath,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(keysPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open keys file %q: %w", keysPath, err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
dump, err := keyretriever.ReadJSON(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read keys file %q: %w", keysPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
browser.ApplyDump(browsers, dump)
|
||||||
|
|
||||||
|
for _, b := range browsers {
|
||||||
|
if _, ok := b.(browser.KeychainPasswordReceiver); ok {
|
||||||
|
log.Infof("Safari has no portable master key; run `dump -b safari` separately for full extraction")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return browsers, nil
|
||||||
|
}
|
||||||
|
|
||||||
// parseCategories converts a comma-separated string into a Category slice.
|
// parseCategories converts a comma-separated string into a Category slice.
|
||||||
// "all" returns all categories.
|
// "all" returns all categories.
|
||||||
func parseCategories(s string) ([]types.Category, error) {
|
func parseCategories(s string) ([]types.Category, error) {
|
||||||
|
|||||||
@@ -69,12 +69,16 @@ func (d Dump) WriteJSON(w io.Writer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadJSON parses a Dump from r.
|
// ReadJSON parses a Dump from r and rejects schema versions this build cannot interpret —
|
||||||
|
// silent misparse of a future v2 schema is worse than a clear error.
|
||||||
func ReadJSON(r io.Reader) (Dump, error) {
|
func ReadJSON(r io.Reader) (Dump, error) {
|
||||||
var d Dump
|
var d Dump
|
||||||
dec := json.NewDecoder(r)
|
dec := json.NewDecoder(r)
|
||||||
if err := dec.Decode(&d); err != nil {
|
if err := dec.Decode(&d); err != nil {
|
||||||
return Dump{}, fmt.Errorf("decode dump: %w", err)
|
return Dump{}, fmt.Errorf("decode dump: %w", err)
|
||||||
}
|
}
|
||||||
|
if d.Version != DumpVersion {
|
||||||
|
return Dump{}, fmt.Errorf("unsupported dump version %q (this build expects %q)", d.Version, DumpVersion)
|
||||||
|
}
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package keyretriever
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadJSON_RejectsUnknownVersion(t *testing.T) {
|
||||||
|
input := bytes.NewBufferString(`{"version":"99","created_at":"2026-05-16T00:00:00Z","host":{"os":"linux","arch":"amd64"},"vaults":[]}`)
|
||||||
|
_, err := ReadJSON(input)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("ReadJSON should reject unknown version, got nil error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "unsupported dump version") {
|
||||||
|
t.Errorf("error should mention unsupported version, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadJSON_RejectsMissingVersion(t *testing.T) {
|
||||||
|
input := bytes.NewBufferString(`{"created_at":"2026-05-16T00:00:00Z","host":{"os":"linux","arch":"amd64"},"vaults":[]}`)
|
||||||
|
_, err := ReadJSON(input)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("ReadJSON should reject empty version, got nil error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadJSON_AcceptsCurrentVersion(t *testing.T) {
|
||||||
|
d := NewDump()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := d.WriteJSON(&buf); err != nil {
|
||||||
|
t.Fatalf("WriteJSON: %v", err)
|
||||||
|
}
|
||||||
|
parsed, err := ReadJSON(&buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadJSON: %v", err)
|
||||||
|
}
|
||||||
|
if parsed.Version != DumpVersion {
|
||||||
|
t.Errorf("Version = %q, want %q", parsed.Version, DumpVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package keyretriever
|
||||||
|
|
||||||
|
// StaticProvider returns pre-supplied master-key bytes; used by cross-host workflows where keys come
|
||||||
|
// from a Dump rather than platform-native retrieval. RetrieveKey ignores Hints and returns the stored
|
||||||
|
// bytes verbatim; an empty StaticProvider returns (nil, nil), the "not applicable" signal accepted
|
||||||
|
// by NewMasterKeys when a tier was not present in the source Dump.
|
||||||
|
type StaticProvider struct {
|
||||||
|
key []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStaticProvider wraps key bytes as a KeyRetriever. A nil/empty key produces a provider that
|
||||||
|
// reports the tier as unavailable (nil, nil) rather than returning a zero-length key.
|
||||||
|
func NewStaticProvider(key []byte) *StaticProvider {
|
||||||
|
return &StaticProvider{key: key}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetrieveKey returns the stored key bytes, ignoring Hints.
|
||||||
|
func (p *StaticProvider) RetrieveKey(_ Hints) ([]byte, error) {
|
||||||
|
if len(p.key) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return p.key, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user