概要:文末提供了
Python 2.x
和3.x
实现的获取macOS
和Linux
环境下Chrome浏览器加密cookies脚本的地址
问题背景
最近我尝试使用脚本,实现终端读取jira面板任务 快速创建子任务指派等功能。整个实现的过程并不复杂,利用Chrome分析各类操作的请求,通过Python模拟即可。
但是HTTP携带的Cookies如何去获取更新确实成为我需要思考的一个问题。
对比普通的爬虫抓取数据,两个场景不同的是
这个脚本始终是我日常工作的电脑上执行操作。
在终端操作的同时,还是会有一些意外的情况需要我通过浏览器去访问Jira页面。脚本的模拟登录和浏览器手动登录都会造成对方Cookies的过期。
所以最好的处理方式是脚本从Chrome获取Cookie,两者共享一份数据。
在macOS系统上,有关Cookies的内容存储在路径~/.config/google-chrome/Default/Cookies
下,这是一个sqlite
数据库文件,通过一个可视化的工具我们可以轻松查看表的格式。
Cookies
其中encrypted_value
就是我们需要的Cookies!让我们通过代码先把数据库内容读取出来。
def get_cookies_filepath(): return '~/Library/Application Support/Google/Chrome/Default/Cookies'def fetch_cookies_from_chrome(): sql = ('select host_key, path, ' + secure_column_name + ', expires_utc, name, value, encrypted_value ' 'from cookies where host_key like ?') with sqlite3.connect(get_cookies_filepath()) as connect: for hk, path, is_secure, expires_utc, cookie_key, val, enc_val \ in conn.execute(sql, (host_key,)): print enc_val
可惜的是这部分的内容是加密的,如何解密成了需要我们解决的难题。
考虑到Chrome的部分实现是开源项目,我尝试去Google这部分的源码。在Github - chromium上找到了相关实现的内容。
README
中提到OSCrypt
实现了一个简单的字符串加密。不同系统上的加密并不完全医院,在Linux和Mac上,Chrome使用了各自提供的系统服务来进行加解密
在文件目录下我找到了自己想要的文件os_crypt_mac.mm
,虽然是用C++实现的,但文件并不大,结合注释还是可以很轻松的理解代码内容的。
让我们来一点点分析一下它
// Generates a newly allocated SymmetricKey object based on the password found// in the Keychain. The generated key is for AES encryption. Returns NULL key// in the case password access is denied or key generation error occurs.crypto::SymmetricKey* GetEncryptionKey()
整个GetEncryptionKey()
函数的工作就是查看是否缓存了SymmetricKey
,如果没有就基于keychain
里取出的password
生成一个SymmetricKey
,用于AES的加密。
// Create an encryption key from our password and salt. The key is // intentionally leaked. cached_encryption_key = crypto::SymmetricKey::DeriveKeyFromPassword( crypto::SymmetricKey::AES, password, salt, kEncryptionIterations, kDerivedKeySizeInBits) .release();
生成SymmetricKey
的函数如上,我们可以获取到的有效信息是
加密方式是
AES
password的获取来源是
keychain
。我尝试在系统的钥匙串管理
里搜索Chrome
,确实得到了想要的东西。
keychain
salt
、kEncryptionIterations
和kDerivedKeySizeInBits
是定义的常量
// Salt for Symmetric key derivation.const char kSalt[] = "saltysalt";// Key size required for 128 bit AES.const size_t kDerivedKeySizeInBits = 128;// Constant for Symmetic key derivation.const size_t kEncryptionIterations = 1003;
整个密钥的构建参数都已经明确,让我们改用Python来实现它
CHROME_COOKIES_ENCRYPTION_ITERATIONS = 1003CHROME_COOKIES_ENCRYPTION_SALT = b'saltysalt'CHROME_COOKIES_ENCRYPTION_DKLEN = 16def get_password_from_keychain(isChrome=True): browser = 'chrome' if isChrome else 'chromium' return keyring.get_password(browser + 'Safe Storage', browser)def get_cookies_erncrypt_key(isChrome=True): return pbkdf2_hmac(hash_name='sha1', password=get_password_from_keychain(isChrome).encode('utf8'), salt=CHROME_COOKIES_ENCRYPTION_SALT, iterations=CHROME_COOKIES_ENCRYPTION_ITERATIONS, dklen=CHROME_COOKIES_ENCRYPTION_DKLEN)
pbkdf2_hmac()
的dklen
参数对应的是kDerivedKeySizeInBits
。因为生成的密钥是128 bit
,按照8 bit
一个字节计算,dklen
的长度就是16
成功获取了密钥以后让我们看一下解密的流程
源码中解密的实现如图
bool OSCrypt::DecryptString(const std::string& ciphertext, std::string* plaintext) { if (ciphertext.empty()) { *plaintext = std::string(); return true; } // Check that the incoming cyphertext was indeed encrypted with the expected // version. If the prefix is not found then we'll assume we're dealing with // old data saved as clear text and we'll return it directly. // Credit card numbers are current legacy data, so false match with prefix // won't happen. if (ciphertext.find(kEncryptionVersionPrefix) != 0) { *plaintext = ciphertext; return true; } // Strip off the versioning prefix before decrypting. std::string raw_ciphertext = ciphertext.substr(strlen(kEncryptionVersionPrefix)); crypto::SymmetricKey* encryption_key = GetEncryptionKey(); if (!encryption_key) { VLOG(1) << "Decryption failed: could not get the key"; return false; } std::string iv(kCCBlockSizeAES128, ' '); crypto::Encryptor encryptor; if (!encryptor.Init(encryption_key, crypto::Encryptor::CBC, iv)) return false; if (!encryptor.Decrypt(raw_ciphertext, plaintext)) { VLOG(1) << "Decryption failed"; return false; } return true; }
解密的参数已经非常明确了
iv
是由16个' '
组成的string
,kCCBlockSizeAES128
是定义在#include <CommonCrypto/CommonCryptor.h>
里的一个常量AES
的模式是CBC
Python
的实现如下
def chrome_decrypt(encrypt_string, isChrome=True): cipher = AES.new(get_cookies_erncrypt_key(isChrome), AES.MODE_CBC, IV=b' ' * 16) decrypted_string = cipher.decrypt(encrypt_string) return decrypted_string
事实上到这一步已经基本完成了。读取数据库,依据host_key
拿到需要的加密cookies,逐个解密即可。但是这里仍然还要几个细节需要处理。
Chrome之前的版本
cookies
实际并未加密,当然也无法保证以后加密方式是否会发生改变。为了区分这部分的内容以及方便为了后续的数据迁移,加密的cookies都会有一个固定的v10
的前缀。
// Prefix for cypher text returned by current encryption version. We prefix// the cypher text with this string so that future data migration can detect// this and migrate to different encryption without data loss.const char kEncryptionVersionPrefix[] = "v10";
似乎现在有
v11
的版本,但我没有遇到所以后面的脚本并未添加
解密之后的结果会有大串的空白字符,应该是填充留下的内容。最好手动清理一下
实际除了
Chrome
还有Chromiunm
的存在,两者cookies
的存储路径以及keychain
的名称各不相同。需要分别处理一下。
结语
实际在寻找解决方案的过程中,我找到了Python 3.4
的一个解决方案n8henrie-pycookiecheat。n8henrie的实现还添加了对Linux的支持。
因为Python 2.x
和Python 3.x
还是有所区别,为了自己的需要我还是用Python 2.7
做了一个实现,支持macOS
。地址在这里。作者比较懒,Linux
的实现应该非常类似,我没这方面需求就不写了 = =
整个实现并不复杂,有趣的应该是找出解决方案的过程。最近在写workflow
的脚本,确实给我带来了一些有趣的问题,后续会慢慢整理出来。
欢迎关注
作者:Noskthing
链接:https://www.jianshu.com/p/c94363c33bae