diff --git a/CHANGELOG.md b/CHANGELOG.md index 7987560..1da6724 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Allow overriding default encoding - Add `--pass-always-with-login` to always include /login as part of pass's password path - Improve compatibility with `gopass` by explicitly using `pass ls` instead of `pass` +- Add `--non-fatal-decryption` to attempt decrypting partially corrupt databases instead of aborting on first failure +- Enable All Contributors framework in project ##### 1.0.0 - Improve detection of NSS in Windows and MacOS diff --git a/README.md b/README.md index b9e9713..4dfe687 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ You can also choose from one of the supported formats with `--format`: * `json` - a machine compatible format - see [JSON](https://en.wikipedia.org/wiki/JSON) * `pass` - a special output format that directly calls to the [passwordstore.org](https://www.passwordstore.org) command to export passwords (*). See also `--pass-*` options. -(*) `pass` can produce unintended consequences. Make sure to backup your password store before using this. +(*) `pass` can produce unintended consequences. Make sure to backup your password store before using this option. ##### Non-interactive mode @@ -164,6 +164,19 @@ There is currently no way to selectively export passwords. Exporting will overwrite existing passwords without warning. Ensure you have a backup or are using the `pass git` functionality. +##### Non fatal password decryption + +By default, encountering a corrupted username or password will abort decryption. +Since version `1.1.0` there is now `--non-fatal-decryption` that tolerates individual failures. + + $ python firefox_decrypt.py --non-fatal-decryption + (...) + Website: https://github.com + Username: '*** decryption failed ***' + Password: '*** decryption failed ***' + +which can also be combined with any of the above `--format` options. + #### Troubleshooting If a problem occurs, please try `firefox_decrypt` in high verbosity mode by calling it with: @@ -176,6 +189,10 @@ If the output does not help you to identify the cause and a solution to the prob - your profile password, as well as other passwords, may be visible in the output – so **please remove any sensitive data** before sharing the output. +##### Silencing error messages + +Logging messages above warning level are included in the standard error output by default as these can be useful to troubleshoot failures. +If you wish to omit this information append ` 2>/dev/null` to your command on UNIX and ` 2> nul` on Windows. ##### Windows diff --git a/firefox_decrypt.py b/firefox_decrypt.py index 10998dc..190920e 100755 --- a/firefox_decrypt.py +++ b/firefox_decrypt.py @@ -59,8 +59,9 @@ def get_version() -> str: """Obtain version information from git if available otherwise use the internal version number """ + def internal_version(): - return '.'.join(map(str, __version_info__[:3])) + ''.join(__version_info__[3:]) + return ".".join(map(str, __version_info__[:3])) + "".join(__version_info__[3:]) try: p = run(["git", "describe", "--tags"], stdout=PIPE, stderr=DEVNULL, text=True) @@ -78,14 +79,14 @@ __version__: str = get_version() class NotFoundError(Exception): - """Exception to handle situations where a credentials file is not found - """ + """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 - """ + """Exception to allow a clean exit from any point in execution""" + CLEAN = 0 ERROR = 1 MISSING_PROFILEINI = 2 @@ -123,8 +124,8 @@ class Exit(Exception): class Credentials: - """Base credentials backend manager - """ + """Base credentials backend manager""" + def __init__(self, db): self.db = db @@ -145,8 +146,8 @@ class Credentials: class SqliteCredentials(Credentials): - """SQLite credentials backend manager - """ + """SQLite credentials backend manager""" + def __init__(self, profile): db = os.path.join(profile, "signons.sqlite") @@ -157,15 +158,16 @@ class SqliteCredentials(Credentials): def __iter__(self) -> Iterator[tuple[str, str, str, int]]: LOG.debug("Reading password database in SQLite format") - self.c.execute("SELECT hostname, encryptedUsername, encryptedPassword, encType " - "FROM moz_logins") + 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 - """ + """Close the sqlite cursor and database connection""" super(SqliteCredentials, self).done() self.c.close() @@ -173,8 +175,8 @@ class SqliteCredentials(Credentials): class JsonCredentials(Credentials): - """JSON credentials backend manager - """ + """JSON credentials backend manager""" + def __init__(self, profile): db = os.path.join(profile, "logins.json") @@ -192,13 +194,16 @@ class JsonCredentials(Credentials): raise Exit(Exit.BAD_SECRETS) for i in logins: - yield (i["hostname"], i["encryptedUsername"], - i["encryptedPassword"], i["encType"]) + yield ( + i["hostname"], + i["encryptedUsername"], + i["encryptedPassword"], + i["encType"], + ) def find_nss(locations, nssname) -> ct.CDLL: - """Locate nss is one of the many possible locations - """ + """Locate nss is one of the many possible locations""" fail_errors: list[tuple[str, str]] = [] OS = ("Windows", "Darwin") @@ -210,7 +215,7 @@ def find_nss(locations, nssname) -> ct.CDLL: if SYSTEM in OS: # 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"]]) + 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 @@ -236,18 +241,29 @@ def find_nss(locations, nssname) -> ct.CDLL: 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( + "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.error( + "Please also include the following on any bug report. " + "Errors seen while searching/loading NSS:" + ) for target, error in fail_errors: LOG.error("Error when loading %s was %s", target, error) @@ -256,8 +272,7 @@ def find_nss(locations, nssname) -> ct.CDLL: def load_libnss(): - """Load libnss into python using the CDLL interface - """ + """Load libnss into python using the CDLL interface""" if SYSTEM == "Windows": nssname = "nss3.dll" locations: list[str] = [ @@ -348,18 +363,19 @@ def load_libnss(): class c_char_p_fromstr(ct.c_char_p): """ctypes char_p override that handles encoding str to bytes""" + def from_param(self): return self.encode(DEFAULT_ENCODING) class NSSProxy: class SECItem(ct.Structure): - """struct needed to interact with libnss - """ + """struct needed to interact with libnss""" + _fields_ = [ - ('type', ct.c_uint), - ('data', ct.c_char_p), # actually: unsigned char * - ('len', ct.c_uint), + ("type", ct.c_uint), + ("data", ct.c_char_p), # actually: unsigned char * + ("len", ct.c_uint), ] def decode_data(self): @@ -367,12 +383,12 @@ class NSSProxy: return _bytes.decode(DEFAULT_ENCODING) class PK11SlotInfo(ct.Structure): - """Opaque structure representing a logical PKCS slot - """ + """Opaque structure representing a logical PKCS slot""" - def __init__(self): + def __init__(self, non_fatal_decryption=False): # Locate libnss and try loading it self.libnss = load_libnss() + self.non_fatal_decryption = non_fatal_decryption SlotInfoPtr = ct.POINTER(self.PK11SlotInfo) SECItemPtr = ct.POINTER(self.SECItem) @@ -382,8 +398,12 @@ class NSSProxy: self._set_ctypes(SlotInfoPtr, "PK11_GetInternalKeySlot") self._set_ctypes(None, "PK11_FreeSlot", SlotInfoPtr) self._set_ctypes(ct.c_int, "PK11_NeedLogin", SlotInfoPtr) - self._set_ctypes(ct.c_int, "PK11_CheckUserPassword", SlotInfoPtr, c_char_p_fromstr) - self._set_ctypes(ct.c_int, "PK11SDR_Decrypt", SECItemPtr, SECItemPtr, ct.c_void_p) + self._set_ctypes( + ct.c_int, "PK11_CheckUserPassword", SlotInfoPtr, c_char_p_fromstr + ) + 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 @@ -392,19 +412,20 @@ class NSSProxy: 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 - """ + """Set input/output types on libnss C functions for automatic type casting""" res = getattr(self.libnss, name) res.argtypes = argtypes res.restype = restype # Transparently handle decoding to string when returning a c_char_p if restype == ct.c_char_p: + def _decode(result, func, *args): try: return result.decode(DEFAULT_ENCODING) except AttributeError: return result + res.errcheck = _decode setattr(self, "_" + name, res) @@ -469,8 +490,7 @@ class NSSProxy: self._PK11_FreeSlot(keyslot) def handle_error(self, exitcode: int, *logerror: Any): - """If an error happens in libnss, handle it and print some debug information - """ + """If an error happens in libnss, handle it and print some debug information""" if logerror: LOG.error(*logerror) else: @@ -495,12 +515,16 @@ class NSSProxy: LOG.debug("Decryption of data returned %s", err_status) try: if err_status: # -1 means password failed, other status are unknown - self.handle_error( - Exit.DECRYPTION_FAILED, + error_msg = ( "Username/Password decryption failed. " - "Credentials damaged or cert/key file mismatch.", + "Credentials damaged or cert/key file mismatch." ) + if self.non_fatal_decryption: + raise ValueError(error_msg) + else: + self.handle_error(Exit.DECRYPTION_FAILED, error_msg) + res = out.decode_data() finally: # Avoid leaking SECItem @@ -513,13 +537,13 @@ class MozillaInteraction: """ Abstraction interface to Mozilla profile and lib NSS """ - def __init__(self): + + def __init__(self, non_fatal_decryption=False): self.profile = None - self.proxy = NSSProxy() + self.proxy = NSSProxy(non_fatal_decryption) def load_profile(self, profile): - """Initialize the NSS library and profile - """ + """Initialize the NSS library and profile""" self.profile = profile self.proxy.initialize(self.profile) @@ -530,8 +554,7 @@ class MozillaInteraction: self.proxy.authenticate(self.profile, interactive) def unload_profile(self): - """Shutdown NSS and deactivate current profile - """ + """Shutdown NSS and deactivate current profile""" self.proxy.shutdown() def decrypt_passwords(self) -> PWStore: @@ -541,7 +564,7 @@ class MozillaInteraction: credentials: Credentials = self.obtain_credentials() LOG.info("Decrypting credentials") - outputs: list[dict[str, str]] = [] + outputs: PWStore = [] url: str user: str @@ -556,11 +579,20 @@ class MozillaInteraction: LOG.debug("Decrypting password data '%s'", passw) passw = self.proxy.decrypt(passw) except (TypeError, ValueError) as e: - LOG.warning("Failed to decode username or password for entry from URL %s", url) + LOG.warning( + "Failed to decode username or password for entry from URL %s", + url, + ) LOG.exception(e) - continue + user = "*** decryption failed ***" + passw = "*** decryption failed ***" - LOG.debug("Decoded username '%s' and password '%s' for website '%s'", user, passw, url) + LOG.debug( + "Decoded username '%s' and password '%s' for website '%s'", + user, + passw, + url, + ) output = {"url": url, "user": user, "password": passw} outputs.append(output) @@ -574,8 +606,7 @@ class MozillaInteraction: return outputs def obtain_credentials(self) -> Credentials: - """Figure out which of the 2 possible backend credential engines is available - """ + """Figure out which of the 2 possible backend credential engines is available""" credentials: Credentials try: credentials = JsonCredentials(self.profile) @@ -583,7 +614,9 @@ class MozillaInteraction: try: credentials = SqliteCredentials(self.profile) except NotFoundError: - LOG.error("Couldn't find credentials file (logins.json or signons.sqlite).") + LOG.error( + "Couldn't find credentials file (logins.json or signons.sqlite)." + ) raise Exit(Exit.MISSING_SECRETS) return credentials @@ -739,14 +772,22 @@ class PassOutputFormat(OutputFormat): LOG.debug("Inserting pass '%s' '%s'", passname, data) # NOTE --force is used. Existing passwords will be overwritten - cmd: list[str] = [self.cmd, "insert", "--force", "--multiline", passname] + cmd: list[str] = [ + self.cmd, + "insert", + "--force", + "--multiline", + passname, + ] LOG.debug("Running command '%s' with stdin '%s'", cmd, data) p = run(cmd, input=data, capture_output=True, text=True) if p.returncode != 0: - LOG.error("ERROR: passwordstore exited with non-zero: %s", p.returncode) + LOG.error( + "ERROR: passwordstore exited with non-zero: %s", p.returncode + ) LOG.error("Stdout: %s\nStderr: %s", p.stdout, p.stderr) raise Exit(Exit.PASSSTORE_ERROR) @@ -843,7 +884,9 @@ def read_profiles(basepath): return profiles -def get_profile(basepath: str, interactive: bool, choice: Optional[str], list_profiles: bool): +def get_profile( + basepath: str, interactive: bool, choice: Optional[str], list_profiles: bool +): """ Select profile to use by either reading profiles.ini or assuming given path is already a profile @@ -887,8 +930,10 @@ def get_profile(basepath: str, interactive: bool, choice: Optional[str], list_pr raise Exit(Exit.NO_SUCH_PROFILE) elif not interactive: - LOG.error("Don't know which profile to decrypt. " - "We are in non-interactive mode and -c/--choice wasn't specified.") + LOG.error( + "Don't know which profile to decrypt. " + "We are in non-interactive mode and -c/--choice wasn't specified." + ) raise Exit(Exit.MISSING_CHOICE) else: @@ -899,7 +944,10 @@ def get_profile(basepath: str, interactive: bool, choice: Optional[str], list_pr 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) + LOG.error( + "Profile location '%s' is not a directory. Has profiles.ini been tampered with?", + profile, + ) raise Exit(Exit.BAD_PROFILEINI) return profile @@ -911,6 +959,7 @@ class ConvertChoices(argparse.Action): mapping the user-specified choices values to the resulting option values. """ + def __init__(self, *args, choices, **kwargs): super().__init__(*args, choices=choices.keys(), **kwargs) self.mapping = choices @@ -920,11 +969,10 @@ class ConvertChoices(argparse.Action): def parse_sys_args() -> argparse.Namespace: - """Parse command line arguments - """ + """Parse command line arguments""" if SYSTEM == "Windows": - profile_path = os.path.join(os.environ['APPDATA'], "Mozilla", "Firefox") + profile_path = os.path.join(os.environ["APPDATA"], "Mozilla", "Firefox") elif os.uname()[0] == "Darwin": profile_path = "~/Library/Application Support/Firefox" else: @@ -935,8 +983,10 @@ def parse_sys_args() -> argparse.Namespace: ) parser.add_argument( "profile", - nargs="?", default=profile_path, - help=f"Path to profile folder (default: {profile_path})") + nargs="?", + default=profile_path, + help=f"Path to profile folder (default: {profile_path})", + ) format_choices = { "human": HumanOutputFormat, @@ -947,25 +997,34 @@ def parse_sys_args() -> argparse.Namespace: } parser.add_argument( - "-f", "--format", + "-f", + "--format", action=ConvertChoices, - choices=format_choices, default=HumanOutputFormat, - help="Format for the output.") + choices=format_choices, + default=HumanOutputFormat, + help="Format for the output.", + ) parser.add_argument( - "-d", "--csv-delimiter", + "-d", + "--csv-delimiter", action="store", default=";", - help="The delimiter for csv output") + help="The delimiter for csv output", + ) parser.add_argument( - "-q", "--csv-quotechar", + "-q", + "--csv-quotechar", action="store", default='"', - help="The quote char for csv output") + help="The quote char for csv output", + ) parser.add_argument( "--no-csv-header", - action="store_false", dest="csv_header", + action="store_false", + dest="csv_header", default=True, - help="Do not include a header in CSV output.") + help="Do not include a header in CSV output.", + ) parser.add_argument( "--pass-username-prefix", action="store", @@ -973,47 +1032,69 @@ def parse_sys_args() -> argparse.Namespace: help=( "Export username as is (default), or with the provided format prefix. " "For instance 'login: ' for browserpass." - )) + ), + ) parser.add_argument( - "-p", "--pass-prefix", + "-p", + "--pass-prefix", action="store", default="web", - help="Folder prefix for export to pass from passwordstore.org (default: %(default)s)") + help="Folder prefix for export to pass from passwordstore.org (default: %(default)s)", + ) parser.add_argument( - "-m", "--pass-cmd", + "-m", + "--pass-cmd", action="store", default="pass", - help="Command/path to use when exporting to pass (default: %(default)s)") + help="Command/path to use when exporting to pass (default: %(default)s)", + ) parser.add_argument( "--pass-always-with-login", action="store_true", - help="Always save as / (default: only when multiple accounts per domain)") + help="Always save as / (default: only when multiple accounts per domain)", + ) parser.add_argument( - "-n", "--no-interactive", - action="store_false", dest="interactive", + "-n", + "--no-interactive", + action="store_false", + dest="interactive", default=True, - help="Disable interactivity.") + help="Disable interactivity.", + ) parser.add_argument( - "-c", "--choice", - help="The profile to use (starts with 1). If only one profile, defaults to that.") - parser.add_argument( - "-l", "--list", + "--non-fatal-decryption", action="store_true", - help="List profiles and exit.") + default=False, + help="If set, corrupted entries will be skipped instead of aborting the process.", + ) parser.add_argument( - "-e", "--encoding", + "-c", + "--choice", + 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( + "-e", + "--encoding", action="store", default=DEFAULT_ENCODING, - help="Override default encoding (%(default)s).") + help="Override default encoding (%(default)s).", + ) parser.add_argument( - "-v", "--verbose", + "-v", + "--verbose", action="count", default=0, - help="Verbosity level. Warning on -vv (highest level) user input will be printed on screen") + 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") + action="version", + version=__version__, + help="Display version of firefox_decrypt and exit", + ) args = parser.parse_args() @@ -1025,8 +1106,7 @@ def parse_sys_args() -> argparse.Namespace: def setup_logging(args) -> None: - """Setup the logging level and configure the basic logger - """ + """Setup the logging level and configure the basic logger""" if args.verbose == 1: level = logging.INFO elif args.verbose >= 2: @@ -1059,8 +1139,7 @@ def identify_system_locale() -> str: def main() -> None: - """Main entry point - """ + """Main entry point""" args = parse_sys_args() setup_logging(args) @@ -1068,8 +1147,11 @@ def main() -> None: global DEFAULT_ENCODING if args.encoding != DEFAULT_ENCODING: - LOG.info("Overriding default encoding from '%s' to '%s'", - DEFAULT_ENCODING, args.encoding) + LOG.info( + "Overriding default encoding from '%s' to '%s'", + DEFAULT_ENCODING, + args.encoding, + ) # Override default encoding if specified by user DEFAULT_ENCODING = args.encoding @@ -1084,17 +1166,20 @@ def main() -> None: ) LOG.debug( - "Running with encodings: %s: %s, %s: %s, %s: %s, %s: %s", - *chain(*encodings) + "Running with encodings: %s: %s, %s: %s, %s: %s, %s: %s", *chain(*encodings) ) for stream, encoding in encodings: if encoding.lower() != DEFAULT_ENCODING: - LOG.warning("Running with unsupported encoding '%s': %s" - " - Things are likely to fail from here onwards", stream, encoding) + LOG.warning( + "Running with unsupported encoding '%s': %s" + " - Things are likely to fail from here onwards", + stream, + encoding, + ) # Load Mozilla profile and initialize NSS before asking the user for input - moz = MozillaInteraction() + moz = MozillaInteraction(args.non_fatal_decryption) basepath = os.path.expanduser(args.profile) diff --git a/tests/handle_corrupted_passwords.t b/tests/handle_corrupted_passwords.t new file mode 100755 index 0000000..264dfcd --- /dev/null +++ b/tests/handle_corrupted_passwords.t @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import unittest +from simpletap.fdecrypt import lib + + +class TestCorruptedPassword(unittest.TestCase): + def validate_one(self, userkey, grepkey, output): + expected = lib.get_user_data(userkey) + matches = lib.grep(grepkey, output, context=1) + + self.assertEqual(matches, expected) + + def validate(self, out): + self.validate_one("decryption_failed", "Username: .* decryption failed", out) + self.validate_one("doesntexist", "doesntexist", out) + self.validate_one("onemore", "onemore", out) + self.validate_one("complex", "cömplex", out) + self.validate_one("jamie", "jãmïe", out) + + def validate_exception(self, out): + # error is "ValueError: Username/Password decryption (...) Credentials damaged (...)" + err = "Credentials damaged or cert/key file mismatch." + match = lib.grep(err, out) + self.assertIn("ValueError: Username/Password", match) + + def validate_error(self, out): + # error is "ERROR - Username/Password decryption (...) Credentials damaged (...)" + err = "Credentials damaged or cert/key file mismatch." + match = lib.grep(err, out) + self.assertIn("ERROR - Username/Password", match) + + def run_firefox_nopassword(self, cmd): + output = lib.run(cmd) + self.validate(output) + self.validate_exception(output) + + def run_firefox_nopassword_error(self, cmd): + # returncode 17 is DECRYPTION_FAILED + output = lib.run_error(cmd, returncode=17) + self.validate_error(output) + + def test_corrupted_skip_firefox_114(self): + self.test = os.path.join(lib.get_test_data(), + "test_profile_firefox_nopassword_114") + + # Must run in non-interactive mode or password prompt will be shown + cmd = lib.get_script() + [self.test, "-n", "--non-fatal-decryption"] + + self.run_firefox_nopassword(cmd) + + def test_corrupted_firefox_114(self): + self.test = os.path.join(lib.get_test_data(), + "test_profile_firefox_nopassword_114") + + # Must run in non-interactive mode or password prompt will be shown + cmd = lib.get_script() + [self.test, "-n"] + + self.run_firefox_nopassword_error(cmd) + + +if __name__ == "__main__": + from simpletap import TAPTestRunner + unittest.main(testRunner=TAPTestRunner(buffer=True), exit=False) + +# vim: ai sts=4 et sw=4 diff --git a/tests/test_data/test_profile_firefox_nopassword_114/cert9.db b/tests/test_data/test_profile_firefox_nopassword_114/cert9.db new file mode 100644 index 0000000..0337cf9 Binary files /dev/null and b/tests/test_data/test_profile_firefox_nopassword_114/cert9.db differ diff --git a/tests/test_data/test_profile_firefox_nopassword_114/key4.db b/tests/test_data/test_profile_firefox_nopassword_114/key4.db new file mode 100644 index 0000000..f0dd3a3 Binary files /dev/null and b/tests/test_data/test_profile_firefox_nopassword_114/key4.db differ diff --git a/tests/test_data/test_profile_firefox_nopassword_114/logins.json b/tests/test_data/test_profile_firefox_nopassword_114/logins.json new file mode 100644 index 0000000..113b323 --- /dev/null +++ b/tests/test_data/test_profile_firefox_nopassword_114/logins.json @@ -0,0 +1 @@ +{"nextId":6,"logins":[{"id":1,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECEgmzKOCavAWBAiab7yWG12/Rw==","encryptedPassword":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECIm+i6iNZ4okBAjDGi5OTiNc9Q==","guid":"{000011d3-d88f-4d72-916e-46aba374ee68}","encType":1,"timeCreated":1688676766618,"timeLastUsed":1688677135670,"timePasswordChanged":1688676766618,"timesUsed":2},{"id":2,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECMmE70ZVP0E4BBCJNgP9zigxQG7wA5Xtf6J/","encryptedPassword":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECAaKjRlvw62zBBDn4tthVPtEKCqkeOeUQZaM","guid":"{dd7a17bb-14c8-46e2-8170-f09cb35a6c01}","encType":1,"timeCreated":1688677150303,"timeLastUsed":1688677150303,"timePasswordChanged":1688677150303,"timesUsed":1},{"id":3,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECCb0mK51cDIgBAjToSpG3VVLnQ==","encryptedPassword":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECB3Oul6W4qDQBBB917xhQ/IHXUnctPwKU6vY","guid":"{badd0e1b-c526-4548-a79b-0dd878389276}","encType":1,"timeCreated":1688677214767,"timeLastUsed":1688677214767,"timePasswordChanged":1688677214767,"timesUsed":1},{"id":4,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECPWy/Wb/zVxsBBCHZJUabIuDWSSPh7IvF4M/","encryptedPassword":"MFIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECBWgpjB1kLSYBCj+3VaCZAPhdWXO40qlQTBj7wzjrAVOCGnug8366ez2dmn4/TpGPxyi","guid":"{ccd01360-2a8c-46d3-993c-5aee0dbbf647}","encType":1,"timeCreated":1688677292142,"timeLastUsed":1688677292142,"timePasswordChanged":1688677292142,"timesUsed":1},{"id":5,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECOcZUkTRCDK5BAjDFjezUYVSFg==","encryptedPassword":"MFoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECM8rG1/2s/7uBDDLU1D80YJO5o2tkjh/WaXUbhOWNgLiCvPaaOFQfaI4/iCaS70SD4UrEXJSBm7ph7o=","guid":"{80816ccc-3b32-4125-89fd-36c388d91ebc}","encType":1,"timeCreated":1688677398674,"timeLastUsed":1688677398674,"timePasswordChanged":1688677398674,"timesUsed":1}],"potentiallyVulnerablePasswords":[],"dismissedBreachAlertsByLoginGUID":{},"version":3} diff --git a/tests/test_data/users/decryption_failed.user b/tests/test_data/users/decryption_failed.user new file mode 100644 index 0000000..4075d1a --- /dev/null +++ b/tests/test_data/users/decryption_failed.user @@ -0,0 +1,3 @@ +Website: https://github.com +Username: '*** decryption failed ***' +Password: '*** decryption failed ***'