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

:)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章