Files
HackBrowserData/browser/chromium/extract_cookie.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

72 lines
2.2 KiB
Go

package chromium
import (
"bytes"
"crypto/sha256"
"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)
value = stripCookieHash(value, host)
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
}
// stripCookieHash removes the SHA256(host_key) prefix from a decrypted cookie value.
// Chrome 130+ (Cookie DB schema version 24) prepends SHA256(domain) to the cookie
// value before encryption to prevent cross-domain cookie replay attacks.
// If the first 32 bytes don't match SHA256(hostKey), the value is returned unchanged,
// which handles both older Chrome versions and tampered data.
func stripCookieHash(value []byte, hostKey string) []byte {
if len(value) < sha256.Size {
return value
}
hash := sha256.Sum256([]byte(hostKey))
if bytes.Equal(value[:sha256.Size], hash[:]) {
return value[sha256.Size:] // empty slice if value was exactly 32 bytes
}
return value
}