From 6e983255250b9bf66f7e61ed3ea7f2d9e0069fc0 Mon Sep 17 00:00:00 2001 From: "lux.r.ck" Date: Mon, 24 Apr 2017 15:48:29 +0800 Subject: [PATCH] Add Chrome/Chromium cookie file support. --- setup.py | 4 +- src/you_get/common.py | 28 +----- src/you_get/cookies.py | 194 +++++++++++++++++++++++++++++++++++++++++ you-get.json | 8 ++ 4 files changed, 207 insertions(+), 27 deletions(-) create mode 100644 src/you_get/cookies.py diff --git a/setup.py b/setup.py index 21246c5f..3f49ec5d 100755 --- a/setup.py +++ b/setup.py @@ -41,5 +41,7 @@ setup( classifiers = proj_info['classifiers'], - entry_points = {'console_scripts': proj_info['console_scripts']} + entry_points = {'console_scripts': proj_info['console_scripts']}, + install_requires = proj_info['install_requires'], + setup_requires = proj_info['setup_requires'] ) diff --git a/src/you_get/common.py b/src/you_get/common.py index 29900c0b..f7ee76a6 100755 --- a/src/you_get/common.py +++ b/src/you_get/common.py @@ -115,6 +115,7 @@ from .util import log, term from .util.git import get_version from .util.strings import get_filename, unescape_html from . import json_output as json_output_ +from .cookies import load_cookies dry_run = False json_output = False @@ -1250,32 +1251,7 @@ def script_main(script_name, download, download_playlist, **kwargs): dry_run = True info_only = False elif o in ('-c', '--cookies'): - try: - cookies = cookiejar.MozillaCookieJar(a) - cookies.load() - except: - import sqlite3 - cookies = cookiejar.MozillaCookieJar() - con = sqlite3.connect(a) - cur = con.cursor() - try: - cur.execute("SELECT host, path, isSecure, expiry, name, value FROM moz_cookies") - for item in cur.fetchall(): - c = cookiejar.Cookie(0, item[4], item[5], - None, False, - item[0], - item[0].startswith('.'), - item[0].startswith('.'), - item[1], False, - item[2], - item[3], item[3]=="", - None, None, {}) - cookies.set_cookie(c) - except: pass - # TODO: Chromium Cookies - # SELECT host_key, path, secure, expires_utc, name, encrypted_value FROM cookies - # http://n8henrie.com/2013/11/use-chromes-cookies-for-easier-downloading-with-python-requests/ - + cookies = load_cookies(a) elif o in ('-l', '--playlist'): playlist = True elif o in ('--no-caption',): diff --git a/src/you_get/cookies.py b/src/you_get/cookies.py new file mode 100644 index 00000000..af0bacaf --- /dev/null +++ b/src/you_get/cookies.py @@ -0,0 +1,194 @@ +import sys +import hashlib + +from ctypes import byref, c_void_p, c_buffer, c_char, c_uint16, c_uint32, c_int32, c_char_p, POINTER, Structure + +from http.cookiejar import Cookie, CookieJar, MozillaCookieJar + +from Crypto.Cipher import AES +# from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +# from cryptography.hazmat.backends import default_backend + + +# https://chromium.googlesource.com/chromium/src/+/master/components/os_crypt/os_crypt_linux.cc#80 +# https://chromium.googlesource.com/chromium/src/+/master/components/os_crypt/keychain_password_mac.mm#50 +def key_mac(browser="Chromium"): + service_name = ("Chromium Safe Storage" if browser == "Chromium" else "Chrome Safe Storage") + + # from github: keyring/backends/_OS_X_API.py + noErr = 0 + _sec = ctypes.CDLL('/System/Library/Frameworks/Security.framework/Versions/A/Security') + SecKeychainFindGenericPassword = _sec.SecKeychainFindGenericPassword + SecKeychainFindGenericPassword.argtypes = ( + c_void_p, + c_uint32, + c_char_p, + c_uint32, + c_char_p, + POINTER(c_uint32), # passwordLength + POINTER(c_void_p), # passwordData + POINTER(sec_keychain_item_ref), # itemRef + ) + SecKeychainFindGenericPassword.restype = c_uint32 + SecKeychainItemFreeContent = _sec.SecKeychainItemFreeContent + SecKeychainItemFreeContent.argtypes = ( + c_void_p, c_void_p, + ) + SecKeychainItemFreeContent.restype = c_uint32 + + passwdlen = c_uint32() + passwd = c_void_p() + status = SecKeychainFindGenericPassword(None, + len(service_name), + service_name, + len(browser), + browser, + passwdlen, + passwd, + None, + ) + if status != noErr: return "" + password = ctypes.create_string_buffer(passwdlen.value) + ctypes.memmove(password, passwd.value, passwdlen.value) + SecKeychainItemFreeContent(None, passwd) + return password.raw.decode('utf-8') +def key(browser="Chromium"): + if sys.platform == "linux": + return "peanuts" # v10 + elif sys.platform == "darwin": + return key_mac(browser) + return "" + + +# https://chromium.googlesource.com/chromium/src/+/master/crypto/symmetric_key.cc#53 +def encrypt_key(key, salt, iterations, key_size_in_bits): + return hashlib.pbkdf2_hmac("sha1", key, salt, iterations, key_size_in_bits / 8) + + +def decode(ciphertext): + if not ciphertext: return ciphertext + + if sys.platform == "win32": + return decode_windows(ciphertext) + + passwd = key(browser="Chromium") or key(browser="Chrome") + + if not passwd: return "" + if sys.platform == "linux": + text = decode_linux(ciphertext, passwd) + elif sys.platform == "darwin": + text = decode_macos(ciphertext, passwd) + else: return ciphertext + + right = text[-1] + if type(right) != int: right = ord(right) + return text[:-right].decode('utf-8', 'replace') + + +# https://chromium.googlesource.com/chromium/src/+/master/components/os_crypt/os_crypt_linux.cc#182 +# avaliable versions: v10 +def decode_linux(ciphertext, key="peanuts"): + # Salt for Symmetric key derivation. + kSalt = b"saltysalt" + # Key size required for 128 bit AES. + kDerivedKeySizeInBits = 128 + # Constant for Symmetic key derivation. + kEncryptionIterations = 1 + # Size of initialization vector for AES 128-bit. + kIVBlockSizeAES128 = 16 + + encrypted = encrypt_key(key.encode("utf-8"), kSalt, kEncryptionIterations, kDerivedKeySizeInBits) # v10 + iv = kIVBlockSizeAES128 * b" " + return AES.new(encrypted, AES.MODE_CBC, iv).decrypt(ciphertext[3:]) + # cipher = Cipher(algorithms.AES(encrypted), modes.CBC(iv), backend=default_backend()) + # d = cipher.decryptor() + # return d.update(ciphertext[3:]) + d.finalize() + + +# https://chromium.googlesource.com/chromium/src/+/master/components/os_crypt/os_crypt_win.cc#48 +def decode_windows(ciphertext): + from ctypes import cdll, windll + from ctypes.wintypes import DWORD + + class DATA_BLOB(Structure): + _fields_ = [("cbData", DWORD), ("pbData", POINTER(c_char))] + + def decrypt(ciphertext): + memcpy = cdll.msvcrt.memcpy + CryptUnprotectData = windll.crypt32.CryptUnprotectData + LocalFree = windll.kernel32.LocalFree + + bufferIn = c_buffer(ciphertext, len(ciphertext)) + blobIn = DATA_BLOB(len(ciphertext), bufferIn) + blobOut = DATA_BLOB() + if CryptUnprotectData(byref(blobIn), None, None, None, None, None, byref(blobOut)): + cbData = int(blobOut.cbData) + pbData = blobOut.pbData + buffer = c_buffer(cbData) + memcpy(buffer, pbData, cbData) + LocalFree(pbData) + return buffer.raw + else: + return "" + return decrypt(ciphertext) + + +# https://chromium.googlesource.com/chromium/src/+/master/components/os_crypt/os_crypt_mac.mm#133 +# avaliable versions: v10 +# TODO: need test +def decode_macos(ciphertext, key=""): + # Salt for Symmetric key derivation. + kSalt = b"saltysalt" + # Key size required for 128 bit AES. + kDerivedKeySizeInBits = 128 + # Constant for Symmetic key derivation. + kEncryptionIterations = 1003 + # TODO(dhollowa): Refactor to allow dependency injection of Keychain. + use_mock_keychain = False + + encrypted = encrypt_key(key.encode("utf-8"), kSalt, kEncryptionIterations, kDerivedKeySizeInBits) # v10 + iv = kIVBlockSizeAES128 * b" " + return AES.new(encrypted, AES.MODE_CBC, iv).decrypt(ciphertext[3:]) + # cipher = Cipher(algorithms.AES(encrypted), modes.CBC(iv), backend=default_backend()) + # d = cipher.decryptor() + # return d.update(ciphertext[3:]) + d.finalize() + + +def load_cookies(a): + try: + cookies = MozillaCookieJar(a) + cookies.load() + except: + import sqlite3 + + cookies = MozillaCookieJar() + con = sqlite3.connect(a) + cur = con.cursor() + + tables = cur.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall() + + stmt = "" + for t in tables: + if t[0] == "cookies": + stmt = "SELECT host_key, path, secure, expires_utc, name, value, encrypted_value FROM cookies"; break + elif t[0] == "moz_cookies": + stmt = "SELECT host, path, isSecure, expiry, name, value FROM moz_cookies"; break + if not stmt: return + + items = cur.execute(stmt).fetchall() + con.close() + + for item in items: + value = item[5] + if len(item) == 7 and not value: value = decode(item[-1]) + c = Cookie(0, item[4], value, + None, False, + item[0], + item[0].startswith('.'), + item[0].startswith('.'), + item[1], False, + item[2], + item[3], item[3]=="", + None, None, {}) + cookies.set_cookie(c) + return cookies diff --git a/you-get.json b/you-get.json index 594742c2..a862e285 100644 --- a/you-get.json +++ b/you-get.json @@ -36,5 +36,13 @@ "console_scripts": [ "you-get = you_get.__main__:main" + ], + + "setup_requires": [ + "setuptools_scm" + ], + + "install_requires": [ + "pycrypto" ] }