-#!/usr/bin/env python
+#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
import json
import logging
import os
+import select
import sqlite3
import sys
from base64 import b64decode
PY3 = sys.version_info.major > 2
LOG = None
VERBOSE = False
+SYS64 = sys.maxsize > 2**32
+
+if not PY3 and os.name == "nt":
+ sys.stderr.write("WARNING: You are using Python 2 on Windows. If your "
+ "passwords include non-alphanumeric characters you "
+ "will run into problems.\n")
+ sys.stderr.write("WARNING: Python 2 + Windows is no longer supported. "
+ "Please use Python 3 instead\n")
+
+# Windows uses a mixture of different codecs for different components
+# ANSI CP1252 for system messages, while NSS uses UTF-8
+# To further complicate things, with python 2.7 the default stdout/stdin codec
+# isn't UTF-8 but language dependent (tested on Windows 7)
+
+if os.name == "nt":
+ SYS_ENCODING = "cp1252"
+ LIB_ENCODING = "utf8"
+else:
+ SYS_ENCODING = "utf8"
+ LIB_ENCODING = "utf8"
+
+# When using pipes stdin/stdout encoding may be None
+USR_ENCODING = sys.stdin.encoding or sys.stdout.encoding or "utf8"
+
+
+def py2_decode(_bytes, encoding=USR_ENCODING):
+ if PY3:
+ return _bytes
+ else:
+ return _bytes.decode(encoding)
+
+
+def py2_encode(_unicode, encoding=USR_ENCODING):
+ if PY3:
+ return _unicode
+ else:
+ return _unicode.encode(encoding)
+
+
+def type_decode(encoding):
+ return lambda x: py2_decode(x, encoding)
def get_version():
the internal version number
"""
def internal_version():
- return '.'.join(map(str, __version_info__))
+ return '.'.join(map(str, __version_info__[:3])) + ''.join(__version_info__[3:])
try:
p = Popen(["git", "describe", "--tags"], stdout=PIPE, stderr=DEVNULL)
if p.returncode:
return internal_version()
else:
- return stdout.strip().decode("utf-8")
+ # Both py2 and py3 return bytes here
+ return stdout.decode(USR_ENCODING).strip()
-__version_info__ = (0, 6, 2)
+__version_info__ = (0, 8, 0, "+git")
__version__ = get_version()
MISSING_SECRETS = 3
BAD_PROFILEINI = 4
LOCATION_NO_DIRECTORY = 5
+ BAD_SECRETS = 6
+ FAIL_LOCATE_NSS = 10
FAIL_LOAD_NSS = 11
FAIL_INIT_NSS = 12
FAIL_NSS_KEYSLOT = 13
try:
logins = data["logins"]
- except:
- raise Exception("Unrecognized format in {0}".format(self.db))
+ except Exception:
+ LOG.error("Unrecognized format in {0}".format(self.db))
+ raise Exit(Exit.BAD_SECRETS)
for i in logins:
yield (i["hostname"], i["encryptedUsername"],
def find_nss(locations, nssname):
"""Locate nss is one of the many possible locations
"""
+ fail_errors = []
+
for loc in locations:
- if os.path.exists(os.path.join(loc, nssname)):
- return loc
+ nsslib = os.path.join(loc, nssname)
+ LOG.debug("Loading NSS library from %s", nsslib)
+
+ if os.name == "nt":
+ # On windows in order to find DLLs referenced by nss3.dll
+ # we need to have those locations on PATH
+ os.environ["PATH"] = ';'.join([loc, os.environ["PATH"]])
+ LOG.debug("PATH is now %s", os.environ["PATH"])
+ # However this doesn't seem to work on all setups and needs to be
+ # set before starting python so as a workaround we chdir to
+ # Firefox's nss3.dll location
+ if loc:
+ if not os.path.isdir(loc):
+ # No point in trying to load from paths that don't exist
+ continue
+
+ workdir = os.getcwd()
+ os.chdir(loc)
+
+ try:
+ nss = ct.CDLL(nsslib)
+ except OSError as e:
+ fail_errors.append((nsslib, str(e)))
+ else:
+ LOG.debug("Loaded NSS library from %s", nsslib)
+ return nss
+ finally:
+ if os.name == "nt" and loc:
+ # Restore workdir changed above
+ os.chdir(workdir)
+
+ else:
+ LOG.error("Couldn't find or load '%s'. This library is essential "
+ "to interact with your Mozilla profile.", nssname)
+ LOG.error("If you are seeing this error please perform a system-wide "
+ "search for '%s' and file a bug report indicating any "
+ "location found. Thanks!", nssname)
+ LOG.error("Alternatively you can try launching firefox_decrypt "
+ "from the location where you found '%s'. "
+ "That is 'cd' or 'chdir' to that location and run "
+ "firefox_decrypt from there.", nssname)
+
+ LOG.error("Please also include the following on any bug report. "
+ "Errors seen while searching/loading NSS:")
- LOG.warn("%s not found on any of the default locations for this platform. "
- "Attempting to continue nonetheless.", nssname)
- return ""
+ for target, error in fail_errors:
+ LOG.error("Error when loading %s was %s", target, py2_decode(str(error), SYS_ENCODING))
+
+ raise Exit(Exit.FAIL_LOCATE_NSS)
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)
+ if SYS64:
+ locations = (
+ "", # Current directory or system lib finder
+ r"C:\Program Files\Mozilla Firefox",
+ r"C:\Program Files\Mozilla Thunderbird",
+ r"C:\Program Files\Nightly",
+ )
+ else:
+ locations = (
+ "", # Current directory or system lib finder
+ r"C:\Program Files (x86)\Mozilla Firefox",
+ r"C:\Program Files (x86)\Mozilla Thunderbird",
+ r"C:\Program Files (x86)\Nightly",
+ # On windows 32bit these folders can also be 32bit
+ r"C:\Program Files\Mozilla Firefox",
+ r"C:\Program Files\Mozilla Thunderbird",
+ r"C:\Program Files\Nightly",
+ )
- os.environ["PATH"] = ';'.join([os.environ["PATH"], firefox])
- LOG.debug("PATH is now %s", os.environ["PATH"])
+ # FIXME this was present in the past adding the location where NSS was found to PATH
+ # I'm not sure why this would be necessary. We don't need to run Firefox...
+ # TODO Test on a Windows machine and see if this works without the PATH change
+ # 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"
"/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)
+ if SYS64:
+ locations = (
+ "", # Current directory or system lib finder
+ "/usr/lib64",
+ "/usr/lib64/nss",
+ "/usr/lib",
+ "/usr/lib/nss",
+ "/usr/local/lib",
+ "/usr/local/lib/nss",
+ "/opt/local/lib",
+ "/opt/local/lib/nss",
+ os.path.expanduser("~/.nix-profile/lib"),
+ )
+ else:
+ locations = (
+ "", # Current directory or system lib finder
+ "/usr/lib",
+ "/usr/lib/nss",
+ "/usr/lib32",
+ "/usr/lib32/nss",
+ "/usr/lib64",
+ "/usr/lib64/nss",
+ "/usr/local/lib",
+ "/usr/local/lib/nss",
+ "/opt/local/lib",
+ "/opt/local/lib/nss",
+ os.path.expanduser("~/.nix-profile/lib"),
+ )
- 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)
+ # If this succeeds libnss was loaded
+ self.NSS = self.find_nss(locations, nssname)
def handle_error(self):
"""If an error happens in libnss, handle it and print some debug information
code = self._PORT_GetError()
name = self._PR_ErrorToName(code)
- name = "NULL" if name is None else name.decode("ascii")
+ name = "NULL" if name is None else name.decode(SYS_ENCODING)
# 0 is the default language (localization related)
text = self._PR_ErrorToString(code, 0)
- text = text.decode("utf8")
+ text = text.decode(SYS_ENCODING)
LOG.debug("%s: %s", name, text)
self.handle_error()
raise Exit(Exit.NEED_MASTER_PASSWORD)
- res = ct.string_at(out.data, out.len).decode("utf8")
+ res = ct.string_at(out.data, out.len).decode(LIB_ENCODING)
finally:
# Avoid leaking SECItem
self._SECITEM_ZfreeItem(out, 0)
LOG.debug("Initializing NSS with profile path '%s'", profile)
self.profile = profile
- e = self.NSS._NSS_Init(self.profile.encode("utf8"))
+ profile = profile.encode(LIB_ENCODING)
+
+ e = self.NSS._NSS_Init(b"sql:" + profile)
LOG.debug("Initializing NSS returned %s", e)
if e != 0:
- LOG.error("Couldn't initialize NSS, maybe '%s' is not a valid profile?", profile)
+ LOG.error("Couldn't initialize NSS, maybe '%s' is not a valid profile?", self.profile)
self.NSS.handle_error()
raise Exit(Exit.FAIL_INIT_NSS)
# 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
+ # this adds an unnecessary 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"))
+ e = self.NSS._PK11_CheckUserPassword(keyslot, password.encode(LIB_ENCODING))
LOG.debug("Checking user password returned %s", e)
raise Exit(Exit.BAD_MASTER_PASSWORD)
else:
- LOG.warn("Attempting decryption with no Master Password")
+ LOG.warning("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
+ """Shutdown NSS and deactivate current profile
"""
e = self.NSS._NSS_Shutdown()
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
LOG.info("Decrypting credentials")
to_export = {}
-
- output_line("[FirefoxAccounts]\n") # NB
+ outputs = []
if output_format == "csv":
csv_writer = csv.DictWriter(
if PY3:
csv_writer.writerow(output)
else:
- csv_writer.writerow({k: v.encode("utf8") for k, v in output.items()})
+ csv_writer.writerow({k: v.encode(USR_ENCODING) for k, v in output.items()})
+ elif output_format == "json":
+ output = {"url": url, "user": user, "password": passw}
+ outputs.append(output)
- # NB else:
- elif 0:
+ 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)
- else:
- # Other out format - NB 26.07.16
- if user and passw:
- user = user+':'
- output_line(url+' = '+user+passw+"\n")
+ sys.stdout.write(py2_encode(line, USR_ENCODING))
+ if output_format == "json":
+ print(json.dumps(outputs))
+
credentials.done()
if not got_password:
- LOG.warn("No passwords found in selected profile")
+ LOG.warning("No passwords found in selected profile")
if export:
return to_export
-def test_password_store(export):
+def test_password_store(export, pass_cmd):
"""Check if pass from passwordstore.org is installed
If it is installed but not initialized, initialize it
"""
LOG.debug("Testing if password store is installed and configured")
try:
- p = Popen(["pass"], stdout=PIPE, stderr=PIPE)
+ p = Popen([pass_cmd], stdout=PIPE, stderr=PIPE)
except OSError as e:
if e.errno == 2:
LOG.error("Password store is not installed and exporting was requested")
return credentials
-def export_pass(to_export, prefix):
+def export_pass(to_export, pass_cmd, 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")
+ if prefix:
+ prefix = u"{0}/".format(prefix)
+
+ LOG.debug("Using pass prefix '%s'", prefix)
+
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)
+ passname = u"{0}{1}/{2}".format(prefix, address, user)
else:
- passname = u"{0}/{1}".format(prefix, address)
+ passname = u"{0}{1}".format(prefix, address)
LOG.debug("Exporting credentials for '%s'", passname)
- data = u"{0}\n{1}\n".format(passw, user)
+ 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]
+ cmd = [pass_cmd, "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"))
+ out, err = p.communicate(data.encode(SYS_ENCODING))
if p.returncode != 0:
LOG.error("ERROR: passwordstore exited with non-zero: %s", p.returncode)
"""
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 not PY3:
+ profile = profile.encode(SYS_ENCODING)
+
+ passmsg = "\nMaster Password for profile {0}: ".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 sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
+ passwd = sys.stdin.readline().rstrip("\n")
+ else:
+ LOG.warning("Master Password not provided, continuing with blank password")
+ passwd = ""
- if PY3:
- return passwd
- else:
- return passwd.decode(input_encoding)
+ return py2_decode(passwd)
def read_profiles(basepath, list_profiles):
LOG.debug("Reading profiles from %s", profileini)
if not os.path.isfile(profileini):
- LOG.warn("profile.ini not found in %s", basepath)
+ LOG.warning("profile.ini not found in %s", basepath)
raise Exit(Exit.MISSING_PROFILEINI)
# Read profiles from Firefox profile folder
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)
+ LOG.warning("Continuing and assuming '%s' is a profile location", basepath)
profile = basepath
if list_profiles:
raise
else:
if not interactive:
-
sections = get_sections(profiles)
if choice and len(choice) == 1:
-
try:
section = sections[(choice[0])]
except KeyError:
# Ask user which profile to open
section = ask_section(profiles, choice)
+ section = py2_decode(section, LIB_ENCODING)
profile = os.path.join(basepath, section)
if not os.path.isdir(profile):
description="Access Firefox/Thunderbird profiles and decrypt existing passwords"
)
parser.add_argument("profile", nargs="?", default=profile_path,
+ type=type_decode(SYS_ENCODING),
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", "username"},
+ default="default",
+ help="Export username as is (default), or with one of the compatibility modes")
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"},
+ parser.add_argument("-m", "--pass-cmd", action="store", default=u"pass",
+ help="Command/path to use when exporting to pass (default: %(default)s)")
+ parser.add_argument("-f", "--format", action="store", choices={"csv", "human", "json"},
default="human", help="Format for the output.")
parser.add_argument("-d", "--delimiter", action="store", default=";",
help="The delimiter for csv output")
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)
+ LOG.debug("Running with encodings: USR: %s, SYS: %s, LIB: %s", USR_ENCODING, SYS_ENCODING, LIB_ENCODING)
# Check whether pass from passwordstore.org is installed
- test_password_store(args.export_pass)
+ test_password_store(args.export_pass, args.pass_cmd)
# Initialize nss before asking the user for input
nss = NSSInteraction()
)
if args.export_pass:
- export_pass(to_export, args.pass_prefix)
+ # List of compatibility modes for username prefixes
+ compat = {
+ "username": "username: ",
+ "browserpass": "login: ",
+ }
+
+ username_prefix = compat.get(args.pass_compat, "")
+ export_pass(to_export, args.pass_cmd, args.pass_prefix, username_prefix)
# And shutdown NSS
nss.unload_profile()