Skip to content

Commit

Permalink
[WIP] Add method to load login credentials as well as cookies
Browse files Browse the repository at this point in the history
The machinery for decrypting login credentials on Chromium-based browsers is
essentially identical to what's used to decrypt cookies.  For a fairly hacky
standalone implementation of login credential decryption, see
https://github.com/priyankchheda/chrome_password_grabber.

Done:

- Add ChromiumBased.load_logins() method.
- Autodetect paths for login credentials on Chrome and Chromium browsers on
  Linux. (Tested both)

TODO:

- Determine correct paths for login credentials on Windows, macOS, and other
  Chromium-based browsers.

  For now, you should be able to test whether the same decryption process
  works by finding the login credentials database path, and passing passing
  `login_file='THAT_PATH'` to the constructor for any `ChromiumBased`
  browser.
- Add loading and decryption of login credentials from Firefox as well.
  Perhaps based on https://github.com/unode/firefox_decrypt?
  • Loading branch information
dlenski committed Apr 16, 2023
1 parent ed0370e commit 217d331
Showing 1 changed file with 66 additions and 8 deletions.
74 changes: 66 additions & 8 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import struct
import subprocess
import sys
from collections import namedtuple
from io import BytesIO
from pathlib import Path
from typing import Union
from urllib.parse import urlsplit

import sqlite3

Expand Down Expand Up @@ -318,25 +320,28 @@ def __get_kdewallet_password_jeepney(self, folder, key):
class ChromiumBased:
"""Super class for all Chromium based browsers"""

def __init__(self, browser:str, cookie_file=None, domain_name="", key_file=None, **kwargs):
def __init__(self, browser:str, cookie_file=None, domain_name="", key_file=None, login_file=None, **kwargs):
self.salt = b'saltysalt'
self.iv = b' ' * 16
self.length = 16
self.browser = browser
self.cookie_file = cookie_file
self.login_file = login_file
self.domain_name = domain_name
self.key_file = key_file
self.__add_key_and_cookie_file(**kwargs)

def __add_key_and_cookie_file(self,
linux_cookies=None, windows_cookies=None, osx_cookies=None,
windows_keys=None, os_crypt_name=None, osx_key_service=None, osx_key_user=None):
windows_keys=None, os_crypt_name=None, osx_key_service=None, osx_key_user=None,
linux_logins=[]):

if sys.platform == 'darwin':
password = _get_osx_keychain_password(osx_key_service, osx_key_user)
iterations = 1003 # number of pbkdf2 iterations on mac
self.v10_key = PBKDF2(password, self.salt, self.length, iterations)
cookie_file = self.cookie_file or _expand_paths(osx_cookies,'osx')
login_file = None # TODO

