mirror of
https://github.com/tiennm99/MTTools.git
synced 2026-10-11 03:13:42 +00:00
feat(go): convert to Go
This commit is contained in:
1 parent
14128aa4b4
commit
1f97934131
5 files changed
+358
-156
No files matched your search
@@ -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/
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
module github.com/tiennm99/export-chrome-cookies
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/mattn/go-sqlite3 v1.14.17
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user