diff --git a/export-chrome-cookies/.gitignore b/export-chrome-cookies/.gitignore index 61da5fc..d4fae3d 100644 --- a/export-chrome-cookies/.gitignore +++ b/export-chrome-cookies/.gitignore @@ -1,166 +1,38 @@ -.idea Cookies Local State cookies.json -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class -# C extensions +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll *.so +*.dylib -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST +# Test binary, built with `go test -c` +*.test -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec +# Code coverage profiles and other test artifacts +*.out +coverage.* +*.coverprofile +profile.cov -# Installer logs -pip-log.txt -pip-delete-this-directory.txt +# Dependency directories (remove the comment below to include it) +# vendor/ -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ +# Go workspace file +go.work +go.work.sum -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments +# env file .env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +# Editor/IDE +# .idea/ +# .vscode/ diff --git a/export-chrome-cookies/README.md b/export-chrome-cookies/README.md index e8e5001..7d957b1 100644 --- a/export-chrome-cookies/README.md +++ b/export-chrome-cookies/README.md @@ -1,43 +1,7 @@ # export-chrome-cookies -Python script to export Google Chrome cookies on Windows. +*In 2025, I rewrote this project using Go. The original Python version of this project can be found at `feature/python` branch.* -## Quick start +**This method not work anymore, even the Python version** -```bash -pip install -r requirements.txt -python main.py -``` - -> **Note:** Run on Windows with Chrome fully closed. Chrome locks its SQLite cookie database while running. - -## Output format - -Cookies are written to `cookies.json` in the current directory. Each entry follows the WebExtension cookie schema: - -```json -{ - "domain": ".example.com", - "name": "session_id", - "value": "abc123", - "path": "/", - "secure": true, - "httpOnly": false, - "sameSite": "lax", - "expirationDate": 1735689600, - "session": false -} -``` - -## Chrome v10+ encryption - -Chrome 80+ on Windows encrypts cookie values using AES-256-GCM with a key stored in `Local State` (protected by DPAPI). The script: -1. Reads the base64-encoded encrypted key from `AppData\Local\Google\Chrome\User Data\Local State`. -2. Decrypts it with `win32crypt.CryptUnprotectData`. -3. Uses the key to decrypt each cookie value from the `Cookies` SQLite database. - -You must run the script **as the same Windows user** who owns the Chrome profile — DPAPI keys are user-scoped. - -## License - -Apache-2.0 — see [LICENSE](LICENSE). +Export Google Chrome cookies in Windows using Go diff --git a/export-chrome-cookies/go.mod b/export-chrome-cookies/go.mod new file mode 100644 index 0000000..8783cb5 --- /dev/null +++ b/export-chrome-cookies/go.mod @@ -0,0 +1,22 @@ +module github.com/tiennm99/export-chrome-cookies + +go 1.24.0 + +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 new file mode 100644 index 0000000..ea08d53 --- /dev/null +++ b/export-chrome-cookies/go.sum @@ -0,0 +1,49 @@ +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 new file mode 100644 index 0000000..75ded23 --- /dev/null +++ b/export-chrome-cookies/main.go @@ -0,0 +1,325 @@ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "database/sql" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "time" + "unsafe" + + "golang.org/x/sys/windows" + _ "modernc.org/sqlite" +) + +type Cookie struct { + 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"` +} + +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 + } + + // 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) + + expiresTime := chromeEpoch.Add(time.Duration(expiresUTC) * time.Microsecond) + unixTime := expiresTime.Sub(unixEpoch).Seconds() + + return unixTime +} + +func convertSameSite(sameSite int) string { + sameSiteMap := map[int]string{ + -1: "unspecified", + 0: "no_restriction", + 1: "lax", + 2: "strict", + 3: "none", + } + + if val, ok := sameSiteMap[sameSite]; ok { + return val + } + return "unspecified" +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func copyFile(src, dst string) error { + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + destFile, err := os.Create(dst) + if err != nil { + return err + } + defer destFile.Close() + + _, err = io.Copy(destFile, sourceFile) + return err +} 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