elif sys.platform.startswith('linux') or 'bsd' in sys.platform.lower():
password = _LinuxPasswordManager(USE_DBUS_LINUX).get_password(os_crypt_name)
Expand All @@ -345,6 +350,7 @@ def __add_key_and_cookie_file(self,
self.v11_key = PBKDF2(password, self.salt, self.length, iterations)

cookie_file = self.cookie_file or _expand_paths(linux_cookies, 'linux')
login_file = self.login_file or _expand_paths(linux_logins,'linux')

elif sys.platform == "win32":
key_file = self.key_file or _expand_paths(windows_keys,'windows')
Expand All @@ -368,6 +374,8 @@ def __add_key_and_cookie_file(self,
else:
cookie_file = _expand_paths(windows_cookies,'windows')

login_file = None # TODO

else:
raise BrowserCookieError(
"OS not recognized. Works on OSX, Windows, and Linux.")
Expand All @@ -376,6 +384,7 @@ def __add_key_and_cookie_file(self,
raise BrowserCookieError('Failed to find {} cookie'.format(self.browser))

self.cookie_file = cookie_file
self.login_file = login_file

def __str__(self):
return self.browser
Expand Down Expand Up @@ -420,6 +429,37 @@ def load(self):
con.close()
return cj

def load_logins(self):
"""Load saved login credentials into a dictionary"""
if not self.login_file:
raise BrowserCookieError('{} saved logins database file not found, or not yet implemented'.format(self.browser))

con = _sqlite3_connect_readonly(self.login_file)
con.text_factory = _text_factory
cur = con.cursor()
cur.execute('SELECT signon_realm, username_value, password_value, date_last_used, date_created, date_password_modified, times_used '
'FROM logins WHERE signon_realm like ? ORDER BY signon_realm, username_value;', ('%{}%'.format(self.domain_name),))

logins = []
for item in cur:
signon_realm, username, password_enc, accessed_nt, created_nt, modified_nt, times_used = item
password=self._decrypt(None, password_enc)
if not username and not password:
continue # No value in exporting this

p = urlsplit(signon_realm)
logins.append(LoginCredential(
host=p.netloc, path=p.path if p.path not in ('', None, '/') else None,
username=username, password=password,
accessed=self._nt_to_unix_timestamp(accessed_nt),
created=self._nt_to_unix_timestamp(created_nt),
modified=self._nt_to_unix_timestamp(modified_nt),
secure=(p.scheme == 'https'),
times_used=times_used,
))
con.close()
return logins

@staticmethod
def _decrypt_windows_chromium(value, encrypted_value):

Expand Down Expand Up @@ -480,10 +520,9 @@ def _decrypt(self, value, encrypted_value):
raise BrowserCookieError('Unable to get key for cookie decryption')
return decrypted.decode('utf-8')


class Chrome(ChromiumBased):
"""Class for Google Chrome"""
def __init__(self, cookie_file=None, domain_name="", key_file=None):
def __init__(self, cookie_file=None, domain_name="", key_file=None, login_file=None):
args = {
'linux_cookies': _genarate_nix_paths_chromium(
[
Expand All @@ -492,6 +531,15 @@ def __init__(self, cookie_file=None, domain_name="", key_file=None):
],
channel=['', '-beta', '-unstable']
),
'linux_logins': _genarate_nix_paths_chromium(
[
'~/.config/google-chrome{channel}/Default/Login Data For Account',
'~/.config/google-chrome{channel}/Profile */Login Data For Account',
'~/.config/google-chrome{channel}/Default/Login Data',
'~/.config/google-chrome{channel}/Profile */Login Data'
],
channel=['', '-beta', '-unstable']
),
'windows_cookies': _genarate_win_paths_chromium(
[
'Google\\Chrome{channel}\\User Data\\Default\\Cookies',
Expand All @@ -516,17 +564,21 @@ def __init__(self, cookie_file=None, domain_name="", key_file=None):
'osx_key_service' : 'Chrome Safe Storage',
'osx_key_user' : 'Chrome'
}
super().__init__(browser='Chrome', cookie_file=cookie_file, domain_name=domain_name, key_file=key_file, **args)
super().__init__(browser='Chrome', cookie_file=cookie_file, domain_name=domain_name, key_file=key_file, login_file=login_file, **args)


class Chromium(ChromiumBased):
"""Class for Chromium"""
def __init__(self, cookie_file=None, domain_name="", key_file=None):
def __init__(self, cookie_file=None, domain_name="", key_file=None, login_file=None):
args = {
'linux_cookies':[
'~/.config/chromium/Default/Cookies',
'~/.config/chromium/Profile */Cookies'
],
'linux_logins':[
'~/.config/chromium/Default/Login Data',
'~/.config/chromium/Profile */Login Data'
],
'windows_cookies': _genarate_win_paths_chromium(
[
'Chromium\\User Data\\Default\\Cookies',
Expand All @@ -546,7 +598,7 @@ def __init__(self, cookie_file=None, domain_name="", key_file=None):
'osx_key_service' : 'Chromium Safe Storage',
'osx_key_user' : 'Chromium'
}
super().__init__(browser='Chromium', cookie_file=cookie_file, domain_name=domain_name, key_file=key_file, **args)
super().__init__(browser='Chromium', cookie_file=cookie_file, domain_name=domain_name, key_file=key_file, login_file=login_file, **args)


class Opera(ChromiumBased):
Expand Down Expand Up @@ -714,7 +766,7 @@ def __init__(self, cookie_file=None, domain_name="", key_file=None):

class Firefox:
"""Class for Firefox"""
def __init__(self, cookie_file=None, domain_name=""):
def __init__(self, cookie_file=None, domain_name="", key_file=None, login_file=None):
self.cookie_file = cookie_file or self.find_cookie_file()
# current sessions are saved in sessionstore.js
self.session_file = os.path.join(
Expand Down Expand Up @@ -845,6 +897,9 @@ def load(self):

return cj

def load_logins(self):
raise NotImplementedError('Loading login credentials is not yet supported for Firefox')


class Safari:
"""Class for Safari"""
Expand Down Expand Up @@ -961,6 +1016,9 @@ def load(self):
return cj


LoginCredential = namedtuple('LoginCredential', 'host path username password accessed created modified secure times_used')


def create_cookie(host, path, secure, expires, name, value, http_only):
"""Shortcut function to create a cookie"""
# HTTPOnly flag goes in _rest, if present (see https://github.com/python/cpython/pull/17471/files#r511187060)
Expand Down

0 comments on commit 217d331

Please sign in to comment.