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

:)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,227评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,755评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,899评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,257评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,617评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,757评论 1 221
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,982评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,715评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,454评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,666评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,148评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,512评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,156评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,112评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,896评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,809评论 2 279
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,691评论 2 272

推荐阅读更多精彩内容

  • 用到的组件 1、通过CocoaPods安装 2、第三方类库安装 3、第三方服务 友盟社会化分享组件 友盟用户反馈 ...
    SunnyLeong阅读 14,503评论 1 180
  • 这是一篇迟来的文章,我们家的小可乐公主已经28天了,还记得刚从产房里面推出来的时候,她睡在她妈妈身边,看上去是那么...
    风中的条子阅读 226评论 0 0
  • 本文转自微信公开课公众号 2016-03-07 微信公开课 近日,接大量用户举报,称在自己的朋友圈或者群聊中流传所...
    美慧公馆阅读 190评论 0 0
  • 南宫长万那日接到宋闵公之令,着上银光闪闪铠甲,手握长戟,列阵于郎城东南,盟军齐师列阵于东北,对阵鲁军,以雪齐军长勺...
    漠陌蚺阅读 457评论 0 0