mirror of
https://github.com/unode/firefox_decrypt.git
synced 2025-12-16 12:01:52 +01:00
[browserpass](https://github.com/dannyvankooten/browserpass) expects a multiline format with password first, and then a line with either `login:`, `user:` or `username:`. Exporting passwords this way makes login form filling work if you have browserpass installed. To keep backwards compatibility the default, if `--pass-compat` is not specified (or specified as `--pass-compat=default`), is to print just the bare username on the second line.
871 lines
28 KiB
Python
Executable File
871 lines
28 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
# Based on original work from: www.dumpzilla.org
|
|
|
|
import argparse
|
|
import csv
|
|
import ctypes as ct
|
|
import json
|
|
import logging
|
|
import os
|
|
import sqlite3
|
|
import sys
|
|
from base64 import b64decode
|
|
from getpass import getpass
|
|
from subprocess import PIPE, Popen
|
|
|
|
try:
|
|
# Python 3
|
|
from subprocess import DEVNULL
|
|
except ImportError:
|
|
# Python 2
|
|
DEVNULL = open(os.devnull, 'w')
|
|
|
|
try:
|
|
# Python 3
|
|
from urllib.parse import urlparse
|
|
except ImportError:
|
|
# Python 2
|
|
from urlparse import urlparse
|
|
|
|
try:
|
|
# Python 3
|
|
from configparser import ConfigParser
|
|
raw_input = input
|
|
except ImportError:
|
|
# Python 2
|
|
from ConfigParser import ConfigParser
|
|
|
|
PY3 = sys.version_info.major > 2
|
|
LOG = None
|
|
VERBOSE = False
|
|
|
|
|
|
def get_version():
|
|
"""Obtain version information from git if available otherwise use
|
|
the internal version number
|
|
"""
|
|
def internal_version():
|
|
return '.'.join(map(str, __version_info__))
|
|
|
|
try:
|
|
p = Popen(["git", "describe", "--tags"], stdout=PIPE, stderr=DEVNULL)
|
|
except OSError:
|
|
return internal_version()
|
|
|
|
stdout, stderr = p.communicate()
|
|
|
|
if p.returncode:
|
|
return internal_version()
|
|
else:
|
|
return stdout.strip().decode("utf-8")
|
|
|
|
|
|
__version_info__ = (0, 7, 0)
|
|
__version__ = get_version()
|
|
|
|
|
|
class NotFoundError(Exception):
|
|
"""Exception to handle situations where a credentials file is not found
|
|
"""
|
|
pass
|
|
|
|
|
|
class Exit(Exception):
|
|
"""Exception to allow a clean exit from any point in execution
|
|
"""
|
|
ERROR = 1
|
|
MISSING_PROFILEINI = 2
|
|
MISSING_SECRETS = 3
|
|
BAD_PROFILEINI = 4
|
|
LOCATION_NO_DIRECTORY = 5
|
|
|
|
FAIL_LOAD_NSS = 11
|
|
FAIL_INIT_NSS = 12
|
|
FAIL_NSS_KEYSLOT = 13
|
|
FAIL_SHUTDOWN_NSS = 14
|
|
BAD_MASTER_PASSWORD = 15
|
|
NEED_MASTER_PASSWORD = 16
|
|
|
|
PASSSTORE_NOT_INIT = 20
|
|
PASSSTORE_MISSING = 21
|
|
PASSSTORE_ERROR = 22
|
|
|
|
READ_GOT_EOF = 30
|
|
MISSING_CHOICE = 31
|
|
NO_SUCH_PROFILE = 32
|
|
|
|
UNKNOWN_ERROR = 100
|
|
KEYBOARD_INTERRUPT = 102
|
|
|
|
def __init__(self, exitcode):
|
|
self.exitcode = exitcode
|
|
|
|
def __unicode__(self):
|
|
return "Premature program exit with exit code {0}".format(self.exitcode)
|
|
|
|
|
|
class Credentials(object):
|
|
"""Base credentials backend manager
|
|
"""
|
|
def __init__(self, db):
|
|
self.db = db
|
|
|
|
LOG.debug("Database location: %s", self.db)
|
|
if not os.path.isfile(db):
|
|
raise NotFoundError("ERROR - {0} database not found\n".format(db))
|
|
|
|
LOG.info("Using %s for credentials.", db)
|
|
|
|
def __iter__(self):
|
|
pass
|
|
|
|
def done(self):
|
|
"""Override this method if the credentials subclass needs to do any
|
|
action after interaction
|
|
"""
|
|
pass
|
|
|
|
|
|
class SqliteCredentials(Credentials):
|
|
"""SQLite credentials backend manager
|
|
"""
|
|
def __init__(self, profile):
|
|
db = os.path.join(profile, "signons.sqlite")
|
|
|
|
super(SqliteCredentials, self).__init__(db)
|
|
|
|
self.conn = sqlite3.connect(db)
|
|
self.c = self.conn.cursor()
|
|
|
|
def __iter__(self):
|
|
LOG.debug("Reading password database in SQLite format")
|
|
self.c.execute("SELECT hostname, encryptedUsername, encryptedPassword, encType "
|
|
"FROM moz_logins")
|
|
for i in self.c:
|
|
# yields hostname, encryptedUsername, encryptedPassword, encType
|
|
yield i
|
|
|
|
def done(self):
|
|
"""Close the sqlite cursor and database connection
|
|
"""
|
|
super(SqliteCredentials, self).done()
|
|
|
|
self.c.close()
|
|
self.conn.close()
|
|
|
|
|
|
class JsonCredentials(Credentials):
|
|
"""JSON credentials backend manager
|
|
"""
|
|
def __init__(self, profile):
|
|
db = os.path.join(profile, "logins.json")
|
|
|
|
super(JsonCredentials, self).__init__(db)
|
|
|
|
def __iter__(self):
|
|
with open(self.db) as fh:
|
|
LOG.debug("Reading password database in JSON format")
|
|
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"], i["encType"])
|
|
|
|
|
|
class NSSDecoder(object):
|
|
class SECItem(ct.Structure):
|
|
"""struct needed to interact with libnss
|
|
"""
|
|
_fields_ = [
|
|
('type', ct.c_uint),
|
|
('data', ct.c_char_p), # actually: unsigned char *
|
|
('len', ct.c_uint),
|
|
]
|
|
|
|
class PK11SlotInfo(ct.Structure):
|
|
"""opaque structure representing a logical PKCS slot
|
|
"""
|
|
|
|
def __init__(self):
|
|
# Locate libnss and try loading it
|
|
self.NSS = None
|
|
self.load_libnss()
|
|
|
|
SlotInfoPtr = ct.POINTER(self.PK11SlotInfo)
|
|
SECItemPtr = ct.POINTER(self.SECItem)
|
|
|
|
self._set_ctypes(ct.c_int, "NSS_Init", ct.c_char_p)
|
|
self._set_ctypes(ct.c_int, "NSS_Shutdown")
|
|
self._set_ctypes(SlotInfoPtr, "PK11_GetInternalKeySlot")
|
|
self._set_ctypes(None, "PK11_FreeSlot", SlotInfoPtr)
|
|
self._set_ctypes(ct.c_int, "PK11_CheckUserPassword", SlotInfoPtr, ct.c_char_p)
|
|
self._set_ctypes(ct.c_int, "PK11SDR_Decrypt", SECItemPtr, SECItemPtr, ct.c_void_p)
|
|
self._set_ctypes(None, "SECITEM_ZfreeItem", SECItemPtr, ct.c_int)
|
|
|
|
# for error handling
|
|
self._set_ctypes(ct.c_int, "PORT_GetError")
|
|
self._set_ctypes(ct.c_char_p, "PR_ErrorToName", ct.c_int)
|
|
self._set_ctypes(ct.c_char_p, "PR_ErrorToString", ct.c_int, ct.c_uint32)
|
|
|
|
def _set_ctypes(self, restype, name, *argtypes):
|
|
"""Set input/output types on libnss C functions for automatic type casting
|
|
"""
|
|
res = getattr(self.NSS, name)
|
|
res.restype = restype
|
|
res.argtypes = argtypes
|
|
setattr(self, "_" + name, res)
|
|
|
|
@staticmethod
|
|
def find_nss(locations, nssname):
|
|
"""Locate nss is one of the many possible locations
|
|
"""
|
|
for loc in locations:
|
|
if os.path.exists(os.path.join(loc, nssname)):
|
|
return loc
|
|
|
|
LOG.warn("%s not found on any of the default locations for this platform. "
|
|
"Attempting to continue nonetheless.", nssname)
|
|
return ""
|
|
|
|
def load_libnss(self):
|
|
"""Load libnss into python using the CDLL interface
|
|
"""
|
|
if os.name == "nt":
|
|
nssname = "nss3.dll"
|
|
locations = (
|
|
"", # Current directory or system lib finder
|
|
r"C:\Program Files (x86)\Mozilla Firefox",
|
|
r"C:\Program Files\Mozilla Firefox"
|
|
)
|
|
firefox = self.find_nss(locations, nssname)
|
|
|
|
os.environ["PATH"] = ';'.join([os.environ["PATH"], firefox])
|
|
LOG.debug("PATH is now %s", os.environ["PATH"])
|
|
|
|
elif os.uname()[0] == "Darwin":
|
|
nssname = "libnss3.dylib"
|
|
locations = (
|
|
"", # Current directory or system lib finder
|
|
"/usr/local/lib/nss",
|
|
"/usr/local/lib",
|
|
"/opt/local/lib/nss",
|
|
"/sw/lib/firefox",
|
|
"/sw/lib/mozilla",
|
|
"/usr/local/opt/nss/lib", # nss installed with Brew on Darwin
|
|
"/opt/pkg/lib/nss", # installed via pkgsrc
|
|
)
|
|
|
|
firefox = self.find_nss(locations, nssname)
|
|
else:
|
|
nssname = "libnss3.so"
|
|
firefox = "" # Current directory or system lib finder
|
|
|
|
try:
|
|
nsslib = os.path.join(firefox, nssname)
|
|
LOG.debug("Loading NSS library from %s", nsslib)
|
|
|
|
self.NSS = ct.CDLL(nsslib)
|
|
|
|
except Exception as e:
|
|
LOG.error("Problems opening '%s' required for password decryption", nssname)
|
|
LOG.error("Error was %s", e)
|
|
raise Exit(Exit.FAIL_LOAD_NSS)
|
|
|
|
def handle_error(self):
|
|
"""If an error happens in libnss, handle it and print some debug information
|
|
"""
|
|
LOG.debug("Error during a call to NSS library, trying to obtain error info")
|
|
|
|
code = self._PORT_GetError()
|
|
name = self._PR_ErrorToName(code)
|
|
name = "NULL" if name is None else name.decode("ascii")
|
|
# 0 is the default language (localization related)
|
|
text = self._PR_ErrorToString(code, 0)
|
|
text = text.decode("utf8")
|
|
|
|
LOG.debug("%s: %s", name, text)
|
|
|
|
def decode(self, data64):
|
|
data = b64decode(data64)
|
|
inp = self.SECItem(0, data, len(data))
|
|
out = self.SECItem(0, None, 0)
|
|
|
|
e = self._PK11SDR_Decrypt(inp, out, None)
|
|
LOG.debug("Decryption of data returned %s", e)
|
|
try:
|
|
if e == -1:
|
|
LOG.error("Password decryption failed. Passwords protected by a Master Password!")
|
|
self.handle_error()
|
|
raise Exit(Exit.NEED_MASTER_PASSWORD)
|
|
|
|
res = ct.string_at(out.data, out.len).decode("utf8")
|
|
finally:
|
|
# Avoid leaking SECItem
|
|
self._SECITEM_ZfreeItem(out, 0)
|
|
|
|
return res
|
|
|
|
|
|
class NSSInteraction(object):
|
|
"""
|
|
Interact with lib NSS
|
|
"""
|
|
def __init__(self):
|
|
self.profile = None
|
|
self.NSS = NSSDecoder()
|
|
|
|
def load_profile(self, profile):
|
|
"""Initialize the NSS library and profile
|
|
"""
|
|
LOG.debug("Initializing NSS with profile path '%s'", profile)
|
|
self.profile = profile
|
|
|
|
e = self.NSS._NSS_Init(b"sql:" + self.profile.encode("utf8"))
|
|
LOG.debug("Initializing NSS returned %s", e)
|
|
|
|
if e != 0:
|
|
LOG.error("Couldn't initialize NSS, maybe '%s' is not a valid profile?", profile)
|
|
self.NSS.handle_error()
|
|
raise Exit(Exit.FAIL_INIT_NSS)
|
|
|
|
def authenticate(self, interactive):
|
|
"""Check if the current profile is protected by a master password,
|
|
prompt the user and unlock the profile.
|
|
"""
|
|
LOG.debug("Retrieving internal key slot")
|
|
keyslot = self.NSS._PK11_GetInternalKeySlot()
|
|
|
|
LOG.debug("Internal key slot %s", keyslot)
|
|
if not keyslot:
|
|
LOG.error("Failed to retrieve internal KeySlot")
|
|
self.NSS.handle_error()
|
|
raise Exit(Exit.FAIL_NSS_KEYSLOT)
|
|
|
|
try:
|
|
# NOTE It would be great to be able to check if the profile is
|
|
# protected by a master password. In C++ one would do:
|
|
# if (keyslot->needLogin):
|
|
# however accessing instance methods is not supported by ctypes.
|
|
# More on this topic: http://stackoverflow.com/a/19636310
|
|
# A possibility would be to define such function using cython but
|
|
# this adds an unecessary runtime dependency
|
|
password = ask_password(self.profile, interactive)
|
|
|
|
if password:
|
|
LOG.debug("Authenticating with password '%s'", password)
|
|
e = self.NSS._PK11_CheckUserPassword(keyslot, password.encode("utf8"))
|
|
|
|
LOG.debug("Checking user password returned %s", e)
|
|
|
|
if e != 0:
|
|
LOG.error("Master password is not correct")
|
|
|
|
self.NSS.handle_error()
|
|
raise Exit(Exit.BAD_MASTER_PASSWORD)
|
|
|
|
else:
|
|
LOG.warn("Attempting decryption with no Master Password")
|
|
finally:
|
|
# Avoid leaking PK11KeySlot
|
|
self.NSS._PK11_FreeSlot(keyslot)
|
|
|
|
def unload_profile(self):
|
|
"""Shutdown NSS and deactive current profile
|
|
"""
|
|
e = self.NSS._NSS_Shutdown()
|
|
|
|
if e != 0:
|
|
LOG.error("Couldn't shutdown current NSS profile")
|
|
|
|
self.NSS.handle_error()
|
|
raise Exit(Exit.FAIL_SHUTDOWN_NSS)
|
|
|
|
def decode_entry(self, user64, passw64):
|
|
"""Decrypt one entry in the database
|
|
"""
|
|
LOG.debug("Decrypting username data '%s'", user64)
|
|
user = self.NSS.decode(user64)
|
|
|
|
LOG.debug("Decrypting password data '%s'", passw64)
|
|
passw = self.NSS.decode(passw64)
|
|
|
|
return user, passw
|
|
|
|
def decrypt_passwords(self, export, output_format="human", csv_delimiter=";", csv_quotechar="|"):
|
|
"""
|
|
Decrypt requested profile using the provided password and print out all
|
|
stored passwords.
|
|
"""
|
|
def output_line(line):
|
|
if PY3:
|
|
sys.stdout.write(line)
|
|
else:
|
|
sys.stdout.write(line.encode("utf8"))
|
|
|
|
# Any password in this profile store at all?
|
|
got_password = False
|
|
header = True
|
|
|
|
credentials = obtain_credentials(self.profile)
|
|
|
|
LOG.info("Decrypting credentials")
|
|
to_export = {}
|
|
|
|
if output_format == "csv":
|
|
csv_writer = csv.DictWriter(
|
|
sys.stdout, fieldnames=["url", "user", "password"],
|
|
lineterminator="\n", delimiter=csv_delimiter,
|
|
quotechar=csv_quotechar, quoting=csv.QUOTE_ALL,
|
|
)
|
|
if header:
|
|
csv_writer.writeheader()
|
|
|
|
for url, user, passw, enctype in credentials:
|
|
got_password = True
|
|
|
|
# enctype informs if passwords are encrypted and protected by
|
|
# a master password
|
|
if enctype:
|
|
user, passw = self.decode_entry(user, passw)
|
|
|
|
LOG.debug("Decoding username '%s' and password '%s' for website '%s'", user, passw, url)
|
|
LOG.debug("Decoding username '%s' and password '%s' for website '%s'", type(user), type(passw), type(url))
|
|
|
|
if export:
|
|
# Keep track of web-address, username and passwords
|
|
# If more than one username exists for the same web-address
|
|
# the username will be used as name of the file
|
|
address = urlparse(url)
|
|
|
|
if address.netloc not in to_export:
|
|
to_export[address.netloc] = {user: passw}
|
|
|
|
else:
|
|
to_export[address.netloc][user] = passw
|
|
|
|
if output_format == "csv":
|
|
output = {"url": url, "user": user, "password": passw}
|
|
if PY3:
|
|
csv_writer.writerow(output)
|
|
else:
|
|
csv_writer.writerow({k: v.encode("utf8") for k, v in output.items()})
|
|
|
|
else:
|
|
output = (
|
|
u"\nWebsite: {0}\n".format(url),
|
|
u"Username: '{0}'\n".format(user),
|
|
u"Password: '{0}'\n".format(passw),
|
|
)
|
|
for line in output:
|
|
output_line(line)
|
|
|
|
credentials.done()
|
|
|
|
if not got_password:
|
|
LOG.warn("No passwords found in selected profile")
|
|
|
|
if export:
|
|
return to_export
|
|
|
|
|
|
def test_password_store(export):
|
|
"""Check if pass from passwordstore.org is installed
|
|
If it is installed but not initialized, initialize it
|
|
"""
|
|
# Nothing to do here if exporting wasn't requested
|
|
if not export:
|
|
LOG.debug("Skipping password store test, not exporting")
|
|
return
|
|
|
|
LOG.debug("Testing if password store is installed and configured")
|
|
|
|
try:
|
|
p = Popen(["pass"], stdout=PIPE, stderr=PIPE)
|
|
except OSError as e:
|
|
if e.errno == 2:
|
|
LOG.error("Password store is not installed and exporting was requested")
|
|
raise Exit(Exit.PASSSTORE_MISSING)
|
|
else:
|
|
LOG.error("Unknown error happened.")
|
|
LOG.error("Error was %s", e)
|
|
raise Exit(Exit.UNKNOWN_ERROR)
|
|
|
|
out, err = p.communicate()
|
|
LOG.debug("pass returned: %s %s", out, err)
|
|
|
|
if p.returncode != 0:
|
|
if 'Try "pass init"' in err:
|
|
LOG.error("Password store was not initialized.")
|
|
LOG.error("Initialize the password store manually by using 'pass init'")
|
|
raise Exit(Exit.PASSSTORE_NOT_INIT)
|
|
else:
|
|
LOG.error("Unknown error happened when running 'pass'.")
|
|
LOG.error("Stdout/Stderr was '%s' '%s'", out, err)
|
|
raise Exit(Exit.UNKNOWN_ERROR)
|
|
|
|
|
|
def obtain_credentials(profile):
|
|
"""Figure out which of the 2 possible backend credential engines is available
|
|
"""
|
|
try:
|
|
credentials = JsonCredentials(profile)
|
|
except NotFoundError:
|
|
try:
|
|
credentials = SqliteCredentials(profile)
|
|
except NotFoundError:
|
|
LOG.error("Couldn't find credentials file (logins.json or signons.sqlite).")
|
|
raise Exit(Exit.MISSING_SECRETS)
|
|
|
|
return credentials
|
|
|
|
|
|
def export_pass(to_export, prefix, username_prefix):
|
|
"""Export given passwords to password store
|
|
|
|
Format of "to_export" should be:
|
|
{"address": {"login": "password", ...}, ...}
|
|
"""
|
|
LOG.info("Exporting credentials to password store")
|
|
for address in to_export:
|
|
for user, passw in to_export[address].items():
|
|
# When more than one account exist for the same address, add
|
|
# the login to the password identifier
|
|
if len(to_export[address]) > 1:
|
|
passname = u"{0}/{1}/{2}".format(prefix, address, user)
|
|
|
|
else:
|
|
passname = u"{0}/{1}".format(prefix, address)
|
|
|
|
LOG.debug("Exporting credentials for '%s'", passname)
|
|
|
|
data = u"{0}\n{1}{2}\n".format(passw, username_prefix, user)
|
|
|
|
LOG.debug("Inserting pass '%s' '%s'", passname, data)
|
|
|
|
# NOTE --force is used. Existing passwords will be overwritten
|
|
cmd = ["pass", "insert", "--force", "--multiline", passname]
|
|
|
|
LOG.debug("Running command '%s' with stdin '%s'", cmd, data)
|
|
|
|
p = Popen(cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE)
|
|
out, err = p.communicate(data.encode("utf8"))
|
|
|
|
if p.returncode != 0:
|
|
LOG.error("ERROR: passwordstore exited with non-zero: %s", p.returncode)
|
|
LOG.error("Stdout/Stderr was '%s' '%s'", out, err)
|
|
raise Exit(Exit.PASSSTORE_ERROR)
|
|
|
|
LOG.debug("Successfully exported '%s'", passname)
|
|
|
|
|
|
def get_sections(profiles):
|
|
"""
|
|
Returns hash of profile numbers and profile names.
|
|
"""
|
|
sections = {}
|
|
i = 1
|
|
for section in profiles.sections():
|
|
if section.startswith("Profile"):
|
|
sections[str(i)] = profiles.get(section, "Path")
|
|
i += 1
|
|
else:
|
|
continue
|
|
return sections
|
|
|
|
|
|
def print_sections(sections, textIOWrapper=sys.stderr):
|
|
"""
|
|
Prints all available sections to an textIOWrapper (defaults to sys.stderr)
|
|
"""
|
|
for i in sorted(sections):
|
|
textIOWrapper.write("{0} -> {1}\n".format(i, sections[i]))
|
|
textIOWrapper.flush()
|
|
|
|
|
|
def ask_section(profiles, choice_arg):
|
|
"""
|
|
Prompt the user which profile should be used for decryption
|
|
"""
|
|
sections = get_sections(profiles)
|
|
|
|
# Do not ask for choice if user already gave one
|
|
if choice_arg and len(choice_arg) == 1:
|
|
choice = choice_arg[0]
|
|
else:
|
|
# If only one menu entry exists, use it without prompting
|
|
if len(sections) == 1:
|
|
choice = "1"
|
|
|
|
else:
|
|
choice = None
|
|
while choice not in sections:
|
|
sys.stderr.write("Select the Firefox profile you wish to decrypt\n")
|
|
print_sections(sections)
|
|
try:
|
|
choice = raw_input()
|
|
except EOFError:
|
|
LOG.error("Could not read Choice, got EOF")
|
|
raise Exit(Exit.READ_GOT_EOF)
|
|
|
|
try:
|
|
final_choice = sections[choice]
|
|
except KeyError:
|
|
LOG.error("Profile No. %s does not exist!", choice)
|
|
raise Exit(Exit.NO_SUCH_PROFILE)
|
|
|
|
LOG.debug("Profile selection matched %s", final_choice)
|
|
|
|
return final_choice
|
|
|
|
|
|
def ask_password(profile, interactive):
|
|
"""
|
|
Prompt for profile password
|
|
"""
|
|
utf8 = "UTF-8"
|
|
input_encoding = utf8 if sys.stdin.encoding in (None, 'ascii') else sys.stdin.encoding
|
|
passmsg = "\nMaster Password for profile {}: ".format(profile)
|
|
|
|
if sys.stdin.isatty() and interactive:
|
|
passwd = getpass(passmsg)
|
|
|
|
else:
|
|
# Ability to read the password from stdin (echo "pass" | ./firefox_...)
|
|
passwd = sys.stdin.readline().rstrip("\n")
|
|
|
|
if PY3:
|
|
return passwd
|
|
else:
|
|
return passwd.decode(input_encoding)
|
|
|
|
|
|
def read_profiles(basepath, list_profiles):
|
|
"""
|
|
Parse Firefox profiles in provided location.
|
|
If list_profiles is true, will exit after listing available profiles.
|
|
"""
|
|
profileini = os.path.join(basepath, "profiles.ini")
|
|
|
|
LOG.debug("Reading profiles from %s", profileini)
|
|
|
|
if not os.path.isfile(profileini):
|
|
LOG.warn("profile.ini not found in %s", basepath)
|
|
raise Exit(Exit.MISSING_PROFILEINI)
|
|
|
|
# Read profiles from Firefox profile folder
|
|
profiles = ConfigParser()
|
|
profiles.read(profileini)
|
|
|
|
LOG.debug("Read profiles %s", profiles.sections())
|
|
|
|
if list_profiles:
|
|
LOG.debug("Listing available profiles...")
|
|
print_sections(get_sections(profiles), sys.stdout)
|
|
raise Exit(0)
|
|
|
|
return profiles
|
|
|
|
|
|
def get_profile(basepath, interactive, choice, list_profiles):
|
|
"""
|
|
Select profile to use by either reading profiles.ini or assuming given
|
|
path is already a profile
|
|
If interactive is false, will not try to ask which profile to decrypt.
|
|
choice contains the choice the user gave us as an CLI arg.
|
|
If list_profiles is true will exits after listing all available profiles.
|
|
"""
|
|
try:
|
|
profiles = read_profiles(basepath, list_profiles)
|
|
except Exit as e:
|
|
if e.exitcode == Exit.MISSING_PROFILEINI:
|
|
LOG.warn("Continuing and assuming '%s' is a profile location", basepath)
|
|
profile = basepath
|
|
|
|
if list_profiles:
|
|
LOG.error("Listing single profiles not permitted.")
|
|
raise
|
|
|
|
if not os.path.isdir(profile):
|
|
LOG.error("Profile location '%s' is not a directory", profile)
|
|
raise
|
|
else:
|
|
raise
|
|
else:
|
|
if not interactive:
|
|
|
|
sections = get_sections(profiles)
|
|
|
|
if choice and len(choice) == 1:
|
|
|
|
try:
|
|
section = sections[(choice[0])]
|
|
except KeyError:
|
|
LOG.error("Profile No. %s does not exist!", choice[0])
|
|
raise Exit(Exit.NO_SUCH_PROFILE)
|
|
|
|
elif len(sections) == 1:
|
|
section = sections['1']
|
|
|
|
else:
|
|
LOG.error("Don't know which profile to decrypt. We are in non-interactive mode and -c/--choice is missing.")
|
|
raise Exit(Exit.MISSING_CHOICE)
|
|
else:
|
|
# Ask user which profile to open
|
|
section = ask_section(profiles, choice)
|
|
|
|
profile = os.path.join(basepath, section)
|
|
|
|
if not os.path.isdir(profile):
|
|
LOG.error("Profile location '%s' is not a directory. Has profiles.ini been tampered with?", profile)
|
|
raise Exit(Exit.BAD_PROFILEINI)
|
|
|
|
return profile
|
|
|
|
|
|
def parse_sys_args():
|
|
"""Parse command line arguments
|
|
"""
|
|
|
|
if os.name == "nt":
|
|
profile_path = os.path.join(os.environ['APPDATA'], "Mozilla", "Firefox")
|
|
elif os.uname()[0] == "Darwin":
|
|
profile_path = "~/Library/Application Support/Firefox"
|
|
else:
|
|
profile_path = "~/.mozilla/firefox"
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Access Firefox/Thunderbird profiles and decrypt existing passwords"
|
|
)
|
|
parser.add_argument("profile", nargs="?", default=profile_path,
|
|
help="Path to profile folder (default: {0})".format(profile_path))
|
|
parser.add_argument("-e", "--export-pass", action="store_true",
|
|
help="Export URL, username and password to pass from passwordstore.org")
|
|
parser.add_argument("--pass-compat", action="store", choices={"default", "browserpass"},
|
|
default="default",
|
|
help="Export username as is (default), or prefixed with `login:` for compatibility with browserpass")
|
|
parser.add_argument("-p", "--pass-prefix", action="store", default=u"web",
|
|
help="Prefix for export to pass from passwordstore.org (default: %(default)s)")
|
|
parser.add_argument("-f", "--format", action="store", choices={"csv", "human"},
|
|
default="human", help="Format for the output.")
|
|
parser.add_argument("-d", "--delimiter", action="store", default=";",
|
|
help="The delimiter for csv output")
|
|
parser.add_argument("-q", "--quotechar", action="store", default='"',
|
|
help="The quote char for csv output")
|
|
parser.add_argument("-t", "--tabular", action="store_true", help=argparse.SUPPRESS)
|
|
parser.add_argument("-n", "--no-interactive", dest="interactive",
|
|
default=True, action="store_false",
|
|
help="Disable interactivity.")
|
|
parser.add_argument("-c", "--choice", nargs=1,
|
|
help="The profile to use (starts with 1). If only one profile, defaults to that.")
|
|
parser.add_argument("-l", "--list", action="store_true",
|
|
help="List profiles and exit.")
|
|
parser.add_argument("-v", "--verbose", action="count", default=0,
|
|
help="Verbosity level. Warning on -vv (highest level) user input will be printed on screen")
|
|
parser.add_argument("--version", action="version", version=__version__,
|
|
help="Display version of firefox_decrypt and exit")
|
|
|
|
args = parser.parse_args()
|
|
|
|
# replace character you can't enter as argument
|
|
if args.delimiter == "\\t":
|
|
args.delimiter = "\t"
|
|
|
|
if args.tabular:
|
|
args.format = "csv"
|
|
args.delimiter = "\t"
|
|
args.quotechar = "'"
|
|
|
|
return args
|
|
|
|
|
|
def setup_logging(args):
|
|
"""Setup the logging level and configure the basic logger
|
|
"""
|
|
if args.verbose == 1:
|
|
level = logging.INFO
|
|
elif args.verbose >= 2:
|
|
level = logging.DEBUG
|
|
else:
|
|
level = logging.WARN
|
|
|
|
logging.basicConfig(
|
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
level=level,
|
|
)
|
|
|
|
global LOG
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
def main():
|
|
"""Main entry point
|
|
"""
|
|
args = parse_sys_args()
|
|
|
|
setup_logging(args)
|
|
if args.tabular:
|
|
LOG.warning("--tabular is deprecated. Use `--format csv --delimiter \\t` instead")
|
|
|
|
LOG.info("Running firefox_decrypt version: %s", __version__)
|
|
LOG.debug("Parsed commandline arguments: %s", args)
|
|
|
|
# Check whether pass from passwordstore.org is installed
|
|
test_password_store(args.export_pass)
|
|
|
|
# Initialize nss before asking the user for input
|
|
nss = NSSInteraction()
|
|
|
|
basepath = os.path.expanduser(args.profile)
|
|
|
|
# Read profiles from profiles.ini in profile folder
|
|
profile = get_profile(basepath, args.interactive, args.choice, args.list)
|
|
|
|
# Start NSS for selected profile
|
|
nss.load_profile(profile)
|
|
# Check if profile is password protected and prompt for a password
|
|
nss.authenticate(args.interactive)
|
|
# Decode all passwords
|
|
to_export = nss.decrypt_passwords(
|
|
export=args.export_pass,
|
|
output_format=args.format,
|
|
csv_delimiter=args.delimiter,
|
|
csv_quotechar=args.quotechar,
|
|
)
|
|
|
|
if args.export_pass:
|
|
username_prefix = "login: " if args.pass_compat == "browserpass" else ""
|
|
export_pass(to_export, args.pass_prefix, username_prefix)
|
|
|
|
# And shutdown NSS
|
|
nss.unload_profile()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except KeyboardInterrupt as e:
|
|
print("Quit.")
|
|
sys.exit(Exit.KEYBOARD_INTERRUPT)
|
|
except Exit as e:
|
|
sys.exit(e.exitcode)
|