mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-05-19 18:58:03 +02:00
feat: add Chromium extract methods and source mapping (#521)
* feat: add Chromium extract methods, source mapping, and tests Implement per-category data extraction for Chromium browsers as typed standalone functions, preparing for Phase 8 wiring into the new Chromium struct. New files: - source.go: dataSource struct, chromiumSources/yandexSources maps, yandexQueryOverrides for Yandex action_url variant - decrypt.go: decryptValue() wrapping platform-specific decryption - extract_password.go: SQLite + decrypt → []LoginEntry - extract_cookie.go: SQLite + decrypt → []CookieEntry - extract_creditcard.go: SQLite + decrypt → []CreditCardEntry - extract_history.go: SQLite → []HistoryEntry - extract_download.go: SQLite → []DownloadEntry - extract_bookmark.go: JSON recursive → []BookmarkEntry - extract_extension.go: JSON → []ExtensionEntry - extract_storage.go: LevelDB → []StorageEntry (local + session) - firefox/source.go: firefoxSources map Tests use real Chrome table schemas for SQLite fixtures, with INSERT helpers to keep test data readable and self-documenting. Ref #520 * fix: remove LevelDB invalid path test (Windows compatibility) leveldb.OpenFile creates the directory on Windows instead of returning an error, causing TestExtractLocalStorage_InvalidPath to fail in CI. This test was verifying LevelDB behavior, not our extraction logic. * refactor: remove unused query parameter from extract functions Only extractPasswords needs the query override (Yandex action_url). The other 7 SQLite extract functions always use their default query, so remove the unnecessary query parameter from their signatures. * refactor: use DetectVersion in decryptValue instead of blind fallback Replace try-then-fallback pattern with explicit version detection using crypto.DetectVersion. Routes v10 to DecryptWithChromium, DPAPI to DecryptWithDPAPI, and adds a TODO placeholder for v20 App-Bound Encryption. * chore: relax gocognit and gocritic linters for test files * revert: restore strict gocognit and gocritic linters for test files * fix: address review feedback on extract methods - Store DetectVersion result in local variable to avoid duplicate call - Scan credit card expiration_month/year as int then convert to string (matches INTEGER column type in real Chrome schema) - Add os.Stat check before leveldb.OpenFile to prevent creating empty directories for non-existent paths - Rename TestExtractExtensions_InvalidJSON to TestExtractExtensions_MissingSettingsPath (JSON is valid, path is missing) * fix: revert creditcard scan to string type for NULL safety modernc.org/sqlite handles INTEGER→string conversion automatically. Scanning into string is safer for nullable columns — NULL becomes "" instead of "0" which would be an invalid month/year.
This commit is contained in:
@@ -196,6 +196,13 @@ linters:
|
||||
- path: "crypto/keyretriever/gcoredump_darwin.go"
|
||||
linters:
|
||||
- gocognit
|
||||
# Temporary: new v2 extract files have no callers until Phase 8 wiring
|
||||
- path: "browser/chromium/(source|decrypt|extract_.*)\\.go"
|
||||
linters:
|
||||
- unused
|
||||
- path: "browser/firefox/source\\.go"
|
||||
linters:
|
||||
- unused
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||
)
|
||||
|
||||
// decryptValue decrypts a Chromium-encrypted value using the master key.
|
||||
// It detects the cipher version from the ciphertext prefix and routes
|
||||
// to the appropriate decryption function.
|
||||
func decryptValue(masterKey, ciphertext []byte) ([]byte, error) {
|
||||
if len(ciphertext) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
version := crypto.DetectVersion(ciphertext)
|
||||
switch version {
|
||||
case crypto.CipherV10:
|
||||
return crypto.DecryptWithChromium(masterKey, ciphertext)
|
||||
case crypto.CipherV20:
|
||||
// TODO: implement App-Bound Encryption (Chrome 127+)
|
||||
return nil, fmt.Errorf("v20 App-Bound Encryption not yet supported")
|
||||
case crypto.CipherDPAPI:
|
||||
return crypto.DecryptWithDPAPI(ciphertext)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported cipher version: %s", version)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
func extractBookmarks(path string) ([]types.BookmarkEntry, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var bookmarks []types.BookmarkEntry
|
||||
roots := gjson.GetBytes(data, "roots")
|
||||
roots.ForEach(func(_, value gjson.Result) bool {
|
||||
walkBookmarks(value, "", &bookmarks)
|
||||
return true
|
||||
})
|
||||
|
||||
sort.Slice(bookmarks, func(i, j int) bool {
|
||||
return bookmarks[i].CreatedAt.After(bookmarks[j].CreatedAt)
|
||||
})
|
||||
return bookmarks, nil
|
||||
}
|
||||
|
||||
// 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" {
|
||||
*out = append(*out, types.BookmarkEntry{
|
||||
Name: node.Get("name").String(),
|
||||
URL: node.Get("url").String(),
|
||||
Folder: folder,
|
||||
CreatedAt: typeutil.TimeEpoch(node.Get("date_added").Int()),
|
||||
})
|
||||
}
|
||||
|
||||
children := node.Get("children")
|
||||
if !children.Exists() || !children.IsArray() {
|
||||
return
|
||||
}
|
||||
currentFolder := node.Get("name").String()
|
||||
for _, child := range children.Array() {
|
||||
walkBookmarks(child, currentFolder, out)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractBookmarks(t *testing.T) {
|
||||
path := createTestJSON(t, "Bookmarks", `{
|
||||
"roots": {
|
||||
"bookmark_bar": {
|
||||
"name": "Bookmarks Bar",
|
||||
"type": "folder",
|
||||
"children": [
|
||||
{"name": "Go", "type": "url", "url": "https://go.dev", "date_added": "13360000000000000"},
|
||||
{
|
||||
"name": "News",
|
||||
"type": "folder",
|
||||
"children": [
|
||||
{"name": "HN", "type": "url", "url": "https://news.ycombinator.com", "date_added": "13350000000000000"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"other": {
|
||||
"name": "Other",
|
||||
"type": "folder",
|
||||
"children": [
|
||||
{"name": "GitHub", "type": "url", "url": "https://github.com", "date_added": "13370000000000000"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
got, err := extractBookmarks(path)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 3)
|
||||
|
||||
// Verify sort order: date added descending (newest first)
|
||||
assert.Equal(t, "GitHub", got[0].Name)
|
||||
assert.Equal(t, "Go", got[1].Name)
|
||||
assert.Equal(t, "HN", got[2].Name)
|
||||
|
||||
// Verify field mapping
|
||||
assert.Equal(t, "https://github.com", got[0].URL)
|
||||
assert.Equal(t, "Other", got[0].Folder)
|
||||
|
||||
// Verify nested folder tracking
|
||||
assert.Equal(t, "https://news.ycombinator.com", got[2].URL)
|
||||
assert.Equal(t, "News", got[2].Folder) // parent folder name
|
||||
}
|
||||
|
||||
func TestExtractBookmarks_FoldersExcluded(t *testing.T) {
|
||||
path := createTestJSON(t, "Bookmarks", `{
|
||||
"roots": {
|
||||
"bookmark_bar": {
|
||||
"name": "Bar",
|
||||
"type": "folder",
|
||||
"children": [
|
||||
{"name": "EmptyFolder", "type": "folder", "children": []},
|
||||
{"name": "Link", "type": "url", "url": "https://example.com", "date_added": "0"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
got, err := extractBookmarks(path)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 1) // only URL entries, not folders
|
||||
assert.Equal(t, "Link", got[0].Name)
|
||||
assert.Equal(t, "Bar", got[0].Folder)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"sort"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
const defaultCookieQuery = `SELECT name, encrypted_value, host_key, path,
|
||||
creation_utc, expires_utc, is_secure, is_httponly,
|
||||
has_expires, is_persistent FROM cookies`
|
||||
|
||||
func extractCookies(masterKey []byte, path string) ([]types.CookieEntry, error) {
|
||||
cookies, err := sqliteutil.QueryRows(path, false, defaultCookieQuery,
|
||||
func(rows *sql.Rows) (types.CookieEntry, error) {
|
||||
var (
|
||||
name, host, cookiePath string
|
||||
isSecure, isHTTPOnly int
|
||||
hasExpire, isPersistent int
|
||||
createdAt, expireAt int64
|
||||
encryptedValue []byte
|
||||
)
|
||||
if err := rows.Scan(&name, &encryptedValue, &host, &cookiePath,
|
||||
&createdAt, &expireAt, &isSecure, &isHTTPOnly,
|
||||
&hasExpire, &isPersistent); err != nil {
|
||||
return types.CookieEntry{}, err
|
||||
}
|
||||
|
||||
value, _ := decryptValue(masterKey, encryptedValue)
|
||||
return types.CookieEntry{
|
||||
Name: name,
|
||||
Host: host,
|
||||
Path: cookiePath,
|
||||
Value: string(value),
|
||||
IsSecure: isSecure != 0,
|
||||
IsHTTPOnly: isHTTPOnly != 0,
|
||||
ExpireAt: typeutil.TimeEpoch(expireAt),
|
||||
CreatedAt: typeutil.TimeEpoch(createdAt),
|
||||
}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Slice(cookies, func(i, j int) bool {
|
||||
return cookies[i].CreatedAt.After(cookies[j].CreatedAt)
|
||||
})
|
||||
return cookies, nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractCookies(t *testing.T) {
|
||||
path := createTestDB(t, "Cookies", cookiesSchema,
|
||||
insertCookie("session", ".old.com", "/", "", 13340000000000000, 13350000000000000, 1, 1),
|
||||
insertCookie("token", ".new.com", "/api", "", 13360000000000000, 13370000000000000, 1, 0),
|
||||
)
|
||||
|
||||
got, err := extractCookies(nil, path)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
|
||||
// Verify sort order: creation time descending (newest first)
|
||||
assert.Equal(t, ".new.com", got[0].Host)
|
||||
assert.Equal(t, ".old.com", got[1].Host)
|
||||
|
||||
// Verify field mapping
|
||||
assert.Equal(t, "token", got[0].Name)
|
||||
assert.Equal(t, "/api", got[0].Path)
|
||||
assert.True(t, got[0].IsSecure)
|
||||
assert.False(t, got[0].IsHTTPOnly) // httpOnly=0
|
||||
assert.False(t, got[0].CreatedAt.IsZero())
|
||||
assert.False(t, got[0].ExpireAt.IsZero())
|
||||
assert.True(t, got[0].ExpireAt.After(got[0].CreatedAt))
|
||||
|
||||
// Verify second cookie flags
|
||||
assert.True(t, got[1].IsHTTPOnly) // httpOnly=1
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
||||
)
|
||||
|
||||
const defaultCreditCardQuery = `SELECT name_on_card, expiration_month, expiration_year,
|
||||
card_number_encrypted 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 string
|
||||
var encNumber []byte
|
||||
if err := rows.Scan(&name, &month, &year, &encNumber); err != nil {
|
||||
return types.CreditCardEntry{}, err
|
||||
}
|
||||
number, _ := decryptValue(masterKey, encNumber)
|
||||
return types.CreditCardEntry{
|
||||
Name: name,
|
||||
Number: string(number),
|
||||
ExpMonth: month,
|
||||
ExpYear: year,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractCreditCards(t *testing.T) {
|
||||
path := createTestDB(t, "Web Data", creditCardsSchema,
|
||||
insertCreditCard("John Doe", 12, 2025, ""),
|
||||
insertCreditCard("Jane Smith", 6, 2027, ""),
|
||||
)
|
||||
|
||||
got, err := extractCreditCards(nil, path)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
|
||||
// Verify field mapping
|
||||
assert.Equal(t, "John Doe", got[0].Name)
|
||||
assert.Equal(t, "12", got[0].ExpMonth)
|
||||
assert.Equal(t, "2025", got[0].ExpYear)
|
||||
// Card number is empty because masterKey is nil (decrypt returns empty)
|
||||
assert.Empty(t, got[0].Number)
|
||||
|
||||
assert.Equal(t, "Jane Smith", got[1].Name)
|
||||
assert.Equal(t, "6", got[1].ExpMonth)
|
||||
assert.Equal(t, "2027", got[1].ExpYear)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"sort"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
const defaultDownloadQuery = `SELECT target_path, tab_url, total_bytes, start_time, end_time FROM downloads`
|
||||
|
||||
func extractDownloads(path string) ([]types.DownloadEntry, error) {
|
||||
downloads, err := sqliteutil.QueryRows(path, false, defaultDownloadQuery,
|
||||
func(rows *sql.Rows) (types.DownloadEntry, error) {
|
||||
var targetPath, url string
|
||||
var totalBytes, startTime, endTime int64
|
||||
if err := rows.Scan(&targetPath, &url, &totalBytes, &startTime, &endTime); err != nil {
|
||||
return types.DownloadEntry{}, err
|
||||
}
|
||||
return types.DownloadEntry{
|
||||
URL: url,
|
||||
TargetPath: targetPath,
|
||||
TotalBytes: totalBytes,
|
||||
StartTime: typeutil.TimeEpoch(startTime),
|
||||
EndTime: typeutil.TimeEpoch(endTime),
|
||||
}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Slice(downloads, func(i, j int) bool {
|
||||
return downloads[i].StartTime.After(downloads[j].StartTime)
|
||||
})
|
||||
return downloads, nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractDownloads(t *testing.T) {
|
||||
path := createTestDB(t, "History", downloadsSchema,
|
||||
insertDownload("/tmp/old.zip", "https://old.com/file.zip", 1024, 13340000000000000, 13340000100000000),
|
||||
insertDownload("/tmp/new.pdf", "https://new.com/doc.pdf", 2048, 13360000000000000, 13360000200000000),
|
||||
)
|
||||
|
||||
got, err := extractDownloads(path)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
|
||||
// Verify sort order: start time descending (newest first)
|
||||
assert.Equal(t, "https://new.com/doc.pdf", got[0].URL)
|
||||
assert.Equal(t, "https://old.com/file.zip", got[1].URL)
|
||||
|
||||
// Verify field mapping
|
||||
assert.Equal(t, "/tmp/new.pdf", got[0].TargetPath)
|
||||
assert.Equal(t, int64(2048), got[0].TotalBytes)
|
||||
assert.False(t, got[0].StartTime.IsZero())
|
||||
assert.False(t, got[0].EndTime.IsZero())
|
||||
assert.True(t, got[0].StartTime.Before(got[0].EndTime))
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
)
|
||||
|
||||
func extractExtensions(path string) ([]types.ExtensionEntry, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Try known JSON paths for extension settings
|
||||
settingKeys := []string{
|
||||
"extensions.settings",
|
||||
"settings.extensions",
|
||||
"settings.settings",
|
||||
}
|
||||
var settings gjson.Result
|
||||
for _, key := range settingKeys {
|
||||
settings = gjson.GetBytes(data, key)
|
||||
if settings.Exists() {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !settings.Exists() {
|
||||
return nil, fmt.Errorf("cannot find extensions in settings")
|
||||
}
|
||||
|
||||
var extensions []types.ExtensionEntry
|
||||
settings.ForEach(func(id, ext gjson.Result) bool {
|
||||
// Skip system/component extensions
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:extensions/common/mojom/manifest.mojom
|
||||
location := ext.Get("location").Int()
|
||||
if location == 5 || location == 10 {
|
||||
return true
|
||||
}
|
||||
|
||||
manifest := ext.Get("manifest")
|
||||
if !manifest.Exists() {
|
||||
return true
|
||||
}
|
||||
|
||||
extensions = append(extensions, types.ExtensionEntry{
|
||||
Name: manifest.Get("name").String(),
|
||||
ID: id.String(),
|
||||
Description: manifest.Get("description").String(),
|
||||
Version: manifest.Get("version").String(),
|
||||
})
|
||||
return true
|
||||
})
|
||||
|
||||
return extensions, nil
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractExtensions(t *testing.T) {
|
||||
path := createTestJSON(t, "Secure Preferences", `{
|
||||
"extensions": {
|
||||
"settings": {
|
||||
"abc123": {
|
||||
"location": 1,
|
||||
"manifest": {
|
||||
"name": "React DevTools",
|
||||
"description": "React debugging",
|
||||
"version": "4.28.0"
|
||||
}
|
||||
},
|
||||
"system-ext": {
|
||||
"location": 5,
|
||||
"manifest": {"name": "System", "version": "1.0"}
|
||||
},
|
||||
"component-ext": {
|
||||
"location": 10,
|
||||
"manifest": {"name": "Component", "version": "1.0"}
|
||||
},
|
||||
"def456": {
|
||||
"location": 1,
|
||||
"manifest": {
|
||||
"name": "uBlock Origin",
|
||||
"description": "Ad blocker",
|
||||
"version": "1.52.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
got, err := extractExtensions(path)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2) // system (location=5) and component (location=10) skipped
|
||||
|
||||
// Verify field mapping (order may vary since gjson.ForEach iterates map)
|
||||
ids := map[string]bool{}
|
||||
for _, ext := range got {
|
||||
ids[ext.ID] = true
|
||||
assert.NotEmpty(t, ext.Name)
|
||||
assert.NotEmpty(t, ext.Version)
|
||||
assert.NotEmpty(t, ext.Description)
|
||||
}
|
||||
assert.True(t, ids["abc123"])
|
||||
assert.True(t, ids["def456"])
|
||||
assert.False(t, ids["system-ext"])
|
||||
}
|
||||
|
||||
func TestExtractExtensions_NoManifestSkipped(t *testing.T) {
|
||||
path := createTestJSON(t, "Secure Preferences", `{
|
||||
"extensions": {
|
||||
"settings": {
|
||||
"no-manifest": {"location": 1}
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
got, err := extractExtensions(path)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, got)
|
||||
}
|
||||
|
||||
func TestExtractExtensions_MissingSettingsPath(t *testing.T) {
|
||||
path := createTestJSON(t, "Secure Preferences", `{"something": "else"}`)
|
||||
_, err := extractExtensions(path)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"sort"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
const defaultHistoryQuery = `SELECT url, title, visit_count, last_visit_time FROM urls`
|
||||
|
||||
func extractHistories(path string) ([]types.HistoryEntry, error) {
|
||||
histories, err := sqliteutil.QueryRows(path, false, defaultHistoryQuery,
|
||||
func(rows *sql.Rows) (types.HistoryEntry, error) {
|
||||
var url, title string
|
||||
var visitCount int
|
||||
var lastVisit int64
|
||||
if err := rows.Scan(&url, &title, &visitCount, &lastVisit); err != nil {
|
||||
return types.HistoryEntry{}, err
|
||||
}
|
||||
return types.HistoryEntry{
|
||||
URL: url,
|
||||
Title: title,
|
||||
VisitCount: visitCount,
|
||||
LastVisit: typeutil.TimeEpoch(lastVisit),
|
||||
}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Slice(histories, func(i, j int) bool {
|
||||
return histories[i].VisitCount > histories[j].VisitCount
|
||||
})
|
||||
return histories, nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractHistories(t *testing.T) {
|
||||
path := createTestDB(t, "History", urlsSchema,
|
||||
insertURL("https://github.com", "GitHub", 100, 13370000000000000),
|
||||
insertURL("https://go.dev", "Go Dev", 50, 13360000000000000),
|
||||
insertURL("https://example.com", "Example", 200, 13350000000000000),
|
||||
)
|
||||
|
||||
got, err := extractHistories(path)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 3)
|
||||
|
||||
// Verify sort order: visit count descending
|
||||
assert.Equal(t, 200, got[0].VisitCount)
|
||||
assert.Equal(t, 100, got[1].VisitCount)
|
||||
assert.Equal(t, 50, got[2].VisitCount)
|
||||
|
||||
// Verify field mapping
|
||||
assert.Equal(t, "https://example.com", got[0].URL)
|
||||
assert.Equal(t, "Example", got[0].Title)
|
||||
assert.False(t, got[0].LastVisit.IsZero())
|
||||
}
|
||||
|
||||
func TestExtractHistories_FileNotFound(t *testing.T) {
|
||||
_, err := extractHistories("/nonexistent/History")
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"sort"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
||||
)
|
||||
|
||||
const defaultLoginQuery = `SELECT origin_url, username_value, password_value, date_created FROM logins`
|
||||
|
||||
func extractPasswords(masterKey []byte, path, query string) ([]types.LoginEntry, error) {
|
||||
if query == "" {
|
||||
query = defaultLoginQuery
|
||||
}
|
||||
|
||||
logins, err := sqliteutil.QueryRows(path, false, query,
|
||||
func(rows *sql.Rows) (types.LoginEntry, error) {
|
||||
var url, username string
|
||||
var pwd []byte
|
||||
var created int64
|
||||
if err := rows.Scan(&url, &username, &pwd, &created); err != nil {
|
||||
return types.LoginEntry{}, err
|
||||
}
|
||||
password, _ := decryptValue(masterKey, pwd)
|
||||
return types.LoginEntry{
|
||||
URL: url,
|
||||
Username: username,
|
||||
Password: string(password),
|
||||
CreatedAt: typeutil.TimeEpoch(created),
|
||||
}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Slice(logins, func(i, j int) bool {
|
||||
return logins[i].CreatedAt.After(logins[j].CreatedAt)
|
||||
})
|
||||
return logins, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
)
|
||||
|
||||
func TestExtractPasswords(t *testing.T) {
|
||||
path := createTestDB(t, "Login Data", loginsSchema,
|
||||
insertLogin("https://old.com", "https://old.com/login", "alice", "", 13340000000000000),
|
||||
insertLogin("https://new.com", "https://new.com/login", "bob", "", 13360000000000000),
|
||||
)
|
||||
|
||||
got, err := extractPasswords(nil, path, "")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
|
||||
// Verify sort order: date created descending (newest first)
|
||||
assert.Equal(t, "https://new.com", got[0].URL)
|
||||
assert.Equal(t, "https://old.com", got[1].URL)
|
||||
|
||||
// Verify field mapping
|
||||
assert.Equal(t, "bob", got[0].Username)
|
||||
assert.False(t, got[0].CreatedAt.IsZero())
|
||||
// Password is empty because masterKey is nil (decrypt returns empty)
|
||||
assert.Empty(t, got[0].Password)
|
||||
}
|
||||
|
||||
func TestExtractPasswords_YandexQueryOverride(t *testing.T) {
|
||||
path := createTestDB(t, "Ya Passman Data", loginsSchema,
|
||||
insertLogin("https://origin.yandex.ru", "https://action.yandex.ru/submit", "user", "", 13350000000000000),
|
||||
)
|
||||
|
||||
// Yandex uses action_url instead of origin_url
|
||||
got, err := extractPasswords(nil, path, yandexQueryOverrides[types.Password])
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 1)
|
||||
assert.Equal(t, "https://action.yandex.ru/submit", got[0].URL) // action_url, not origin_url
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/types"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
db, err := leveldb.OpenFile(path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var entries []types.StorageEntry
|
||||
iter := db.NewIterator(nil, nil)
|
||||
defer iter.Release()
|
||||
|
||||
for iter.Next() {
|
||||
url, name := parseStorageKey(iter.Key(), separator)
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, types.StorageEntry{
|
||||
URL: url,
|
||||
Key: name,
|
||||
Value: string(iter.Value()),
|
||||
})
|
||||
}
|
||||
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 "", ""
|
||||
}
|
||||
return string(parts[0]), string(parts[1])
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
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",
|
||||
})
|
||||
|
||||
got, err := extractLocalStorage(dir)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 3) // "noseparator" entry skipped
|
||||
|
||||
// Verify field mapping by collecting into a lookup
|
||||
byKey := map[string]string{}
|
||||
for _, entry := range got {
|
||||
byKey[entry.URL+"/"+entry.Key] = entry.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"])
|
||||
}
|
||||
|
||||
func TestExtractSessionStorage(t *testing.T) {
|
||||
dir := createTestLevelDB(t, map[string]string{
|
||||
"https://example.com-token": "abc123",
|
||||
"https://example.com-user": "alice",
|
||||
})
|
||||
|
||||
got, err := extractSessionStorage(dir)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
|
||||
byKey := map[string]string{}
|
||||
for _, entry := range got {
|
||||
byKey[entry.Key] = entry.Value
|
||||
}
|
||||
assert.Equal(t, "abc123", byKey["token"])
|
||||
assert.Equal(t, "alice", byKey["user"])
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package chromium
|
||||
|
||||
import "github.com/moond4rk/hackbrowserdata/types"
|
||||
|
||||
// dataSource maps a Category to one or more candidate file paths within a profile directory.
|
||||
// paths are tried in order; the first one that exists is used.
|
||||
type dataSource struct {
|
||||
paths []string // candidate relative paths in priority order
|
||||
isDir bool // true for LevelDB directories
|
||||
}
|
||||
|
||||
// chromiumSources defines the standard Chromium file layout.
|
||||
var chromiumSources = map[types.Category]dataSource{
|
||||
types.Password: {paths: []string{"Login Data"}},
|
||||
types.Cookie: {paths: []string{"Network/Cookies", "Cookies"}},
|
||||
types.History: {paths: []string{"History"}},
|
||||
types.Download: {paths: []string{"History"}}, // same file, different query
|
||||
types.Bookmark: {paths: []string{"Bookmarks"}},
|
||||
types.CreditCard: {paths: []string{"Web Data"}},
|
||||
types.Extension: {paths: []string{"Secure Preferences"}},
|
||||
types.LocalStorage: {paths: []string{"Local Storage/leveldb"}, isDir: true},
|
||||
types.SessionStorage: {paths: []string{"Session Storage"}, isDir: true},
|
||||
}
|
||||
|
||||
// yandexSourceOverrides contains only the entries that differ from chromiumSources.
|
||||
// At initialization time, these are merged into a copy of chromiumSources.
|
||||
var yandexSourceOverrides = map[types.Category]dataSource{
|
||||
types.Password: {paths: []string{"Ya Passman Data"}},
|
||||
types.CreditCard: {paths: []string{"Ya Credit Cards"}},
|
||||
}
|
||||
|
||||
// yandexSources returns chromiumSources with Yandex-specific overrides applied.
|
||||
func yandexSources() map[types.Category]dataSource {
|
||||
sources := make(map[types.Category]dataSource, len(chromiumSources))
|
||||
for k, v := range chromiumSources {
|
||||
sources[k] = v
|
||||
}
|
||||
for k, v := range yandexSourceOverrides {
|
||||
sources[k] = v
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
// yandexQueryOverrides provides SQL query overrides for Yandex Browser.
|
||||
// Yandex uses action_url instead of origin_url for password storage.
|
||||
var yandexQueryOverrides = map[types.Category]string{
|
||||
types.Password: `SELECT action_url, username_value, password_value, date_created FROM logins`,
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Real Chrome table schemas — extracted via `sqlite3 <db> ".schema <table>"`.
|
||||
// Using complete schemas ensures our SQL queries work against real browser data.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const loginsSchema = `CREATE TABLE logins (
|
||||
origin_url VARCHAR NOT NULL,
|
||||
action_url VARCHAR,
|
||||
username_element VARCHAR,
|
||||
username_value VARCHAR,
|
||||
password_element VARCHAR,
|
||||
password_value BLOB,
|
||||
submit_element VARCHAR,
|
||||
signon_realm VARCHAR NOT NULL,
|
||||
date_created INTEGER NOT NULL,
|
||||
blacklisted_by_user INTEGER NOT NULL,
|
||||
scheme INTEGER NOT NULL,
|
||||
password_type INTEGER,
|
||||
times_used INTEGER,
|
||||
form_data BLOB,
|
||||
display_name VARCHAR,
|
||||
icon_url VARCHAR,
|
||||
federation_url VARCHAR,
|
||||
skip_zero_click INTEGER,
|
||||
generation_upload_status INTEGER,
|
||||
possible_username_pairs BLOB,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
date_last_used INTEGER NOT NULL DEFAULT 0,
|
||||
moving_blocked_for BLOB,
|
||||
date_password_modified INTEGER NOT NULL DEFAULT 0,
|
||||
sender_email VARCHAR,
|
||||
sender_name VARCHAR,
|
||||
date_received INTEGER,
|
||||
sharing_notification_displayed INTEGER NOT NULL DEFAULT 0,
|
||||
keychain_identifier BLOB,
|
||||
sender_profile_image_url VARCHAR,
|
||||
date_last_filled INTEGER NOT NULL DEFAULT 0,
|
||||
actor_login_approved INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE (origin_url, username_element, username_value, password_element, signon_realm)
|
||||
)`
|
||||
|
||||
const cookiesSchema = `CREATE TABLE cookies (
|
||||
creation_utc INTEGER NOT NULL,
|
||||
host_key TEXT NOT NULL,
|
||||
top_frame_site_key TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
encrypted_value BLOB NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
expires_utc INTEGER NOT NULL,
|
||||
is_secure INTEGER NOT NULL,
|
||||
is_httponly INTEGER NOT NULL,
|
||||
last_access_utc INTEGER NOT NULL,
|
||||
has_expires INTEGER NOT NULL,
|
||||
is_persistent INTEGER NOT NULL,
|
||||
priority INTEGER NOT NULL,
|
||||
samesite INTEGER NOT NULL,
|
||||
source_scheme INTEGER NOT NULL,
|
||||
source_port INTEGER NOT NULL,
|
||||
last_update_utc INTEGER NOT NULL,
|
||||
source_type INTEGER NOT NULL,
|
||||
has_cross_site_ancestor INTEGER NOT NULL
|
||||
)`
|
||||
|
||||
const urlsSchema = `CREATE TABLE urls (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
url LONGVARCHAR,
|
||||
title LONGVARCHAR,
|
||||
visit_count INTEGER DEFAULT 0 NOT NULL,
|
||||
typed_count INTEGER DEFAULT 0 NOT NULL,
|
||||
last_visit_time INTEGER NOT NULL,
|
||||
hidden INTEGER DEFAULT 0 NOT NULL
|
||||
)`
|
||||
|
||||
const downloadsSchema = `CREATE TABLE downloads (
|
||||
id INTEGER PRIMARY KEY,
|
||||
guid VARCHAR NOT NULL,
|
||||
current_path LONGVARCHAR NOT NULL,
|
||||
target_path LONGVARCHAR NOT NULL,
|
||||
start_time INTEGER NOT NULL,
|
||||
received_bytes INTEGER NOT NULL,
|
||||
total_bytes INTEGER NOT NULL,
|
||||
state INTEGER NOT NULL,
|
||||
danger_type INTEGER NOT NULL,
|
||||
interrupt_reason INTEGER NOT NULL,
|
||||
hash BLOB NOT NULL,
|
||||
end_time INTEGER NOT NULL,
|
||||
opened INTEGER NOT NULL,
|
||||
last_access_time INTEGER NOT NULL,
|
||||
transient INTEGER NOT NULL,
|
||||
referrer VARCHAR NOT NULL,
|
||||
site_url VARCHAR NOT NULL,
|
||||
embedder_download_data VARCHAR NOT NULL,
|
||||
tab_url VARCHAR NOT NULL,
|
||||
tab_referrer_url VARCHAR NOT NULL,
|
||||
http_method VARCHAR NOT NULL,
|
||||
by_ext_id VARCHAR NOT NULL,
|
||||
by_ext_name VARCHAR NOT NULL,
|
||||
by_web_app_id VARCHAR NOT NULL,
|
||||
etag VARCHAR NOT NULL,
|
||||
last_modified VARCHAR NOT NULL,
|
||||
mime_type VARCHAR(255) NOT NULL,
|
||||
original_mime_type VARCHAR(255) NOT NULL
|
||||
)`
|
||||
|
||||
const creditCardsSchema = `CREATE TABLE credit_cards (
|
||||
guid VARCHAR PRIMARY KEY,
|
||||
name_on_card VARCHAR,
|
||||
expiration_month INTEGER,
|
||||
expiration_year INTEGER,
|
||||
card_number_encrypted BLOB,
|
||||
date_modified INTEGER NOT NULL DEFAULT 0,
|
||||
origin VARCHAR DEFAULT '',
|
||||
use_count INTEGER NOT NULL DEFAULT 0,
|
||||
use_date INTEGER NOT NULL DEFAULT 0,
|
||||
billing_address_id VARCHAR,
|
||||
nickname VARCHAR
|
||||
)`
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// INSERT helpers — each returns one SQL statement with only the fields
|
||||
// our extract functions care about; other NOT NULL columns get defaults.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func insertLogin(originURL, actionURL, username, pwdHex string, dateCreated int64) string {
|
||||
return fmt.Sprintf(
|
||||
`INSERT INTO logins (origin_url, action_url, username_element, username_value,
|
||||
password_element, password_value, submit_element, signon_realm, date_created,
|
||||
blacklisted_by_user, scheme)
|
||||
VALUES ('%s', '%s', '', '%s', '', x'%s', '', '%s', %d, 0, 0)`,
|
||||
originURL, actionURL, username, pwdHex, originURL, dateCreated,
|
||||
)
|
||||
}
|
||||
|
||||
func insertCookie(name, host, path, encValueHex string, creationUTC, expiresUTC int64, secure, httpOnly int) string {
|
||||
return fmt.Sprintf(
|
||||
`INSERT INTO cookies (creation_utc, host_key, top_frame_site_key, name, value,
|
||||
encrypted_value, path, expires_utc, is_secure, is_httponly, last_access_utc,
|
||||
has_expires, is_persistent, priority, samesite, source_scheme, source_port,
|
||||
last_update_utc, source_type, has_cross_site_ancestor)
|
||||
VALUES (%d, '%s', '', '%s', '', x'%s', '%s', %d, %d, %d, %d, 1, 1, 1, 0, 2, 443, %d, 0, 0)`,
|
||||
creationUTC, host, name, encValueHex, path, expiresUTC, secure, httpOnly, creationUTC, creationUTC,
|
||||
)
|
||||
}
|
||||
|
||||
func insertURL(url, title string, visitCount int, lastVisitTime int64) string {
|
||||
return fmt.Sprintf(
|
||||
`INSERT INTO urls (url, title, visit_count, typed_count, last_visit_time, hidden)
|
||||
VALUES ('%s', '%s', %d, 0, %d, 0)`,
|
||||
url, title, visitCount, lastVisitTime,
|
||||
)
|
||||
}
|
||||
|
||||
func insertDownload(targetPath, tabURL string, totalBytes, startTime, endTime int64) string {
|
||||
return fmt.Sprintf(
|
||||
`INSERT INTO downloads (id, guid, current_path, target_path, start_time, received_bytes,
|
||||
total_bytes, state, danger_type, interrupt_reason, hash, end_time, opened, last_access_time,
|
||||
transient, referrer, site_url, embedder_download_data, tab_url, tab_referrer_url,
|
||||
http_method, by_ext_id, by_ext_name, by_web_app_id, etag, last_modified, mime_type, original_mime_type)
|
||||
VALUES (NULL, '', '', '%s', %d, %d, %d, 1, 0, 0, x'', %d, 0, 0, 0, '', '', '', '%s', '', 'GET', '', '', '', '', '', '', '')`,
|
||||
targetPath, startTime, totalBytes, totalBytes, endTime, tabURL,
|
||||
)
|
||||
}
|
||||
|
||||
func insertCreditCard(name string, month, year int, encNumberHex string) string {
|
||||
return fmt.Sprintf(
|
||||
`INSERT INTO credit_cards (guid, name_on_card, expiration_month, expiration_year, card_number_encrypted)
|
||||
VALUES ('%s-%d-%d', '%s', %d, %d, x'%s')`,
|
||||
name, month, year, name, month, year, encNumberHex,
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test fixture builders
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// createTestDB creates a SQLite database with the given schema and insert statements.
|
||||
func createTestDB(t *testing.T, name, schema string, inserts ...string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), name)
|
||||
db, err := sql.Open("sqlite", path)
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(schema)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, stmt := range inserts {
|
||||
_, err = db.Exec(stmt)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// createTestJSON creates a file with the given JSON content.
|
||||
func createTestJSON(t *testing.T, name, content string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), name)
|
||||
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
|
||||
return path
|
||||
}
|
||||
|
||||
// createTestLevelDB creates a LevelDB directory with the given key-value pairs.
|
||||
func createTestLevelDB(t *testing.T, entries map[string]string) string {
|
||||
t.Helper()
|
||||
dir := filepath.Join(t.TempDir(), "leveldb")
|
||||
db, err := leveldb.OpenFile(dir, nil)
|
||||
require.NoError(t, err)
|
||||
for k, v := range entries {
|
||||
require.NoError(t, db.Put([]byte(k), []byte(v), nil))
|
||||
}
|
||||
require.NoError(t, db.Close())
|
||||
return dir
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package firefox
|
||||
|
||||
import "github.com/moond4rk/hackbrowserdata/types"
|
||||
|
||||
// dataSource maps a Category to one or more candidate file paths within a profile directory.
|
||||
type dataSource struct {
|
||||
paths []string // candidate relative paths in priority order
|
||||
isDir bool // true for directories (unused in Firefox, all sources are files)
|
||||
}
|
||||
|
||||
// firefoxSources defines the Firefox file layout.
|
||||
// Firefox does not support SessionStorage or CreditCard extraction.
|
||||
var firefoxSources = map[types.Category]dataSource{
|
||||
types.Password: {paths: []string{"logins.json"}},
|
||||
types.Cookie: {paths: []string{"cookies.sqlite"}},
|
||||
types.History: {paths: []string{"places.sqlite"}},
|
||||
types.Download: {paths: []string{"places.sqlite"}}, // same file as History
|
||||
types.Bookmark: {paths: []string{"places.sqlite"}}, // same file as History
|
||||
types.Extension: {paths: []string{"extensions.json"}},
|
||||
types.LocalStorage: {paths: []string{"webappsstore.sqlite"}},
|
||||
}
|
||||
Reference in New Issue
Block a user