From 068b82178fc89af244f3ad54673f906647f5199d Mon Sep 17 00:00:00 2001 From: Roger Date: Sat, 4 Apr 2026 18:52:54 +0800 Subject: [PATCH] fix: improve extract parsing with proper decoding and error handling (#543) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: implement proper Chromium localStorage LevelDB parsing * feat: add IsMeta field to StorageEntry and keep META entries * fix: add error logging for decryption and missing data fields * fix: address PR review for localStorage parsing * fix: use naïve instead of café in Latin-1 test to avoid typos false positive * fix: extension enabled detection and sessionStorage decoding * fix: session storage origin resolution and extension enabled detection * fix: address PR review comments for storage parsing --- browser/chromium/extract_bookmark.go | 5 +- browser/chromium/extract_cookie.go | 6 +- browser/chromium/extract_creditcard.go | 13 +- browser/chromium/extract_extension.go | 13 +- browser/chromium/extract_password.go | 6 +- browser/chromium/extract_storage.go | 246 ++++++++++++++++++++--- browser/chromium/extract_storage_test.go | 238 ++++++++++++++++++++-- browser/firefox/extract_password.go | 11 +- output/reflect_test.go | 6 +- types/models.go | 10 +- 10 files changed, 497 insertions(+), 57 deletions(-) diff --git a/browser/chromium/extract_bookmark.go b/browser/chromium/extract_bookmark.go index ed89f6b..d61c2cf 100644 --- a/browser/chromium/extract_bookmark.go +++ b/browser/chromium/extract_bookmark.go @@ -31,9 +31,12 @@ func extractBookmarks(path string) ([]types.BookmarkEntry, error) { // walkBookmarks recursively traverses the bookmark tree, collecting URL entries. func walkBookmarks(node gjson.Result, folder string, out *[]types.BookmarkEntry) { - if node.Get("type").String() == "url" { + nodeType := node.Get("type").String() + if nodeType == "url" { *out = append(*out, types.BookmarkEntry{ + ID: node.Get("id").Int(), Name: node.Get("name").String(), + Type: nodeType, URL: node.Get("url").String(), Folder: folder, CreatedAt: typeutil.TimeEpoch(node.Get("date_added").Int()), diff --git a/browser/chromium/extract_cookie.go b/browser/chromium/extract_cookie.go index bd2fa75..b6c696b 100644 --- a/browser/chromium/extract_cookie.go +++ b/browser/chromium/extract_cookie.go @@ -6,6 +6,7 @@ import ( "database/sql" "sort" + "github.com/moond4rk/hackbrowserdata/log" "github.com/moond4rk/hackbrowserdata/types" "github.com/moond4rk/hackbrowserdata/utils/sqliteutil" "github.com/moond4rk/hackbrowserdata/utils/typeutil" @@ -31,7 +32,10 @@ func extractCookies(masterKey []byte, path string) ([]types.CookieEntry, error) return types.CookieEntry{}, err } - value, _ := decryptValue(masterKey, encryptedValue) + value, err := decryptValue(masterKey, encryptedValue) + if err != nil { + log.Debugf("decrypt cookie %s on %s: %v", name, host, err) + } value = stripCookieHash(value, host) return types.CookieEntry{ Name: name, diff --git a/browser/chromium/extract_creditcard.go b/browser/chromium/extract_creditcard.go index 4e9ed06..3cf7fe9 100644 --- a/browser/chromium/extract_creditcard.go +++ b/browser/chromium/extract_creditcard.go @@ -3,23 +3,28 @@ package chromium import ( "database/sql" + "github.com/moond4rk/hackbrowserdata/log" "github.com/moond4rk/hackbrowserdata/types" "github.com/moond4rk/hackbrowserdata/utils/sqliteutil" ) -const defaultCreditCardQuery = `SELECT name_on_card, expiration_month, expiration_year, +const defaultCreditCardQuery = `SELECT COALESCE(guid, ''), name_on_card, expiration_month, expiration_year, card_number_encrypted, COALESCE(nickname, ''), COALESCE(billing_address_id, '') FROM credit_cards` func extractCreditCards(masterKey []byte, path string) ([]types.CreditCardEntry, error) { return sqliteutil.QueryRows(path, false, defaultCreditCardQuery, func(rows *sql.Rows) (types.CreditCardEntry, error) { - var name, month, year, nickName, address string + var guid, name, month, year, nickName, address string var encNumber []byte - if err := rows.Scan(&name, &month, &year, &encNumber, &nickName, &address); err != nil { + if err := rows.Scan(&guid, &name, &month, &year, &encNumber, &nickName, &address); err != nil { return types.CreditCardEntry{}, err } - number, _ := decryptValue(masterKey, encNumber) + number, err := decryptValue(masterKey, encNumber) + if err != nil { + log.Debugf("decrypt credit card for %s: %v", name, err) + } return types.CreditCardEntry{ + GUID: guid, Name: name, Number: string(number), ExpMonth: month, diff --git a/browser/chromium/extract_extension.go b/browser/chromium/extract_extension.go index 185137c..7e4519f 100644 --- a/browser/chromium/extract_extension.go +++ b/browser/chromium/extract_extension.go @@ -60,7 +60,7 @@ func extractExtensionsWithKeys(path string, keys []string) ([]types.ExtensionEnt Description: manifest.Get("description").String(), Version: manifest.Get("version").String(), HomepageURL: manifest.Get("homepage_url").String(), - Enabled: ext.Get("state").Int() == 1, + Enabled: isExtensionEnabled(ext), }) return true }) @@ -68,6 +68,17 @@ func extractExtensionsWithKeys(path string, keys []string) ([]types.ExtensionEnt return extensions, nil } +// isExtensionEnabled checks whether an extension is enabled. +// Modern Chrome uses disable_reasons (array): empty [] = enabled, non-empty [1] = disabled. +// Older Chrome uses state (int): 1 = enabled. +func isExtensionEnabled(ext gjson.Result) bool { + reasons := ext.Get("disable_reasons") + if reasons.Exists() { + return reasons.IsArray() && len(reasons.Array()) == 0 + } + return ext.Get("state").Int() == 1 +} + // extractOperaExtensions extracts extensions from Opera's Secure Preferences, // which stores extension data under "extensions.opsettings" instead of the // standard "extensions.settings". diff --git a/browser/chromium/extract_password.go b/browser/chromium/extract_password.go index e6964e6..37e46a8 100644 --- a/browser/chromium/extract_password.go +++ b/browser/chromium/extract_password.go @@ -4,6 +4,7 @@ import ( "database/sql" "sort" + "github.com/moond4rk/hackbrowserdata/log" "github.com/moond4rk/hackbrowserdata/types" "github.com/moond4rk/hackbrowserdata/utils/sqliteutil" "github.com/moond4rk/hackbrowserdata/utils/typeutil" @@ -24,7 +25,10 @@ func extractPasswordsWithQuery(masterKey []byte, path, query string) ([]types.Lo if err := rows.Scan(&url, &username, &pwd, &created); err != nil { return types.LoginEntry{}, err } - password, _ := decryptValue(masterKey, pwd) + password, err := decryptValue(masterKey, pwd) + if err != nil { + log.Debugf("decrypt password for %s: %v", url, err) + } return types.LoginEntry{ URL: url, Username: username, diff --git a/browser/chromium/extract_storage.go b/browser/chromium/extract_storage.go index b4f20c8..f70928a 100644 --- a/browser/chromium/extract_storage.go +++ b/browser/chromium/extract_storage.go @@ -2,27 +2,33 @@ package chromium import ( "bytes" + "encoding/binary" "fmt" "os" + "strings" + "unicode/utf16" "github.com/syndtr/goleveldb/leveldb" "github.com/moond4rk/hackbrowserdata/types" ) +// Chromium localStorage LevelDB key prefixes and string format bytes. +// Reference: https://chromium.googlesource.com/chromium/src/+/main/components/services/storage/dom_storage/local_storage_impl.cc +const ( + localStorageVersionKey = "VERSION" + localStorageMetaPrefix = "META:" + localStorageMetaAccessKey = "METAACCESS:" + localStorageDataPrefix = '_' + chromiumStringUTF16Format = 0 + chromiumStringLatin1Format = 1 +) + +const maxLocalStorageValueLength = 2048 + func extractLocalStorage(path string) ([]types.StorageEntry, error) { - return extractLevelDB(path, []byte("\x00")) -} - -func extractSessionStorage(path string) ([]types.StorageEntry, error) { - return extractLevelDB(path, []byte("-")) -} - -// extractLevelDB iterates over all entries in a LevelDB directory, -// splitting each key by the separator into (url, name). -func extractLevelDB(path string, separator []byte) ([]types.StorageEntry, error) { - if _, err := os.Stat(path); os.IsNotExist(err) { - return nil, fmt.Errorf("leveldb path not found: %s", path) + if _, err := os.Stat(path); err != nil { + return nil, fmt.Errorf("leveldb path %q: %w", path, err) } db, err := leveldb.OpenFile(path, nil) if err != nil { @@ -35,24 +41,214 @@ func extractLevelDB(path string, separator []byte) ([]types.StorageEntry, error) defer iter.Release() for iter.Next() { - url, name := parseStorageKey(iter.Key(), separator) - if url == "" { + entry, ok := parseLocalStorageEntry(iter.Key(), iter.Value()) + if !ok { continue } - entries = append(entries, types.StorageEntry{ - URL: url, - Key: name, - Value: string(iter.Value()), - }) + entries = append(entries, entry) } return entries, iter.Error() } -// parseStorageKey splits a LevelDB key into (url, name) by the given separator. -func parseStorageKey(key, separator []byte) (url, name string) { - parts := bytes.SplitN(key, separator, 2) - if len(parts) != 2 { - return "", "" +// parseLocalStorageEntry classifies a LevelDB key/value pair and decodes it. +// Returns false for VERSION entries and any unrecognized keys. META entries are kept with IsMeta=true. +func parseLocalStorageEntry(key, value []byte) (types.StorageEntry, bool) { + switch { + case bytes.Equal(key, []byte(localStorageVersionKey)): + return types.StorageEntry{}, false + case bytes.HasPrefix(key, []byte(localStorageMetaAccessKey)): + return types.StorageEntry{ + IsMeta: true, + URL: string(bytes.TrimPrefix(key, []byte(localStorageMetaAccessKey))), + Value: fmt.Sprintf("meta data, value bytes is %v", value), + }, true + case bytes.HasPrefix(key, []byte(localStorageMetaPrefix)): + return types.StorageEntry{ + IsMeta: true, + URL: string(bytes.TrimPrefix(key, []byte(localStorageMetaPrefix))), + Value: fmt.Sprintf("meta data, value bytes is %v", value), + }, true + case len(key) > 0 && key[0] == localStorageDataPrefix: + return parseLocalStorageDataEntry(key[1:], value), true + default: + return types.StorageEntry{}, false } - return string(parts[0]), string(parts[1]) +} + +// parseLocalStorageDataEntry decodes a data entry with format: origin\x00. +func parseLocalStorageDataEntry(key, value []byte) types.StorageEntry { + entry := types.StorageEntry{ + Value: decodeLocalStorageValue(value), + } + + separator := bytes.IndexByte(key, 0) + if separator < 0 { + return entry + } + + entry.URL = string(key[:separator]) + scriptKey, err := decodeChromiumString(key[separator+1:]) + if err != nil { + return entry + } + entry.Key = scriptKey + return entry +} + +// decodeChromiumString decodes a Chromium-encoded string. +// Format byte 0x01 = Latin-1, 0x00 = UTF-16 LE. +func decodeChromiumString(b []byte) (string, error) { + if len(b) == 0 { + return "", fmt.Errorf("empty chromium string") + } + switch b[0] { + case chromiumStringLatin1Format: + return decodeLatin1(b[1:]), nil + case chromiumStringUTF16Format: + return decodeUTF16LE(b[1:]) + default: + return "", fmt.Errorf("unknown chromium string format 0x%02x", b[0]) + } +} + +// decodeLatin1 converts ISO-8859-1 bytes to a valid UTF-8 Go string. +// Latin-1 byte values map 1:1 to Unicode code points U+0000–U+00FF. +func decodeLatin1(b []byte) string { + runes := make([]rune, len(b)) + for i, c := range b { + runes[i] = rune(c) + } + return string(runes) +} + +// decodeUTF16LE decodes a UTF-16 Little-Endian byte slice to a Go string. +func decodeUTF16LE(b []byte) (string, error) { + if len(b) == 0 { + return "", nil + } + if len(b)%2 != 0 { + return "", fmt.Errorf("invalid UTF-16 byte length %d", len(b)) + } + u16s := make([]uint16, len(b)/2) + for i := range u16s { + u16s[i] = binary.LittleEndian.Uint16(b[i*2:]) + } + return string(utf16.Decode(u16s)), nil +} + +func decodeLocalStorageValue(value []byte) string { + if len(value) >= maxLocalStorageValueLength { + return fmt.Sprintf( + "value is too long, length is %d, supported max length is %d", + len(value), maxLocalStorageValueLength, + ) + } + decoded, err := decodeChromiumString(value) + if err != nil { + return fmt.Sprintf("unsupported value encoding: %v", err) + } + return decoded +} + +// extractSessionStorage reads Chromium session storage LevelDB. +// +// LevelDB key format: +// +// namespace-- (origin mapping) +// map-- (actual data, UTF-16 LE) +// next-map-id / version (metadata, skipped) +func extractSessionStorage(path string) ([]types.StorageEntry, error) { + if _, err := os.Stat(path); err != nil { + return nil, fmt.Errorf("leveldb path %q: %w", path, err) + } + db, err := leveldb.OpenFile(path, nil) + if err != nil { + return nil, err + } + defer db.Close() + + // Pass 1: build map_id → origin lookup from namespace entries. + // Key: "namespace--", Value: "" (ASCII digits). + originByMapID := make(map[string]string) + iter := db.NewIterator(nil, nil) + for iter.Next() { + key := string(iter.Key()) + if !strings.HasPrefix(key, "namespace-") { + continue + } + // Extract origin by finding "-https://", "-http://", or "-chrome://" in the key. + // Namespace GUIDs use underscores (e.g., "03b2df3a_0d95_4d55_ae57_...") so + // there is no ambiguity with the origin separator. + origin := extractNamespaceOrigin(key) + if origin == "" { + continue + } + mapID := string(iter.Value()) + originByMapID[mapID] = origin + } + iter.Release() + if err := iter.Error(); err != nil { + return nil, fmt.Errorf("read namespace entries: %w", err) + } + + // Pass 2: read map entries and resolve origins. + var entries []types.StorageEntry + iter2 := db.NewIterator(nil, nil) + defer iter2.Release() + + mapPrefix := []byte("map-") + for iter2.Next() { + key := iter2.Key() + if !bytes.HasPrefix(key, mapPrefix) { + continue + } + rest := key[len(mapPrefix):] // "-" + sep := bytes.IndexByte(rest, '-') + if sep < 0 { + continue + } + mapID := string(rest[:sep]) + keyName := string(rest[sep+1:]) + + origin := originByMapID[mapID] + if origin == "" { + origin = mapID // fallback to map_id if namespace not found + } + + value := decodeSessionStorageValue(iter2.Value()) + entries = append(entries, types.StorageEntry{ + URL: origin, + Key: keyName, + Value: value, + }) + } + return entries, iter2.Error() +} + +// extractNamespaceOrigin extracts the origin from a namespace key. +// Key format: "namespace--" +// The GUID uses underscores, so we find the origin by looking for "-http" or "-chrome". +func extractNamespaceOrigin(key string) string { + for _, prefix := range []string{"-https://", "-http://", "-chrome://"} { + idx := strings.Index(key, prefix) + if idx >= 0 { + return key[idx+1:] + } + } + return "" +} + +// decodeSessionStorageValue decodes a session storage value. +// Values are raw UTF-16 LE (no format byte prefix, unlike localStorage). +func decodeSessionStorageValue(value []byte) string { + if len(value) == 0 { + return "" + } + if len(value)%2 == 0 { + decoded, err := decodeUTF16LE(value) + if err == nil { + return decoded + } + } + return string(value) } diff --git a/browser/chromium/extract_storage_test.go b/browser/chromium/extract_storage_test.go index 4a17902..453cac4 100644 --- a/browser/chromium/extract_storage_test.go +++ b/browser/chromium/extract_storage_test.go @@ -1,38 +1,211 @@ package chromium import ( + "encoding/binary" "testing" + "unicode/utf16" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// --------------------------------------------------------------------------- +// decodeChromiumString +// --------------------------------------------------------------------------- + +func TestDecodeChromiumString(t *testing.T) { + tests := []struct { + name string + input []byte + want string + wantErr string + }{ + { + name: "latin1 ascii", + input: testEncodeLatin1("abc123"), + want: "abc123", + }, + { + name: "latin1 non-ascii", + input: append([]byte{chromiumStringLatin1Format}, 0x6E, 0x61, 0xEF, 0x76, 0x65), // "naïve" in Latin-1 + want: "na\u00efve", // U+00EF = ï + }, + { + name: "utf16le ascii", + input: testEncodeUTF16("hello"), + want: "hello", + }, + { + name: "utf16le japanese", + input: testEncodeUTF16("テスト"), + want: "テスト", + }, + { + name: "utf16le empty content", + input: []byte{chromiumStringUTF16Format}, + 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", + }, + { + name: "empty input", + input: []byte{}, + wantErr: "empty chromium string", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodeChromiumString(tt.input) + if tt.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +// --------------------------------------------------------------------------- +// parseLocalStorageEntry +// --------------------------------------------------------------------------- + +func TestParseLocalStorageEntry(t *testing.T) { + tests := []struct { + name string + key []byte + value []byte + wantParsed bool + wantMeta bool + wantURL string + wantKey string + wantValue string + }{ + { + name: "skip VERSION", + key: []byte(localStorageVersionKey), + wantParsed: false, + }, + { + name: "META entry", + key: []byte(localStorageMetaPrefix + "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: "METAACCESS entry", + key: []byte(localStorageMetaAccessKey + "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 data entry", + key: append([]byte("_https://example.com\x00"), testEncodeLatin1("token")...), + value: testEncodeLatin1("abc123"), + wantParsed: true, + wantURL: "https://example.com", + wantKey: "token", + wantValue: "abc123", + }, + { + name: "utf16 data entry", + key: append([]byte("_https://example.com\x00"), testEncodeUTF16("テスト")...), + value: testEncodeUTF16("データ"), + wantParsed: true, + wantURL: "https://example.com", + wantKey: "テスト", + wantValue: "データ", + }, + { + name: "missing origin separator", + key: []byte("_https://example.com"), + value: testEncodeLatin1("abc123"), + wantParsed: true, + wantURL: "", + wantKey: "", + wantValue: "abc123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + entry, parsed := parseLocalStorageEntry(tt.key, tt.value) + assert.Equal(t, tt.wantParsed, parsed) + if !parsed { + return + } + assert.Equal(t, tt.wantMeta, entry.IsMeta) + assert.Equal(t, tt.wantURL, entry.URL) + assert.Equal(t, tt.wantKey, entry.Key) + assert.Equal(t, tt.wantValue, entry.Value) + }) + } +} + +// --------------------------------------------------------------------------- +// extractLocalStorage (integration with LevelDB) +// --------------------------------------------------------------------------- + func TestExtractLocalStorage(t *testing.T) { dir := createTestLevelDB(t, map[string]string{ - "https://example.com\x00token": "abc123", - "https://example.com\x00theme": "dark", - "https://other.com\x00session_id": "xyz789", - "noseparator": "should-be-skipped", + localStorageVersionKey: "1", + localStorageMetaPrefix + "https://example.com": string([]byte{0x08, 0x96, 0x01}), + localStorageMetaAccessKey + "https://example.com": string([]byte{0x10, 0x20}), + string(append([]byte("_https://example.com\x00"), testEncodeLatin1("token")...)): string(testEncodeLatin1("abc123")), + string(append([]byte("_https://example.com\x00"), testEncodeUTF16("テスト")...)): string(testEncodeUTF16("データ")), }) got, err := extractLocalStorage(dir) require.NoError(t, err) - require.Len(t, got, 3) // "noseparator" entry skipped + require.Len(t, got, 4, "VERSION filtered, META kept, data kept") - // Verify field mapping by collecting into a lookup + metaCount := 0 byKey := map[string]string{} - for _, entry := range got { - byKey[entry.URL+"/"+entry.Key] = entry.Value + for _, e := range got { + assert.Equal(t, "https://example.com", e.URL) + if e.IsMeta { + metaCount++ + assert.Contains(t, e.Value, "meta data, value bytes is") + continue + } + byKey[e.Key] = e.Value } - assert.Equal(t, "abc123", byKey["https://example.com/token"]) - assert.Equal(t, "dark", byKey["https://example.com/theme"]) - assert.Equal(t, "xyz789", byKey["https://other.com/session_id"]) + assert.Equal(t, 2, metaCount) + assert.Equal(t, "abc123", byKey["token"]) + assert.Equal(t, "データ", byKey["テスト"]) } +// --------------------------------------------------------------------------- +// extractSessionStorage +// --------------------------------------------------------------------------- + func TestExtractSessionStorage(t *testing.T) { dir := createTestLevelDB(t, map[string]string{ - "https://example.com-token": "abc123", - "https://example.com-user": "alice", + // Namespace entry: maps guid+origin → map_id + "namespace-abcd1234_5678_9abc_def0_111111111111-https://github.com/": "100", + "namespace-abcd1234_5678_9abc_def0_111111111111-https://example.com/": "101", + // Map entries: actual data (values are raw UTF-16 LE) + "map-100-__darkreader__wasEnabledForHost": string(testEncodeUTF16Raw("false")), + "map-101-token": string(testEncodeUTF16Raw("abc123")), + // Metadata: should be skipped + "next-map-id": "200", + "version": "1", }) got, err := extractSessionStorage(dir) @@ -41,8 +214,41 @@ func TestExtractSessionStorage(t *testing.T) { byKey := map[string]string{} for _, entry := range got { - byKey[entry.Key] = entry.Value + byKey[entry.URL+"/"+entry.Key] = entry.Value } - assert.Equal(t, "abc123", byKey["token"]) - assert.Equal(t, "alice", byKey["user"]) + assert.Equal(t, "false", byKey["https://github.com//__darkreader__wasEnabledForHost"]) + assert.Equal(t, "abc123", byKey["https://example.com//token"]) +} + +// --------------------------------------------------------------------------- +// Test helpers +// --------------------------------------------------------------------------- + +func testEncodeLatin1(s string) []byte { + return append([]byte{chromiumStringLatin1Format}, []byte(s)...) +} + +func testEncodeUTF16(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 +} + +// testEncodeUTF16Raw encodes as raw UTF-16 LE without format byte prefix +// (used by session storage values). +func testEncodeUTF16Raw(s string) []byte { + encoded := utf16.Encode([]rune(s)) + result := make([]byte, 0, len(encoded)*2) + for _, r := range encoded { + var raw [2]byte + binary.LittleEndian.PutUint16(raw[:], r) + result = append(result, raw[:]...) + } + return result } diff --git a/browser/firefox/extract_password.go b/browser/firefox/extract_password.go index e8c0291..e0f3ddb 100644 --- a/browser/firefox/extract_password.go +++ b/browser/firefox/extract_password.go @@ -9,6 +9,7 @@ import ( "github.com/tidwall/gjson" "github.com/moond4rk/hackbrowserdata/crypto" + "github.com/moond4rk/hackbrowserdata/log" "github.com/moond4rk/hackbrowserdata/types" "github.com/moond4rk/hackbrowserdata/utils/typeutil" ) @@ -43,8 +44,14 @@ func extractPasswords(masterKey []byte, path string) ([]types.LoginEntry, error) url = v.Get("hostname").String() } - user, _ := decryptPBE(v.Get("encryptedUsername").String(), masterKey) - pwd, _ := decryptPBE(v.Get("encryptedPassword").String(), masterKey) + user, err := decryptPBE(v.Get("encryptedUsername").String(), masterKey) + if err != nil { + log.Debugf("decrypt firefox username for %s: %v", url, err) + } + pwd, err := decryptPBE(v.Get("encryptedPassword").String(), masterKey) + if err != nil { + log.Debugf("decrypt firefox password for %s: %v", url, err) + } logins = append(logins, types.LoginEntry{ URL: url, diff --git a/output/reflect_test.go b/output/reflect_test.go index b338392..033d79e 100644 --- a/output/reflect_test.go +++ b/output/reflect_test.go @@ -55,11 +55,11 @@ func TestStructCSVHeader(t *testing.T) { }{ {"LoginEntry", types.LoginEntry{}, []string{"url", "username", "password", "created_at"}}, {"CookieEntry", types.CookieEntry{}, []string{"host", "path", "name", "value", "is_secure", "is_http_only", "has_expire", "is_persistent", "expire_at", "created_at"}}, - {"BookmarkEntry", types.BookmarkEntry{}, []string{"name", "url", "folder", "created_at"}}, + {"BookmarkEntry", types.BookmarkEntry{}, []string{"id", "name", "type", "url", "folder", "created_at"}}, {"HistoryEntry", types.HistoryEntry{}, []string{"url", "title", "visit_count", "last_visit"}}, {"DownloadEntry", types.DownloadEntry{}, []string{"url", "target_path", "mime_type", "total_bytes", "start_time", "end_time"}}, - {"CreditCardEntry", types.CreditCardEntry{}, []string{"name", "number", "exp_month", "exp_year", "nick_name", "address"}}, - {"StorageEntry", types.StorageEntry{}, []string{"url", "key", "value"}}, + {"CreditCardEntry", types.CreditCardEntry{}, []string{"guid", "name", "number", "exp_month", "exp_year", "nick_name", "address"}}, + {"StorageEntry", types.StorageEntry{}, []string{"is_meta", "url", "key", "value"}}, {"ExtensionEntry", types.ExtensionEntry{}, []string{"name", "id", "description", "version", "homepage_url", "enabled"}}, } for _, tt := range tests { diff --git a/types/models.go b/types/models.go index a090579..ea5278b 100644 --- a/types/models.go +++ b/types/models.go @@ -26,7 +26,9 @@ type CookieEntry struct { // BookmarkEntry represents a single browser bookmark. type BookmarkEntry struct { + ID int64 `json:"id" csv:"id"` Name string `json:"name" csv:"name"` + Type string `json:"type" csv:"type"` URL string `json:"url" csv:"url"` Folder string `json:"folder" csv:"folder"` CreatedAt time.Time `json:"created_at" csv:"created_at"` @@ -52,6 +54,7 @@ type DownloadEntry struct { // CreditCardEntry represents a single saved credit card. type CreditCardEntry struct { + GUID string `json:"guid" csv:"guid"` Name string `json:"name" csv:"name"` Number string `json:"number" csv:"number"` ExpMonth string `json:"exp_month" csv:"exp_month"` @@ -62,9 +65,10 @@ type CreditCardEntry struct { // StorageEntry represents a single key-value pair from local or session storage. type StorageEntry struct { - URL string `json:"url" csv:"url"` - Key string `json:"key" csv:"key"` - Value string `json:"value" csv:"value"` + IsMeta bool `json:"is_meta" csv:"is_meta"` + URL string `json:"url" csv:"url"` + Key string `json:"key" csv:"key"` + Value string `json:"value" csv:"value"` } // ExtensionEntry represents a single browser extension.