fix: correctly parse Chromium localStorage LevelDB entries (#539)

This commit is contained in:
chleynx
2026-04-04 01:01:55 +08:00
committed by GitHub
parent d0971ca098
commit c493804ede
2 changed files with 317 additions and 64 deletions
+208 -22
View File
@@ -1,33 +1,219 @@
package localstorage
import (
"encoding/binary"
"testing"
"unicode/utf16"
"github.com/stretchr/testify/assert"
"golang.org/x/text/encoding/unicode"
"github.com/stretchr/testify/require"
"github.com/syndtr/goleveldb/leveldb"
)
var testCases = []struct {
in []byte
wanted []byte
actual []byte
}{
{
in: []byte{0x0, 0x7b, 0x0, 0x22, 0x0, 0x72, 0x0, 0x65, 0x0, 0x66, 0x0, 0x65, 0x0, 0x72, 0x0, 0x5f, 0x0, 0x6b, 0x0, 0x65, 0x0, 0x79, 0x0, 0x22, 0x0, 0x3a, 0x0, 0x22, 0x0, 0x68, 0x0, 0x74, 0x0, 0x74, 0x0, 0x70, 0x0, 0x73, 0x0, 0x3a, 0x0, 0x2f, 0x0, 0x2f, 0x0, 0x77, 0x0, 0x77, 0x0, 0x77, 0x0, 0x2e, 0x0, 0x76, 0x0, 0x6f, 0x0, 0x6c, 0x0, 0x63, 0x0, 0x65, 0x0, 0x6e, 0x0, 0x67, 0x0, 0x69, 0x0, 0x6e, 0x0, 0x65, 0x0, 0x2e, 0x0, 0x63, 0x0, 0x6f, 0x0, 0x6d, 0x0, 0x2f, 0x0, 0x70, 0x0, 0x72, 0x0, 0x6f, 0x0, 0x64, 0x0, 0x75, 0x0, 0x63, 0x0, 0x74, 0x0, 0x73, 0x0, 0x2f, 0x0, 0x66, 0x0, 0x65, 0x0, 0x69, 0x0, 0x6c, 0x0, 0x69, 0x0, 0x61, 0x0, 0x6e, 0x0, 0x22, 0x0, 0x2c, 0x0, 0x22, 0x0, 0x72, 0x0, 0x65, 0x0, 0x66, 0x0, 0x65, 0x0, 0x72, 0x0, 0x5f, 0x0, 0x74, 0x0, 0x69, 0x0, 0x74, 0x0, 0x6c, 0x0, 0x65, 0x0, 0x22, 0x0, 0x3a, 0x0, 0x22, 0x0, 0xde, 0x98, 0xde, 0x8f, 0x2d, 0x0, 0x6b, 0x70, 0x71, 0x5c, 0x15, 0x5f, 0xce, 0x64, 0x22, 0x0, 0x2c, 0x0, 0x22, 0x0, 0x72, 0x0, 0x65, 0x0, 0x66, 0x0, 0x65, 0x0, 0x72, 0x0, 0x5f, 0x0, 0x6d, 0x0, 0x61, 0x0, 0x6e, 0x0, 0x75, 0x0, 0x61, 0x0, 0x6c, 0x0, 0x5f, 0x0, 0x6b, 0x0, 0x65, 0x0, 0x79, 0x0, 0x22, 0x0, 0x3a, 0x0, 0x22, 0x0, 0x22, 0x0, 0x7d, 0x0},
wanted: []byte(`{"refer_key":"https://www.volcengine.com/product/feilian","refer_title":"飞连_SSO单点登录_VPN_终端安全合规_便捷Wifi认证-火山引擎","refer_manual_key":""}`),
actual: []byte{0x7b, 0x22, 0x72, 0x65, 0x66, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x22, 0x3a, 0x22, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x6f, 0x6c, 0x63, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x73, 0x2f, 0x66, 0x65, 0x69, 0x6c, 0x69, 0x61, 0x6e, 0x22, 0x2c, 0x22, 0x72, 0x65, 0x66, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x22, 0x3a, 0x22, 0xc3, 0x9e, 0xe9, 0xa3, 0x9e, 0xe8, 0xbc, 0xad, 0x6b, 0xe7, 0x81, 0xb1, 0xe5, 0xb0, 0x95, 0xe5, 0xbf, 0x8e, 0xe6, 0x90, 0xa2, 0x2c, 0x22, 0x72, 0x65, 0x66, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x5f, 0x6b, 0x65, 0x79, 0x22, 0x3a, 0x22, 0x22, 0x7d, 0xef, 0xbf, 0xbd},
},
}
func TestLocalStorageKeyToUTF8(t *testing.T) {
func TestDecodeChromiumString(t *testing.T) {
t.Parallel()
for _, tc := range testCases {
actual, err := convertUTF16toUTF8(tc.in, unicode.BigEndian)
if err != nil {
t.Error(err)
}
// TODO: fix this, value from local storage if contains chinese characters, need convert utf16 to utf8
// but now, it can't convert, so just skip it.
assert.Equal(t, tc.actual, actual, "chinese characters can't actual convert")
tests := []struct {
name string
input []byte
want string
wantErr string
}{
{
name: "latin1",
input: encodeChromiumLatin1("abc123"),
want: "abc123",
},
{
name: "utf16le",
input: encodeChromiumUTF16("飞连"),
want: "飞连",
},
{
name: "unknown format",
input: []byte{2, 'x'},
wantErr: "unknown chromium string format",
},
{
name: "invalid utf16 byte length",
input: []byte{chromiumStringUTF16Format, 0x61},
wantErr: "invalid UTF-16 byte length",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := decodeChromiumString(tc.input)
if tc.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tc.want, got)
})
}
}
func TestParseChromiumLocalStorageEntry(t *testing.T) {
t.Parallel()
tests := []struct {
name string
key []byte
value []byte
wantParsed bool
wantMeta bool
wantURL string
wantKey string
wantValue string
wantContains string
}{
{
name: "skip version key",
key: []byte(chromiumLocalStorageVersionKey),
wantParsed: false,
},
{
name: "meta key",
key: []byte(chromiumLocalStorageMetaPrefix + "https://example.com"),
value: []byte{0x08, 0x96, 0x01},
wantParsed: true,
wantMeta: true,
wantURL: "https://example.com",
wantValue: "meta data, value bytes is [8 150 1]",
},
{
name: "meta access key",
key: []byte(chromiumLocalStorageMetaAccessKey + "https://example.com"),
value: []byte{0x10, 0x20},
wantParsed: true,
wantMeta: true,
wantURL: "https://example.com",
wantValue: "meta data, value bytes is [16 32]",
},
{
name: "latin1 business key",
key: append([]byte("_https://example.com\x00"), encodeChromiumLatin1("token")...),
value: encodeChromiumLatin1("abc123"),
wantParsed: true,
wantURL: "https://example.com",
wantKey: "token",
wantValue: "abc123",
},
{
name: "utf16 business key",
key: append([]byte("_https://example.com\x00"), encodeChromiumUTF16("飞连")...),
value: encodeChromiumUTF16("终端安全"),
wantParsed: true,
wantURL: "https://example.com",
wantKey: "飞连",
wantValue: "终端安全",
},
{
name: "unsupported business key format",
key: append([]byte("_https://example.com\x00"), []byte{2, 'x'}...),
value: encodeChromiumLatin1("abc123"),
wantParsed: true,
wantURL: "https://example.com",
wantContains: "unsupported chromium localStorage key encoding",
wantValue: "abc123",
},
{
name: "missing origin separator",
key: append([]byte("_https://example.com"), encodeChromiumLatin1("token")...),
value: encodeChromiumLatin1("abc123"),
wantParsed: true,
wantContains: "missing origin separator",
wantValue: "abc123",
},
{
name: "unsupported value format",
key: append([]byte("_https://example.com\x00"), encodeChromiumLatin1("token")...),
value: []byte{2, 'x'},
wantParsed: true,
wantURL: "https://example.com",
wantKey: "token",
wantValue: "unsupported chromium localStorage value encoding: unknown chromium string format 0x02",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, parsed := parseChromiumLocalStorageEntry(tc.key, tc.value)
assert.Equal(t, tc.wantParsed, parsed)
assert.Equal(t, tc.wantMeta, got.IsMeta)
assert.Equal(t, tc.wantURL, got.URL)
assert.Equal(t, tc.wantValue, got.Value)
if tc.wantContains != "" {
assert.Contains(t, got.Key, tc.wantContains)
return
}
assert.Equal(t, tc.wantKey, got.Key)
})
}
}
func TestExtractChromiumLocalStorage(t *testing.T) {
dir := t.TempDir()
db, err := leveldb.OpenFile(dir, nil)
require.NoError(t, err)
testEntries := map[string][]byte{
chromiumLocalStorageVersionKey: []byte("1"),
chromiumLocalStorageMetaPrefix + "https://example.com": {0x08, 0x96, 0x01},
chromiumLocalStorageMetaAccessKey + "https://example.com": {0x10, 0x20},
string(append([]byte("_https://example.com\x00"), encodeChromiumLatin1("token")...)): encodeChromiumLatin1("abc123"),
string(append([]byte("_https://example.com\x00"), encodeChromiumUTF16("飞连")...)): encodeChromiumUTF16("终端安全"),
}
for key, value := range testEntries {
require.NoError(t, db.Put([]byte(key), value, nil))
}
require.NoError(t, db.Close())
got, err := extractChromiumLocalStorage(dir)
require.NoError(t, err)
require.Len(t, got, 4)
metaCount := 0
valuesByKey := make(map[string]string)
for _, entry := range got {
if entry.IsMeta {
metaCount++
assert.Equal(t, "https://example.com", entry.URL)
assert.Contains(t, entry.Value, "meta data, value bytes is")
continue
}
valuesByKey[entry.Key] = entry.Value
assert.Equal(t, "https://example.com", entry.URL)
}
assert.Equal(t, 2, metaCount)
assert.Equal(t, "abc123", valuesByKey["token"])
assert.Equal(t, "终端安全", valuesByKey["飞连"])
}
func encodeChromiumLatin1(s string) []byte {
return append([]byte{chromiumStringLatin1Format}, []byte(s)...)
}
func encodeChromiumUTF16(s string) []byte {
encoded := utf16.Encode([]rune(s))
result := make([]byte, 1, 1+len(encoded)*2)
result[0] = chromiumStringUTF16Format
for _, r := range encoded {
var raw [2]byte
binary.LittleEndian.PutUint16(raw[:], r)
result = append(result, raw[:]...)
}
return result
}