feat(go): convert to Go

This commit is contained in:
tiennm99 committed 2025-11-29 13:00:05 +07:00
1 parent 14128aa4b4
commit 1f97934131
5 files changed
+358 -156

No files matched your search

+24 -154
View File
@@ -1,166 +1,36 @@
.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/
+2 -2
View File
@@ -1,5 +1,5 @@
# export-chrome-cookies
Export Google Chrome cookies in Windows using Python
*In 2025, I rewrote this project using Go. The original Python version of this project can be found at `feature/python` branch.*
Modified from [https://thepythoncode.com/article/extract-chrome-cookies-python]()
Export Google Chrome cookies in Windows using Go
+5
View File
@@ -0,0 +1,5 @@
module github.com/tiennm99/export-chrome-cookies
go 1.21
require github.com/mattn/go-sqlite3 v1.14.17
+2
View File
@@ -0,0 +1,2 @@
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+325
View File
@@ -0,0 +1,325 @@
package main
import (
"database/sql"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
_ "github.com/mattn/go-sqlite3"
)
// 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"`
}
// ConvertToUnixTime converts Windows FILETIME (microseconds since 1601) to Unix epoch time (seconds since 1970)
func ConvertToUnixTime(expiresUTC int64) int64 {
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
// January 1, 1601 to January 1, 1970 is 11644473600 seconds
return (microseconds / 1000000) - 11644473600
}
// ConvertSameSite converts SQLite SameSite value to string representation
func ConvertSameSite(sameSite int64) string {
sameSiteMapping := map[int64]string{
-1: "unspecified",
0: "no_restriction",
1: "lax",
2: "strict",
3: "none",
}
result, exists := sameSiteMapping[sameSite]
if !exists {
return "unspecified"
}
return result
}
// 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")
}
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)
}
// 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)
if err != nil {
return err
}
defer source.Close()
destination, err := os.Create(dst)
if err != nil {
return err
}
defer destination.Close()
_, err = io.Copy(destination, source)
return err
}