概要:文末提供了
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 <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.x
和Python 3.x
還是有所區別,爲了自己的需要我還是用Python 2.7
做了一個實現,支持macOS
。地址在這裏。作者比較懶,Linux
的實現應該非常類似,我沒這方面需求就不寫了 = =
整個實現並不複雜,有趣的應該是找出解決方案的過程。最近在寫workflow
的腳本,確實給我帶來了一些有趣的問題,後續會慢慢整理出來。
:)