Chromium: Linux, keyrings && Secret Service, passwords encryption and store

By | 12/10/2019
 

One of the motives to go deeper into the keyrings (see the What is: Linux keyring, gnome-keyring, Secret Service, and D-Bus post) was the fact that Chromium, surprise-surprise, keep passwords unencrypted if a Linux system has no keyring and/or Secret Service enabled.

So, let’s try to find how and where Chromium store passwords, and the main question – are they stored encrypted, or no?

Chromium and keyring

The documentation states that:

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.

I.e.:

  1. Chromium stores passwords in a local SQLite database called “Login Data” ('/home/setevoy/.config/chromium/Default/Login Data')
  2. if gnome-keyring or KWallet are installed then Chromium will encrypt passwords in its database using a generated password which will be stored in such a keyring
  3. in other case and if Secret Service is not active at all – passwords will be kept “as is”, plain text

Well – let’s check it.

Chromium without a keyring

At first, let’s try to obtain a password from an SQLite when a system has no keyring configured.

To specify which backend to use we can add the google-chrome --password-store=basic option to store passwords as a “plain text”.

Also, check for other parameters – gnome or kwallet, see the 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.)

Besides that, let’s check D-Bus services to make sure we have no Secret Service enabled:

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

Okay.

Now, create a new directory for Chromium’s data:

mkdir /tmp/data-chrome-test-1

Run the browser with this directory as its home and with the –password-store=basic option to keep passwords “unencrypted”:

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

Now, log in to some website, close the browser, and check its database:

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

Cool – the password_value field of the SQLite database now ahs some password stored.

By the way, you can get a table’s scheme by using the .schema command:

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

Chromium with a keyring

Now, let’s go further and let’s try to obtain a password from a Chromium’s SQLite database when a keyring is enabled. For this, enable Secret Service support in the KeePass, which we will use instead of the “standard”  gnome-keyring, and create a new directory for Chromium’s data:

mkdir /tmp/data-chrome-test-2

Run browser again, but this time specify --password-store=gnome, and change the user-data-dir to use a new, clear SQLite database:

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

Log in somewhere again, save a password, check the database:

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

Aha!

At-first – the prefix here is v11, instead of the v10, as it was in the password from the “unencrypted” database, at second – the string itself is longer.

So – passwords are stored in different ways. But – what about their encryption here?

Chromium passwords decrypt

Let’s try to figure out the following:

  1. first – we got the password from the “unencrypted” storage not as plain text – why so?
  2. second – how can we get a readable password from the “encrypted” storage?

To see this – let’s go to the Chromium’s source code.

Chrome v10 vs v11

First – what are those v10 and v11 prefixes?

Go to the os_crypt_linux.cc file content – https://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,
};
...

Aha, so v10 is a prefix for unencrypted passwords, while v11 – for encrypted.

Although in both cases we got our passwords as some weird symbols string, not a password in a clear way.

Will see now why so.

Start from the end of the file, from the DecryptString() function:

  1. DecryptString(): stores a version in the version variable (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() trying to get an encryption key by calling the GetEncryptionKey() function passing the 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() in its turn calls the g_get_password() function:
    ...
      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() is a pointer to the GetPasswordV10() and GetPasswordV11() functions:
    ...
    std::string* (*g_get_password[])() = {
        &GetPasswordV10, &GetPasswordV11,
    };
    ...

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

  5. and inside of the GetPasswordV10()… We are seeing… What??? Password?!?
    ...
      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

So, it was much easier:

  1. if a keyring service is present – then Chromium will generate a password, will store it in this keyring
  2. if no keyring present and/or Secret Service is not enabled – then Chromium will use its hardcoded master-password “peanuts

And later using this password – will be constructed a master-key to be used to encrypt passwords in the SQLite database.

Python script to obtain Chromium’s passwords

Now, let’s check all of this in a practice, to see is it works as we suggested from the code above.

Among other things, we can see that here is AES 128 bit CBC encryption used:

...
// Key size required for 128 bit AES.
const size_t kDerivedKeySizeInBits = 128;
...

Next, in the GetEncryptionKey() we can see how the encryption key is constructed:

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

Thus, to decrypt a record from the database we need in the following data, which is set in the file’s const variables:

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

To decrypt passwords – googled such a simple script here>>>, after a bit updating it for self-usage I  got the next code:

#! /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)

Run it:

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

Great – we got our password from the unencrypted database.

Chromium && Secret Service

Now, enable Secret Storage support in the KeePass (see the What is: Linux keyring, gnome-keyring, Secret Service, и D-Bus) to emulate an installed gnome-keepass and restart KeePass.

Add a new directory for the Chromium’s data:

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

Run Chromium with the data-chrome-test-2 data-directory and specify the --password-store=gnome option:

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

Log in somewhere, save password again, and now we can observe that Chromium created two new records in the KeePass database:

  1. Chrome Safe Storage Control
  2. Chromium Safe Storage

Check their attributes for more information about each:

Also, you can check the D-Bus and Secret Service to see which collection is used now:

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

Now, let’s try to use the password from the secret attribute from the Chromium Safe Storage entry as the pb_pass variable’s value in our script.

Exit from the Chromium to unlock the database, otherwise, you’ll see the following error:

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

Update the script – a database’s path:

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

And the password – instead of the “peanuts” in the pb_pass set the value taken from the KeePass’s Chromium Safe Storage entry:

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

Try it:

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

Cool! We got our decrypted password.

Summary

Actually, the main thing I tried to figure out: password in the Chromium’s SQLite database are always stored in an encrypted way, not just a plain-text.

Just for the systems with no keyring service like gnome-keyring with the Secret Service enabled – the password used for the encryption will be the same overall Chromium’s user, which is obviously can not be assumed as a secure approach.



Also published on Medium.