mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-05-23 19:14:01 +02:00
fix: per-tier master-key retrievers for mixed-cipher profiles (#579)
* fix: per-tier master-key retrievers for mixed-cipher profiles
This commit is contained in:
@@ -155,16 +155,18 @@ func (r *SecurityCmdRetriever) retrieveKeyOnce(storage string) ([]byte, error) {
|
||||
return darwinParams.deriveKey(secret), nil
|
||||
}
|
||||
|
||||
// DefaultRetriever returns the macOS retriever chain, tried in order:
|
||||
// DefaultRetrievers returns the macOS Retrievers. macOS has only a V10 tier (v11 and v20 cipher
|
||||
// prefixes are not used by Chromium on this platform), populated by a within-tier first-success
|
||||
// chain tried in order:
|
||||
//
|
||||
// 1. GcoredumpRetriever — CVE-2025-24204 exploit (root only)
|
||||
// 2. KeychainPasswordRetriever — direct unlock, skipped when password is empty
|
||||
// 3. SecurityCmdRetriever — `security` CLI fallback (may trigger a dialog)
|
||||
func DefaultRetriever(keychainPassword string) KeyRetriever {
|
||||
retrievers := []KeyRetriever{&GcoredumpRetriever{}}
|
||||
func DefaultRetrievers(keychainPassword string) Retrievers {
|
||||
chain := []KeyRetriever{&GcoredumpRetriever{}}
|
||||
if keychainPassword != "" {
|
||||
retrievers = append(retrievers, &KeychainPasswordRetriever{Password: keychainPassword})
|
||||
chain = append(chain, &KeychainPasswordRetriever{Password: keychainPassword})
|
||||
}
|
||||
retrievers = append(retrievers, &SecurityCmdRetriever{cache: make(map[string]securityResult)})
|
||||
return NewChain(retrievers...)
|
||||
chain = append(chain, &SecurityCmdRetriever{cache: make(map[string]securityResult)})
|
||||
return Retrievers{V10: NewChain(chain...)}
|
||||
}
|
||||
|
||||
@@ -68,19 +68,30 @@ func (r *DBusRetriever) RetrieveKey(storage, _ string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("%q: %w", storage, errStorageNotFound)
|
||||
}
|
||||
|
||||
// FallbackRetriever uses the hardcoded "peanuts" password when D-Bus is unavailable.
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:components/os_crypt/os_crypt_linux.cc;l=100
|
||||
type FallbackRetriever struct{}
|
||||
// PosixRetriever produces Chromium's kV10Key by applying PBKDF2 to the hardcoded password
|
||||
// "peanuts". Matches Chromium's upstream PosixKeyProvider (components/os_crypt/async/browser/
|
||||
// posix_key_provider.cc): a deterministic 16-byte AES-128 key used to encrypt ciphertexts with
|
||||
// the "v10" prefix when no keyring is available (headless servers, Docker, CI).
|
||||
type PosixRetriever struct{}
|
||||
|
||||
func (r *FallbackRetriever) RetrieveKey(_, _ string) ([]byte, error) {
|
||||
func (r *PosixRetriever) RetrieveKey(_, _ string) ([]byte, error) {
|
||||
return linuxParams.deriveKey([]byte("peanuts")), nil
|
||||
}
|
||||
|
||||
// DefaultRetriever returns the Linux retriever chain:
|
||||
// D-Bus Secret Service first, then "peanuts" fallback.
|
||||
func DefaultRetriever() KeyRetriever {
|
||||
return NewChain(
|
||||
&DBusRetriever{},
|
||||
&FallbackRetriever{},
|
||||
)
|
||||
// DefaultRetrievers returns the Linux Retrievers, one per cipher tier. Chromium on Linux emits
|
||||
// distinct prefixes for distinct key sources:
|
||||
//
|
||||
// - v10 prefix → PBKDF2("peanuts") — Chromium's kV10Key, emitted when no keyring is available
|
||||
// (headless servers, Docker, CI).
|
||||
// - v11 prefix → PBKDF2(keyring secret) — Chromium's kV11Key, emitted when D-Bus Secret
|
||||
// Service (GNOME Keyring / KWallet) is reachable.
|
||||
//
|
||||
// A profile can carry both prefixes if the host moved between keyring-equipped and headless
|
||||
// sessions, so both tiers run independently with per-tier logging rather than a first-success
|
||||
// chain.
|
||||
func DefaultRetrievers() Retrievers {
|
||||
return Retrievers{
|
||||
V10: &PosixRetriever{},
|
||||
V11: &DBusRetriever{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFallbackRetriever(t *testing.T) {
|
||||
r := &FallbackRetriever{}
|
||||
func TestPosixRetriever(t *testing.T) {
|
||||
r := &PosixRetriever{}
|
||||
|
||||
key, err := r.RetrieveKey("Chrome", "")
|
||||
require.NoError(t, err)
|
||||
@@ -27,32 +27,40 @@ func TestFallbackRetriever(t *testing.T) {
|
||||
}
|
||||
assert.False(t, allZero, "derived key should not be all zeros")
|
||||
|
||||
// "peanuts" is a fixed fallback password, so the result should be
|
||||
// the same regardless of storage name or number of calls.
|
||||
// "peanuts" is a hardcoded password, so the result should be the same regardless of storage
|
||||
// name or number of calls.
|
||||
key2, err := r.RetrieveKey("Brave", "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, key, key2, "fallback key should be the same for any storage")
|
||||
assert.Equal(t, key, key2, "kV10Key should be constant across any storage label")
|
||||
}
|
||||
|
||||
// TestFallbackRetriever_MatchesChromiumKV10Key pins FallbackRetriever's
|
||||
// output to Chromium's kV10Key reference bytes in os_crypt_linux.cc.
|
||||
func TestFallbackRetriever_MatchesChromiumKV10Key(t *testing.T) {
|
||||
// TestPosixRetriever_MatchesChromiumKV10Key pins PosixRetriever's output to Chromium's kV10Key
|
||||
// reference bytes (PBKDF2-HMAC-SHA1 of "peanuts" with "saltysalt", 1 iteration, 16 bytes).
|
||||
func TestPosixRetriever_MatchesChromiumKV10Key(t *testing.T) {
|
||||
want := []byte{
|
||||
0xfd, 0x62, 0x1f, 0xe5, 0xa2, 0xb4, 0x02, 0x53,
|
||||
0x9d, 0xfa, 0x14, 0x7c, 0xa9, 0x27, 0x27, 0x78,
|
||||
}
|
||||
r := &FallbackRetriever{}
|
||||
r := &PosixRetriever{}
|
||||
key, err := r.RetrieveKey("", "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, key)
|
||||
}
|
||||
|
||||
func TestDefaultRetriever_Linux(t *testing.T) {
|
||||
r := DefaultRetriever()
|
||||
chain, ok := r.(*ChainRetriever)
|
||||
require.True(t, ok, "DefaultRetriever should return a *ChainRetriever")
|
||||
func TestDefaultRetrievers_Linux(t *testing.T) {
|
||||
r := DefaultRetrievers()
|
||||
|
||||
assert.Len(t, chain.retrievers, 2, "chain should have 2 retrievers")
|
||||
assert.IsType(t, &DBusRetriever{}, chain.retrievers[0], "first retriever should be DBusRetriever")
|
||||
assert.IsType(t, &FallbackRetriever{}, chain.retrievers[1], "second retriever should be FallbackRetriever")
|
||||
// V10 slot: peanuts-derived kV10Key — PosixRetriever.
|
||||
assert.IsType(t, &PosixRetriever{}, r.V10, "V10 slot should hold PosixRetriever (peanuts kV10Key)")
|
||||
|
||||
// V11 slot: D-Bus keyring kV11Key — DBusRetriever.
|
||||
assert.IsType(t, &DBusRetriever{}, r.V11, "V11 slot should hold DBusRetriever (keyring kV11Key)")
|
||||
|
||||
// V20 slot: ABE is Windows-only, nil on Linux.
|
||||
assert.Nil(t, r.V20, "V20 slot must stay nil on Linux")
|
||||
|
||||
// Smoke: both populated slots must actually retrieve (PosixRetriever always succeeds; DBus may
|
||||
// fail in test env, which is fine — we only want to confirm the wiring, not real keys).
|
||||
require.NotNil(t, r.V10)
|
||||
require.NotNil(t, r.V11)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,14 @@ func (r *DPAPIRetriever) RetrieveKey(_, localStatePath string) ([]byte, error) {
|
||||
return masterKey, nil
|
||||
}
|
||||
|
||||
func DefaultRetriever() KeyRetriever {
|
||||
return NewChain(&ABERetriever{}, &DPAPIRetriever{})
|
||||
// DefaultRetrievers returns the Windows Retrievers: DPAPI for v10 (Chrome's os_crypt.encrypted_key)
|
||||
// and ABE for v20 (Chrome 127+ os_crypt.app_bound_encrypted_key retrieved via reflective injection
|
||||
// into the browser's elevation service). Both run independently — a single Chrome profile upgraded
|
||||
// from pre-v127 carries mixed v10+v20 ciphertexts, and both tiers must be attempted to decrypt the
|
||||
// full profile (see issue #578).
|
||||
func DefaultRetrievers() Retrievers {
|
||||
return Retrievers{
|
||||
V10: &DPAPIRetriever{},
|
||||
V20: &ABERetriever{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package keyretriever
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// MasterKeys bundles the per-cipher-version Chromium master keys used to decrypt data from a
|
||||
// single profile. decryptValue dispatches on the ciphertext's version prefix and picks the
|
||||
// matching key; a missing (nil) key for a tier means "that cipher version cannot be decrypted",
|
||||
// but the other tiers remain usable — a Chrome 127+ profile upgraded from pre-127 carries mixed
|
||||
// v10+v20 ciphertexts, and Linux profiles may carry mixed v10+v11 for analogous reasons.
|
||||
//
|
||||
// - V10: Chrome 80+ key with "v10" cipher prefix.
|
||||
// - Windows: os_crypt.encrypted_key decrypted by user-level DPAPI (AES-GCM ciphertexts).
|
||||
// - macOS: derived from Keychain via PBKDF2(1003, SHA-1) (AES-CBC ciphertexts).
|
||||
// - Linux: derived from "peanuts" hardcoded password (Chromium's kV10Key, AES-CBC).
|
||||
// - V11: Chrome Linux key with "v11" cipher prefix, derived from D-Bus Secret Service
|
||||
// (KWallet / GNOME Keyring) via PBKDF2. Nil on Windows and macOS (v11 prefix not used there).
|
||||
// - V20: Chrome 127+ Windows key with "v20" cipher prefix, retrieved via reflective injection
|
||||
// into the browser's elevation service. Nil on non-Windows platforms.
|
||||
type MasterKeys struct {
|
||||
V10 []byte
|
||||
V11 []byte
|
||||
V20 []byte
|
||||
}
|
||||
|
||||
// Retrievers is the per-tier retriever configuration passed to NewMasterKeys. Each slot runs
|
||||
// independently — failure or absence of one tier does not affect others. Platform injectors set
|
||||
// only the slots that apply to their platform and leave the rest nil (e.g. Linux populates
|
||||
// V10+V11, leaves V20 nil).
|
||||
type Retrievers struct {
|
||||
V10 KeyRetriever
|
||||
V11 KeyRetriever
|
||||
V20 KeyRetriever
|
||||
}
|
||||
|
||||
// NewMasterKeys fetches every configured tier in r independently and returns the assembled
|
||||
// MasterKeys together with any per-tier errors joined into one. Nil retrievers and retrievers
|
||||
// returning (nil, nil) (the "not applicable" signal — e.g. ABERetriever on a non-ABE fork)
|
||||
// contribute nil keys silently; only non-nil errors propagate.
|
||||
//
|
||||
// The returned error, when non-nil, is an errors.Join of per-tier failures formatted as
|
||||
// "<tier>: <err>" (e.g. "v10: dpapi decrypt: ..."). Callers are expected to log it at whatever
|
||||
// severity fits their context — this function itself never logs, leaving logging policy to its
|
||||
// callers. Other pieces of the keyretriever package (e.g. ChainRetriever) may still log on their
|
||||
// own failures; the "no-logging" guarantee is scoped to NewMasterKeys.
|
||||
func NewMasterKeys(r Retrievers, storage, localStatePath string) (MasterKeys, error) {
|
||||
var keys MasterKeys
|
||||
var errs []error
|
||||
|
||||
for _, t := range []struct {
|
||||
name string
|
||||
r KeyRetriever
|
||||
dst *[]byte
|
||||
}{
|
||||
{"v10", r.V10, &keys.V10},
|
||||
{"v11", r.V11, &keys.V11},
|
||||
{"v20", r.V20, &keys.V20},
|
||||
} {
|
||||
if t.r == nil {
|
||||
continue
|
||||
}
|
||||
k, err := t.r.RetrieveKey(storage, localStatePath)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", t.name, err))
|
||||
continue
|
||||
}
|
||||
*t.dst = k
|
||||
}
|
||||
return keys, errors.Join(errs...)
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package keyretriever
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// recordingRetriever captures call count and arguments so tests can verify each tier's retriever
|
||||
// is invoked exactly once with the expected storage and localStatePath.
|
||||
type recordingRetriever struct {
|
||||
key []byte
|
||||
err error
|
||||
|
||||
calls int
|
||||
gotStorage string
|
||||
gotPath string
|
||||
}
|
||||
|
||||
func (r *recordingRetriever) RetrieveKey(storage, localStatePath string) ([]byte, error) {
|
||||
r.calls++
|
||||
r.gotStorage = storage
|
||||
r.gotPath = localStatePath
|
||||
return r.key, r.err
|
||||
}
|
||||
|
||||
func TestNewMasterKeys_Matrix(t *testing.T) {
|
||||
k10 := bytes.Repeat([]byte{0x10}, 32)
|
||||
k11 := bytes.Repeat([]byte{0x11}, 32)
|
||||
k20 := bytes.Repeat([]byte{0x20}, 32)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
v10 *recordingRetriever
|
||||
v11 *recordingRetriever
|
||||
v20 *recordingRetriever
|
||||
wantV10 []byte
|
||||
wantV11 []byte
|
||||
wantV20 []byte
|
||||
wantErrParts []string // substrings that must all appear in the joined error; nil = no error
|
||||
}{
|
||||
{
|
||||
name: "Windows happy path (V10+V20 ok, V11 not configured)",
|
||||
v10: &recordingRetriever{key: k10},
|
||||
v20: &recordingRetriever{key: k20},
|
||||
wantV10: k10, wantV20: k20,
|
||||
},
|
||||
{
|
||||
name: "Linux happy path (V10+V11 ok, V20 not configured)",
|
||||
v10: &recordingRetriever{key: k10},
|
||||
v11: &recordingRetriever{key: k11},
|
||||
wantV10: k10, wantV11: k11,
|
||||
},
|
||||
{
|
||||
name: "macOS happy path (V10 only)",
|
||||
v10: &recordingRetriever{key: k10},
|
||||
wantV10: k10,
|
||||
},
|
||||
{
|
||||
name: "all three tiers succeed",
|
||||
v10: &recordingRetriever{key: k10},
|
||||
v11: &recordingRetriever{key: k11},
|
||||
v20: &recordingRetriever{key: k20},
|
||||
wantV10: k10, wantV11: k11, wantV20: k20,
|
||||
},
|
||||
{
|
||||
name: "one tier errors, others succeed (degraded)",
|
||||
v10: &recordingRetriever{key: k10},
|
||||
v20: &recordingRetriever{err: errors.New("inject failed")},
|
||||
wantV10: k10,
|
||||
wantErrParts: []string{"v20: inject failed"},
|
||||
},
|
||||
{
|
||||
name: "two tiers error, one succeeds",
|
||||
v10: &recordingRetriever{key: k10},
|
||||
v11: &recordingRetriever{err: errors.New("dbus failed")},
|
||||
v20: &recordingRetriever{err: errors.New("inject failed")},
|
||||
wantV10: k10,
|
||||
wantErrParts: []string{"v11: dbus failed", "v20: inject failed"},
|
||||
},
|
||||
{
|
||||
name: "all three tiers error (total failure)",
|
||||
v10: &recordingRetriever{err: errors.New("dpapi failed")},
|
||||
v11: &recordingRetriever{err: errors.New("dbus failed")},
|
||||
v20: &recordingRetriever{err: errors.New("inject failed")},
|
||||
wantErrParts: []string{"v10: dpapi failed", "v11: dbus failed", "v20: inject failed"},
|
||||
},
|
||||
{
|
||||
name: "tier returns (nil, nil) — not applicable, silent",
|
||||
v10: &recordingRetriever{key: k10},
|
||||
v20: &recordingRetriever{}, // ABERetriever on non-ABE fork
|
||||
wantV10: k10,
|
||||
},
|
||||
{
|
||||
name: "all tiers (nil, nil) — no keys, no errors",
|
||||
v10: &recordingRetriever{},
|
||||
v11: &recordingRetriever{},
|
||||
v20: &recordingRetriever{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var r Retrievers
|
||||
if tt.v10 != nil {
|
||||
r.V10 = tt.v10
|
||||
}
|
||||
if tt.v11 != nil {
|
||||
r.V11 = tt.v11
|
||||
}
|
||||
if tt.v20 != nil {
|
||||
r.V20 = tt.v20
|
||||
}
|
||||
|
||||
keys, err := NewMasterKeys(r, "chrome", "/tmp/Local State")
|
||||
assert.Equal(t, tt.wantV10, keys.V10)
|
||||
assert.Equal(t, tt.wantV11, keys.V11)
|
||||
assert.Equal(t, tt.wantV20, keys.V20)
|
||||
|
||||
if len(tt.wantErrParts) == 0 {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
for _, part := range tt.wantErrParts {
|
||||
assert.Contains(t, err.Error(), part, "joined error should mention each failing tier")
|
||||
}
|
||||
}
|
||||
|
||||
// Every configured retriever must be called exactly once — this is the property
|
||||
// that prevents any regression where a tier is silently bypassed.
|
||||
for name, mock := range map[string]*recordingRetriever{"V10": tt.v10, "V11": tt.v11, "V20": tt.v20} {
|
||||
if mock == nil {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, 1, mock.calls, "%s retriever should be called exactly once", name)
|
||||
assert.Equal(t, "chrome", mock.gotStorage)
|
||||
assert.Equal(t, "/tmp/Local State", mock.gotPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewMasterKeys_AllNilRetrievers(t *testing.T) {
|
||||
// All slots nil — macOS/Linux with no retriever wiring, or Windows with neither tier set up.
|
||||
keys, err := NewMasterKeys(Retrievers{}, "chrome", "/tmp/Local State")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, keys.V10)
|
||||
assert.Nil(t, keys.V11)
|
||||
assert.Nil(t, keys.V20)
|
||||
}
|
||||
|
||||
func TestNewMasterKeys_PartialNil(t *testing.T) {
|
||||
// Only V10 wired — typical macOS shape. V11/V20 left nil.
|
||||
k10 := []byte("v10-key-bytes-for-testing")
|
||||
r := &recordingRetriever{key: k10}
|
||||
keys, err := NewMasterKeys(Retrievers{V10: r}, "Chrome", "")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, k10, keys.V10)
|
||||
assert.Nil(t, keys.V11)
|
||||
assert.Nil(t, keys.V20)
|
||||
assert.Equal(t, 1, r.calls)
|
||||
assert.Equal(t, "Chrome", r.gotStorage)
|
||||
}
|
||||
|
||||
func TestNewMasterKeys_ErrorWrapping(t *testing.T) {
|
||||
// errors.Is should traverse errors.Join to find the original error — useful for callers
|
||||
// that want to check for specific error types without string matching.
|
||||
sentinel := errors.New("sentinel")
|
||||
r := Retrievers{V20: &recordingRetriever{err: sentinel}}
|
||||
|
||||
_, err := NewMasterKeys(r, "chrome", "")
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, sentinel, "errors.Is should find wrapped sentinel error")
|
||||
}
|
||||
+9
-1
@@ -14,6 +14,12 @@ const (
|
||||
// CipherV20 is Chrome 127+ App-Bound Encryption.
|
||||
CipherV20 CipherVersion = "v20"
|
||||
|
||||
// CipherV12 is Chromium's SecretPortalKeyProvider (Flatpak / xdg-desktop-portal) tier —
|
||||
// HKDF-SHA256 + AES-256-GCM with a secret retrieved via org.freedesktop.portal.Desktop.
|
||||
// Recognized by DetectVersion so decryptValue can emit a known-gap error rather than a
|
||||
// generic "unsupported cipher version" message; not yet implemented.
|
||||
CipherV12 CipherVersion = "v12"
|
||||
|
||||
// CipherDPAPI is pre-Chrome 80 raw DPAPI encryption (no version prefix).
|
||||
CipherDPAPI CipherVersion = "dpapi"
|
||||
|
||||
@@ -32,6 +38,8 @@ func DetectVersion(ciphertext []byte) CipherVersion {
|
||||
return CipherV10
|
||||
case "v11":
|
||||
return CipherV11
|
||||
case "v12":
|
||||
return CipherV12
|
||||
case "v20":
|
||||
return CipherV20
|
||||
default:
|
||||
@@ -43,7 +51,7 @@ func DetectVersion(ciphertext []byte) CipherVersion {
|
||||
// Returns the ciphertext unchanged if no known prefix is found.
|
||||
func stripPrefix(ciphertext []byte) []byte {
|
||||
ver := DetectVersion(ciphertext)
|
||||
if ver == CipherV10 || ver == CipherV11 || ver == CipherV20 {
|
||||
if ver == CipherV10 || ver == CipherV11 || ver == CipherV12 || ver == CipherV20 {
|
||||
return ciphertext[versionPrefixLen:]
|
||||
}
|
||||
return ciphertext
|
||||
|
||||
@@ -14,6 +14,7 @@ func TestDetectVersion(t *testing.T) {
|
||||
}{
|
||||
{"v10 prefix", []byte("v10" + "encrypted_data"), CipherV10},
|
||||
{"v11 prefix", []byte("v11" + "encrypted_data"), CipherV11},
|
||||
{"v12 prefix", []byte("v12" + "encrypted_data"), CipherV12},
|
||||
{"v20 prefix", []byte("v20" + "encrypted_data"), CipherV20},
|
||||
{"no prefix (DPAPI)", []byte{0x01, 0x00, 0x00, 0x00}, CipherDPAPI},
|
||||
{"short input", []byte{0x01, 0x02}, CipherDPAPI},
|
||||
@@ -36,6 +37,7 @@ func Test_stripPrefix(t *testing.T) {
|
||||
}{
|
||||
{"strips v10", []byte("v10PAYLOAD"), []byte("PAYLOAD")},
|
||||
{"strips v11", []byte("v11PAYLOAD"), []byte("PAYLOAD")},
|
||||
{"strips v12", []byte("v12PAYLOAD"), []byte("PAYLOAD")},
|
||||
{"strips v20", []byte("v20PAYLOAD"), []byte("PAYLOAD")},
|
||||
{"keeps DPAPI unchanged", []byte{0x01, 0x00, 0x00}, []byte{0x01, 0x00, 0x00}},
|
||||
{"keeps short unchanged", []byte{0x01}, []byte{0x01}},
|
||||
|
||||
Reference in New Issue
Block a user