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

概要:文末提供了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数据库文件,通过一个可视化的工具我们可以轻松查看表的格式。

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
  • 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 = 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个' '组成的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的脚本,确实给我带来了一些有趣的问题,后续会慢慢整理出来。

欢迎关注
v2-8db7982e22615b7e49fe095009798785_hd.jpg

:)

推荐阅读更多精彩内容

  • 用到的组件 1、通过CocoaPods安装 2、第三方类库安装 3、第三方服务 友盟社会化分享组件 友盟用户反馈 ...
    Silence_广阅读 8,115评论 4 158
  • 一转眼21天过去了,每天一篇日记俨然成为习惯。 感谢群里的辣妈们关注每篇日记,让我感到写日记的价值。不但学到东西还...
    佳_ed21阅读 27评论 0 0
  • 这是一篇迟来的文章,我们家的小可乐公主已经28天了,还记得刚从产房里面推出来的时候,她睡在她妈妈身边,看上去是那么...
    风中的条子阅读 33评论 0 0
  • 本文转自微信公开课公众号 2016-03-07 微信公开课 近日,接大量用户举报,称在自己的朋友圈或者群聊中流传所...
    美慧公馆阅读 52评论 0 0
  • 南宫长万那日接到宋闵公之令,着上银光闪闪铠甲,手握长戟,列阵于郎城东南,盟军齐师列阵于东北,对阵鲁军,以雪齐军长勺...
    漠陌蚺阅读 80评论 0 0