Files
HackBrowserData/browser/chromium/testutil_test.go
T
Roger 2c4e871e59 fix: strip host_key prefix from Chrome 130+ cookie values (#526)
* fix: strip SHA256(host_key) prefix from Chrome 130+ cookie values

Chrome 130 (Cookie DB schema v24) prepends SHA256(domain) to cookie
values before encryption to prevent cross-domain replay attacks.
After decryption, this 32-byte hash must be verified and stripped.

Changes:
- Add stripCookieHash() that verifies SHA256(host_key) and strips
  the prefix only when it matches (auto-compatible with older Chrome)
- Fix edge case: cookies with empty values (exactly 32 bytes = hash only)
- Add decrypt_test.go with v10 round-trip encryption/decryption test
- Add stripCookieHash test cases for v24+, older Chrome, empty values,
  short values, and host mismatch scenarios

Closes #524

* fix: strip SHA256(host_key) prefix from Chrome 130+ cookie values

Chrome 130 (Cookie DB schema v24) prepends SHA256(domain) to cookie
values before encryption to prevent cross-domain replay attacks.
After decryption, this 32-byte hash must be verified and stripped.

Changes:
- Add stripCookieHash() that verifies SHA256(host_key) and strips
  the prefix only when it matches (auto-compatible with older Chrome)
- Fix edge case: cookies with empty values (exactly 32 bytes = hash only)
- Add table-driven decrypt tests for v10/v20/DPAPI per platform
- Add Windows-specific DPAPI round-trip test using CryptProtectData
- Add shared testAESKey constant in testutil_test.go
- Add stripCookieHash tests for v24+, older Chrome, empty values,
  short values, and host mismatch scenarios
- Extend lint CI to run on ubuntu, windows, and macos

Closes #524

* fix: remove DPAPI test from darwin/linux (returns nil on Linux)

DecryptWithDPAPI returns nil error on Linux (silent no-op) but error
on macOS, causing the test to fail on Ubuntu CI. DPAPI round-trip
testing is properly covered in decrypt_windows_test.go.

* fix: resolve Windows CI lint errors exposed by multi-platform lint

- Add _ = before windows.CloseHandle calls to satisfy errcheck
- Add build tag to params.go (only used on macOS/Linux, not Windows)

* fix: add .gitattributes to force LF and refactor cookie tests

- Add .gitattributes with `* text=auto eol=lf` to prevent CRLF
  conversion on Windows CI causing gofumpt false positives
- Add .gitattributes to .gitignore whitelist
- Refactor stripCookieHash tests into table-driven style

* fix: address Copilot review on decrypt tests

- Assert error on wrong key instead of ignoring it (AES-CBC returns
  padding error, not silent empty result)
- Guard empty plaintext in encryptWithDPAPI to prevent nil pointer panic
- Convert uint32 to int for make/copy slice bounds in Windows test

* fix: assert specific error message in wrong key decrypt test
2026-04-04 01:41:01 +08:00

237 lines
8.0 KiB
Go

package chromium
import (
"database/sql"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/syndtr/goleveldb/leveldb"
_ "modernc.org/sqlite"
)
// ---------------------------------------------------------------------------
// Shared test constants for Chromium encryption.
// Reusable across decrypt, cookie, password, and creditcard tests.
// ---------------------------------------------------------------------------
// testAESKey is a 16-byte AES-128 key for constructing test ciphertext.
var testAESKey = []byte("0123456789abcdef")
// ---------------------------------------------------------------------------
// 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
}