概要:文末提供了
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
数据库文件,通过一个可视化的工具我们可以轻松查看表的格式。
其中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
,确实得到了想要的东西。
-
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 = 1003
CHROME_COOKIES_ENCRYPTION_SALT = b'saltysalt'
CHROME_COOKIES_ENCRYPTION_DKLEN = 16
def 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
里的一个常量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
的脚本,确实给我带来了一些有趣的问题,后续会慢慢整理出来。
:)