diff --git a/README.md b/README.md index 33aca5f..fc3182f 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 @@ -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 - - diff --git a/chrome.py b/chrome.py index 571f500..cca983e 100644 --- a/chrome.py +++ b/chrome.py @@ -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 @@ -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 """ @@ -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: @@ -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: @@ -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: @@ -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(',')= 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(',')