Chromium: Linux, keyrings && Secret Service, шифрование и хранение паролей

Автор: | 09/12/2019
 

Одним из поводов настолько углубиться в keyrings (см. What is: Linux keyring, gnome-keyring, Secret Service, и D-Bus) был факт того, что Chromium, внезапно, при отсутствии keyring в Linux будет хранить пароли в “открытом виде”.

Собственно, давайте попробуем найти – как и где Chromium хранит пароли, и, самое важное – шифрует ли он их?

Chromium и keyring

Документация нам говорит:

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.

Т.е:

  1. пароли Chromium хранит в SQLite базе “Login Data” ('/home/setevoy/.config/chromium/Default/Login Data')
  2. если gnome-keyring или KWallet установлены – то Chrome будет шифровать данные в базе с помощью пароля, который Chromium хранит в этом хранилище
  3. если их нет, и Secret Service не активен – то пароли будут храниться в открытом виде, plain text

Проверим.

Chromium без keyring

Попробуем получить пароль из базы с Chromium без keyring.

Что бы указать ему – какой бекенд использовать, можно использовать опцию google-chrome --password-store=basic – для хранения plain text.

Другие параметры – gnome или kwallet, см. man chromium:

[simterm]

--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.)

[/simterm]

Кроме того – проверим D-Bus сервисы, что бы убедиться, что никакой Secret Service сейчас не активен:

[simterm]

$ qdbus --session org.freedesktop.DBus / org.freedesktop.DBus.GetConnectionUnixProcessID org.freedesktop.secrets
Error: org.freedesktop.DBus.Error.NameHasNoOwner
Could not get PID of name 'org.freedesktop.secrets': no such name

[/simterm]

Хорошо.

Создаём каталог для данных Chromium:

[simterm]

$ mkdir /tmp/data-chrome-test-1

[/simterm]

Запускаем браузер с указанием home на эту директорию и –password-store=basic, что бы сохрнаять пароли в открытом виде:

[simterm]

$ chromium --user-data-dir=/tmp/data-chrome-test-1 --password-store=basic

[/simterm]

Логинимся куда-то, сохраняем пароль, закрываем браузер, проверяем базу:

[simterm]

$ sqlite3 /tmp/data-chrome-test-1/Default/Login\ Data 'select username_value, password_value from logins;'
testuser|v10�#�c�yF�b�f���

[/simterm]

Отлично – в поле password_value SQLite базы какой-то пароль есть.

Кстати, просмотреть все поля и их типы в таблице logins можно с помощью .schema:

[simterm]

$ sqlite3 ~/.config/chromium/Default/Login\ Data '.schema logins'
CREATE TABLE IF NOT EXISTS "logins" (origin_url VARCHAR NOT NULL, action_url VARCHAR, \
username_element VARCHAR, username_value VARCHAR, password_element VARCHAR, password_value BLOB \
...));
CREATE INDEX logins_signon ON logins (signon_realm);

[/simterm]

Chromium с keyring

Теперь попробуем получить пароль из SQLite базы с Chromium, который использует keyring. Для этого – включаем поддержку Secret Service в KeePass, который нам заменит “стандартный”  gnome-keyring, и создаём второй каталог для данных Chromium:

[simterm]

$ mkdir /tmp/data-chrome-test-2

[/simterm]

Запускаем браузер, но теперь указываем --password-store=gnome, и меняем user-data-dir, что бы использовать чистую SQLite базу с паролями:

[simterm]

$ chromium --user-data-dir=/tmp/data-chrome-test-2 --password-store=gnome

[/simterm]

Ещё раз логинимся, сохраняем пароль, проверяем базу:

[simterm]

$ sqlite3 /tmp/data-chrome-test-2/Default/Login\ Data 'select username_value, password_value from logins;'
testuser|v11���H!@���2�jA�

[/simterm]

Ага!

Во-первых – префикс тут v11, а не v10, во-вторых – строка явно длиннее.

Chromium passwords decrypt

Теперь попробуем разобраться:

  1. во-первых – пароль из “незашифрованого” хранилища отдаётся явно не plain text – почему?
  2. во-вторых – как получить пароль из “зашифрованого” хранилища?

Для проверки – поковыряем исходники.

Chrome v10 vs v11

Для начала – что за v10 и v11?

Смотрим содержимое os_crypt_linux.cchttps://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_linux.cc?l=38:

