Instantly share code, notes, and snippets.
Created
August 27, 2024 17:16
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save hargup/92154883d667bf8920fcebe4013a79cb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import http.cookiejar | |
import json | |
import os | |
import re | |
import shutil | |
import sqlite3 | |
import subprocess | |
import sys | |
import tempfile | |
from cryptography.fernet import Fernet | |
# Supported browsers | |
SUPPORTED_BROWSERS = { | |
'chrome', | |
'chromium', | |
'brave', | |
'edge', | |
'opera', | |
'vivaldi', | |
'firefox', | |
'safari', | |
} | |
def get_cookie_database_path(browser_name, profile=None): | |
"""Get the path to the browser's cookie database.""" | |
if browser_name not in SUPPORTED_BROWSERS: | |
raise ValueError(f'Unsupported browser: {browser_name}') | |
if sys.platform == 'win32': | |
appdata = os.environ['LOCALAPPDATA'] | |
browser_dir = { | |
'chrome': r'Google\Chrome\User Data', | |
'chromium': r'Chromium\User Data', | |
'brave': r'BraveSoftware\Brave-Browser\User Data', | |
'edge': r'Microsoft\Edge\User Data', | |
'opera': r'Opera Software\Opera Stable', | |
'vivaldi': r'Vivaldi\User Data', | |
}[browser_name] | |
cookie_path = os.path.join(appdata, browser_dir, 'Default' if profile is None else profile, 'Cookies') | |
elif sys.platform == 'darwin': | |
appdata = os.path.expanduser('~/Library/Application Support') | |
browser_dir = { | |
'chrome': 'Google/Chrome', | |
'chromium': 'Chromium', | |
'brave': 'BraveSoftware/Brave-Browser', | |
'edge': 'Microsoft Edge', | |
'opera': 'com.operasoftware.Opera', | |
'vivaldi': 'Vivaldi', | |
'firefox': 'Firefox', | |
}[browser_name] | |
cookie_path = os.path.join(appdata, browser_dir, 'Default' if profile is None else profile, 'Cookies') | |
else: | |
config_home = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) | |
browser_dir = { | |
'chrome': 'google-chrome', | |
'chromium': 'chromium', | |
'brave': 'BraveSoftware/Brave-Browser', | |
'edge': 'microsoft-edge', | |
'opera': 'opera', | |
'vivaldi': 'vivaldi', | |
}[browser_name] | |
cookie_path = os.path.join(config_home, browser_dir, 'Default' if profile is None else profile, 'Cookies') | |
if not os.path.exists(cookie_path): | |
raise FileNotFoundError(f'Cookie database not found: {cookie_path}') | |
return cookie_path | |
def extract_cookies_from_chrome(browser_name, profile=None): | |
"""Extract cookies from a Chromium-based browser.""" | |
cookie_path = get_cookie_database_path(browser_name, profile) | |
# Read encryption key | |
if sys.platform == 'linux': | |
try: | |
# Path to local state file | |
local_state_path = os.path.join(os.path.dirname(cookie_path), 'Local State') | |
with open(local_state_path, 'r', encoding='utf-8') as f: | |
local_state = json.load(f) | |
encrypted_key = local_state['os_crypt']['encrypted_key'] | |
# Decode the encrypted key | |
encrypted_key = base64.b64decode(encrypted_key) | |
# Remove the DPAPI prefix | |
encrypted_key = encrypted_key[5:] | |
# Decrypt the key using the keyring | |
key = subprocess.check_output( | |
['secret-tool', 'lookup', 'service', f'{browser_name} Safe Storage'], | |
stderr=subprocess.DEVNULL, | |
) | |
# Decrypt the cookies | |
fernet = Fernet(key) | |
decrypted_key = fernet.decrypt(encrypted_key) | |
except (FileNotFoundError, subprocess.CalledProcessError): | |
print( | |
f'Warning: Could not decrypt cookies from {browser_name}. ' | |
'Make sure you have the `libsecret` package installed and ' | |
f'the {browser_name} Safe Storage password saved in the keyring.', | |
file=sys.stderr, | |
) | |
return http.cookiejar.MozillaCookieJar() # Change this line | |
with tempfile.TemporaryDirectory() as tmpdir: | |
# Copy the cookie database to a temporary file | |
temp_cookie_path = os.path.join(tmpdir, 'Cookies.db') | |
shutil.copyfile(cookie_path, temp_cookie_path) | |
# Open the database and extract cookies | |
conn = sqlite3.connect(temp_cookie_path) | |
cursor = conn.cursor() | |
cursor.execute( | |
'SELECT host_key, name, value, path, expires_utc, is_secure, encrypted_value ' | |
'FROM cookies' | |
) | |
jar = http.cookiejar.MozillaCookieJar() # Change this line | |
for row in cursor.fetchall(): | |
host, name, value, path, expires, secure, encrypted_value = row | |
# Decrypt the cookie value if necessary | |
if encrypted_value: | |
if sys.platform == 'linux': | |
try: | |
value = fernet.decrypt(encrypted_value).decode() | |
except Exception: | |
print(f'Warning: Failed to decrypt cookie {name} for {host}', file=sys.stderr) | |
continue | |
elif sys.platform == 'win32': | |
# Decrypt using Win32CryptProtectData | |
from cryptography.hazmat.backends import default_backend | |
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes | |
from cryptography.hazmat.primitives.padding import PKCS7 | |
encrypted_value = encrypted_value[3:] # Remove DPAPI prefix | |
cipher = Cipher(algorithms.AES(decrypted_key), modes.CBC(b'\0' * 16), backend=default_backend()) | |
decryptor = cipher.decryptor() | |
padded_data = decryptor.update(encrypted_value) + decryptor.finalize() | |
unpadder = PKCS7(128).unpadder() | |
value = unpadder.update(padded_data) + unpadder.finalize() | |
value = value.decode() | |
elif sys.platform == 'darwin': | |
# Decrypt using macOS Security framework | |
try: | |
value = subprocess.check_output( | |
[ | |
'security', | |
'find-generic-password', | |
'-wa', | |
f'{browser_name} Safe Storage', | |
'-s', | |
f'{host} {name} {path}', | |
], | |
stderr=subprocess.DEVNULL, | |
).decode().strip() | |
except subprocess.CalledProcessError: | |
print(f'Warning: Failed to decrypt cookie {name} for {host}', file=sys.stderr) | |
continue | |
# Create a cookie object | |
cookie = http.cookiejar.Cookie( | |
version=0, | |
name=name, | |
value=value, | |
port=None, | |
port_specified=False, | |
domain=host, | |
domain_specified=True, | |
domain_initial_dot=host.startswith('.'), | |
path=path, | |
path_specified=True, | |
secure=secure, | |
expires=expires / 1000000 - 11644473600 if expires else None, # Convert Webkit timestamp to Unix timestamp | |
discard=False, | |
comment=None, | |
comment_url=None, | |
rest={}, | |
) | |
jar.set_cookie(cookie) | |
conn.close() | |
return jar | |
def extract_cookies_from_firefox(profile=None): | |
"""Extract cookies from Firefox.""" | |
if sys.platform == 'darwin': | |
firefox_dir = os.path.expanduser('~/Library/Application Support/Firefox/Profiles') | |
else: | |
firefox_dir = os.path.join(os.path.expanduser('~'), '.mozilla', 'firefox') | |
# print(firefox_dir) | |
if profile is None: | |
profiles = [d for d in os.listdir(firefox_dir) if os.path.isdir(os.path.join(firefox_dir, d))] | |
# print(f"Profiles: {profiles}") | |
if not profiles: | |
raise FileNotFoundError('No Firefox profiles found') | |
# Find the most recently updated profile | |
profile = max(profiles, key=lambda p: os.path.getmtime(os.path.join(firefox_dir, p))) | |
cookie_path = os.path.join(firefox_dir, profile, 'cookies.sqlite') | |
if not os.path.exists(cookie_path): | |
raise FileNotFoundError(f'Firefox cookie database not found: {cookie_path}') | |
with tempfile.TemporaryDirectory() as tmpdir: | |
temp_cookie_path = os.path.join(tmpdir, 'cookies.sqlite') | |
shutil.copyfile(cookie_path, temp_cookie_path) | |
conn = sqlite3.connect(temp_cookie_path) | |
cursor = conn.cursor() | |
cursor.execute( | |
'SELECT host, name, value, path, expiry, isSecure FROM moz_cookies' | |
) | |
jar = http.cookiejar.MozillaCookieJar() # Change this line | |
for row in cursor.fetchall(): | |
host, name, value, path, expiry, secure = row | |
cookie = http.cookiejar.Cookie( | |
version=0, | |
name=name, | |
value=value, | |
port=None, | |
port_specified=False, | |
domain=host, | |
domain_specified=True, | |
domain_initial_dot=host.startswith('.'), | |
path=path, | |
path_specified=True, | |
secure=secure, | |
expires=expiry, | |
discard=False, | |
comment=None, | |
comment_url=None, | |
rest={}, | |
) | |
jar.set_cookie(cookie) | |
conn.close() | |
return jar | |
def extract_cookies_from_safari(): | |
"""Extract cookies from Safari.""" | |
cookie_path = os.path.expanduser('~/Library/Cookies/Cookies.binarycookies') | |
if not os.path.exists(cookie_path): | |
raise FileNotFoundError(f'Safari cookie database not found: {cookie_path}') | |
# Parsing binary cookies is not trivial, so use a third-party library | |
# Install with: pip install browser-cookie3 | |
try: | |
import browser_cookie3 | |
temp_jar = browser_cookie3.safari() | |
jar = http.cookiejar.MozillaCookieJar() | |
for cookie in temp_jar: | |
jar.set_cookie(cookie) | |
except ImportError: | |
raise ImportError( | |
'Could not extract cookies from Safari because browser-cookie3 is not installed. ' | |
'Install with: pip install browser-cookie3' | |
) | |
return jar | |
def save_cookies_to_file(jar, filename): | |
"""Save cookies to a file.""" | |
jar.save(filename, ignore_discard=True, ignore_expires=True) | |
if __name__ == '__main__': | |
if len(sys.argv) < 3: | |
print('Usage: python copy_cookies.py <browser> <output_file> [profile]', file=sys.stderr) | |
sys.exit(1) | |
browser_name = sys.argv[1].lower() | |
output_file = sys.argv[2] | |
profile = sys.argv[3] if len(sys.argv) > 3 else None | |
if browser_name in ('chrome', 'chromium', 'brave', 'edge', 'opera', 'vivaldi'): | |
jar = extract_cookies_from_chrome(browser_name, profile) | |
elif browser_name == 'firefox': | |
jar = extract_cookies_from_firefox(profile) | |
elif browser_name == 'safari': | |
jar = extract_cookies_from_safari() | |
else: | |
raise ValueError(f'Unsupported browser: {browser_name}') | |
save_cookies_to_file(jar, output_file) | |
print(f'Saved cookies to {output_file}') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment