#!/usr/bin/env python
# -*- coding: utf-8 -*-
# portable decryption functions and BSD DB parsing by Laurent Clevy (@lorenzo2472)
# from https://github.com/lclevy/firepwd/blob/master/firepwd.py

import hmac
import json
import sqlite3
import struct
import sys
import traceback
import os

from lazagne.config.module_info import ModuleInfo
from lazagne.config.crypto.pyDes import triple_des, CBC
from lazagne.config.crypto.pyaes import AESModeOfOperationCBC
from lazagne.config.dico import get_dic
from lazagne.config.constant import constant
from pyasn1.codec.der import decoder
from binascii import unhexlify
from base64 import b64decode
from hashlib import sha1, pbkdf2_hmac

try:
    from ConfigParser import RawConfigParser  # Python 2.7
except ImportError:
    from configparser import RawConfigParser  # Python 3

if sys.version_info[0]:
    python_version = sys.version_info[0]

def l(n):
    try:
        return long(n)
    except NameError:
        return int(n)


CKA_ID = unhexlify('f8000000000000000000000000000001')
AES_BLOCK_SIZE = 16


def convert_to_byte(s):
    if python_version == 2:
        return s
    else:
        return s.encode()


def o(c):
    if python_version == 2:
        return ord(c)
    else:
        return c


def long_to_bytes(n, blocksize=0):
    """long_to_bytes(n:long, blocksize:int) : string
    Convert a long integer to a byte string.
    If optional blocksize is given and greater than zero, pad the front of the
    byte string with binary zeros so that the length is a multiple of
    blocksize.
    """
    # after much testing, this algorithm was deemed to be the fastest
    s = convert_to_byte('')
    n = l(n)
    while n > 0:
        s = struct.pack('>I', n & 0xffffffff) + s
        n = n >> 32

    # strip off leading zeros
    for i in range(len(s)):
        if s[i] != convert_to_byte('\000')[0]:
            break
    else:
        # only happens when n == 0
        s = convert_to_byte('\000')
        i = 0
    s = s[i:]
    # add back some pad bytes.  this could be done more efficiently w.r.t. the
    # de-padding being done above, but sigh...
    if blocksize > 0 and len(s) % blocksize:
        s = (blocksize - len(s) % blocksize) * convert_to_byte('\000') + s

    return s


class Mozilla(ModuleInfo):

    def __init__(self, browser_name, path):
        self.path = os.path.expanduser(path)
        ModuleInfo.__init__(self, browser_name, category='browsers')

    def get_firefox_profiles(self, directory):
        """
        List all profiles
        """
        cp = RawConfigParser()
        profile_list = []

        try:
            cp.read(os.path.join(directory, 'profiles.ini'))
            for section in cp.sections():
                if section.startswith('Profile') and cp.has_option(section, 'Path'):
                    profile_path = None

                    if cp.has_option(section, 'IsRelative'):
                        if cp.get(section, 'IsRelative') == '1':
                            profile_path = os.path.join(directory, cp.get(section, 'Path').strip())
                        elif cp.get(section, 'IsRelative') == '0':
                            profile_path = cp.get(section, 'Path').strip()

                    else: # No "IsRelative" in profiles.ini
                        profile_path = os.path.join(directory, cp.get(section, 'Path').strip())

                    if profile_path:
                        profile_list.append(profile_path)

        except Exception as e:
            self.error(u'An error occurred while reading profiles.ini: {}'.format(e))
        return profile_list

    def get_key(self, profile):
        """
        Get main key used to encrypt all data (user / password).
        Depending on the Firefox version, could be stored in key3.db or key4.db file.
        """
        try:
            row = None
            # Remove error when file is empty
            with open(os.path.join(profile, 'key4.db'), 'rb') as f:
                content = f.read()

            if content:
                conn = sqlite3.connect(os.path.join(profile, 'key4.db'))  # Firefox 58.0.2 / NSS 3.35 with key4.db in SQLite
                c = conn.cursor()
                # First check password
                c.execute("SELECT item1,item2 FROM metadata WHERE id = 'password';")
                try:
                    row = c.next()  # Python 2
                except Exception:
                    row = next(c)  # Python 3

        except Exception:
            self.debug(traceback.format_exc())

        else:
            if row:
                (global_salt, master_password, entry_salt) = self.manage_masterpassword(master_password=b'', key_data=row)

                if global_salt:
                    try:
                        # Decrypt 3DES key to decrypt "logins.json" content
                        c.execute("SELECT a11,a102 FROM nssPrivate;")
                        for row in c:
                            if row[0]:
                                break

                        a11 = row[0]  # CKA_VALUE
                        a102 = row[1]  # f8000000000000000000000000000001, CKA_ID

                        if python_version == 2:
                            a102 = str(a102)

                        if a102 == CKA_ID:
                            # a11  : CKA_VALUE
                            # a102 : f8000000000000000000000000000001, CKA_ID
                            # self.print_asn1(a11, len(a11), 0)
                            # SEQUENCE {
                            #     SEQUENCE {
                            #         OBJECTIDENTIFIER 1.2.840.113549.1.12.5.1.3
                            #         SEQUENCE {
                            #             OCTETSTRING entry_salt_for_3des_key
                            #             INTEGER 01
                            #         }
                            #     }
                            #     OCTETSTRING encrypted_3des_key (with 8 bytes of PKCS#7 padding)
                            # }
                            decoded_a11 = decoder.decode(a11)
                            key = self.decrypt_3des(decoded_a11, master_password, global_salt)
                            if key:
                                self.debug(u'key: {key}'.format(key=repr(key)))
                                yield key[:24]
                        # else:
                            # Nothing saved

                    except Exception:
                        self.debug(traceback.format_exc())

        try:
            key3_file = os.path.join(profile, 'key3.db')
            if os.path.exists(key3_file):
                key_data = self.read_bsddb(key3_file)
                # Check masterpassword
                (global_salt, master_password, entry_salt) = self.manage_masterpassword(master_password=u'',
                                                                                        key_data=key_data,
                                                                                        new_version=False)
                if global_salt:
                    key = self.extract_secret_key(key_data=key_data,
                                                  global_salt=global_salt,
                                                  master_password=master_password,
                                                  entry_salt=entry_salt)
                    if key:
                        self.debug(u'key: {key}'.format(key=repr(key)))
                        yield key[:24]
        except Exception:
            self.debug(traceback.format_exc())

    @staticmethod
    def get_short_le(d, a):
        return struct.unpack('<H', d[a:a + 2])[0]

    @staticmethod
    def get_long_be(d, a):
        return struct.unpack('>L', d[a:a + 4])[0]

    def print_asn1(self, d, l, rl):
        """
        Used for debug
        """
        type_ = o(d[0])
        length = o(d[1])
        if length & 0x80 > 0:  # http://luca.ntop.org/Teaching/Appunti/asn1.html,
            # nByteLength = length & 0x7f
            length = o(d[2])
            # Long form. Two to 127 octets. Bit 8 of first octet has value "1" and
            # bits 7-1 give the number of additional length octets.
            skip = 1
        else:
            skip = 0

        if type_ == 0x30:
            seq_len = length
            read_len = 0
            while seq_len > 0:
                len2 = self.print_asn1(d[2 + skip + read_len:], seq_len, rl + 1)
                seq_len = seq_len - len2
                read_len = read_len + len2
            return length + 2
        elif type_ in (0x6, 0x5, 0x4, 0x2):  # OID, OCTETSTRING, NULL, INTEGER
            return length + 2
        elif length == l - 2:
            self.print_asn1(d[2:], length, rl + 1)
            return length

    def read_bsddb(self, name):
        """
        Extract records from a BSD DB 1.85, hash mode
        Obsolete with Firefox 58.0.2 and NSS 3.35, as key4.db (SQLite) is used
        """
        with open(name, 'rb') as f:
            # http://download.oracle.com/berkeley-db/db.1.85.tar.gz
            header = f.read(4 * 15)
            magic = self.get_long_be(header, 0)
            if magic != 0x61561:
                self.warning(u'Bad magic number')
                return False

            version = self.get_long_be(header, 4)
            if version != 2:
                self.warning(u'Bad version !=2 (1.85)')
                return False

            pagesize = self.get_long_be(header, 12)
            nkeys = self.get_long_be(header, 0x38)
            readkeys = 0
            page = 1
            db1 = []

            while readkeys < nkeys:
                f.seek(pagesize * page)
                offsets = f.read((nkeys + 1) * 4 + 2)
                offset_vals = []
                i = 0
                nval = 0
                val = 1
                keys = 0

                while nval != val:
                    keys += 1
                    key = self.get_short_le(offsets, 2 + i)
                    val = self.get_short_le(offsets, 4 + i)
                    nval = self.get_short_le(offsets, 8 + i)
                    offset_vals.append(key + pagesize * page)
                    offset_vals.append(val + pagesize * page)
                    readkeys += 1
                    i += 4

                offset_vals.append(pagesize * (page + 1))
                val_key = sorted(offset_vals)
                for i in range(keys * 2):
                    f.seek(val_key[i])
                    data = f.read(val_key[i + 1] - val_key[i])
                    db1.append(data)
                page += 1

        db = {}
        for i in range(0, len(db1), 2):
            db[db1[i + 1]] = db1[i]

        return db

    @staticmethod
    def decrypt_3des(decoded_item, master_password, global_salt):
        """
        User master key is also encrypted (if provided, the master_password could be used to encrypt it)
        """
        # See http://www.drh-consultancy.demon.co.uk/key3.html
        pbeAlgo = str(decoded_item[0][0][0])
        if pbeAlgo == '1.2.840.113549.1.12.5.1.3': # pbeWithSha1AndTripleDES-CBC
            entry_salt = decoded_item[0][0][1][0].asOctets()
            cipher_t = decoded_item[0][1].asOctets()

            # See http://www.drh-consultancy.demon.co.uk/key3.html
            hp = sha1(global_salt + master_password).digest()
            pes = entry_salt + convert_to_byte('\x00') * (20 - len(entry_salt))
            chp = sha1(hp + entry_salt).digest()
            k1 = hmac.new(chp, pes + entry_salt, sha1).digest()
            tk = hmac.new(chp, pes, sha1).digest()
            k2 = hmac.new(chp, tk + entry_salt, sha1).digest()
            k = k1 + k2
            iv = k[-8:]
            key = k[:24]
            return triple_des(key, CBC, iv).decrypt(cipher_t)
        
        # New version
        elif pbeAlgo == '1.2.840.113549.1.5.13': # pkcs5 pbes2

            assert str(decoded_item[0][0][1][0][0]) == '1.2.840.113549.1.5.12'
            assert str(decoded_item[0][0][1][0][1][3][0]) == '1.2.840.113549.2.9'
            assert str(decoded_item[0][0][1][1][0]) == '2.16.840.1.101.3.4.1.42'
            # https://tools.ietf.org/html/rfc8018#page-23
            entry_salt = decoded_item[0][0][1][0][1][0].asOctets()
            iteration_count = int(decoded_item[0][0][1][0][1][1])
            key_length = int(decoded_item[0][0][1][0][1][2])
            assert key_length == 32 

            k = sha1(global_salt + master_password).digest()
            key = pbkdf2_hmac('sha256', k, entry_salt, iteration_count, dklen=key_length)    

            # https://hg.mozilla.org/projects/nss/rev/fc636973ad06392d11597620b602779b4af312f6#l6.49
            iv = b'\x04\x0e' + decoded_item[0][0][1][1][1].asOctets()
            # 04 is OCTETSTRING, 0x0e is length == 14
            encrypted_value = decoded_item[0][1].asOctets()
            aes = AESModeOfOperationCBC(key, iv=iv)
            cleartxt = b"".join([aes.decrypt(encrypted_value[i:i + AES_BLOCK_SIZE])
                             for i in range(0, len(encrypted_value), AES_BLOCK_SIZE)])

            return cleartxt

    def extract_secret_key(self, key_data, global_salt, master_password, entry_salt):

        if unhexlify('f8000000000000000000000000000001') not in key_data:
            return None

        priv_key_entry = key_data[unhexlify('f8000000000000000000000000000001')]
        salt_len = o(priv_key_entry[1])
        name_len = o(priv_key_entry[2])
        priv_key_entry_asn1 = decoder.decode(priv_key_entry[3 + salt_len + name_len:])
        # data = priv_key_entry[3 + salt_len + name_len:]
        # self.print_asn1(data, len(data), 0)

        # See https://github.com/philsmd/pswRecovery4Moz/blob/master/pswRecovery4Moz.txt
        priv_key = self.decrypt_3des(priv_key_entry_asn1, master_password, global_salt)
        # self.print_asn1(priv_key, len(priv_key), 0)
        priv_key_asn1 = decoder.decode(priv_key)
        pr_key = priv_key_asn1[0][2].asOctets()
        # self.print_asn1(pr_key, len(pr_key), 0)
        pr_key_asn1 = decoder.decode(pr_key)
        # id = pr_key_asn1[0][1]
        key = long_to_bytes(pr_key_asn1[0][3])
        return key

    @staticmethod
    def decode_login_data(data):
        asn1data = decoder.decode(b64decode(data))  # First base64 decoding, then ASN1DERdecode
        # For login and password, keep :(key_id, iv, ciphertext)
        return asn1data[0][0].asOctets(), asn1data[0][1][1].asOctets(), asn1data[0][2].asOctets()

    def get_login_data(self, profile):
        """
        Get encrypted data (user / password) and host from the json or sqlite files
        """
        conn = sqlite3.connect(os.path.join(profile, 'signons.sqlite'))
        logins = []
        c = conn.cursor()
        try:
            c.execute('SELECT * FROM moz_logins;')
        except sqlite3.OperationalError:  # Since Firefox 32, json is used instead of sqlite3
            try:
                logins_json = os.path.join(profile, 'logins.json')
                if os.path.isfile(logins_json):
                    with open(logins_json) as f:
                        loginf = f.read()
                        if loginf:
                            json_logins = json.loads(loginf)
                            if 'logins' not in json_logins:
                                self.debug('No logins key in logins.json')
                                return logins
                            for row in json_logins['logins']:
                                enc_username = row['encryptedUsername']
                                enc_password = row['encryptedPassword']
                                logins.append((self.decode_login_data(enc_username),
                                               self.decode_login_data(enc_password), row['hostname']))
                            return logins
            except Exception:
                self.debug(traceback.format_exc())
                return []

        # Using sqlite3 database
        for row in c:
            enc_username = row[6]
            enc_password = row[7]
            logins.append((self.decode_login_data(enc_username), self.decode_login_data(enc_password), row[1]))
        return logins

    def manage_masterpassword(self, master_password=b'', key_data=None, new_version=True):
        """
        Check if a master password is set.
        If so, try to find it using a dictionary attack
        """
        (global_salt, master_password, entry_salt) = self.is_master_password_correct(master_password=master_password,
                                                                                     key_data=key_data,
                                                                                     new_version=new_version)

        if not global_salt:
            self.info(u'Master Password is used !')
            (global_salt, master_password, entry_salt) = self.brute_master_password(key_data=key_data,
                                                                                    new_version=new_version)
            if not master_password:
                return '', '', ''

        return global_salt, master_password, entry_salt

    def is_master_password_correct(self, key_data, master_password=b'', new_version=True):
        try:
            entry_salt = b""
            if not new_version:
                # See http://www.drh-consultancy.demon.co.uk/key3.html
                pwd_check = key_data.get(b'password-check')
                if not pwd_check:
                    return '', '', ''
                # Hope not breaking something (not tested for old version)
                # entry_salt_len = o(pwd_check[1])
                # entry_salt = pwd_check[3: 3 + entry_salt_len]
                # encrypted_passwd = pwd_check[-16:]
                global_salt = key_data[b'global-salt']

            else:
                global_salt = key_data[0]  # Item1
                item2 = key_data[1]
                # self.print_asn1(item2, len(item2), 0)
                # SEQUENCE {
                # 	SEQUENCE {
                # 		OBJECTIDENTIFIER 1.2.840.113549.1.12.5.1.3
                # 		SEQUENCE {
                # 			OCTETSTRING entry_salt_for_passwd_check
                # 			INTEGER 01
                # 		}
                # 	}
                # 	OCTETSTRING encrypted_password_check
                # }
                decoded_item2 = decoder.decode(item2)

            cleartext_data = self.decrypt_3des(decoded_item2, master_password, global_salt)
            if cleartext_data != convert_to_byte('password-check\x02\x02'):
                return '', '', ''

            return global_salt, master_password, entry_salt
        except Exception:
            self.debug(traceback.format_exc())
            return '', '', ''

    def brute_master_password(self, key_data, new_version=True):
        """
        Try to find master_password doing a dictionary attack using the 500 most used passwords
        """
        wordlist = constant.passwordFound + get_dic()
        num_lines = (len(wordlist) - 1)
        self.info(u'%d most used passwords !!! ' % num_lines)

        for word in wordlist:
            global_salt, master_password, entry_salt = self.is_master_password_correct(key_data=key_data,
                                                                                       master_password=word.strip(),
                                                                                       new_version=new_version)
            if master_password:
                self.info(u'Master password found: {}'.format(master_password))
                return global_salt, master_password, entry_salt

        self.warning(u'No password has been found using the default list')
        return '', '', ''

    @staticmethod
    def remove_padding(self, data):
        """
        Remove PKCS#7 padding
        """
        try:
            nb = struct.unpack('B', data[-1])[0]  # Python 2
        except Exception:
            nb = data[-1]  # Python 3

        try:
            return data[:-nb]
        except Exception:
            self.debug(traceback.format_exc())
            return data

    def decrypt(self, key, iv, ciphertext):
        """
        Decrypt ciphered data (user / password) using the key previously found
        """
        data = triple_des(key, CBC, iv).decrypt(ciphertext)
        return self.remove_padding(data)

    def run(self):
        """
        Main function
        """
        if os.path.exists(self.path):

            pwd_found = []
            for profile in self.get_firefox_profiles(self.path):
                self.info(u'Profile path found: {profile}'.format(profile=profile))

                credentials = self.getLoginData(profile)
                if credentials:
                    for key in self.get_key(profile):
                        for user, password, url in credentials:
                            try:
                                pwd_found.append({
                                    'URL': url,
                                    'Login': self.decrypt(key=key, iv=user[1], ciphertext=user[2]).decode("utf-8"),
                                    'Password': self.decrypt(key=key, iv=password[1], ciphertext=password[2]).decode("utf-8"),
                                })
                            except Exception as e:
                                self.debug(u'An error occurred decrypting the password: {error}'.format(error=e))
                else:
                    self.info(u'Database empty')

            return pwd_found


# Name, path
firefox_browsers = [
    (u'firefox', u"~/Library/Application Support/Firefox/"),
    # Check these paths on Mac systems
    # (u'BlackHawk', u'{APPDATA}\\NETGATE Technologies\\BlackHawk'),
    # (u'Cyberfox', u'{APPDATA}\\8pecxstudios\\Cyberfox'),
    # (u'Comodo IceDragon', u'{APPDATA}\\Comodo\\IceDragon'),
    # (u'K-Meleon', u'{APPDATA}\\K-Meleon'),
    # (u'Icecat', u'{APPDATA}\\Mozilla\\icecat'),
]

firefox_browsers = [Mozilla(browser_name=name, path=path) for name, path in firefox_browsers]
