为了账号安全,请及时绑定邮箱和手机立即绑定

Python实现获取macOS系统Chrome的Cookies数据

标签:
Python

webp

概要:文末提供了Python 2.x3.x实现的获取macOSLinux环境下Chrome浏览器加密cookies脚本的地址

问题背景

最近我尝试使用脚本,实现终端读取jira面板任务 快速创建子任务指派等功能。整个实现的过程并不复杂,利用Chrome分析各类操作的请求,通过Python模拟即可。

但是HTTP携带的Cookies如何去获取更新确实成为我需要思考的一个问题。

对比普通的爬虫抓取数据,两个场景不同的是

  1. 这个脚本始终是我日常工作的电脑上执行操作。

  2. 在终端操作的同时,还是会有一些意外的情况需要我通过浏览器去访问Jira页面。脚本的模拟登录和浏览器手动登录都会造成对方Cookies的过期。

所以最好的处理方式是脚本从Chrome获取Cookie,两者共享一份数据。


在macOS系统上,有关Cookies的内容存储在路径~/.config/google-chrome/Default/Cookies下,这是一个sqlite数据库文件,通过一个可视化的工具我们可以轻松查看表的格式。

webp

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,确实得到了想要的东西。

webp

keychain

  • saltkEncryptionIterationskDerivedKeySizeInBits是定义的常量

// 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个' '组成的stringkCCBlockSizeAES128是定义在#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.xPython 3.x还是有所区别,为了自己的需要我还是用Python 2.7做了一个实现,支持macOS。地址在这里。作者比较懒,Linux的实现应该非常类似,我没这方面需求就不写了 = =

整个实现并不复杂,有趣的应该是找出解决方案的过程。最近在写workflow的脚本,确实给我带来了一些有趣的问题,后续会慢慢整理出来。



欢迎关注

webp



作者:Noskthing
链接:https://www.jianshu.com/p/c94363c33bae


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消