...
// 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():

  1. DecryptString(): в version сохраняет версию (v10 или v11):
    ...
      if (base::StartsWith(ciphertext, kObfuscationPrefix[Version::V10],
                           base::CompareCase::SENSITIVE)) {
        version = Version::V10;
    ...

    https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_linux.cc?l=197

  2. 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;
      }
    ...

    https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_linux.cc?gsn=GetEncryptionKey&l=211

  3. GetEncryptionKey() в свою очередь вызывает g_get_password():
    ...
      std::string* password = g_get_password[version]();
      if (!password)
        return nullptr;
    ...

    https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_linux.cc?gsn=GetEncryptionKey&l=119

  4. а g_get_password() является указателем на функции GetPasswordV10() и GetPasswordV11():
    ...
    std::string* (*g_get_password[])() = {
        &GetPasswordV10, &GetPasswordV11,
    };
    ...

    https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_linux.cc?l=109

  5. а в функции GetPasswordV10()… Мы видим… Что??? Пароль?!?
    ...
      if (!g_cache.Get().password_v10_cache.get()) {
        g_cache.Get().password_v10_cache.reset(new std::string("peanuts"));
      }
    ...

    https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_linux.cc?l=86

Так что всё оказалось намного проще:

  1. если keyring есть – то Chromium сгенерирует мастер-пароль, и сохранит в базе этого keyring-сервиса
  2. если 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;
...

Следовательно, что бы расшифровать запись из базы данных – нам нужны такие данные, всё указано сразу в файле в переменных:

  1. мастер-пароль password – именно он возвращается в GetPasswordV10() или GetPasswordV11()
  2. соль – const char kSalt[] = "saltysalt"
  3. итерации – const size_t kEncryptionIterations = 1
  4. длина ключа – 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)

Запускаем:

[simterm]

$ ./get_chrome_pass.py
Decrypting the string: b'v10\xd4#\xd6c\xabyF\xc6b\xdef\x06\xce\x14\xe3\xc5'
test911911

[/simterm]

Отлично – есть наш пароль.

В этот раз мы получили его из “незашифрованного” хранилища.

Chromium && Secret Service

Теперь – включаем поддержку Secret Storage в KeePass (см What is: Linux keyring, gnome-keyring, Secret Service, и D-Bus), что бы эмулировать установленный gnome-keepass, перезапускаем KeePass.

Добавляем папку для новой базы данных Chromium:

[simterm]

$ mkdir /tmp/data-chrome-test-2/

[/simterm]

Запускаем Chromium с каталогом данных data-chrome-test-2 из неё и указываем --password-store=gnome:

[simterm]

$ chromium --user-data-dir=/tmp/data-chrome-test-2/ --password-store=gnome

[/simterm]

Логинимся, сохраняем пароль ещё раз – и Chromium в KeePass создал нам две записи:

  1. Chrome Safe Storage Control
  2. Chromium Safe Storage

Смотрим их атрибуты – там всё указано:

Можно посмотреть через D-Bus и Secret Service, что бы проверить коллекцию:

[simterm]

$ secret-tool search Title 'Chromium Safe Storage'
[/org/freedesktop/secrets/collection/Main/f0fdc4706ef44958b716e28c13d66bed]
label = Chromium Safe Storage
secret = P5pUwxbWaIBBVU0+LATOcw==
...
schema = chrome_libsecret_os_crypt_password_v2
...
attribute.Title = Chromium Safe Storage
...
attribute.application = chromium
...
attribute.Path = /Chromium Safe Storage

[/simterm]

Теперь попробуем использовать этот пароль (значение атрибута secret выше) из Chromium Safe Storage в качестве значения переменной pb_pass в нашем скрипте.

Выходим из Chromium, что бы разлочить базу, иначе будет ошибка вида:

[simterm]

$ ./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

[/simterm]

В скрипте меняем базу (путь к ней):

...
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:

...
#    pb_pass = "peanuts".encode('utf8')                                                                                                                                                                                                       
    pb_pass = "P5pUwxbWaIBBVU0+LATOcw==".encode('utf8')
...

Пробуем:

[simterm]

$ ./get_chrome_pass.py
Decrypting the string: b'v11\xfc\x82\xf7H\n!@\x86\xb7\x982\xa8\x1fjA\xfd'
test911911

[/simterm]

Готово – получили пароль из зашифрованного значения SQLite базы.

Вывод

Собственно, самое важное – пароли в SQLite шифруются всегда. Просто для систем, в которых нет бекенда типа gnome-keyring с поддержкой Secret Service – то пароль для шифрования всегда будет один и тот же для всех пользователей Chromium, что, разумеется, нельзя рассматривать, как защиту.