How to read the key3.db file?

  • The file key3.db contains the key that is used to encrypt the Firefox passwords stored in logins.json file.

    I don't use master password in Firefox, but according to this article, my passwords are encrypted with a random key that is stored in key3.db file.

    I tried to open the file using the SqliteBrowser, but I got an error: "Invalid file format". Can anyone know how to read this file so I will be able to get the key for my Firefox passwords?

  • If no master password is set, Firefox uses an empty password.

    For a dirty Python script to decrypt and dump the data (plus some more), see the following code. It is an old project of mine and I just updated the reading of the passwords and usernames which were saved in signons.sqlite before (now in logins.json). I can't guarantee that (everything) is working, but it did in my test case.

    #!/usr/bin/env python
    
    from ctypes import *
    import struct
    import base64
    import platform
    import os
    import sqlite3
    import json
    
    # TODO: if python 3. use
    #from configParser import ConfigParser
    # else
    from ConfigParser import ConfigParser
    
    #
    # Some little firefox forensic script delivering following data:
    # - saved passwords (decrypted if no master pw is set)
    # - visited pages
    # - download history
    # - form history
    # - cookies 
    #
    # TODO:
    # - flash cookies and stuff
    # - finish comments
    # - add little brute forcer for master pw
    # - dump master pw ?
    # - add some more compatibility stuff (mac if requested, else fuck it)
    # - use some user defined exceptions instead plain exception
    # - cache
    # - think about which data from the db to return ;)
    #
    # ATTENTION: On my win7 system i needed a newer sqlite version.
    # see http://code.activestate.com/lists/python-tutor/96183/
    # - easy (and dirty) solution: get new sqlite.dll, replace in ...python/DLLS/
    #
    # CREDITS:
    # - Decrypt saved passwords: https://github.com/pradeep1288/ffpasscracker
    # - Myself for doing a .schema on every db to get the structure *yay for me*
    #
    
    
    #Password structures for firefox >= 3.5 ?!
    class SECItem(Structure):
        _fields_ = [('type',c_uint),('data',c_void_p),('len',c_uint)]
    class secuPWData(Structure):
        _fields_ = [('source',c_ubyte),('data',c_char_p)]
    (SECWouldBlock,SECFailure,SECSuccess)=(-2,-1,0)
    (PW_NONE,PW_FROMFILE,PW_PLAINTEXT,PW_EXTERNAL)=(0,1,2,3)
    
    
    class FileNotFoundException(Exception): pass
    
    
    
    class Firefox_data_generic(object):
        """
        Some general informations:
        - Every timestamp is a unix timestamp in millisec (/1000 to use with pythons time stuff)
        - 
        """
    
        def __init__(self, profilpath = None, compatibility_infos = None ):
            if platform.system().lower() == 'windows':
                find_profil_path =  self.__find_profile_path_windows
                load_libraries = self.__load_libraries_win
            else: # TODO. care about mac (which i don't atm)
                find_profil_path = self.__find_profile_path_linux
                load_libraries = self.__load_libraries_linux
    
            if profilpath is None:
                self.profilpath = find_profil_path()
            else:
                self.profilpath = profilpath
            self.default_profil = self.__get_default_profile()
    
            if compatibility_infos is None:
                self.ff_version, self.platform_dir, self.app_dir = self._get_info_from__compatibility_ini()
            else:
                self.ff_version, self.platform_dir, self.app_dir = compatibility_infos
    
            load_libraries(*self.ff_version)
    
            # set up for the specified ff version
            major, minor = self.ff_version
            # We don' care about stone age ff versions
            if self.ff_version < (3, 5):
                raise Exception('Nobody got time for implementing the features for this fucking old ff version')
            # download history moved from downloads.sqlite to places.sqlite
            if major >= 26:
                self.get_all_downloads = self.__get_all_downloads_places
                print 'get downloads from places.sqlite'
            else:
                self.get_all_downloads = self.__get_all_downloads_downloads
                print 'get downloads from downloads.sqlite'
    
    
            #print 'Profilepath is:', self.profilpath
    
        #############################################################
        # Placeholder
        #############################################################
        def get_all_downloads(self, profile = None):
            """ returns [(url, fileName, saveTo, size, endTime, done), ...]
            monkey patched to __get_all_downloads_downloads for version < 26 (TODO: ?)
            or else to __get_all_downloads_places
            """
            raise NotImplementedError('get_all_downloads not implemented')
    
    
        #############################################################
        # General working functions
        #############################################################  
        def _get_info_from__compatibility_ini(self, profile = None):
            if profile is None:
                profile = self.default_profil
            config_file = '/'.join((profile, 'compatibility.ini'))
            if not os.path.isfile(config_file):
                raise FileNotFoundException('compatibility.ini not found at "%s"' % config_file)
            config = ConfigParser()
            config.read(config_file) # TODO: catch return (list of succesfully read configs)
            major, minor = config.get('Compatibility', 'LastVersion').split('_')[0].split('.')[:2]
            # TODO: check if value exists
            return ( (int(major), int(minor)), config.get('Compatibility', 'LastPlatformDir'), config.get('Compatibility', 'LastAppDir'))
    
        def get_all_visited(self, profile = None):
            """ Return all visited urls in the form of [(url, visit_count, last_visit_date), ...]
            Ordered by date asc.
            If no profile path is suplied use the default one.
            Should work from v. 3.0 and above (TODO: ?)
            """
            if profile is None:
                profile = self.default_profil
            db_path = '/'.join((profile, 'places.sqlite'))
            if not os.path.isfile(db_path):
                return None # TODO: raise exception
            con = sqlite3.connect(db_path)
            ret = con.execute('SELECT url, visit_count, last_visit_date FROM moz_places where visit_count > 0 order by last_visit_date asc;').fetchall()
            con.close()
            return ret
    
        def search_history(self, custom_filter, profile = None):
            for elem in self.get_all_visited(profile):
                if custom_filter(elem):
                    yield elem      
    
    
        def test_master_pw(self, password, profile = None):
            if profile is None:
                profile = self.default_profil
            self.libnss.NSS_Init(profile) # TODO: check for errors here ? Init it once for all functions ?
            keySlot = self.libnss.PK11_GetInternalKeySlot()
            ret = self.libnss.PK11_CheckUserPassword( c_int(keySlot), c_char_p(password)) == SECSuccess
            self.libnss.PK11_FreeSlot(c_int(keySlot))
            self.libnss.NSS_Shutdown()
            return ret
    
        def is_master_password_set(self, profile = None):
            return not self.test_master_pw('', profile)
    
    
    
    
        def get_saved_passes(self, profilpath = None, decrypt = True, masterpass = ''):
            """ Try to get and decrypt saved passwords.
            The result (if successfuly) is in the form:[(hostname, httpRealm, formSubmitURL, usernameField, passwordField, username, password, encType, timeCreated, timeLastUsed, timePasswordChanged, timesUsed), ...]
            username and password are decrypted if no master password is set or they are not encrypted (doh) or the decrypt flag is set to False.
            In this cases the unaltered data is returned.
            """
            # decrypting part from https://github.com/pradeep1288/ffpasscracker 
            # for db schema: http://www.securityxploded.com/firepasswordviewer.php#for_firefox_3_5_and_above
            #  or just do a .schema moz_logins in the signons.sqlite /* signons.sqlite is gone and replaced by logins.json */
            if profilpath is None:
                profilpath = self.default_profil
    
    
            db_path = '/'.join((profilpath, 'logins.json'))
            if not os.path.isfile(db_path):
                raise FileNotFoundException('signons.sqlite not found at "%s"' % db_path)
            with open(db_path, 'rb') as fd:
                login_data = json.load(fd)
    
            # care about 'would_block' (-2) ?
            if self.libnss.NSS_Init(profilpath) != SECSuccess:
                # TODO: react to this
                print """Error Initalizing NSS_Init,\n
                propably no usefull results"""
                raise Exception('NSS_Init failed') # TODO: error code ?
    
            # TODO: check errors
            slot = self.libnss.PK11_GetInternalKeySlot()
            self.libnss.PK11_CheckUserPassword(slot, masterpass)
            self.libnss.PK11_Authenticate(slot, True, 0)
    
            pwdata = secuPWData()
            pwdata.source = PW_NONE
            pwdata.data=0
    
            uname = SECItem()
            passwd = SECItem()
            dectext = SECItem()
            con = sqlite3.connect(db_path)
            ret = list()
            for row in login_data['logins']:        
                if not decrypt: # not encrypted
                    row["encryptedUsername"] = base64.b64decode(row["encryptedUsername"])
                    row["encryptedPassword"] = base64.b64decode(row["encryptedPassword"])
                    ret.append(row)
                    continue
                # decrypt it                 
                uname.data  = cast(c_char_p(base64.b64decode(row["encryptedUsername"])),c_void_p)
                uname.len = len(base64.b64decode(row["encryptedUsername"]))
                passwd.data = cast(c_char_p(base64.b64decode(row["encryptedPassword"])),c_void_p)
                passwd.len=len(base64.b64decode(row["encryptedPassword"]))
                if self.libnss.PK11SDR_Decrypt(byref(uname),byref(dectext), 0)==-1:
                    raise Exception('Username decrypt exception with:' + str(row))
                row["encryptedUsername"] = string_at(dectext.data,dectext.len)
                if self.libnss.PK11SDR_Decrypt(byref(passwd),byref(dectext), 0)==-1:
                    raise Exception('Password decrypt exception with:' + str(row))
                row["encryptedPassword"] = string_at(dectext.data, dectext.len)
                ret.append(row)
            self.libnss.NSS_Shutdown()
            con.close()
            return ret
    
        def get_all_cookies(self, profile = None): # yummy
            """ Returns all cookies in the form of [(baseDomain, appId, inBrowserElement, name, value, host, path, expiry, lastAccessed, creationTime, isSecure, isHttpOnly), ...]
            Should work for versions >= 3.0 (TODO: ?)
            """
            if profile is None:
                profile = self.default_profil
            db_path = '/'.join((profile, 'cookies.sqlite'))
            con = sqlite3.connect(db_path)
            ret = con.execute('SELECT baseDomain, appId, inBrowserElement, name, value, host, path, expiry, lastAccessed, creationTime, isSecure, isHttpOnly FROM moz_cookies;').fetchall()
            con.close()
            return ret
    
        def get_all_form_history(self, profile = None, orderBy = 'firstUsed', orderASC = False):
            """ Returns all form history in the for of: [(fieldname, value, timesUsed, firstUsed, lastUsed), ...]
            Should work for versions >= 3.0 (TODO: ?)
            """
            if profile is None:
                profile = self.default_profil
            db_path = '/'.join((profile, 'formhistory.sqlite'))
            con = sqlite3.connect(db_path)
            ret = con.execute('SELECT fieldname, value, timesUsed, firstUsed, lastUsed from moz_formhistory order by %s %s;' % (orderBy, ('ASC' if orderASC else 'DESC'))).fetchall()
            con.close()
            return ret
    
        def search_form_history(self, custom_filter, profile = None, orderBy = 'firstUsed', orderASC = False):
            for elem in self.get_all_form_history(profile, orderBy, orderASC):
                if custom_filter(elem):
                    yield elem 
    
        #############################################################
        # Version and platform specific functions
        #############################################################
        def __find_profile_path_windows(self):
            """ Should work for win >= 2000 (TODO: ?)
            """
            # TODO: should work for Windows 2000, XP, Vista, and Windows 7, (win 8 TODO: ?)
            path = '/'.join((os.environ['APPDATA'], 'Mozilla/Firefox/Profiles'))
            if not os.path.isdir(path):
                raise FileNotFoundException('Profile folder "%s" not found.' % path)
            return path
    
        def __find_profile_path_linux(self):
            # TODO: check where the default profile path on different distros are
            path = '/'.join((os.environ['HOME'], '.mozilla/firefox'))
            if not os.path.isdir(path):
                raise FileNotFoundException('Profile folder "%s" not found.' % path)
            return path
    
        def __get_default_profile(self):
            try:
                profile = ( p for p in os.listdir(self.profilpath) if p.endswith('.default') and os.path.isdir('/'.join((self.profilpath, p)))  ).next()
                return '/'.join((self.profilpath, profile))
            except StopIteration:
                return None
    
        def __load_libraries_win(self, major, minor):
            #TODO: check relevant version changes
            os.environ['PATH'] = ';'.join([self.platform_dir, os.environ['PATH']])
            self.libnss = CDLL('/'.join((self.platform_dir, "nss3.dll")))
    
        def __load_libraries_linux(self, major, minor):
            #TODO: check relevant version changes
            #TODO: could that lib be somewhere else (not in path) ? (i.e: is this safe ?)
            self.libnss = CDLL("libnss3.so")
    
        def __get_all_downloads_downloads(self, profile = None):       
            #TODO: untested but should work
            if profile is None:
                profile = self.default_profil
            db_path = '/'.join((profile, 'downloads.sqlite'))
            con = sqlite3.connect(db_path)
            ret = con.execute('SELECT source, name, target, maxBytes, endTime, state FROM moz_downloads;').fetchall()
            con.close()
            return ret
    
        def __get_all_downloads_places(self, profile = None):
            if profile is None:
                profile = self.default_profil
            db_path = '/'.join((profile, 'places.sqlite'))
            con = sqlite3.connect(db_path)
            ret = list()
            for download in con.execute("""SELECT url, a.content, b.content, c.content from moz_annos a, moz_annos b, moz_annos c, moz_places p
                where a.place_id = b.place_id and b.place_id = c.place_id and p.id = a.place_id
                and a.anno_attribute_id = (select id from moz_anno_attributes where name = 'downloads/destinationFileName')
                and b.anno_attribute_id = (select id from moz_anno_attributes where name = 'downloads/destinationFileURI')
                and c.anno_attribute_id = (select id from moz_anno_attributes where name = 'downloads/metaData');
            """).fetchall():
                values = json.loads(download[3])
                ret.append( (download[0], download[1], download[2], values['fileSize'], values['endTime'], values['state']) )
            con.close()       
            return ret
    
    
    
    
    
    
    
    if __name__ == '__main__':
        profile = "C:/Users/USERNAME/AppData/Roaming/Mozilla/Firefox/Profiles/FF_PROFILE"
        ff = Firefox_data_generic()
        print 'version and paths:'
        print ff._get_info_from__compatibility_ini()
    
        print 'Master password is set:', ff.is_master_password_set(profile=profile) 
    
        print "Test some password", ff.test_master_pw('test1234567')
    
        print 'passwords:'
        print ff.get_saved_passes(profilpath=profile, decrypt=True, masterpass="")
    
        print 'cookies:'
        print ff.get_all_cookies()
    
        #print 'form history:'    
        #print ff.get_all_form_history(orderBy='name', True)
        print 'form history from search bar:'
        print list(ff.search_form_history(lambda x: 'searchbar' in x[0]))
    
        #print 'visited sites:'    
        #print ff.get_all_visited()
        print 'visited sites with login in url:'
        print list(ff.search_history(lambda x: 'login' in x[0]))
    
        print 'downloads:'
        print ff.get_all_downloads()
    

    It should work on Windows and Linux, but I just tested the changes on a Windows system.

    Edit: I replaced the script to crack the passwords with one to just dump different information (including the passwords and usernames in decrypted form with a given password)

License under CC-BY-SA with attribution


Content dated before 7/24/2021 11:53 AM