mirror of
https://github.com/tiennm99/MTTools.git
synced 2026-10-11 03:13:42 +00:00
Revert "Merge branch 'feature/export-chrome-cookies-go' into main"
Keeps the branch in history only; main's content is unchanged.
This commit is contained in:
1 parent
0693cb3724
commit
fbc08a2435
7 files changed
+330
-423
No files matched your search
@@ -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/
|
||||
@@ -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).
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
@@ -0,0 +1,3 @@
|
||||
pycryptodome==3.20.0
|
||||
pypiwin32==223
|
||||
pywin32==306
|
||||
Reference in new issue
Block a user