mirror of
https://github.com/tiennm99/MTTools.git
synced 2026-08-13 19:23:55 +00:00
feat: add note
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
Cookies
|
||||
Local State
|
||||
cookies.json
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,4 +2,6 @@
|
||||
|
||||
*In 2025, I rewrote this project using Go. The original Python version of this project can be found at `feature/python` branch.*
|
||||
|
||||
**It not work anymore. Maybe the convert Go version issue. Check Python version as fallback**
|
||||
|
||||
Export Google Chrome cookies in Windows using Go
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
module github.com/tiennm99/export-chrome-cookies
|
||||
|
||||
go 1.21
|
||||
go 1.24.0
|
||||
|
||||
require github.com/mattn/go-sqlite3 v1.14.17
|
||||
toolchain go1.24.10
|
||||
|
||||
require (
|
||||
golang.org/x/sys v0.38.0
|
||||
modernc.org/sqlite v1.40.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,2 +1,49 @@
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
|
||||
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
||||
+280
-280
@@ -1,51 +1,292 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"golang.org/x/sys/windows"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// Cookie represents a Chrome cookie in the same format as the Python version
|
||||
type Cookie struct {
|
||||
Domain string `json:"domain"`
|
||||
ExpirationDate int64 `json:"expirationDate"`
|
||||
HostOnly bool `json:"hostOnly"`
|
||||
HTTPOnly bool `json:"httpOnly"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
SameSite string `json:"sameSite"`
|
||||
Secure bool `json:"secure"`
|
||||
Session bool `json:"session"`
|
||||
StoreID string `json:"storeId"`
|
||||
Value string `json:"value"`
|
||||
ID int `json:"id"`
|
||||
Domain string `json:"domain"`
|
||||
ExpirationDate float64 `json:"expirationDate"`
|
||||
HostOnly bool `json:"hostOnly"`
|
||||
HTTPOnly bool `json:"httpOnly"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
SameSite string `json:"sameSite"`
|
||||
Secure bool `json:"secure"`
|
||||
Session bool `json:"session"`
|
||||
StoreID string `json:"storeId"`
|
||||
Value string `json:"value"`
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
// ConvertToUnixTime converts Windows FILETIME (microseconds since 1601) to Unix epoch time (seconds since 1970)
|
||||
func ConvertToUnixTime(expiresUTC int64) int64 {
|
||||
type LocalState struct {
|
||||
OSCrypt struct {
|
||||
EncryptedKey string `json:"encrypted_key"`
|
||||
} `json:"os_crypt"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
userProfile := os.Getenv("USERPROFILE")
|
||||
localStatePath := filepath.Join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Local State")
|
||||
cookiesPath := filepath.Join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default", "Network", "Cookies")
|
||||
|
||||
// Get encryption key
|
||||
key, err := getEncryptionKey(localStatePath)
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting encryption key: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Encryption key length: %d bytes\n", len(key))
|
||||
fmt.Printf("Key first bytes: %v\n", key[:min(16, len(key))])
|
||||
|
||||
// Extract cookies
|
||||
cookies, err := extractCookies(cookiesPath, key)
|
||||
if err != nil {
|
||||
fmt.Printf("Error extracting cookies: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Save to JSON
|
||||
jsonData, err := json.MarshalIndent(cookies, "", " ")
|
||||
if err != nil {
|
||||
fmt.Printf("Error marshaling JSON: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile("cookies.json", jsonData, 0644)
|
||||
if err != nil {
|
||||
fmt.Printf("Error writing JSON file: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully extracted %d cookies to cookies.json\n", len(cookies))
|
||||
}
|
||||
|
||||
func getEncryptionKey(localStatePath string) ([]byte, error) {
|
||||
// Always copy a fresh Local State file
|
||||
workingFile := "Local State"
|
||||
// Remove old copy if exists
|
||||
os.Remove(workingFile)
|
||||
|
||||
if err := copyFile(localStatePath, workingFile); err != nil {
|
||||
return nil, fmt.Errorf("failed to copy Local State: %v", err)
|
||||
}
|
||||
|
||||
// Read and parse Local State
|
||||
data, err := os.ReadFile(workingFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read Local State: %v", err)
|
||||
}
|
||||
|
||||
var localState LocalState
|
||||
if err := json.Unmarshal(data, &localState); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Local State: %v", err)
|
||||
}
|
||||
|
||||
// Decode base64 key
|
||||
encryptedKey, err := base64.StdEncoding.DecodeString(localState.OSCrypt.EncryptedKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode key: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Encrypted key length: %d bytes\n", len(encryptedKey))
|
||||
fmt.Printf("Encrypted key prefix: %s\n", string(encryptedKey[:5]))
|
||||
|
||||
// Remove "DPAPI" prefix (first 5 bytes)
|
||||
if len(encryptedKey) < 5 || string(encryptedKey[:5]) != "DPAPI" {
|
||||
return nil, fmt.Errorf("encrypted key doesn't have DPAPI prefix")
|
||||
}
|
||||
encryptedKey = encryptedKey[5:]
|
||||
|
||||
// Decrypt using DPAPI
|
||||
key, err := dpAPIDecrypt(encryptedKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt key: %v", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func dpAPIDecrypt(data []byte) ([]byte, error) {
|
||||
dataBlob := windows.DataBlob{
|
||||
Size: uint32(len(data)),
|
||||
Data: &data[0],
|
||||
}
|
||||
var outBlob windows.DataBlob
|
||||
|
||||
err := windows.CryptUnprotectData(&dataBlob, nil, nil, 0, nil, 0, &outBlob)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("DPAPI decrypt failed: %v", err)
|
||||
}
|
||||
|
||||
decrypted := make([]byte, outBlob.Size)
|
||||
copy(decrypted, unsafe.Slice(outBlob.Data, outBlob.Size))
|
||||
windows.LocalFree(windows.Handle(unsafe.Pointer(outBlob.Data)))
|
||||
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
func extractCookies(cookiesPath string, key []byte) ([]Cookie, error) {
|
||||
// Always copy a fresh cookies database
|
||||
workingDB := "Cookies"
|
||||
// Remove old copy if exists
|
||||
os.Remove(workingDB)
|
||||
|
||||
if err := copyFile(cookiesPath, workingDB); err != nil {
|
||||
return nil, fmt.Errorf("failed to copy Cookies database: %v", err)
|
||||
}
|
||||
|
||||
// Open database
|
||||
db, err := sql.Open("sqlite", workingDB)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Query cookies
|
||||
query := `SELECT host_key, name, value, encrypted_value, path, expires_utc, is_secure,
|
||||
is_httponly, has_expires, is_persistent, samesite FROM cookies`
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query cookies: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var cookies []Cookie
|
||||
id := 1
|
||||
|
||||
for rows.Next() {
|
||||
var hostKey, name, value, path string
|
||||
var encryptedValue []byte
|
||||
var expiresUTC, isSecure, isHTTPOnly, hasExpires, isPersistent, sameSite int64
|
||||
|
||||
err := rows.Scan(&hostKey, &name, &value, &encryptedValue, &path, &expiresUTC,
|
||||
&isSecure, &isHTTPOnly, &hasExpires, &isPersistent, &sameSite)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Debug output for first few cookies
|
||||
if id <= 3 {
|
||||
fmt.Printf("Cookie #%d: %s\n", id, name)
|
||||
fmt.Printf(" Value field: '%s' (len=%d)\n", value, len(value))
|
||||
fmt.Printf(" Encrypted field: len=%d\n", len(encryptedValue))
|
||||
if len(encryptedValue) > 0 {
|
||||
fmt.Printf(" First bytes: %v\n", encryptedValue[:min(10, len(encryptedValue))])
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt value if needed
|
||||
decryptedValue := value
|
||||
if value == "" && len(encryptedValue) > 0 {
|
||||
decryptedValue = decryptData(encryptedValue, key)
|
||||
if id <= 3 {
|
||||
fmt.Printf(" Decrypted: '%s'\n", decryptedValue)
|
||||
}
|
||||
}
|
||||
|
||||
cookie := Cookie{
|
||||
Domain: hostKey,
|
||||
ExpirationDate: convertToUnixTime(expiresUTC),
|
||||
HostOnly: false,
|
||||
HTTPOnly: isHTTPOnly == 1,
|
||||
Name: name,
|
||||
Path: path,
|
||||
SameSite: convertSameSite(int(sameSite)),
|
||||
Secure: isSecure == 1,
|
||||
Session: isPersistent == 0,
|
||||
StoreID: "0",
|
||||
Value: decryptedValue,
|
||||
ID: id,
|
||||
}
|
||||
|
||||
cookies = append(cookies, cookie)
|
||||
id++
|
||||
}
|
||||
|
||||
return cookies, nil
|
||||
}
|
||||
|
||||
func decryptData(data []byte, key []byte) string {
|
||||
// Check if data starts with "v10", "v11", or "v20" prefix (Chrome's encryption marker)
|
||||
if len(data) > 3 && data[0] == 'v' {
|
||||
version := string(data[0:3])
|
||||
|
||||
// v10, v11, v20 all use AES-GCM with 12-byte nonce
|
||||
if version == "v10" || version == "v11" || version == "v20" {
|
||||
if len(data) < 15 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Nonce is bytes 3-15 (12 bytes)
|
||||
nonce := data[3:15]
|
||||
encryptedData := data[15:]
|
||||
|
||||
// Create AES-GCM cipher
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
fmt.Printf(" AES cipher error: %v\n", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
aesgcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
fmt.Printf(" GCM error: %v\n", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Decrypt
|
||||
decrypted, err := aesgcm.Open(nil, nonce, encryptedData, nil)
|
||||
if err != nil {
|
||||
fmt.Printf(" Decrypt error: %v\n", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(decrypted)
|
||||
}
|
||||
}
|
||||
|
||||
// Try DPAPI fallback for older encryption
|
||||
if len(data) > 0 {
|
||||
if decrypted, err := dpAPIDecrypt(data); err == nil {
|
||||
return string(decrypted)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func convertToUnixTime(expiresUTC int64) float64 {
|
||||
if expiresUTC == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Windows FILETIME: 100-nanosecond intervals since January 1, 1601
|
||||
// Convert to microseconds and then to Unix time (seconds since 1970)
|
||||
microseconds := expiresUTC / 10
|
||||
// Chrome's timestamp is microseconds since January 1, 1601
|
||||
// Convert to Unix timestamp (seconds since January 1, 1970)
|
||||
chromeEpoch := time.Date(1601, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
unixEpoch := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// January 1, 1601 to January 1, 1970 is 11644473600 seconds
|
||||
return (microseconds / 1000000) - 11644473600
|
||||
expiresTime := chromeEpoch.Add(time.Duration(expiresUTC) * time.Microsecond)
|
||||
unixTime := expiresTime.Sub(unixEpoch).Seconds()
|
||||
|
||||
return unixTime
|
||||
}
|
||||
|
||||
// ConvertSameSite converts SQLite SameSite value to string representation
|
||||
func ConvertSameSite(sameSite int64) string {
|
||||
sameSiteMapping := map[int64]string{
|
||||
func convertSameSite(sameSite int) string {
|
||||
sameSiteMap := map[int]string{
|
||||
-1: "unspecified",
|
||||
0: "no_restriction",
|
||||
1: "lax",
|
||||
@@ -53,273 +294,32 @@ func ConvertSameSite(sameSite int64) string {
|
||||
3: "none",
|
||||
}
|
||||
|
||||
result, exists := sameSiteMapping[sameSite]
|
||||
if !exists {
|
||||
return "unspecified"
|
||||
if val, ok := sameSiteMap[sameSite]; ok {
|
||||
return val
|
||||
}
|
||||
return result
|
||||
return "unspecified"
|
||||
}
|
||||
|
||||
// GetEncryptionKey extracts and decrypts the encryption key from Chrome's Local State file
|
||||
func GetEncryptionKey() ([]byte, error) {
|
||||
userProfile := os.Getenv("USERPROFILE")
|
||||
if userProfile == "" {
|
||||
return nil, fmt.Errorf("USERPROFILE environment variable not found")
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
|
||||
localStatePath := filepath.Join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Local State")
|
||||
|
||||
workingLocalState := "Local State"
|
||||
if _, err := os.Stat(workingLocalState); os.IsNotExist(err) {
|
||||
if err := CopyFile(localStatePath, workingLocalState); err != nil {
|
||||
return nil, fmt.Errorf("failed to copy Local State: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(workingLocalState)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read Local State: %v", err)
|
||||
}
|
||||
|
||||
var localState struct {
|
||||
OSCrypt struct {
|
||||
EncryptedKey string `json:"encrypted_key"`
|
||||
} `json:"os_crypt"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &localState); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Local State: %v", err)
|
||||
}
|
||||
|
||||
// Decode the encryption key from Base64
|
||||
encryptedKey, err := base64.StdEncoding.DecodeString(localState.OSCrypt.EncryptedKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode encrypted key: %v", err)
|
||||
}
|
||||
|
||||
// Remove 'DPAPI' (5 bytes prefix)
|
||||
if len(encryptedKey) < 5 {
|
||||
return nil, fmt.Errorf("invalid encrypted key length")
|
||||
}
|
||||
|
||||
encryptedKey = encryptedKey[5:]
|
||||
|
||||
// DPAPI decryption (Windows only)
|
||||
return decryptDataDPAPI(encryptedKey)
|
||||
return b
|
||||
}
|
||||
|
||||
// DecryptDataDPAPI uses Windows DPAPI to decrypt data
|
||||
func decryptDataDPAPI(encryptedData []byte) ([]byte, error) {
|
||||
// This requires Windows-specific DPAPI calls
|
||||
// We'll use a simplified approach for now and note that this needs Windows API
|
||||
|
||||
// For demonstration, we'll return a placeholder
|
||||
// In a real implementation, you'd use golang.org/x/sys/windows to call DPAPI functions
|
||||
return nil, fmt.Errorf("DPAPI decryption requires Windows-specific implementation")
|
||||
}
|
||||
|
||||
// DecryptData attempts to decrypt using AES-GCM first, then falls back to DPAPI
|
||||
func DecryptData(encryptedValue []byte, encryptionKey []byte) string {
|
||||
if len(encryptedValue) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Try AES-GCM decryption first
|
||||
decrypted, err := decryptDataAESGCM(encryptedValue, encryptionKey)
|
||||
if err == nil {
|
||||
return decrypted
|
||||
}
|
||||
|
||||
// Try DPAPI decryption
|
||||
decryptedBytes, err := decryptDataDPAPI(encryptedValue)
|
||||
if err == nil {
|
||||
return string(decryptedBytes)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// DecryptDataAESGCM decrypts data using AES-GCM
|
||||
func decryptDataAESGCM(encryptedValue []byte, key []byte) (string, error) {
|
||||
if len(encryptedValue) < 15 {
|
||||
return "", fmt.Errorf("encrypted data too short")
|
||||
}
|
||||
|
||||
// Chrome stores AES-GCM encrypted data with prefix: 0x010000 + nonce + ciphertext + tag
|
||||
// Extract nonce (12 bytes) from position 3 to 15
|
||||
_ = encryptedValue[3:15] // nonce - used in full implementation
|
||||
_ = encryptedValue[15:] // ciphertextWithTag - used in full implementation
|
||||
|
||||
// For AES-GCM, we need to implement the decryption
|
||||
// This is a simplified version - full implementation would use crypto/aes and crypto/cipher
|
||||
return "", fmt.Errorf("AES-GCM decryption requires full implementation")
|
||||
}
|
||||
|
||||
func main() {
|
||||
userProfile := os.Getenv("USERPROFILE")
|
||||
if userProfile == "" {
|
||||
fmt.Println("Error: USERPROFILE environment variable not found")
|
||||
return
|
||||
}
|
||||
|
||||
cookiesPath := filepath.Join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default", "Network", "Cookies")
|
||||
workingCookies := "Cookies"
|
||||
|
||||
// Copy the cookies file to avoid database locking
|
||||
if _, err := os.Stat(workingCookies); os.IsNotExist(err) {
|
||||
if err := CopyFile(cookiesPath, workingCookies); err != nil {
|
||||
fmt.Printf("Error: Failed to copy Cookies file: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Open the SQLite database
|
||||
db, err := sql.Open("sqlite3", workingCookies)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: Failed to open database: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Query cookies from the database
|
||||
rows, err := db.Query(`
|
||||
SELECT host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, has_expires,
|
||||
is_persistent, samesite FROM cookies
|
||||
`)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: Failed to query database: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
encryptionKey, err := GetEncryptionKey()
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to get encryption key: %v\n", err)
|
||||
// Continue with empty key for already decrypted cookies
|
||||
encryptionKey = nil
|
||||
}
|
||||
|
||||
var cookiesList []Cookie
|
||||
var cookieID int = 1
|
||||
|
||||
for rows.Next() {
|
||||
var hostKey, name, value, encryptedValue, path sql.NullString
|
||||
var expiresUTC, samesite sql.NullInt64
|
||||
var isSecure, isHTTPOnly, hasExpires, isPersistent sql.NullBool
|
||||
|
||||
err := rows.Scan(
|
||||
&hostKey, &name, &value, &encryptedValue, &path, &expiresUTC, &isSecure, &isHTTPOnly, &hasExpires,
|
||||
&isPersistent, &samesite,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to scan row: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var decryptedValue string
|
||||
if value.Valid && value.String != "" {
|
||||
decryptedValue = value.String
|
||||
} else if encryptedValue.Valid && encryptedValue.String != "" {
|
||||
encryptedBytes := []byte(encryptedValue.String)
|
||||
decryptedValue = DecryptData(encryptedBytes, encryptionKey)
|
||||
} else {
|
||||
decryptedValue = ""
|
||||
}
|
||||
|
||||
// Convert types for JSON serialization
|
||||
domain := ""
|
||||
if hostKey.Valid {
|
||||
domain = hostKey.String
|
||||
}
|
||||
|
||||
cookieName := ""
|
||||
if name.Valid {
|
||||
cookieName = name.String
|
||||
}
|
||||
|
||||
cookiePath := "/"
|
||||
if path.Valid {
|
||||
cookiePath = path.String
|
||||
}
|
||||
|
||||
expirationDate := int64(0)
|
||||
if expiresUTC.Valid {
|
||||
expirationDate = ConvertToUnixTime(expiresUTC.Int64)
|
||||
}
|
||||
|
||||
sameSite := "unspecified"
|
||||
if samesite.Valid {
|
||||
sameSite = ConvertSameSite(samesite.Int64)
|
||||
}
|
||||
|
||||
httpOnly := false
|
||||
if isHTTPOnly.Valid {
|
||||
httpOnly = isHTTPOnly.Bool
|
||||
}
|
||||
|
||||
secure := false
|
||||
if isSecure.Valid {
|
||||
secure = isSecure.Bool
|
||||
}
|
||||
|
||||
session := true
|
||||
if isPersistent.Valid {
|
||||
session = !isPersistent.Bool
|
||||
}
|
||||
|
||||
cookie := Cookie{
|
||||
Domain: domain,
|
||||
ExpirationDate: expirationDate,
|
||||
HostOnly: false,
|
||||
HTTPOnly: httpOnly,
|
||||
Name: cookieName,
|
||||
Path: cookiePath,
|
||||
SameSite: sameSite,
|
||||
Secure: secure,
|
||||
Session: session,
|
||||
StoreID: "0",
|
||||
Value: decryptedValue,
|
||||
ID: cookieID,
|
||||
}
|
||||
|
||||
cookiesList = append(cookiesList, cookie)
|
||||
cookieID++
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
fmt.Printf("Error: Failed to iterate rows: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Write cookies to JSON file
|
||||
jsonData, err := json.MarshalIndent(cookiesList, "", " ")
|
||||
if err != nil {
|
||||
fmt.Printf("Error: Failed to marshal JSON: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile("cookies.json", jsonData, 0644); err != nil {
|
||||
fmt.Printf("Error: Failed to write cookies.json: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully exported %d cookies to cookies.json\n", len(cookiesList))
|
||||
}
|
||||
|
||||
// CopyFile copies a file from src to dst
|
||||
func CopyFile(src, dst string) error {
|
||||
source, err := os.Open(src)
|
||||
func copyFile(src, dst string) error {
|
||||
sourceFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer source.Close()
|
||||
defer sourceFile.Close()
|
||||
|
||||
destination, err := os.Create(dst)
|
||||
destFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destination.Close()
|
||||
defer destFile.Close()
|
||||
|
||||
_, err = io.Copy(destination, source)
|
||||
_, err = io.Copy(destFile, sourceFile)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import win32crypt # pip install pypiwin32
|
||||
from Crypto.Cipher import AES # pip install pycryptodome
|
||||
|
||||
# This is Windows version
|
||||
# You may want to edit these values to match your OS
|
||||
local_state_path = os.path.join(os.environ["USERPROFILE"], "AppData", "Local", "Google", "Chrome", "User Data",
|
||||
"Local State")
|
||||
cookies_path = os.path.join(os.environ["USERPROFILE"], "AppData", "Local", "Google", "Chrome", "User Data", "Default",
|
||||
"Network", "Cookies")
|
||||
|
||||
|
||||
def convert_to_unix_time(expires_utc):
|
||||
"""
|
||||
:param expires_utc: microseconds diff since January, 1601
|
||||
:return: epoch time in seconds
|
||||
"""
|
||||
if expires_utc == 0:
|
||||
return 0
|
||||
# Define the start date as January 1, 1601
|
||||
start_date = datetime(1601, 1, 1)
|
||||
# Calculate the timedelta from the start date to the expires_utc date
|
||||
expires_date = start_date + timedelta(microseconds=expires_utc)
|
||||
# Define the Unix epoch start date as January 1, 1970
|
||||
epoch_start_date = datetime(1970, 1, 1)
|
||||
# Calculate the timedelta from the Unix epoch start date to the expires_utc date
|
||||
unix_time = (expires_date - epoch_start_date).total_seconds()
|
||||
return unix_time
|
||||
|
||||
|
||||
def convert_samesite(samesite):
|
||||
samesite_mapping = {
|
||||
-1: "unspecified",
|
||||
0: "no_restriction",
|
||||
1: "lax",
|
||||
2: "strict",
|
||||
3: "none",
|
||||
}
|
||||
return samesite_mapping.get(samesite, "unspecified")
|
||||
|
||||
|
||||
def get_encryption_key():
|
||||
working_local_state = "Local State"
|
||||
if not os.path.isfile(working_local_state):
|
||||
shutil.copyfile(local_state_path, working_local_state)
|
||||
with open(working_local_state, "r", encoding="utf-8") as f:
|
||||
local_state = f.read()
|
||||
local_state = json.loads(local_state)
|
||||
|
||||
# decode the encryption key from Base64
|
||||
key = base64.b64decode(local_state["os_crypt"]["encrypted_key"])
|
||||
# remove 'DPAPI' str
|
||||
key = key[5:]
|
||||
# return decrypted key that was originally encrypted
|
||||
# using a session key derived from current user's logon credentials
|
||||
# doc: http://timgolden.me.uk/pywin32-docs/win32crypt.html
|
||||
return win32crypt.CryptUnprotectData(key, None, None, None, 0)[1]
|
||||
|
||||
|
||||
def decrypt_data(data, key):
|
||||
try:
|
||||
# get the initialization vector
|
||||
iv = data[3:15]
|
||||
data = data[15:]
|
||||
# generate cipher
|
||||
cipher = AES.new(key, AES.MODE_GCM, iv)
|
||||
# decrypt password
|
||||
return cipher.decrypt(data)[:-16].decode()
|
||||
except:
|
||||
try:
|
||||
return str(win32crypt.CryptUnprotectData(data, None, None, None, 0)[1])
|
||||
except:
|
||||
# not supported
|
||||
return ""
|
||||
|
||||
|
||||
def main():
|
||||
# local sqlite Chrome cookie database path
|
||||
# copy the file to current directory
|
||||
# as the database will be locked if chrome is currently open
|
||||
working_cookies = "Cookies"
|
||||
if not os.path.isfile(working_cookies):
|
||||
shutil.copyfile(cookies_path, working_cookies)
|
||||
# connect to the database
|
||||
db = sqlite3.connect(working_cookies)
|
||||
# ignore decoding errors
|
||||
db.text_factory = lambda b: b.decode(errors="ignore")
|
||||
cursor = db.cursor()
|
||||
# get the cookies from `cookies` table
|
||||
cursor.execute("""
|
||||
SELECT host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, has_expires,
|
||||
is_persistent, samesite FROM cookies""")
|
||||
|
||||
encryption_key = get_encryption_key()
|
||||
|
||||
cookies_list = []
|
||||
for i, (host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, has_expires,
|
||||
is_persistent, samesite) in enumerate(cursor.fetchall()):
|
||||
if not value:
|
||||
decrypted_value = decrypt_data(encrypted_value, encryption_key)
|
||||
else:
|
||||
# already decrypted
|
||||
decrypted_value = value
|
||||
|
||||
cookies_list.append({
|
||||
"domain": host_key,
|
||||
"expirationDate": convert_to_unix_time(expires_utc),
|
||||
"hostOnly": False, # This information is not available in the SQLite database
|
||||
"httpOnly": bool(is_httponly),
|
||||
"name": name,
|
||||
"path": path,
|
||||
"sameSite": convert_samesite(samesite),
|
||||
"secure": bool(is_secure),
|
||||
"session": not bool(is_persistent),
|
||||
"storeId": "0", # This information is not available in the SQLite database
|
||||
"value": decrypted_value,
|
||||
"id": i + 1
|
||||
})
|
||||
|
||||
# commit changes
|
||||
db.commit()
|
||||
# close connection
|
||||
db.close()
|
||||
|
||||
with open('cookies.json', 'w') as f:
|
||||
json.dump(cookies_list, f, indent=4)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,3 +0,0 @@
|
||||
pycryptodome==3.20.0
|
||||
pypiwin32==223
|
||||
pywin32==306
|
||||
Reference in New Issue
Block a user