diff --git a/export-chrome-cookies/.gitignore b/export-chrome-cookies/.gitignore index 1a9accd..d4fae3d 100644 --- a/export-chrome-cookies/.gitignore +++ b/export-chrome-cookies/.gitignore @@ -1,4 +1,6 @@ Cookies +Local State +cookies.json diff --git a/export-chrome-cookies/README.md b/export-chrome-cookies/README.md index 431e493..bc4946b 100644 --- a/export-chrome-cookies/README.md +++ b/export-chrome-cookies/README.md @@ -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 diff --git a/export-chrome-cookies/go.mod b/export-chrome-cookies/go.mod index 4a5fd99..8783cb5 100644 --- a/export-chrome-cookies/go.mod +++ b/export-chrome-cookies/go.mod @@ -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 +) diff --git a/export-chrome-cookies/go.sum b/export-chrome-cookies/go.sum index 04784a2..ea08d53 100644 --- a/export-chrome-cookies/go.sum +++ b/export-chrome-cookies/go.sum @@ -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= diff --git a/export-chrome-cookies/main.go b/export-chrome-cookies/main.go index 0854919..75ded23 100644 --- a/export-chrome-cookies/main.go +++ b/export-chrome-cookies/main.go @@ -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 -} \ No newline at end of file +} diff --git a/export-chrome-cookies/main.py b/export-chrome-cookies/main.py deleted file mode 100644 index 3356128..0000000 --- a/export-chrome-cookies/main.py +++ /dev/null @@ -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() diff --git a/export-chrome-cookies/requirements.txt b/export-chrome-cookies/requirements.txt deleted file mode 100644 index 19f6e51..0000000 --- a/export-chrome-cookies/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -pycryptodome==3.20.0 -pypiwin32==223 -pywin32==306