├── tests ├── test_data │ ├── master_password │ ├── users │ │ ├── header_csv_default.user │ │ ├── header_csv_tabular.user │ │ ├── header_csv_tab_vertbar.user │ │ ├── header_csv_semicol_singlequot.user │ │ ├── onemore_tabular.user │ │ ├── onemore_csv_default.user │ │ ├── onemore_csv_tab_vertbar.user │ │ ├── doesntexist_tabular.user │ │ ├── onemore_csv_semicol_singlequot.user │ │ ├── complex_csv_default.user │ │ ├── complex_tabular.user │ │ ├── doesntexist_csv_default.user │ │ ├── complex_csv_tab_vertbar.user │ │ ├── doesntexist_csv_tab_vertbar.user │ │ ├── doesntexist_csv_semicol_singlequot.user │ │ ├── onemore.user │ │ ├── complex_csv_semicol_singlequot.user │ │ ├── jamie_tabular.user │ │ ├── jamie_csv_default.user │ │ ├── jamie_csv_tab_vertbar.user │ │ ├── complex.user │ │ ├── doesntexist.user │ │ ├── jamie_csv_semicol_singlequot.user │ │ ├── jamie.user │ │ ├── decryption_failed.user │ │ ├── header_json_default.user │ │ ├── jamie_json_default.user │ │ ├── complex_json_default.user │ │ ├── doesntexist_json_default.user │ │ └── onemore_json_default.user │ ├── test_profile_firefox_LЮшр │ │ ├── key4.db │ │ ├── cert9.db │ │ └── logins.json │ ├── outputs │ │ ├── list_fail.output │ │ ├── non_interactive_choice_missing.output │ │ ├── list_single_20.output │ │ ├── list_single_46.output │ │ ├── list_single_59.output │ │ ├── list_single_144.output │ │ └── list.output │ ├── test_profile_firefox_144 │ │ ├── key4.db │ │ ├── cert9.db │ │ └── logins.json │ ├── test_profile_firefox_20 │ │ ├── cert8.db │ │ ├── key3.db │ │ └── signons.sqlite │ ├── test_profile_firefox_46 │ │ ├── cert8.db │ │ ├── key3.db │ │ └── logins.json │ ├── test_profile_firefox_59 │ │ ├── cert9.db │ │ ├── key4.db │ │ └── logins.json │ ├── test_profile_firefox_nopassword_46 │ │ ├── key3.db │ │ ├── cert8.db │ │ └── logins.json │ ├── test_profile_firefox_nopassword_59 │ │ ├── key4.db │ │ ├── cert9.db │ │ └── logins.json │ ├── test_profile_firefox_nopassword_114 │ │ ├── cert9.db │ │ ├── key4.db │ │ └── logins.json │ ├── profiles.ini │ └── exported_passwords.csv ├── simpletap │ ├── version.py │ ├── __main__.py │ ├── runner.py │ ├── __init__.py │ ├── fdecrypt.py │ └── result.py ├── version.t ├── show_encodings ├── list_profiles.t ├── list_single_profile.t ├── README.md ├── non_interactive_choice.t ├── handle_corrupted_passwords.t ├── profile_ini.t ├── direct_profile.t ├── json.t ├── problems ├── run_all └── csv.t ├── .gitignore ├── pyproject.toml ├── .github ├── ISSUE_TEMPLATE │ ├── i-didn-t-find-documentation-on-how-to---.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── main.yml ├── CHANGELOG.md ├── .all-contributorsrc ├── CONTRIBUTORS.md ├── README.md ├── LICENSE └── firefox_decrypt.py /tests/test_data/master_password: -------------------------------------------------------------------------------- 1 | сЮЛОажс$4vz*VçàhxpfCbmwo 2 | -------------------------------------------------------------------------------- /tests/test_data/users/header_csv_default.user: -------------------------------------------------------------------------------- 1 | "url";"user";"password" 2 | -------------------------------------------------------------------------------- /tests/test_data/users/header_csv_tabular.user: -------------------------------------------------------------------------------- 1 | 'url' 'user' 'password' 2 | -------------------------------------------------------------------------------- /tests/test_data/users/header_csv_tab_vertbar.user: -------------------------------------------------------------------------------- 1 | |url| |user| |password| 2 | -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_LЮшр/key4.db: -------------------------------------------------------------------------------- 1 | ../test_profile_firefox_59/key4.db -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_LЮшр/cert9.db: -------------------------------------------------------------------------------- 1 | ../test_profile_firefox_59/cert9.db -------------------------------------------------------------------------------- /tests/test_data/users/header_csv_semicol_singlequot.user: -------------------------------------------------------------------------------- 1 | 'url';'user';'password' 2 | -------------------------------------------------------------------------------- /tests/test_data/users/onemore_tabular.user: -------------------------------------------------------------------------------- 1 | 'https://github.com' 'onemore' '}]¢öðæ[{' 2 | -------------------------------------------------------------------------------- /tests/test_data/outputs/list_fail.output: -------------------------------------------------------------------------------- 1 | - ERROR - Listing single profiles not permitted. 2 | -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_LЮшр/logins.json: -------------------------------------------------------------------------------- 1 | ../test_profile_firefox_59/logins.json -------------------------------------------------------------------------------- /tests/test_data/users/onemore_csv_default.user: -------------------------------------------------------------------------------- 1 | "https://github.com";"onemore";"}]¢öðæ[{" 2 | -------------------------------------------------------------------------------- /tests/test_data/users/onemore_csv_tab_vertbar.user: -------------------------------------------------------------------------------- 1 | |https://github.com| |onemore| |}]¢öðæ[{| 2 | -------------------------------------------------------------------------------- /tests/test_data/users/doesntexist_tabular.user: -------------------------------------------------------------------------------- 1 | 'https://github.com' 'doesntexist' 'xrbSDzYf94gfk' 2 | -------------------------------------------------------------------------------- /tests/test_data/users/onemore_csv_semicol_singlequot.user: -------------------------------------------------------------------------------- 1 | 'https://github.com';'onemore';'}]¢öðæ[{' 2 | -------------------------------------------------------------------------------- /tests/test_data/users/complex_csv_default.user: -------------------------------------------------------------------------------- 1 | "https://github.com";"cömplex";"сЮЛОажс$4vz*VçàhxpfCbmwo" 2 | -------------------------------------------------------------------------------- /tests/test_data/users/complex_tabular.user: -------------------------------------------------------------------------------- 1 | 'https://github.com' 'cömplex' 'сЮЛОажс$4vz*VçàhxpfCbmwo' 2 | -------------------------------------------------------------------------------- /tests/test_data/users/doesntexist_csv_default.user: -------------------------------------------------------------------------------- 1 | "https://github.com";"doesntexist";"xrbSDzYf94gfk" 2 | -------------------------------------------------------------------------------- /tests/simpletap/version.py: -------------------------------------------------------------------------------- 1 | # Autogenerated from git tags using update-version.sh 2 | __version__ = "1.0.1" 3 | -------------------------------------------------------------------------------- /tests/test_data/users/complex_csv_tab_vertbar.user: -------------------------------------------------------------------------------- 1 | |https://github.com| |cömplex| |сЮЛОажс$4vz*VçàhxpfCbmwo| 2 | -------------------------------------------------------------------------------- /tests/test_data/users/doesntexist_csv_tab_vertbar.user: -------------------------------------------------------------------------------- 1 | |https://github.com| |doesntexist| |xrbSDzYf94gfk| 2 | -------------------------------------------------------------------------------- /tests/test_data/users/doesntexist_csv_semicol_singlequot.user: -------------------------------------------------------------------------------- 1 | 'https://github.com';'doesntexist';'xrbSDzYf94gfk' 2 | -------------------------------------------------------------------------------- /tests/test_data/users/onemore.user: -------------------------------------------------------------------------------- 1 | Website: https://github.com 2 | Username: 'onemore' 3 | Password: '}]¢öðæ[{' 4 | -------------------------------------------------------------------------------- /tests/test_data/users/complex_csv_semicol_singlequot.user: -------------------------------------------------------------------------------- 1 | 'https://github.com';'cömplex';'сЮЛОажс$4vz*VçàhxpfCbmwo' 2 | -------------------------------------------------------------------------------- /tests/test_data/users/jamie_tabular.user: -------------------------------------------------------------------------------- 1 | 'https://github.com' 'jãmïe' 'Apassword withtabs,;colonandsemi''"andquotes' 2 | -------------------------------------------------------------------------------- /tests/test_data/users/jamie_csv_default.user: -------------------------------------------------------------------------------- 1 | "https://github.com";"jãmïe";"Apassword withtabs,;colonandsemi'""andquotes" 2 | -------------------------------------------------------------------------------- /tests/test_data/users/jamie_csv_tab_vertbar.user: -------------------------------------------------------------------------------- 1 | |https://github.com| |jãmïe| |Apassword withtabs,;colonandsemi'"andquotes| 2 | -------------------------------------------------------------------------------- /tests/test_data/users/complex.user: -------------------------------------------------------------------------------- 1 | Website: https://github.com 2 | Username: 'cömplex' 3 | Password: 'сЮЛОажс$4vz*VçàhxpfCbmwo' 4 | -------------------------------------------------------------------------------- /tests/test_data/users/doesntexist.user: -------------------------------------------------------------------------------- 1 | Website: https://github.com 2 | Username: 'doesntexist' 3 | Password: 'xrbSDzYf94gfk' 4 | -------------------------------------------------------------------------------- /tests/test_data/users/jamie_csv_semicol_singlequot.user: -------------------------------------------------------------------------------- 1 | 'https://github.com';'jãmïe';'Apassword withtabs,;colonandsemi''"andquotes' 2 | -------------------------------------------------------------------------------- /tests/test_data/users/jamie.user: -------------------------------------------------------------------------------- 1 | Website: https://github.com 2 | Username: 'jãmïe' 3 | Password: 'Apassword withtabs,;colonandsemi'"andquotes' 4 | -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_144/key4.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unode/firefox_decrypt/HEAD/tests/test_data/test_profile_firefox_144/key4.db -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_20/cert8.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unode/firefox_decrypt/HEAD/tests/test_data/test_profile_firefox_20/cert8.db -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_20/key3.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unode/firefox_decrypt/HEAD/tests/test_data/test_profile_firefox_20/key3.db -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_46/cert8.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unode/firefox_decrypt/HEAD/tests/test_data/test_profile_firefox_46/cert8.db -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_46/key3.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unode/firefox_decrypt/HEAD/tests/test_data/test_profile_firefox_46/key3.db -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_59/cert9.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unode/firefox_decrypt/HEAD/tests/test_data/test_profile_firefox_59/cert9.db -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_59/key4.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unode/firefox_decrypt/HEAD/tests/test_data/test_profile_firefox_59/key4.db -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_144/cert9.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unode/firefox_decrypt/HEAD/tests/test_data/test_profile_firefox_144/cert9.db -------------------------------------------------------------------------------- /tests/test_data/users/decryption_failed.user: -------------------------------------------------------------------------------- 1 | Website: https://github.com 2 | Username: '*** decryption failed ***' 3 | Password: '*** decryption failed ***' 4 | -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_20/signons.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unode/firefox_decrypt/HEAD/tests/test_data/test_profile_firefox_20/signons.sqlite -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_nopassword_46/key3.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unode/firefox_decrypt/HEAD/tests/test_data/test_profile_firefox_nopassword_46/key3.db -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_nopassword_59/key4.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unode/firefox_decrypt/HEAD/tests/test_data/test_profile_firefox_nopassword_59/key4.db -------------------------------------------------------------------------------- /tests/test_data/outputs/non_interactive_choice_missing.output: -------------------------------------------------------------------------------- 1 | - ERROR - Don't know which profile to decrypt. We are in non-interactive mode and -c/--choice wasn't specified. 2 | -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_nopassword_114/cert9.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unode/firefox_decrypt/HEAD/tests/test_data/test_profile_firefox_nopassword_114/cert9.db -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_nopassword_114/key4.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unode/firefox_decrypt/HEAD/tests/test_data/test_profile_firefox_nopassword_114/key4.db -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_nopassword_46/cert8.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unode/firefox_decrypt/HEAD/tests/test_data/test_profile_firefox_nopassword_46/cert8.db -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_nopassword_59/cert9.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unode/firefox_decrypt/HEAD/tests/test_data/test_profile_firefox_nopassword_59/cert9.db -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tests/all.log 2 | __pycache__/ 3 | build/ 4 | dist/ 5 | *.egg-info/ 6 | 7 | # npm and all-contributors 8 | node_modules/ 9 | package-lock.json 10 | package.json 11 | yarn.lock 12 | -------------------------------------------------------------------------------- /tests/test_data/outputs/list_single_20.output: -------------------------------------------------------------------------------- 1 | - WARNING - profile.ini not found in test_data/test_profile_firefox_20 2 | - WARNING - Continuing and assuming 'test_data/test_profile_firefox_20' is a profile location 3 | - ERROR - Listing single profiles not permitted. 4 | -------------------------------------------------------------------------------- /tests/test_data/outputs/list_single_46.output: -------------------------------------------------------------------------------- 1 | - WARNING - profile.ini not found in test_data/test_profile_firefox_46 2 | - WARNING - Continuing and assuming 'test_data/test_profile_firefox_46' is a profile location 3 | - ERROR - Listing single profiles not permitted. 4 | -------------------------------------------------------------------------------- /tests/test_data/outputs/list_single_59.output: -------------------------------------------------------------------------------- 1 | - WARNING - profile.ini not found in test_data/test_profile_firefox_59 2 | - WARNING - Continuing and assuming 'test_data/test_profile_firefox_59' is a profile location 3 | - ERROR - Listing single profiles not permitted. 4 | -------------------------------------------------------------------------------- /tests/test_data/outputs/list_single_144.output: -------------------------------------------------------------------------------- 1 | - WARNING - profile.ini not found in test_data/test_profile_firefox_144 2 | - WARNING - Continuing and assuming 'test_data/test_profile_firefox_144' is a profile location 3 | - ERROR - Listing single profiles not permitted. 4 | -------------------------------------------------------------------------------- /tests/test_data/outputs/list.output: -------------------------------------------------------------------------------- 1 | 1 -> test_profile_firefox_20 2 | 2 -> test_profile_firefox_46 3 | 3 -> test_profile_firefox_nopassword_46 4 | 4 -> test_profile_firefox_59 5 | 5 -> test_profile_firefox_LЮшр 6 | 6 -> test_profile_firefox_nopassword_59 7 | 7 -> test_profile_firefox_144 8 | -------------------------------------------------------------------------------- /tests/simpletap/__main__.py: -------------------------------------------------------------------------------- 1 | """Main entry point""" 2 | 3 | import sys 4 | 5 | from unittest.main import main, TestProgram, USAGE_AS_MAIN 6 | from .runner import TAPTestRunner 7 | 8 | 9 | if sys.argv[0].endswith("__main__.py"): 10 | sys.argv[0] = "python3 -m simpletap" 11 | 12 | __unittest = True 13 | 14 | TestProgram.USAGE = USAGE_AS_MAIN 15 | 16 | main(module=None, testRunner=TAPTestRunner()) 17 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "firefox-decrypt" 3 | version = "1.1.1" 4 | description = "Firefox Decrypt is a tool to extract passwords from Mozilla (Firefox™, Waterfox™, Thunderbird®, SeaMonkey®) profiles" 5 | authors = [{name = "Renato Alves"}] 6 | requires-python = ">=3.9" 7 | license = {text = "GPL-3.0-only"} 8 | readme = "README.md" 9 | 10 | [project.scripts] 11 | firefox_decrypt = "firefox_decrypt:run_ffdecrypt" 12 | 13 | [build-system] 14 | requires = ["setuptools", "setuptools-scm"] 15 | build-backend = "setuptools.build_meta" 16 | -------------------------------------------------------------------------------- /tests/version.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | from simpletap.fdecrypt import lib 6 | 7 | 8 | class TestVersion(unittest.TestCase): 9 | def test_version(self): 10 | cmd = lib.get_script() + ["--version"] 11 | 12 | output = lib.run(cmd, workdir="/") 13 | expected = lib.get_internal_version() 14 | 15 | self.assertEqual(output, expected) 16 | 17 | 18 | if __name__ == "__main__": 19 | from simpletap import TAPTestRunner 20 | unittest.main(testRunner=TAPTestRunner(buffer=True), exit=False) 21 | 22 | # vim: ai sts=4 et sw=4 23 | -------------------------------------------------------------------------------- /tests/test_data/users/header_json_default.user: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://github.com", 4 | "user": "doesntexist", 5 | "password": "xrbSDzYf94gfk" 6 | }, 7 | { 8 | "url": "https://github.com", 9 | "user": "onemore", 10 | "password": "}]\u00a2\u00f6\u00f0\u00e6[{" 11 | }, 12 | { 13 | "url": "https://github.com", 14 | "user": "c\u00f6mplex", 15 | "password": "\u0441\u042e\u041b\u041e\u0430\u0436\u0441$4vz*V\u00e7\u00e0hxpfCbmwo" 16 | }, 17 | { 18 | "url": "https://github.com", 19 | "user": "j\u00e3m\u00efe", 20 | "password": "Apassword\twithtabs,;colonandsemi'\"andquotes" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /tests/test_data/users/jamie_json_default.user: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://github.com", 4 | "user": "doesntexist", 5 | "password": "xrbSDzYf94gfk" 6 | }, 7 | { 8 | "url": "https://github.com", 9 | "user": "onemore", 10 | "password": "}]\u00a2\u00f6\u00f0\u00e6[{" 11 | }, 12 | { 13 | "url": "https://github.com", 14 | "user": "c\u00f6mplex", 15 | "password": "\u0441\u042e\u041b\u041e\u0430\u0436\u0441$4vz*V\u00e7\u00e0hxpfCbmwo" 16 | }, 17 | { 18 | "url": "https://github.com", 19 | "user": "j\u00e3m\u00efe", 20 | "password": "Apassword\twithtabs,;colonandsemi'\"andquotes" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/i-didn-t-find-documentation-on-how-to---.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: I didn't find documentation on how to... 3 | about: Tell us what you are missing in the docs or feel unclear about 4 | 5 | --- 6 | 7 | 13 | 14 | **Can you clarify...** 15 | (Elaborate your question here) 16 | -------------------------------------------------------------------------------- /tests/test_data/users/complex_json_default.user: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://github.com", 4 | "user": "doesntexist", 5 | "password": "xrbSDzYf94gfk" 6 | }, 7 | { 8 | "url": "https://github.com", 9 | "user": "onemore", 10 | "password": "}]\u00a2\u00f6\u00f0\u00e6[{" 11 | }, 12 | { 13 | "url": "https://github.com", 14 | "user": "c\u00f6mplex", 15 | "password": "\u0441\u042e\u041b\u041e\u0430\u0436\u0441$4vz*V\u00e7\u00e0hxpfCbmwo" 16 | }, 17 | { 18 | "url": "https://github.com", 19 | "user": "j\u00e3m\u00efe", 20 | "password": "Apassword\twithtabs,;colonandsemi'\"andquotes" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /tests/test_data/users/doesntexist_json_default.user: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://github.com", 4 | "user": "doesntexist", 5 | "password": "xrbSDzYf94gfk" 6 | }, 7 | { 8 | "url": "https://github.com", 9 | "user": "onemore", 10 | "password": "}]\u00a2\u00f6\u00f0\u00e6[{" 11 | }, 12 | { 13 | "url": "https://github.com", 14 | "user": "c\u00f6mplex", 15 | "password": "\u0441\u042e\u041b\u041e\u0430\u0436\u0441$4vz*V\u00e7\u00e0hxpfCbmwo" 16 | }, 17 | { 18 | "url": "https://github.com", 19 | "user": "j\u00e3m\u00efe", 20 | "password": "Apassword\twithtabs,;colonandsemi'\"andquotes" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /tests/test_data/users/onemore_json_default.user: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://github.com", 4 | "user": "doesntexist", 5 | "password": "xrbSDzYf94gfk" 6 | }, 7 | { 8 | "url": "https://github.com", 9 | "user": "onemore", 10 | "password": "}]\u00a2\u00f6\u00f0\u00e6[{" 11 | }, 12 | { 13 | "url": "https://github.com", 14 | "user": "c\u00f6mplex", 15 | "password": "\u0441\u042e\u041b\u041e\u0430\u0436\u0441$4vz*V\u00e7\u00e0hxpfCbmwo" 16 | }, 17 | { 18 | "url": "https://github.com", 19 | "user": "j\u00e3m\u00efe", 20 | "password": "Apassword\twithtabs,;colonandsemi'\"andquotes" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea or enhancement 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /tests/test_data/profiles.ini: -------------------------------------------------------------------------------- 1 | [General] 2 | StartWithLastProfile=1 3 | 4 | [Profile1] 5 | Name=firefox_20 6 | IsRelative=1 7 | Path=test_profile_firefox_20 8 | 9 | [Profile2] 10 | Name=firefox_46 11 | IsRelative=1 12 | Path=test_profile_firefox_46 13 | 14 | [Profile3] 15 | Name=firefox_nopassword_46 16 | IsRelative=1 17 | Path=test_profile_firefox_nopassword_46 18 | 19 | [Profile4] 20 | Name=firefox_59 21 | IsRelative=1 22 | Path=test_profile_firefox_59 23 | 24 | [Profile5] 25 | Name=firefox_non-ascii 26 | IsRelative=1 27 | Path=test_profile_firefox_LЮшр 28 | 29 | [Profile6] 30 | Name=firefox_nopassword_59 31 | IsRelative=1 32 | Path=test_profile_firefox_nopassword_59 33 | 34 | [Profile7] 35 | Name=firefox_144 36 | IsRelative=1 37 | Path=test_profile_firefox_144 38 | -------------------------------------------------------------------------------- /tests/test_data/exported_passwords.csv: -------------------------------------------------------------------------------- 1 | "url","username","password","httpRealm","formActionOrigin","guid","timeCreated","timeLastUsed","timePasswordChanged" 2 | "https://github.com","doesntexist","xrbSDzYf94gfk",,"https://github.com","{9e169e37-94b5-4b8c-bb19-5ad584e637d7}","1458308750178","1458308750178","1458308750178" 3 | "https://github.com","onemore","}]¢öðæ[{",,"https://github.com","{45c02941-b799-437a-8a6c-e979e7d1862c}","1458308837800","1458308837800","1458308837800" 4 | "https://github.com","cömplex","сЮЛОажс$4vz*VçàhxpfCbmwo",,"https://github.com","{e02610a1-cc74-480a-aa6d-19f8ff6afb15}","1458309068022","1458309068022","1458309068022" 5 | "https://github.com","jãmïe","Apassword withtabs,;colonandsemi'""andquotes",,"https://github.com","{f3f4850e-f608-49a4-8856-81510ce1265d}","1502989487187","1502989487187","1502989487187" -------------------------------------------------------------------------------- /tests/show_encodings: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from __future__ import annotations 18 | 19 | import locale 20 | import sys 21 | 22 | encoding = locale.getpreferredencoding() 23 | 24 | if encoding is None: 25 | encoding = "Unknown" 26 | 27 | print( 28 | "Running with encodings: stdin:", 29 | sys.stdin.encoding, 30 | " stdout:", 31 | sys.stdout.encoding, 32 | " locale:", 33 | encoding 34 | ) 35 | -------------------------------------------------------------------------------- /tests/list_profiles.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import unittest 6 | from simpletap.fdecrypt import lib 7 | 8 | 9 | class TestListProfiles(unittest.TestCase): 10 | def test_listing_profiles(self): 11 | """list profiles should show the profile list""" 12 | cmd = lib.get_script() + ["-l", lib.get_test_data()] 13 | 14 | output = lib.run(cmd) 15 | expected = lib.get_output_data("list") 16 | 17 | self.assertEqual(output, expected) 18 | 19 | def test_listing_single_profiles(self): 20 | """list profiles should fail if provided a single profile""" 21 | test = os.path.join(lib.get_test_data(), 22 | "test_profile_firefox_nopassword") 23 | cmd = lib.get_script() + ["-l", test] 24 | 25 | output = lib.run_error(cmd, returncode=2) 26 | output = lib.grep("ERROR", output) 27 | output = lib.remove_log_date_time(output) 28 | expected = lib.get_output_data("list_fail") 29 | 30 | self.assertEqual(output, expected) 31 | 32 | 33 | if __name__ == "__main__": 34 | from simpletap import TAPTestRunner 35 | unittest.main(testRunner=TAPTestRunner(buffer=True), exit=False) 36 | 37 | # vim: ai sts=4 et sw=4 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Help us help you 4 | 5 | --- 6 | 7 | **I have a problem, can you help?** 8 | Likely yes, but before opening a new issue, please check if the existing [troubleshooting information](https://github.com/unode/firefox_decrypt#troubleshooting) addresses your problem. 9 | Quite often, particularly if you are on Windows or MacOSX, you are likely missing a dependency or trying to execute Firefox Decrypt with an unsupported configuration. 10 | 11 | **Help us reproduce your problem** 12 | If you are new to this consider reading [this great article](https://www.chiark.greenend.org.uk/~sgtatham/bugs.html) on how to report a bug/problem. 13 | Try to be helpful/informative or we won't be able to help you. 14 | Helping us reproduce your problem is half the work to getting the problem fixed. 15 | 16 | **Describe the bug** 17 | A clear and concise description of what the bug or problem is. 18 | 19 | **To Reproduce** 20 | Steps to reproduce the behavior: 21 | 1. Launched `firefox_decrypt` with the following command-line arguments ... 22 | 2. Selected the profile by doing ... 23 | 3. Typed in the password ... 24 | 4. Something didn't work (or worked differently from expected) ... 25 | 5. ... 26 | 27 | **Expected behavior** 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Logs and Screenshots** 31 | Logs as described in [troubleshooting](https://github.com/unode/firefox_decrypt#troubleshooting) are precious, include them if possible. 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Your system (please complete the following information):** 35 | - OS/System: [e.g. Windows 10 - German - 64bits, Ubuntu Linux 18.04 - 64bits- en_US (locale), Android Nougat (Termux?, other...)] 36 | - Firefox/Thunderbird/Seabird/...: [e.g. Firefox 59.0.3 - 64bits] 37 | - Python: [e.g. 3.7.0 - 64bits] 38 | - firefox_decrypt: [e.g. 0.7.0] 39 | 40 | **Additional context** 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /tests/list_single_profile.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import unittest 6 | from simpletap.fdecrypt import lib 7 | 8 | 9 | class TestSingleProfile(unittest.TestCase): 10 | def listing_from_single_profile(self): 11 | test = os.path.join(lib.get_test_data(), self.test_profile) 12 | cmd = lib.get_script() + ["-l", test] 13 | 14 | expected = lib.get_output_data(self.output_data) 15 | expected_exitcode = 2 16 | 17 | output = lib.remove_full_pwd( 18 | lib.remove_log_date_time( 19 | lib.run_error(cmd, returncode=expected_exitcode))) 20 | 21 | if lib.platform == "Windows": 22 | # Paths in Windows cause failures with string comparison 23 | output = output.replace("\\", "/") 24 | self.assertEqual(output, expected) 25 | 26 | @unittest.skipIf(lib.platform == "Windows", 27 | "Windows DLL isn't backwards compatible") 28 | def test_firefox_20(self): 29 | self.test_profile = "test_profile_firefox_20" 30 | self.output_data = "list_single_20" 31 | self.listing_from_single_profile() 32 | 33 | @unittest.skipIf(lib.platform == "Windows", 34 | "Windows DLL isn't backwards compatible") 35 | def test_firefox_46(self): 36 | self.test_profile = "test_profile_firefox_46" 37 | self.output_data = "list_single_46" 38 | self.listing_from_single_profile() 39 | 40 | def test_firefox_59(self): 41 | self.test_profile = "test_profile_firefox_59" 42 | self.output_data = "list_single_59" 43 | self.listing_from_single_profile() 44 | 45 | def test_firefox_144(self): 46 | self.test_profile = "test_profile_firefox_144" 47 | self.output_data = "list_single_144" 48 | self.listing_from_single_profile() 49 | 50 | if __name__ == "__main__": 51 | from simpletap import TAPTestRunner 52 | unittest.main(testRunner=TAPTestRunner(buffer=True), exit=False) 53 | 54 | # vim: ai sts=4 et sw=4 55 | -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_46/logins.json: -------------------------------------------------------------------------------- 1 | {"nextId":5,"logins":[{"id":1,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"Email","passwordField":"Passwd","encryptedUsername":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECCL5EUYch2LmBBBbEe73zOfdClOM2IUEoyYR","encryptedPassword":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECC8l4fpeMPltBBDeVjk+naPhBDyU/c380bFZ","guid":"{9e169e37-94b5-4b8c-bb19-5ad584e637d7}","encType":1,"timeCreated":1458308750178,"timeLastUsed":1458308750178,"timePasswordChanged":1458308750178,"timesUsed":1},{"id":2,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"Email","passwordField":"Passwd","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECLgW61zVAbl+BAgICvoShq2cfw==","encryptedPassword":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECM1RHJSWRMoWBBBwnfH0vfuwraEjHoEgIBO1","guid":"{45c02941-b799-437a-8a6c-e979e7d1862c}","encType":1,"timeCreated":1458308837800,"timeLastUsed":1458308837800,"timePasswordChanged":1458308837800,"timesUsed":1},{"id":3,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECHYSvLvYiJopBBBMMyML40m58VrST5JbJkrS","encryptedPassword":"MFIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECIF2RmdyO8vfBCjLIslu+MdAjUPQGs55m0js+6MCCojFFKr4/974gNfzQcFmAf2EoTZq","guid":"{e02610a1-cc74-480a-aa6d-19f8ff6afb15}","encType":1,"timeCreated":1458309068022,"timeLastUsed":1458309068022,"timePasswordChanged":1458309068022,"timesUsed":1},{"id":4,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECPNrCh0rm8wXBAiNP/i1zWgebQ==","encryptedPassword":"MFoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECLwyAZRYVfJfBDCsMdho/UAw3JLGJjZmdGKiTkwBPCdLojQmA4AElUHYlBdyEA0r3qMmkzp/5QoXXvQ=","guid":"{f3f4850e-f608-49a4-8856-81510ce1265d}","encType":1,"timeCreated":1502989487187,"timeLastUsed":1502989487187,"timePasswordChanged":1502989487187,"timesUsed":1}],"disabledHosts":[],"version":1} -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_59/logins.json: -------------------------------------------------------------------------------- 1 | {"nextId":6,"logins":[{"id":2,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECDAMJYvxVWmNBBAYOR+4wZeLSB7kqJ/GDhj3","encryptedPassword":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECBQ0N0EftdcPBBD9CaBvRSe9MhhqBjbd3UG8","guid":"{749a98c7-c83e-4033-aafc-647f562b7166}","encType":1,"timeCreated":1515902314887,"timeLastUsed":1515902314887,"timePasswordChanged":1515902314887,"timesUsed":1},{"id":3,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECF7kv84cNrhKBAgHD6N4RU01Tg==","encryptedPassword":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECBUufYeWbuziBBAraNDREdVus+piXPZaR/Ym","guid":"{3946cc16-e11a-48e7-8128-7ccfe76497a2}","encType":1,"timeCreated":1515902330602,"timeLastUsed":1515902330602,"timePasswordChanged":1515902330602,"timesUsed":1},{"id":4,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECJzC0s27eOVuBBAaivvk2xSAcu3VP6oAkODX","encryptedPassword":"MFIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECNa3fxQUbhzwBCjyWS8Qx2UiUcoq3nvLmPXWtc4bdm88HLfIMTGJcM7WvDALDHdWIAwY","guid":"{f2242a97-e40a-4540-a3f9-d6135326d76a}","encType":1,"timeCreated":1515902347570,"timeLastUsed":1515902347570,"timePasswordChanged":1515902347570,"timesUsed":1},{"id":5,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECJXdeSs0MeMMBAhRbgoUvJ9GJA==","encryptedPassword":"MFoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECCSrh9ud0IorBDA4ncCjHIDjDlUIliEvJ7at4r2M68qLKFHTGEsiUkRJjRJ0ir6Zy59rKq4EtVnrzMI=","guid":"{48dc6764-a352-4e7d-af8a-b3605ef86cce}","encType":1,"timeCreated":1515902367721,"timeLastUsed":1515902367721,"timePasswordChanged":1515902367721,"timesUsed":1}],"disabledHosts":[],"version":2} -------------------------------------------------------------------------------- /tests/simpletap/runner.py: -------------------------------------------------------------------------------- 1 | # simpletap - "Simple" TAP output with unittest 2 | # 3 | # Copyright (c) 2014-2016 Renato Alves 4 | # 5 | # This code is released under the MIT license. 6 | # Refer to LICENSE for further details. 7 | 8 | import unittest 9 | import warnings 10 | from unittest.signals import registerResult 11 | 12 | from .result import TAPTestResult 13 | 14 | 15 | class TAPTestRunner(unittest.runner.TextTestRunner): 16 | """A test runner that displays results using the Test Anything Protocol 17 | syntax. 18 | 19 | Inherits from TextTestRunner the default runner. 20 | """ 21 | resultclass = TAPTestResult 22 | 23 | def run(self, test): 24 | result = self._makeResult() 25 | registerResult(result) 26 | result.failfast = self.failfast 27 | result.buffer = self.buffer 28 | 29 | with warnings.catch_warnings(): 30 | if getattr(self, "warnings", None): 31 | # if self.warnings is set, use it to filter all the warnings 32 | warnings.simplefilter(self.warnings) 33 | # if the filter is 'default' or 'always', special-case the 34 | # warnings from the deprecated unittest methods to show them 35 | # no more than once per module, because they can be fairly 36 | # noisy. The -Wd and -Wa flags can be used to bypass this 37 | # only when self.warnings is None. 38 | if self.warnings in ['default', 'always']: 39 | warnings.filterwarnings( 40 | 'module', 41 | category=DeprecationWarning, 42 | message='Please use assert\\w+ instead.') 43 | startTestRun = getattr(result, 'startTestRun', None) 44 | if startTestRun is not None: 45 | result.total_tests = test.countTestCases() 46 | startTestRun() 47 | try: 48 | test(result) 49 | finally: 50 | stopTestRun = getattr(result, 'stopTestRun', None) 51 | if stopTestRun is not None: 52 | stopTestRun() 53 | 54 | return result 55 | -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_nopassword_46/logins.json: -------------------------------------------------------------------------------- 1 | {"nextId":5,"logins":[{"id":1,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECElcFTsExRYoBBDELNMlUWEF8MvPnn97cOqc","encryptedPassword":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECDq7QZaoF/rgBBD201J1/uCSsKnZKjJ8bUuU","guid":"{821b7723-13a6-4fa2-87fc-5b51584bfa7d}","encType":1,"timeCreated":1458309400994,"timeLastUsed":1458309400994,"timePasswordChanged":1458309400994,"timesUsed":1},{"id":2,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECNCH1QZiaIBeBAgAMbsh44FNxg==","encryptedPassword":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECBKppdNy/usdBBAuPAg2OPLjQ4wnYkXnyhug","guid":"{ad4aaec4-82a2-4cf7-a5dd-04189eb82d8e}","encType":1,"timeCreated":1458309417505,"timeLastUsed":1458309417505,"timePasswordChanged":1458309417505,"timesUsed":1},{"id":3,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECBrNkqk9CdrBBBDjBqA6ZO0OwxHu17OG8uYL","encryptedPassword":"MFIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECF/OT1LALAWNBCgX1rxBKhav/Mt6yxqGPvlcOlWeievC3Yb5pgTf8LK7puKpNi5nwv+I","guid":"{3c954c50-c294-4483-9624-480f29a86fbb}","encType":1,"timeCreated":1458309435586,"timeLastUsed":1458309435586,"timePasswordChanged":1458309435586,"timesUsed":1},{"id":4,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECAZi7ocIMd4JBAjML8oDjSwDew==","encryptedPassword":"MFoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECAvMgCp3611EBDCaeRdAjj41JtMroK/ztX5ECLAL6c0d4Dhga68+VHNcYZNYrBwYjtYX3Kp9Os+2DXM=","guid":"{10313124-7b26-4108-b9a7-fe00bf32aeaf}","encType":1,"timeCreated":1502989574011,"timeLastUsed":1502989574011,"timePasswordChanged":1502989574011,"timesUsed":1}],"disabledHosts":[],"version":1} -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_nopassword_59/logins.json: -------------------------------------------------------------------------------- 1 | {"nextId":6,"logins":[{"id":2,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECDAMJYvxVWmNBBAYOR+4wZeLSB7kqJ/GDhj3","encryptedPassword":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECBQ0N0EftdcPBBD9CaBvRSe9MhhqBjbd3UG8","guid":"{749a98c7-c83e-4033-aafc-647f562b7166}","encType":1,"timeCreated":1515902314887,"timeLastUsed":1515902314887,"timePasswordChanged":1515902314887,"timesUsed":1},{"id":3,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECF7kv84cNrhKBAgHD6N4RU01Tg==","encryptedPassword":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECBUufYeWbuziBBAraNDREdVus+piXPZaR/Ym","guid":"{3946cc16-e11a-48e7-8128-7ccfe76497a2}","encType":1,"timeCreated":1515902330602,"timeLastUsed":1515902330602,"timePasswordChanged":1515902330602,"timesUsed":1},{"id":4,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECJzC0s27eOVuBBAaivvk2xSAcu3VP6oAkODX","encryptedPassword":"MFIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECNa3fxQUbhzwBCjyWS8Qx2UiUcoq3nvLmPXWtc4bdm88HLfIMTGJcM7WvDALDHdWIAwY","guid":"{f2242a97-e40a-4540-a3f9-d6135326d76a}","encType":1,"timeCreated":1515902347570,"timeLastUsed":1515902347570,"timePasswordChanged":1515902347570,"timesUsed":1},{"id":5,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECJXdeSs0MeMMBAhRbgoUvJ9GJA==","encryptedPassword":"MFoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECCSrh9ud0IorBDA4ncCjHIDjDlUIliEvJ7at4r2M68qLKFHTGEsiUkRJjRJ0ir6Zy59rKq4EtVnrzMI=","guid":"{48dc6764-a352-4e7d-af8a-b3605ef86cce}","encType":1,"timeCreated":1515902367721,"timeLastUsed":1515902367721,"timePasswordChanged":1515902367721,"timesUsed":1}],"disabledHosts":[],"version":2} -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | ### Running tests 2 | 3 | To run all tests, simply run `./run_all`. 4 | Also see section in general README [here](../README.md#testing) 5 | 6 | ### Writing tests 7 | 8 | Tests are executed using the [bash-tap](https://github.com/wbsch/bash_tap) testing framework. 9 | 10 | #### Requirements 11 | 12 | Tests must meet the following criteria: 13 | - A test must have a `.t` extension. 14 | - In order to be picked up by the `run_all` script, the test must also be executable. (`chmod +x testfile.t`) 15 | 16 | #### Test structure 17 | 18 | Test files contain the following: 19 | 20 | #!/usr/bin/env bash 21 | 22 | # File containing all testing functionality and helper functions 23 | . bash_tap_fd.sh 24 | 25 | # Obtain master password used in the test framework 26 | PASSWD=$(get_password) 27 | # Basically the firefox_decrypt.py command 28 | CMD=$(get_script) 29 | # Test data to use. One of the profiles under tests/test_data/ or the test_data folder itself in which case profile.ini is used. 30 | TEST="$(get_test_data)" 31 | # For interactive tests this consists of the commands that a user would type 32 | PAYLOAD="2\n${PASSWD}" 33 | 34 | # Each line is one test. If the line has a non-zero exit code the test fails 35 | echo ${PAYLOAD} | ${CMD} --args ${TEST} ... 36 | # Some tests also use diff and grep to ensure the output matches what is expected 37 | 38 | 39 | #### Syntax 40 | 41 | Some tests make use of some lesser known bash constructs such as `<(command)`. 42 | This syntax is called **process substitution** and is documented [here](http://www.tldp.org/LDP/abs/html/process-sub.html). 43 | 44 | #### Existing test profiles 45 | 46 | In order to test compatibility with different versions of Firefox there are currently 3 profiles that can be picked from: 47 | - `test_profile_firefox_20` - Firefox 20.0 (uses an sqlite storage backend for secrets) 48 | - `test_profile_firefox_46` - Firefox 46.0 (uses a json storage backend for secrets) 49 | - `test_profile_firefox_nopassword` - Firefox 46.0 (secrets are not protected by a master password) 50 | 51 | The password used in the protected profiles lives in `tests/test_data/master_password` and can be obtained by calling the `get_password` helper function in tests. 52 | 53 | #### Logins 54 | 55 | Each testing profile contains 3 users. Their details are found under `tests/test_data/users/`. 56 | These can be used to validate that `firefox_decrypt` outputs the correct answer, including encoding and handling of special characters. 57 | -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_nopassword_114/logins.json: -------------------------------------------------------------------------------- 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} 2 | -------------------------------------------------------------------------------- /tests/non_interactive_choice.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | from simpletap.fdecrypt import lib 6 | 7 | 8 | class TestNonInteractiveChoice(unittest.TestCase): 9 | def validate_one(self, userkey, grepkey, output): 10 | expected = lib.get_user_data(userkey) 11 | matches = lib.grep(grepkey, output, context=1) 12 | 13 | self.assertEqual(matches, expected) 14 | 15 | def validate(self, out): 16 | self.validate_one("doesntexist", "doesntexist", out) 17 | self.validate_one("onemore", "onemore", out) 18 | self.validate_one("complex", "cömplex", out) 19 | self.validate_one("jamie", "jãmïe", out) 20 | 21 | @unittest.skipIf(lib.platform == "Windows", 22 | "Windows DLL isn't backwards compatible") 23 | def test_firefox_20(self): 24 | cmd = lib.get_script() + [lib.get_test_data(), "-nc", "1"] 25 | pwd = lib.get_password() 26 | 27 | out = lib.run(cmd, stdin=pwd) 28 | self.validate(out) 29 | 30 | @unittest.skipIf(lib.platform == "Windows", 31 | "Windows DLL isn't backwards compatible") 32 | def test_firefox_46(self): 33 | cmd = lib.get_script() + [lib.get_test_data(), "-nc", "2"] 34 | pwd = lib.get_password() 35 | 36 | out = lib.run(cmd, stdin=pwd) 37 | self.validate(out) 38 | 39 | def test_firefox_59(self): 40 | cmd = lib.get_script() + [lib.get_test_data(), "-nc", "4"] 41 | pwd = lib.get_password() 42 | 43 | out = lib.run(cmd, stdin=pwd) 44 | self.validate(out) 45 | 46 | def test_firefox_144(self): 47 | cmd = lib.get_script() + [lib.get_test_data(), "-nc", "7"] 48 | pwd = lib.get_password() 49 | 50 | out = lib.run(cmd, stdin=pwd) 51 | self.validate(out) 52 | 53 | @unittest.skipIf(lib.platform == "Windows", 54 | "Windows DLL isn't backwards compatible") 55 | def test_firefox_nopass_46(self): 56 | cmd = lib.get_script() + [lib.get_test_data(), "-nc", "3"] 57 | 58 | out = lib.run(cmd) 59 | self.validate(out) 60 | 61 | def test_firefox_nopass_59(self): 62 | cmd = lib.get_script() + [lib.get_test_data(), "-nc", "6"] 63 | 64 | out = lib.run(cmd) 65 | self.validate(out) 66 | 67 | def test_firefox_missing_choice(self): 68 | cmd = lib.get_script() + [lib.get_test_data(), "-n"] 69 | 70 | out = lib.run_error(cmd, returncode=31) # 31 is MISSING_CHOICE exit 71 | output = lib.remove_log_date_time(out) 72 | expected = lib.get_output_data("non_interactive_choice_missing") 73 | self.assertEqual(output, expected) 74 | 75 | 76 | if __name__ == "__main__": 77 | from simpletap import TAPTestRunner 78 | unittest.main(testRunner=TAPTestRunner(buffer=True), exit=False) 79 | 80 | # vim: ai sts=4 et sw=4 81 | -------------------------------------------------------------------------------- /tests/handle_corrupted_passwords.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import unittest 6 | from simpletap.fdecrypt import lib 7 | 8 | 9 | class TestCorruptedPassword(unittest.TestCase): 10 | def validate_one(self, userkey, grepkey, output): 11 | expected = lib.get_user_data(userkey) 12 | # Ignore DEBUG/verbose information when looking for the specified key 13 | output = lib.grep("^(?!.*DEBUG).*$", output, context=0) 14 | matches = lib.grep(grepkey, output, context=1) 15 | 16 | self.assertEqual(matches, expected) 17 | 18 | def validate(self, out): 19 | self.validate_one("decryption_failed", "Username: .* decryption failed", out) 20 | self.validate_one("doesntexist", "doesntexist", out) 21 | self.validate_one("onemore", "onemore", out) 22 | self.validate_one("complex", "cömplex", out) 23 | self.validate_one("jamie", "jãmïe", out) 24 | 25 | def validate_exception(self, out): 26 | # error is "ValueError: Username/Password decryption (...) Credentials damaged (...)" 27 | err = "Credentials damaged or cert/key file mismatch." 28 | match = lib.grep(err, out) 29 | self.assertIn("ValueError: Username/Password", match) 30 | 31 | def validate_error(self, out): 32 | # error is "ERROR - Username/Password decryption (...) Credentials damaged (...)" 33 | err = "Credentials damaged or cert/key file mismatch." 34 | match = lib.grep(err, out) 35 | self.assertIn("ERROR - Username/Password", match) 36 | 37 | def run_firefox_nopassword(self, cmd): 38 | output = lib.run(cmd) 39 | self.validate(output) 40 | self.validate_exception(output) 41 | 42 | def run_firefox_nopassword_error(self, cmd): 43 | # returncode 17 is DECRYPTION_FAILED 44 | output = lib.run_error(cmd, returncode=17) 45 | self.validate_error(output) 46 | 47 | def test_corrupted_skip_firefox_114(self): 48 | self.test = os.path.join(lib.get_test_data(), 49 | "test_profile_firefox_nopassword_114") 50 | 51 | # Must run in non-interactive mode or password prompt will be shown 52 | cmd = lib.get_script() + [self.test, "-n", "--non-fatal-decryption", "-vv"] 53 | 54 | self.run_firefox_nopassword(cmd) 55 | 56 | def test_corrupted_firefox_114(self): 57 | self.test = os.path.join(lib.get_test_data(), 58 | "test_profile_firefox_nopassword_114") 59 | 60 | # Must run in non-interactive mode or password prompt will be shown 61 | cmd = lib.get_script() + [self.test, "-n"] 62 | 63 | self.run_firefox_nopassword_error(cmd) 64 | 65 | 66 | if __name__ == "__main__": 67 | from simpletap import TAPTestRunner 68 | unittest.main(testRunner=TAPTestRunner(buffer=True), exit=False) 69 | 70 | # vim: ai sts=4 et sw=4 71 | -------------------------------------------------------------------------------- /tests/test_data/test_profile_firefox_144/logins.json: -------------------------------------------------------------------------------- 1 | {"nextId":9,"logins":[{"id":5,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"","passwordField":"","encryptedUsername":"MEMEEPgAAAAAAAAAAAAAAAAAAAEwHQYJYIZIAWUDBAEqBBA32ujOEDe/S7l+R5qUx9DIBBA6Fk55XKPeSGBb7JNswwjG","encryptedPassword":"MEMEEPgAAAAAAAAAAAAAAAAAAAEwHQYJYIZIAWUDBAEqBBD2Y3fmH3MXtHblh6RHPbwABBANs30wTDJ7liNuXjG9Cm2L","guid":"{9e169e37-94b5-4b8c-bb19-5ad584e637d7}","encType":1,"timeCreated":1458308750178,"timeLastUsed":1458308750178,"timePasswordChanged":1458308750178,"timesUsed":1,"syncCounter":1,"everSynced":false,"encryptedUnknownFields":"MEMEEPgAAAAAAAAAAAAAAAAAAAEwHQYJYIZIAWUDBAEqBBDayEAe/q7z8rwfkSNoSba5BBBlirvz4zmFmzdJGl5rQxYQ"},{"id":6,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"","passwordField":"","encryptedUsername":"MEMEEPgAAAAAAAAAAAAAAAAAAAEwHQYJYIZIAWUDBAEqBBDeXaGPHelKYauF+yPqZAwTBBDSv5c+vrTw0jX9M1XaWHbR","encryptedPassword":"MEMEEPgAAAAAAAAAAAAAAAAAAAEwHQYJYIZIAWUDBAEqBBCZjpN626TRtMpjnr4S60l+BBDAuhloVQJ8MM/TbxDuq8MU","guid":"{45c02941-b799-437a-8a6c-e979e7d1862c}","encType":1,"timeCreated":1458308837800,"timeLastUsed":1458308837800,"timePasswordChanged":1458308837800,"timesUsed":1,"syncCounter":1,"everSynced":false,"encryptedUnknownFields":"MEMEEPgAAAAAAAAAAAAAAAAAAAEwHQYJYIZIAWUDBAEqBBAePwNYCkgmN4+84IyRV0wXBBBQibbSlhcCKtD0KAQ2675k"},{"id":7,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"","passwordField":"","encryptedUsername":"MEMEEPgAAAAAAAAAAAAAAAAAAAEwHQYJYIZIAWUDBAEqBBA3ClAVOaMkttUgRcEjpXLZBBCf3Rm2T+yOaB7iT7DhzSE+","encryptedPassword":"MGMEEPgAAAAAAAAAAAAAAAAAAAEwHQYJYIZIAWUDBAEqBBB/eh3g6fIkilRggfNXCmdABDC7NtiCxn0et4LbjDtLPaAJm40ZRULV1o1GTnazHc4IU0d/Eboe6ep/m9C3vdvG8j0=","guid":"{e02610a1-cc74-480a-aa6d-19f8ff6afb15}","encType":1,"timeCreated":1458309068022,"timeLastUsed":1458309068022,"timePasswordChanged":1458309068022,"timesUsed":1,"syncCounter":1,"everSynced":false,"encryptedUnknownFields":"MEMEEPgAAAAAAAAAAAAAAAAAAAEwHQYJYIZIAWUDBAEqBBCSEEJhJ6gyXP8+gZjszH+MBBAGJ/hAu3PWwoG1gVIUBIo/"},{"id":8,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"","passwordField":"","encryptedUsername":"MEMEEPgAAAAAAAAAAAAAAAAAAAEwHQYJYIZIAWUDBAEqBBBi3rqwnfcInef9okaI/taFBBBKbPGfKmf5EOcUGCcT+Ne0","encryptedPassword":"MGMEEPgAAAAAAAAAAAAAAAAAAAEwHQYJYIZIAWUDBAEqBBBhGGLJj3zT72qBdhi463Z/BDB2O4zjg8yLT3OjWCA0T0PLUreXfR0LkYzj79M5y4e553xLdPD0nyiA3k3k+w0vYnI=","guid":"{f3f4850e-f608-49a4-8856-81510ce1265d}","encType":1,"timeCreated":1502989487187,"timeLastUsed":1502989487187,"timePasswordChanged":1502989487187,"timesUsed":1,"syncCounter":1,"everSynced":false,"encryptedUnknownFields":"MEMEEPgAAAAAAAAAAAAAAAAAAAEwHQYJYIZIAWUDBAEqBBD0A2lojMDF7xf96+/XcCRPBBANptPzAwo24M5yj0IsOe2Z"}],"disabledHosts":[],"version":3,"potentiallyVulnerablePasswords":[],"dismissedBreachAlertsByLoginGUID":{}} -------------------------------------------------------------------------------- /tests/profile_ini.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | from simpletap.fdecrypt import lib 6 | 7 | 8 | class TestProfileIni(unittest.TestCase): 9 | def setUp(self): 10 | self.test = lib.get_test_data() 11 | self.pwd = lib.get_password() 12 | 13 | def validate_one(self, userkey, grepkey, output): 14 | expected = lib.get_user_data(userkey) 15 | matches = lib.grep(grepkey, output, context=1) 16 | 17 | self.assertEqual(matches, expected) 18 | 19 | def validate(self, out): 20 | self.validate_one("doesntexist", "doesntexist", out) 21 | self.validate_one("onemore", "onemore", out) 22 | self.validate_one("complex", "cömplex", out) 23 | self.validate_one("jamie", "jãmïe", out) 24 | 25 | @unittest.skipIf(lib.platform == "Windows", 26 | "Windows DLL isn't backwards compatible") 27 | def test_firefox_20(self): 28 | cmd = lib.get_script() + [self.test] 29 | choice = "1" 30 | payload = '\n'.join((choice, self.pwd)) 31 | 32 | output = lib.run(cmd, stdin=payload) 33 | self.validate(output) 34 | 35 | @unittest.skipIf(lib.platform == "Windows", 36 | "Windows DLL isn't backwards compatible") 37 | def test_firefox_46(self): 38 | cmd = lib.get_script() + [self.test] 39 | choice = "2" 40 | payload = '\n'.join((choice, self.pwd)) 41 | 42 | output = lib.run(cmd, stdin=payload) 43 | self.validate(output) 44 | 45 | def test_firefox_59(self): 46 | cmd = lib.get_script() + [self.test] 47 | choice = "4" 48 | payload = '\n'.join((choice, self.pwd)) 49 | 50 | output = lib.run(cmd, stdin=payload) 51 | self.validate(output) 52 | 53 | def test_firefox_144(self): 54 | cmd = lib.get_script() + [self.test] 55 | choice = "7" 56 | payload = '\n'.join((choice, self.pwd)) 57 | 58 | output = lib.run(cmd, stdin=payload) 59 | self.validate(output) 60 | 61 | def test_firefox_non_ascii(self): 62 | cmd = lib.get_script() + [self.test] 63 | choice = "5" 64 | payload = '\n'.join((choice, self.pwd)) 65 | 66 | output = lib.run(cmd, stdin=payload) 67 | self.validate(output) 68 | 69 | @unittest.skipIf(lib.platform == "Windows", 70 | "Windows DLL isn't backwards compatible") 71 | def test_firefox_nopass_46(self): 72 | cmd = lib.get_script() + [self.test] 73 | payload = "3" 74 | 75 | output = lib.run(cmd, stdin=payload) 76 | self.validate(output) 77 | 78 | def test_firefox_nopass_59(self): 79 | cmd = lib.get_script() + [self.test] 80 | payload = "6" 81 | 82 | output = lib.run(cmd, stdin=payload) 83 | self.validate(output) 84 | 85 | 86 | if __name__ == "__main__": 87 | from simpletap import TAPTestRunner 88 | unittest.main(testRunner=TAPTestRunner(buffer=True), exit=False) 89 | 90 | # vim: ai sts=4 et sw=4 91 | -------------------------------------------------------------------------------- /tests/direct_profile.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import unittest 6 | from simpletap.fdecrypt import lib 7 | 8 | 9 | class TestDirectProfilePass(unittest.TestCase): 10 | def validate_one(self, userkey, grepkey, output): 11 | expected = lib.get_user_data(userkey) 12 | matches = lib.grep(grepkey, output, context=1) 13 | 14 | self.assertEqual(matches, expected) 15 | 16 | def validate(self, out): 17 | self.validate_one("doesntexist", "doesntexist", out) 18 | self.validate_one("onemore", "onemore", out) 19 | self.validate_one("complex", "cömplex", out) 20 | self.validate_one("jamie", "jãmïe", out) 21 | 22 | def run_firefox_with_password(self): 23 | cmd = lib.get_script() + [self.test] 24 | pwd = lib.get_password() 25 | 26 | output = lib.run(cmd, stdin=pwd) 27 | self.validate(output) 28 | 29 | def run_firefox_nopassword(self): 30 | # Must run in non-interactive mode or password prompt will be shown 31 | cmd = lib.get_script() + [self.test, "-n"] 32 | 33 | output = lib.run(cmd) 34 | self.validate(output) 35 | 36 | @unittest.skipIf(lib.platform == "Windows", 37 | "Windows DLL isn't backwards compatible") 38 | def test_firefox_20(self): 39 | self.test = os.path.join(lib.get_test_data(), 40 | "test_profile_firefox_20") 41 | self.run_firefox_with_password() 42 | 43 | @unittest.skipIf(lib.platform == "Windows", 44 | "Windows DLL isn't backwards compatible") 45 | def test_firefox_46(self): 46 | self.test = os.path.join(lib.get_test_data(), 47 | "test_profile_firefox_46") 48 | self.run_firefox_with_password() 49 | 50 | def test_firefox_59(self): 51 | self.test = os.path.join(lib.get_test_data(), 52 | "test_profile_firefox_59") 53 | self.run_firefox_with_password() 54 | 55 | def test_firefox_144(self): 56 | self.test = os.path.join(lib.get_test_data(), 57 | "test_profile_firefox_144") 58 | self.run_firefox_with_password() 59 | 60 | def test_firefox_non_ascii(self): 61 | self.test = os.path.join(lib.get_test_data(), 62 | "test_profile_firefox_LЮшр/") 63 | self.run_firefox_with_password() 64 | 65 | @unittest.skipIf(lib.platform == "Windows", 66 | "Windows DLL isn't backwards compatible") 67 | def test_firefox_nopass_46(self): 68 | self.test = os.path.join(lib.get_test_data(), 69 | "test_profile_firefox_nopassword_46") 70 | self.run_firefox_nopassword() 71 | 72 | def test_firefox_nopass_59(self): 73 | self.test = os.path.join(lib.get_test_data(), 74 | "test_profile_firefox_nopassword_59") 75 | self.run_firefox_nopassword() 76 | 77 | 78 | if __name__ == "__main__": 79 | from simpletap import TAPTestRunner 80 | unittest.main(testRunner=TAPTestRunner(buffer=True), exit=False) 81 | 82 | # vim: ai sts=4 et sw=4 83 | -------------------------------------------------------------------------------- /tests/json.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import unittest 6 | from simpletap.fdecrypt import lib 7 | 8 | 9 | class TestJSON(unittest.TestCase): 10 | def setUp(self): 11 | self.test = lib.get_test_data() 12 | self.pwd = lib.get_password() 13 | 14 | def validate_one(self, userkey, grepkey, output): 15 | expected = lib.get_user_data(userkey) 16 | 17 | expected = lib.remove_log_date_time(expected, dropmatches=True) 18 | matches = lib.remove_log_date_time(output, dropmatches=True) 19 | 20 | self.assertEqual(matches, expected) 21 | 22 | def validate_default(self, out): 23 | self.validate_one("header_json_default", '"password"', out) 24 | self.validate_one("doesntexist_json_default", "doesntexist", out) 25 | self.validate_one("onemore_json_default", "onemore", out) 26 | self.validate_one("complex_json_default", "cömplex", out) 27 | self.validate_one("jamie_json_default", "jãmïe", out) 28 | 29 | @unittest.skipIf(lib.platform == "Windows", 30 | "Windows DLL isn't backwards compatible") 31 | def test_firefox_20_default(self): 32 | test = os.path.join(self.test, "test_profile_firefox_20") 33 | 34 | cmd = lib.get_script() + [test, "--format", "json"] 35 | output = lib.run(cmd, stdin=self.pwd, stderr=sys.stderr) 36 | self.validate_default(output) 37 | 38 | @unittest.skipIf(lib.platform == "Windows", 39 | "Windows DLL isn't backwards compatible") 40 | def test_firefox_46_default(self): 41 | test = os.path.join(self.test, "test_profile_firefox_46") 42 | 43 | cmd = lib.get_script() + [test, "--format", "json"] 44 | output = lib.run(cmd, stdin=self.pwd, stderr=sys.stderr) 45 | self.validate_default(output) 46 | 47 | def test_firefox_59_default(self): 48 | test = os.path.join(self.test, "test_profile_firefox_59") 49 | 50 | cmd = lib.get_script() + [test, "--format", "json"] 51 | output = lib.run(cmd, stdin=self.pwd, stderr=sys.stderr) 52 | self.validate_default(output) 53 | 54 | def test_firefox_144_default(self): 55 | test = os.path.join(self.test, "test_profile_firefox_144") 56 | 57 | cmd = lib.get_script() + [test, "--format", "json"] 58 | output = lib.run(cmd, stdin=self.pwd, stderr=sys.stderr) 59 | self.validate_default(output) 60 | 61 | @unittest.skipIf(lib.platform == "Windows", 62 | "Windows DLL isn't backwards compatible") 63 | def test_firefox_nopassword_46_default(self): 64 | test = os.path.join(self.test, "test_profile_firefox_nopassword_46") 65 | 66 | cmd = lib.get_script() + [test, "-n", "--format", "json"] 67 | output = lib.run(cmd, stderr=sys.stderr) 68 | self.validate_default(output) 69 | 70 | def test_firefox_nopassword_59_default(self): 71 | test = os.path.join(self.test, "test_profile_firefox_nopassword_59") 72 | 73 | cmd = lib.get_script() + [test, "-n", "--format", "json"] 74 | output = lib.run(cmd, stderr=sys.stderr) 75 | self.validate_default(output) 76 | 77 | 78 | if __name__ == "__main__": 79 | from simpletap import TAPTestRunner 80 | unittest.main(testRunner=TAPTestRunner(buffer=True), exit=False) 81 | 82 | # vim: ai sts=4 et sw=4 83 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Test firefox_decrypt 2 | on: [workflow_dispatch, push] 3 | 4 | jobs: 5 | test-firefox: 6 | runs-on: ${{ matrix.os }}-latest 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | os: [ubuntu, macos, windows] 11 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] 12 | # We need NSS 3.113+ to support Firefox 144 profiles 13 | # See https://github.com/unode/firefox_decrypt/issues/120 14 | nss-source: ['latest-esr', 'latest', 'NSS_3_117_RTM', 'system'] 15 | exclude: 16 | - os: macos 17 | # Not installing from source on MacOS 18 | nss-source: NSS_3_117_RTM 19 | - os: macos 20 | # Firefox bundles don't work well for us 21 | nss-source: latest 22 | - os: macos 23 | # Firefox bundles don't work well for us 24 | nss-source: latest-esr 25 | - os: windows 26 | # Not installing from source on Windows 27 | nss-source: NSS_3_117_RTM 28 | - os: windows 29 | # No system lib for Windows 30 | nss-source: system 31 | - os: ubuntu 32 | # Official nss packages are way too old 33 | nss-source: system 34 | env: 35 | # Needed to force UTF-8 and have consistent behavior in Windows 36 | PYTHONUTF8: 1 37 | steps: 38 | - uses: actions/checkout@v5 39 | - name: Set up Python 40 | uses: actions/setup-python@v5 41 | with: 42 | python-version: ${{ matrix.python-version }} 43 | 44 | - name: Setup Firefox ${{ matrix.nss-source }} 45 | if: | 46 | startsWith(matrix.nss-source, 'latest') && ( 47 | matrix.os == 'ubuntu' || matrix.os == 'windows' 48 | ) 49 | uses: browser-actions/setup-firefox@latest 50 | with: 51 | firefox-version: ${{ matrix.nss-source }} 52 | 53 | - name: Install nss via homebrew 54 | if: | 55 | (matrix.os == 'macos' && matrix.nss-source == 'system') 56 | run: | 57 | brew install nss 58 | brew list --versions nss 59 | 60 | - name: Cache NSS built from source 61 | id: cache-nss 62 | uses: actions/cache@v4 63 | with: 64 | path: | 65 | nss-${{ matrix.nss-source }} 66 | nspr 67 | dist 68 | key: ${{ matrix.nss-source }}-${{ matrix.os }} 69 | 70 | - name: Build (Release) NSS from source 71 | if: | 72 | ( 73 | startsWith(matrix.nss-source, 'NSS') && matrix.os == 'ubuntu' && steps.cache-nss.outputs.cache-hit != 'true' 74 | ) 75 | run: | 76 | apt update && apt install -y mercurial git ninja-build python3-pip 77 | python3 -m pip install gyp-next 78 | hg clone https://hg.mozilla.org/projects/nspr 79 | wget https://hg.mozilla.org/projects/nss/archive/${{ matrix.nss-source }}.zip 80 | unzip ${{ matrix.nss-source }}.zip 81 | cd nss-${{ matrix.nss-source }} 82 | ./build.sh -o --enable-legacy-db 83 | 84 | - name: Prepare system-level lib resolution 85 | if: | 86 | ( 87 | startsWith(matrix.nss-source, 'NSS') && matrix.os == 'ubuntu' 88 | ) 89 | run: | 90 | echo "${{ github.workspace }}/dist/Release/lib" | sudo tee /etc/ld.so.conf.d/nss-libs.conf 91 | sudo ldconfig 92 | 93 | - name: Run tests 94 | run: | 95 | cd tests 96 | python show_encodings 97 | python run_all -v 98 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #### Changelog 2 | 3 | ##### 1.1.1+git 4 | - Include Darwin homebrew path in nss search locations 5 | 6 | ##### 1.1.1 7 | - Fix unhandled exception with deleted passwords - see #99 8 | - Environment variable `NSS_LIB_PATH` can now be used to specify `libnss` location 9 | 10 | ##### 1.1.0 11 | - Include `pyproject.toml` to facilitate usage via `pipx` 12 | - Allow overriding default encoding 13 | - Add `--pass-always-with-login` to always include /login as part of pass's password path 14 | - Improve compatibility with `gopass` by explicitly using `pass ls` instead of `pass` 15 | - Add `--non-fatal-decryption` to attempt decrypting partially corrupt databases instead of aborting on first failure 16 | - Enable All Contributors framework in project 17 | 18 | ##### 1.0.0 19 | - Improve detection of NSS in Windows and MacOS 20 | - Skip decoding failures or malformed records 21 | - UTF-8 is now required for all interaction 22 | - Python UTF-8 mode is recommended on Windows 23 | - Tests are now automated on Linux, MacOS and Windows 24 | 25 | ##### 1.0.0-rc1 26 | - Output formats have been internally refactored for easier extensibility. 27 | There is now 'human', 'csv', 'tabular', 'json' and 'pass' 28 | - This version hopefully fixes the long standing encoding issues in Windows and MacOSX 29 | - `--quotechar` is now `--csv-quotechar`. 30 | - `--delimiter` is now `--csv-delimiter`. 31 | - `--tabular` is now `--format tabular`. 32 | - `--export-pass` is now `--format pass`. 33 | - Drop support for Python 2. Python 3.9 is now the required minimal version. 34 | - Add compatibility with browserpass via `--pass-compat=browserpass` 35 | - Add compatibility mode `username` for a `username:` prefix 36 | - Add `--pass-cmd` to allow specifying pass's location or script name. 37 | - Using `--pass-prefix=''` prevents creation of a prefix: `web/address/...` becomes `address/...` 38 | - Fix an encoding bug due to non-ASCII characters leading to a user's profile path 39 | - Explicitly target 32/64bit Mozilla folders depending on Python bitness 40 | 41 | ##### 0.7.0 42 | - Fix PK11 slot memory leak 43 | - Configurable pass-export prefix via `--pass-prefix` 44 | - Deprecate `--tabular`, add `--format` parameter and support CSV format 45 | - Fix minor bug with formatting of profile selection prompt 46 | - Support several default locations for libnss on Darwin 47 | - Support for password-store in SQLite format starting with Firefox v59 48 | 49 | ##### 0.6.2 50 | - Add `--tabular` output 51 | 52 | ##### 0.6.1 53 | - Fix a bug on `--version` affecting primarily Python 3 (@criztovyl) 54 | 55 | ##### 0.6 56 | - Fix a bug leading to segmentation fault crashes on newer platforms 57 | - Passing `--version` now displays firefox\_decrypt's version 58 | 59 | ##### 0.5.4 60 | - Search for NSS on additional folders when on Windows 61 | 62 | ##### 0.5.3 63 | - Compatibility improvements with Windows and OSX 64 | 65 | ##### 0.5.2 66 | - Non-interative mode (`-n/--no-interactive`, `-l/--list`, `-c/--choice`) 67 | 68 | ##### 0.5.1 69 | - Testsuite is now in place 70 | 71 | ##### 0.5 72 | - Fix encoding/decoding problems in python 2 - #5 73 | - Exporting passwords to *pass* now includes the login name 74 | - Exported password identifiers no longer include login names unless multiple 75 | credentials exist for the same address. 76 | 77 | ##### 0.4.2 78 | - If profile\_path provided doesn't contain profiles.ini assume it is an actual profile 79 | 80 | ##### 0.4.1 81 | - If only a single profile is found do not prompt user for profile 82 | - Document that the tool also works for Thunderbird profiles 83 | 84 | ##### 0.4 85 | - Add option to export passwords to *pass* from http://passwordstore.org 86 | 87 | ##### 0.3 88 | - Polyglot Python 2 and 3. Python 3 now supported. 89 | - Improved debugging information with -v or -vv 90 | 91 | ##### 0.2 92 | - Added support for logins.json. New format since Firefox 32. 93 | 94 | ##### 0.1 95 | - Initial version supporting Firefox 3.5 and up. 96 | -------------------------------------------------------------------------------- /tests/simpletap/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test Anything Protocol extension to Python's unit testing framework 3 | 4 | This module contains TAPTestRunner and TAPTestResult which are used to produce 5 | a test report in a TAP compatible format. All remaining functionality comes 6 | from Python's own unittest module. 7 | 8 | The core of the tests does not need any change and is purely unittest code. 9 | The sole difference is in the __name__ == "__main__" section. 10 | 11 | Simple usage: 12 | 13 | import unittest 14 | 15 | class IntegerArithmeticTestCase(unittest.TestCase): 16 | def testAdd(self): # test method names begin 'test*' 17 | "test adding values" 18 | self.assertEqual((1 + 2), 3) 19 | self.assertEqual(0 + 1, 1) 20 | 21 | def testMultiply(self): 22 | "test multiplying values" 23 | self.assertEqual((0 * 10), 0) 24 | self.assertEqual((5 * 8), 40) 25 | 26 | def testFail(self): 27 | "a failing test" 28 | self.assertEqual(0, 1) 29 | 30 | @unittest.expectedFailure 31 | def testExpectFail(self): 32 | "we saw this coming" 33 | self.assertEqual(0, 1) 34 | 35 | @unittest.skipIf(True, "Skipping this one") 36 | def testSkip(self): 37 | "pending a fix" 38 | self.assertEqual(0, 1) 39 | 40 | def testError(self): 41 | "oops something went wrong" 42 | no_such_variable + 1 # Oops! 43 | 44 | if __name__ == "__main__": 45 | from simpletap import TAPTestRunner 46 | unittest.main(testRunner=TAPTestRunner()) 47 | 48 | 49 | When saved in a file called ``test.py`` and executed would produce: 50 | 51 | 1..6 52 | ok 1 - test.py: test adding values 53 | not ok 2 - test.py: oops something went wrong 54 | # ERROR: NameError on file test.py line 30 in testError: 'no_such_variable + 1 # Oops!': 55 | # global name 'no_such_variable' is not defined 56 | skip 3 - test.py: we saw this coming 57 | # EXPECTED_FAILURE: AssertionError on file test.py line 21 in testExpectFail: 'self.assertEqual(0, 1)': 58 | # 0 != 1 59 | not ok 4 - test.py: a failing test 60 | # FAIL: AssertionError on file test.py line 16 in testFail: 'self.assertEqual(0, 1)': 61 | # 0 != 1 62 | ok 5 - test.py: test multiplying values 63 | skip 6 - test.py: pending a fix 64 | # SKIP: 65 | # Skipping this one 66 | 67 | 68 | You can also launch simpletap directly from the command line in much the 69 | same way you do with unittest: 70 | 71 | python3 -m simpletap test.IntegerArithmeticTestCase 72 | 73 | 74 | For more information refer to the unittest documentation: 75 | 76 | http://docs.python.org/library/unittest.html 77 | 78 | Copyright (c) 2014-2016 Renato Alves 79 | 80 | Permission is hereby granted, free of charge, to any person obtaining a copy of 81 | this software and associated documentation files (the "Software"), to deal in 82 | the Software without restriction, including without limitation the rights to 83 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 84 | the Software, and to permit persons to whom the Software is furnished to do so, 85 | subject to the following conditions: 86 | 87 | The above copyright notice and this permission notice shall be included in all 88 | copies or substantial portions of the Software. 89 | 90 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 91 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 92 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 93 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 94 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 95 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 96 | 97 | https://opensource.org/licenses/MIT 98 | """ 99 | 100 | from .result import TAPTestResult 101 | from .runner import TAPTestRunner 102 | from .version import __version__ # noqa 103 | 104 | 105 | __all__ = ['TAPTestResult', 'TAPTestRunner'] 106 | -------------------------------------------------------------------------------- /tests/simpletap/fdecrypt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | import re 6 | import datetime 7 | import tempfile 8 | import platform 9 | from subprocess import run, CalledProcessError, PIPE, STDOUT 10 | 11 | 12 | class Test: 13 | def __init__(self): 14 | self.testdir = os.path.realpath(os.path.dirname(os.path.dirname(__file__))) 15 | self.platform = platform.system() 16 | self.interpreter = "python3" 17 | 18 | self.check_interpreter() 19 | 20 | def check_interpreter(self): 21 | out = run([self.interpreter, "--version"], capture_output=True) 22 | 23 | try: 24 | major, minor, _ = out.stdout.decode("utf-8").strip().split(" ")[1].split(".") 25 | valid_version = (int(major) == 3) & (int(minor) >= 9) 26 | except (ValueError, IndexError, TypeError): 27 | raise Exception("Couldn't parse version of 'python3 --version'") 28 | 29 | if not valid_version: 30 | raise Exception(f"{self.interpreter} binary has version {major}.{minor} but at least 3.9 is required") 31 | 32 | def run(self, cmd, stdin=None, stderr=STDOUT, workdir=None): 33 | if stderr == sys.stderr: 34 | with tempfile.NamedTemporaryFile(mode="w+t") as err: 35 | try: 36 | p = run(cmd, check=True, encoding="utf8", cwd=workdir, 37 | input=stdin, stdout=PIPE, stderr=err) 38 | except CalledProcessError as e: 39 | if e.returncode: 40 | err.flush() 41 | err.seek(0) 42 | sys.stderr.write(err.read()) 43 | raise 44 | 45 | else: 46 | return p.stdout 47 | else: 48 | p = run(cmd, check=True, encoding="utf8", cwd=workdir, 49 | input=stdin, stdout=PIPE, stderr=stderr) 50 | 51 | return p.stdout 52 | 53 | def run_error(self, cmd, returncode, stdin=None, stderr=STDOUT, workdir=None): 54 | try: 55 | output = self.run(cmd, stdin=stdin, stderr=stderr, workdir=workdir) 56 | except CalledProcessError as e: 57 | if e.returncode != returncode: 58 | raise ValueError("Expected exit code {} but saw {}".format(returncode, e.returncode)) 59 | else: 60 | output = e.stdout 61 | 62 | return output 63 | 64 | def get_password(self): 65 | with open(os.path.join(self.get_test_data(), "master_password")) as fh: 66 | return fh.read() 67 | 68 | def get_script(self): 69 | return [self.interpreter, "{}/../firefox_decrypt.py".format(self.testdir)] 70 | 71 | def get_test_data(self): 72 | return os.path.join(self.testdir, "test_data") 73 | 74 | def _get_dir_data(self, subdir, target): 75 | with open(os.path.join(self.get_test_data(), subdir, "{}.{}".format(target, subdir[:-1]))) as fh: 76 | return fh.read() 77 | 78 | def get_user_data(self, target): 79 | return self._get_dir_data("users", target) 80 | 81 | def get_output_data(self, target): 82 | return self._get_dir_data("outputs", target) 83 | 84 | def get_internal_version(self): 85 | with open(os.path.join(self.testdir, "..", "CHANGELOG.md")) as fh: 86 | for line in fh: 87 | if line.startswith("###") and "." in line: 88 | return line.strip("# ") 89 | 90 | def remove_full_pwd(self, output): 91 | return output.replace(os.path.join(self.testdir, ''), '') 92 | 93 | def remove_log_date_time(self, input, dropmatches=False): 94 | output = [] 95 | date = str(datetime.datetime.now().date()) 96 | for line in input.split('\n'): 97 | if line.startswith(date): 98 | if not dropmatches: 99 | output.append(line.split(' ', 2)[-1]) 100 | else: 101 | output.append(line) 102 | 103 | return '\n'.join(output) 104 | 105 | def grep(self, pattern, output, context=0): 106 | r = re.compile(pattern) 107 | lines = output.split('\n') 108 | 109 | acc = [] 110 | for i in range(len(lines)): 111 | if r.search(lines[i]): 112 | acc.extend(lines[i-context:1+i+context]) 113 | 114 | return '\n'.join(acc) + '\n' 115 | 116 | 117 | lib = Test() 118 | 119 | if __name__ == "__main__": 120 | pass 121 | 122 | # vim: ai sts=4 et sw=4 123 | -------------------------------------------------------------------------------- /tests/problems: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import print_function 4 | import sys 5 | import re 6 | import argparse 7 | from collections import defaultdict 8 | 9 | 10 | def color(text, c): 11 | """ 12 | Add color on the keyword that identifies the state of the test 13 | """ 14 | if sys.stdout.isatty(): 15 | clear = "\033[0m" 16 | 17 | colors = { 18 | "red": "\033[1m\033[91m", 19 | "yellow": "\033[1m\033[93m", 20 | "green": "\033[1m\033[92m", 21 | } 22 | return colors[c] + text + clear 23 | else: 24 | return text 25 | 26 | 27 | def parse_args(): 28 | parser = argparse.ArgumentParser(description="Report on test results") 29 | parser.add_argument('--summary', action="store_true", 30 | help="Display only the totals in each category") 31 | parser.add_argument('tapfile', default="all.log", nargs="?", 32 | help="File containing TAP output") 33 | return parser.parse_args() 34 | 35 | 36 | def print_category(tests): 37 | if not cmd_args.summary: 38 | for key in sorted(tests): 39 | print("%-32s %4d" % (key, tests[key])) 40 | 41 | 42 | def pad(i): 43 | return " " * i 44 | 45 | 46 | if __name__ == "__main__": 47 | cmd_args = parse_args() 48 | 49 | errors = defaultdict(int) 50 | skipped = defaultdict(int) 51 | expected = defaultdict(int) 52 | unexpected = defaultdict(int) 53 | passed = defaultdict(int) 54 | 55 | file = re.compile(r"^# (?:./)?(\S+\.t)(?:\.exe)?$") 56 | timestamp = re.compile(r"^# (\d+(?:\.\d+)?) ==>.*$") 57 | start = None 58 | stop = None 59 | 60 | with open(cmd_args.tapfile) as fh: 61 | for line in fh: 62 | if start is None: 63 | # First line contains the starting timestamp 64 | match = timestamp.match(line) 65 | if match: 66 | start = float(match.group(1)) 67 | else: 68 | start = 0 69 | continue 70 | 71 | match = file.match(line) 72 | if match: 73 | filename = match.group(1) 74 | 75 | if line.startswith("ok "): 76 | passed[filename] += 1 77 | 78 | if line.startswith("not "): 79 | errors[filename] += 1 80 | 81 | if line.startswith("# SKIP:"): 82 | skipped[filename] += 1 83 | 84 | if line.startswith("# EXPECTED_FAILURE:"): 85 | expected[filename] += 1 86 | 87 | if line.startswith("# UNEXPECTED_SUCCESS:"): 88 | unexpected[filename] += 1 89 | 90 | # Last line contains the ending timestamp 91 | match = timestamp.match(line) 92 | if match: 93 | stop = float(match.group(1)) 94 | else: 95 | stop = 0 96 | 97 | if start is None: 98 | start = 0 99 | 100 | v = "{0:>5d}" 101 | passed_str = "Passed:" + pad(24) 102 | passed_int = v.format(sum(passed.values())) 103 | error_str = "Failed:" + pad(24) 104 | error_int = v.format(sum(errors.values())) 105 | unexpected_str = "Unexpected successes:" + pad(10) 106 | unexpected_int = v.format(sum(unexpected.values())) 107 | skipped_str = "Skipped:" + pad(23) 108 | skipped_int = v.format(sum(skipped.values())) 109 | expected_str = "Expected failures:" + pad(13) 110 | expected_int = v.format(sum(expected.values())) 111 | runtime_str = "Runtime:" + pad(20) 112 | runtime_int = "{0:>8.2f} seconds".format(stop - start) 113 | 114 | if cmd_args.summary: 115 | print(color(passed_str, "green"), passed_int) 116 | print(color(error_str, "red"), error_int) 117 | print(color(unexpected_str, "red"), unexpected_int) 118 | print(color(skipped_str, "yellow"), skipped_int) 119 | print(color(expected_str, "yellow"), expected_int) 120 | print(runtime_str, runtime_int) 121 | 122 | else: 123 | print(color(error_str, "red")) 124 | print_category(errors) 125 | print() 126 | print(color(unexpected_str, "red")) 127 | print_category(unexpected) 128 | print() 129 | print(color(skipped_str, "yellow")) 130 | print_category(skipped) 131 | print() 132 | print(color(expected_str, "yellow")) 133 | print_category(expected) 134 | 135 | # If we encoutered any failures, return non-zero code 136 | sys.exit(1 if int(error_int) or int(unexpected_int) else 0) 137 | -------------------------------------------------------------------------------- /tests/simpletap/result.py: -------------------------------------------------------------------------------- 1 | # simpletap - "Simple" TAP output with unittest 2 | # 3 | # Copyright (c) 2014-2021 Renato Alves 4 | # 5 | # This code is released under the MIT license. 6 | # Refer to LICENSE for further details. 7 | 8 | import os 9 | import sys 10 | import unittest 11 | import traceback 12 | import inspect 13 | 14 | 15 | def _color(text, c): 16 | """ 17 | Add color on the keyword that identifies the state of the test 18 | """ 19 | if sys.stdout.isatty(): 20 | clear = "\033[0m" 21 | 22 | colors = { 23 | "red": "\033[1m\033[91m", 24 | "yellow": "\033[1m\033[93m", 25 | "green": "\033[1m\033[92m", 26 | } 27 | return colors[c] + text + clear 28 | else: 29 | return text 30 | 31 | 32 | class TAPTestResult(unittest.result.TestResult): 33 | def __init__(self, stream=sys.stderr, descriptions=True, verbosity=1): 34 | super(TAPTestResult, self).__init__(stream, descriptions, verbosity) 35 | self.stream = unittest.runner._WritelnDecorator(stream) 36 | self.descriptions = descriptions 37 | self.verbosity = verbosity 38 | # Buffer stdout and stderr 39 | self.buffer = True 40 | self.total_tests = "unk" 41 | 42 | def getDescription(self, test): 43 | doc_first_line = test.shortDescription() 44 | if self.descriptions and doc_first_line: 45 | return doc_first_line 46 | else: 47 | try: 48 | method = test._testMethodName 49 | except AttributeError: 50 | return "Preparation error on: {0}".format(test.description) 51 | else: 52 | return "{0} ({1})".format(method, test.__class__.__name__) 53 | 54 | def startTestRun(self): 55 | self.stream.writeln("1..{0}".format(self.total_tests)) 56 | 57 | def stopTest(self, test): 58 | """Prevent flushing of stdout/stderr buffers until later""" 59 | pass 60 | 61 | def _restoreStdout(self): 62 | """Restore sys.stdout and sys.stderr, don't merge buffered output yet 63 | """ 64 | if self.buffer: 65 | sys.stdout = self._original_stdout 66 | sys.stderr = self._original_stderr 67 | 68 | @staticmethod 69 | def _do_stream(data, stream): 70 | """Helper function for _mergeStdout""" 71 | for line in data.splitlines(True): 72 | # newlines should be taken literally and be comments in TAP 73 | line = line.replace("\\n", "\n# ") 74 | 75 | # Add a comment sign before each line 76 | if line.startswith("#"): 77 | stream.write(line) 78 | else: 79 | stream.write("# " + line) 80 | 81 | if not line.endswith('\n'): 82 | stream.write('\n') 83 | 84 | def _mergeStdout(self): 85 | """Merge buffered output with main streams 86 | """ 87 | 88 | if self.buffer: 89 | output = self._stdout_buffer.getvalue() 90 | error = self._stderr_buffer.getvalue() 91 | if output: 92 | self._do_stream(output, sys.stdout) 93 | if error: 94 | self._do_stream(error, sys.stderr) 95 | 96 | self._stdout_buffer.seek(0) 97 | self._stdout_buffer.truncate() 98 | self._stderr_buffer.seek(0) 99 | self._stderr_buffer.truncate() 100 | 101 | # Needed to fix the stopTest override 102 | self._mirrorOutput = False 103 | 104 | def report(self, test, status=None, err=None): 105 | # Restore stdout/stderr but don't flush just yet 106 | self._restoreStdout() 107 | 108 | desc = self.getDescription(test) 109 | 110 | try: 111 | exception, msg, tb = err 112 | except (TypeError, ValueError): 113 | exception_name = "" 114 | msg = err 115 | tb = None 116 | else: 117 | exception_name = exception.__name__ 118 | msg = str(msg) 119 | 120 | trace_msg = "" 121 | 122 | # Extract line where error happened for easier debugging 123 | trace = traceback.extract_tb(tb) 124 | # Iterate from the end and stop on first match 125 | for t in trace[::-1]: 126 | # t = (filename, line_number, function_name, raw_line) 127 | if t[2].startswith("test"): 128 | trace_msg = " on file {0} line {1} in {2}: '{3}'".format(*t) 129 | break 130 | 131 | # Retrieve the name of the file containing the test 132 | filename = os.path.basename(inspect.getfile(test.__class__)) 133 | 134 | if status: 135 | 136 | if status == "SKIP": 137 | self.stream.writeln("{0} {1} - {2}: {3} # skip".format( 138 | _color("ok", "yellow"), self.testsRun, filename, desc) 139 | ) 140 | elif status == "EXPECTED_FAILURE": 141 | self.stream.writeln("{0} {1} - {2}: {3} # TODO".format( 142 | _color("ok", "yellow"), self.testsRun, filename, desc) 143 | ) 144 | elif status == "UNEXPECTED_SUCCESS": 145 | self.stream.writeln("{0} {1} - {2}: {3} # FIXED".format( 146 | _color("not ok", "yellow"), self.testsRun, filename, desc) 147 | ) 148 | else: 149 | self.stream.writeln("{0} {1} - {2}: {3}".format( 150 | _color("not ok", "red"), self.testsRun, filename, desc) 151 | ) 152 | 153 | if exception_name: 154 | self.stream.writeln("# {0}: {1}{2}:".format( 155 | status, exception_name, trace_msg) 156 | ) 157 | else: 158 | self.stream.writeln("# {0}:".format(status)) 159 | 160 | # Magic 3 is just for pretty indentation 161 | padding = " " * (len(status) + 3) 162 | 163 | for line in msg.splitlines(): 164 | # Force displaying new-line characters as literal new lines 165 | line = line.replace("\\n", "\n# ") 166 | self.stream.writeln("#{0}{1}".format(padding, line)) 167 | else: 168 | self.stream.writeln("{0} {1} - {2}: {3}".format( 169 | _color("ok", "green"), self.testsRun, filename, desc) 170 | ) 171 | 172 | # Flush all buffers to stdout 173 | self._mergeStdout() 174 | 175 | def addSuccess(self, test): 176 | super(TAPTestResult, self).addSuccess(test) 177 | self.report(test) 178 | 179 | def addError(self, test, err): 180 | super(TAPTestResult, self).addError(test, err) 181 | self.report(test, "ERROR", err) 182 | 183 | def addFailure(self, test, err): 184 | super(TAPTestResult, self).addFailure(test, err) 185 | self.report(test, "FAIL", err) 186 | 187 | def addSkip(self, test, reason): 188 | super(TAPTestResult, self).addSkip(test, reason) 189 | self.report(test, "SKIP", reason) 190 | 191 | def addExpectedFailure(self, test, err): 192 | super(TAPTestResult, self).addExpectedFailure(test, err) 193 | self.report(test, "EXPECTED_FAILURE", err) 194 | 195 | def addUnexpectedSuccess(self, test): 196 | super(TAPTestResult, self).addUnexpectedSuccess(test) 197 | self.report(test, "UNEXPECTED_SUCCESS", str(test)) 198 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "firefox_decrypt", 3 | "projectOwner": "unode", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "CONTRIBUTORS.md", 8 | "README.md" 9 | ], 10 | "imageSize": 100, 11 | "commit": true, 12 | "commitConvention": "gitmoji", 13 | "contributors": [ 14 | { 15 | "login": "unode", 16 | "name": "Renato Alves", 17 | "avatar_url": "https://avatars.githubusercontent.com/u/122319?v=4", 18 | "profile": "https://gitlab.com/unode", 19 | "contributions": [ 20 | "a11y", 21 | "question", 22 | "bug", 23 | "code", 24 | "content", 25 | "data", 26 | "design", 27 | "doc", 28 | "example", 29 | "ideas", 30 | "infra", 31 | "maintenance", 32 | "mentoring", 33 | "platform", 34 | "plugin", 35 | "projectManagement", 36 | "promotion", 37 | "research", 38 | "review", 39 | "security", 40 | "test", 41 | "tool", 42 | "tutorial", 43 | "userTesting" 44 | ] 45 | }, 46 | { 47 | "login": "NeffIsBack", 48 | "name": "Alex", 49 | "avatar_url": "https://avatars.githubusercontent.com/u/61382599?v=4", 50 | "profile": "https://github.com/NeffIsBack", 51 | "contributions": [ 52 | "platform" 53 | ] 54 | }, 55 | { 56 | "login": "criztovyl", 57 | "name": "Christoph Schulz", 58 | "avatar_url": "https://avatars.githubusercontent.com/u/2174918?v=4", 59 | "profile": "https://criztovyl.space/", 60 | "contributions": [ 61 | "bug", 62 | "code", 63 | "doc", 64 | "example", 65 | "maintenance", 66 | "test", 67 | "userTesting" 68 | ] 69 | }, 70 | { 71 | "login": "sedrubal", 72 | "name": "sedrubal", 73 | "avatar_url": "https://avatars.githubusercontent.com/u/5571650?v=4", 74 | "profile": "https://sedrubal.de/", 75 | "contributions": [ 76 | "code", 77 | "test" 78 | ] 79 | }, 80 | { 81 | "login": "DwordPtr", 82 | "name": "Bryan", 83 | "avatar_url": "https://avatars.githubusercontent.com/u/3793678?v=4", 84 | "profile": "https://github.com/DwordPtr", 85 | "contributions": [ 86 | "code" 87 | ] 88 | }, 89 | { 90 | "login": "edwintorok", 91 | "name": "Török Edwin", 92 | "avatar_url": "https://avatars.githubusercontent.com/u/721894?v=4", 93 | "profile": "https://github.com/edwintorok", 94 | "contributions": [ 95 | "code", 96 | "doc" 97 | ] 98 | }, 99 | { 100 | "login": "eseifert", 101 | "name": "Erich Seifert", 102 | "avatar_url": "https://avatars.githubusercontent.com/u/3323691?v=4", 103 | "profile": "https://github.com/eseifert", 104 | "contributions": [ 105 | "code", 106 | "doc" 107 | ] 108 | }, 109 | { 110 | "login": "catleeball", 111 | "name": "Cat Lee Ball", 112 | "avatar_url": "https://avatars.githubusercontent.com/u/43632885?v=4", 113 | "profile": "https://catball.dev/", 114 | "contributions": [ 115 | "doc" 116 | ] 117 | }, 118 | { 119 | "login": "Gounlaf", 120 | "name": "Florian Levis", 121 | "avatar_url": "https://avatars.githubusercontent.com/u/236413?v=4", 122 | "profile": "http://www.levisflorian.name/", 123 | "contributions": [ 124 | "code" 125 | ] 126 | }, 127 | { 128 | "login": "thomasmerz", 129 | "name": "Thomas Merz", 130 | "avatar_url": "https://avatars.githubusercontent.com/u/18568381?v=4", 131 | "profile": "https://github.com/thomasmerz", 132 | "contributions": [ 133 | "infra" 134 | ] 135 | }, 136 | { 137 | "login": "stweil", 138 | "name": "Stefan Weil", 139 | "avatar_url": "https://avatars.githubusercontent.com/u/6734573?v=4", 140 | "profile": "https://github.com/stweil", 141 | "contributions": [ 142 | "a11y" 143 | ] 144 | }, 145 | { 146 | "login": "tennox", 147 | "name": "Manuel", 148 | "avatar_url": "https://avatars.githubusercontent.com/u/2084639?v=4", 149 | "profile": "https://dev.page/tennox", 150 | "contributions": [ 151 | "code" 152 | ] 153 | }, 154 | { 155 | "login": "Anthropohedron", 156 | "name": "Anthropohedron", 157 | "avatar_url": "https://avatars.githubusercontent.com/u/107431?v=4", 158 | "profile": "https://github.com/Anthropohedron", 159 | "contributions": [ 160 | "code", 161 | "platform" 162 | ] 163 | }, 164 | { 165 | "login": "alejandro-amo", 166 | "name": "Alejandro Amo", 167 | "avatar_url": "https://avatars.githubusercontent.com/u/1114811?v=4", 168 | "profile": "http://alejandroamo.eu/", 169 | "contributions": [ 170 | "code", 171 | "platform" 172 | ] 173 | }, 174 | { 175 | "login": "yermulnik", 176 | "name": "George L. Yermulnik", 177 | "avatar_url": "https://avatars.githubusercontent.com/u/1274789?v=4", 178 | "profile": "https://github.com/yermulnik", 179 | "contributions": [ 180 | "code", 181 | "doc" 182 | ] 183 | }, 184 | { 185 | "login": "rma-x", 186 | "name": "rma-x", 187 | "avatar_url": "https://avatars.githubusercontent.com/u/4435732?v=4", 188 | "profile": "https://github.com/rma-x", 189 | "contributions": [ 190 | "ideas" 191 | ] 192 | }, 193 | { 194 | "login": "utrack", 195 | "name": "Nick Koptelov", 196 | "avatar_url": "https://avatars.githubusercontent.com/u/3862920?v=4", 197 | "profile": "https://github.com/utrack", 198 | "contributions": [ 199 | "bug" 200 | ] 201 | }, 202 | { 203 | "login": "longforrich", 204 | "name": "longforrich", 205 | "avatar_url": "https://avatars.githubusercontent.com/u/53457069?v=4", 206 | "profile": "https://github.com/longforrich", 207 | "contributions": [ 208 | "code", 209 | "ideas" 210 | ] 211 | }, 212 | { 213 | "login": "embeddedc", 214 | "name": "embeddedc", 215 | "avatar_url": "https://avatars.githubusercontent.com/u/4259379?v=4", 216 | "profile": "https://github.com/embeddedc", 217 | "contributions": [ 218 | "bug" 219 | ] 220 | }, 221 | { 222 | "login": "go9girl", 223 | "name": "go9girl", 224 | "avatar_url": "https://avatars.githubusercontent.com/u/159335355?v=4", 225 | "profile": "https://github.com/go9girl", 226 | "contributions": [ 227 | "a11y", 228 | "ideas" 229 | ] 230 | }, 231 | { 232 | "login": "CedricLevasseur", 233 | "name": "Cédric Levasseur", 234 | "avatar_url": "https://avatars.githubusercontent.com/u/233306?v=4", 235 | "profile": "http://www.cedriclevasseur.com/", 236 | "contributions": [ 237 | "code" 238 | ] 239 | } 240 | ], 241 | "contributorsPerLine": 7, 242 | "contributorsSortAlphabetically": true, 243 | "badgeTemplate": "[![All Contributors](https://img.shields.io/github/all-contributors/unode/firefox_decrypt?color=ee8449&style=flat-square)](CONTRIBUTORS.md#contributors)", 244 | "linkToUsage": true 245 | } 246 | -------------------------------------------------------------------------------- /tests/run_all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function 5 | import os 6 | import sys 7 | import glob 8 | import argparse 9 | import logging 10 | import time 11 | import platform 12 | from multiprocessing import cpu_count 13 | from threading import Thread 14 | from subprocess import run 15 | from queue import Queue, Empty 16 | 17 | TIMEOUT = .2 18 | 19 | if platform.system() == "Windows": 20 | CMD = ["python3"] 21 | else: 22 | CMD = [] 23 | 24 | 25 | def comment(text): 26 | newtext = [] 27 | for line in text.split("\n"): 28 | if line.strip(): # skip blank or space-only lines 29 | if line.startswith("ok") or line.startswith("not ok"): 30 | line = "# (original state) " + line 31 | elif not line.startswith("# "): 32 | line = "# " + line 33 | 34 | newtext.append(line) 35 | 36 | return "\n".join(newtext) 37 | 38 | 39 | def run_test(testqueue, outqueue, threadname): 40 | start = time.time() 41 | while True: 42 | try: 43 | test = testqueue.get(block=True, timeout=TIMEOUT) 44 | except Empty: 45 | break 46 | 47 | log.info("Running test %s", test) 48 | 49 | failed = False 50 | 51 | try: 52 | p = run(CMD + [os.path.abspath(test)], capture_output=True, 53 | env=os.environ, text=True) 54 | except Exception as e: 55 | log.exception(e) 56 | failed = True 57 | reason = str(e) 58 | out = "" 59 | err = "" 60 | else: 61 | if p.returncode: 62 | log.info( 63 | "Test %s exitcode was %s. Tests shouldn't use exit code. " 64 | "They are expected to exit cleanly and output 'ok' or 'not ok'", 65 | test, p.returncode 66 | ) 67 | failed = True 68 | reason = "Exit code was {0}".format(p.returncode) 69 | out = p.stdout 70 | err = p.stderr 71 | 72 | test_head = "# {0}\n".format(os.path.basename(test)) 73 | 74 | if failed: 75 | output = (test_head, comment(out), comment(err), "\nnot ok - {0}\n".format(reason)) 76 | else: 77 | output = (test_head, out, err) 78 | 79 | log.debug("Collected output %s", output) 80 | outqueue.put(output) 81 | 82 | testqueue.task_done() 83 | 84 | log.warning("Finished %s thread after %s seconds", 85 | threadname, round(time.time() - start, 3)) 86 | 87 | 88 | class TestRunner(object): 89 | def __init__(self): 90 | self.threads = [] 91 | self.tap = open(cmd_args.tapfile, 'w') 92 | 93 | self._parallelq = Queue() 94 | self._serialq = Queue() 95 | self._outputq = Queue() 96 | 97 | def _find_tests(self): 98 | for test in glob.glob("*.t"): 99 | if os.access(test, os.X_OK): 100 | # Executables only 101 | if self._is_parallelizable(test): 102 | log.debug("Treating as parallel: %s", test) 103 | self._parallelq.put(test) 104 | else: 105 | log.debug("Treating as serial: %s", test) 106 | self._serialq.put(test) 107 | else: 108 | log.debug("Ignored test %s as it is not executable", test) 109 | 110 | log.info("Parallel tests: %s", self._parallelq.qsize()) 111 | log.info("Serial tests: %s", self._serialq.qsize()) 112 | 113 | def _prepare_threads(self): 114 | # Serial thread 115 | self.threads.append( 116 | Thread(target=run_test, args=(self._serialq, self._outputq, "Serial")) 117 | ) 118 | # Parallel threads 119 | self.threads.extend([ 120 | Thread(target=run_test, args=(self._parallelq, self._outputq, "Parallel")) 121 | for i in range(cpu_count()) 122 | ]) 123 | log.info("Spawned %s threads to run tests", len(self.threads)) 124 | 125 | def _start_threads(self): 126 | for thread in self.threads: 127 | # Threads die when main thread dies 128 | log.debug("Starting thread %s", thread) 129 | thread.daemon = True 130 | thread.start() 131 | 132 | def _print_timestamp_to_tap(self): 133 | now = time.time() 134 | timestamp = "# {0} ==> {1}\n".format( 135 | now, 136 | time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(now)), 137 | ) 138 | 139 | log.debug("Adding timestamp %s to TAP file", timestamp) 140 | self.tap.write(timestamp) 141 | 142 | def _is_parallelizable(self, test): 143 | if cmd_args.serial: 144 | return False 145 | 146 | # This is a pretty weird way to do it, and not realiable. 147 | # We are dealing with some binary tests though. 148 | with open(test, 'rb') as fh: 149 | header = fh.read(100).split(b"\n") 150 | if (len(header) >= 2 and (b"/usr/bin/env python3" in header[0] or 151 | b"bash_tap" in header[1])): 152 | return True 153 | else: 154 | return False 155 | 156 | def _get_remaining_tests(self): 157 | return self._parallelq.qsize() + self._serialq.qsize() 158 | 159 | def is_running(self): 160 | for thread in self.threads: 161 | if thread.is_alive(): 162 | return True 163 | 164 | return False 165 | 166 | def start(self): 167 | self._find_tests() 168 | self._prepare_threads() 169 | 170 | self._print_timestamp_to_tap() 171 | 172 | finished = 0 173 | total = self._get_remaining_tests() 174 | 175 | self._start_threads() 176 | 177 | while self.is_running() or not self._outputq.empty(): 178 | try: 179 | outputs = self._outputq.get(block=True, timeout=TIMEOUT) 180 | except Empty: 181 | continue 182 | 183 | log.debug("Outputting to TAP: %s", outputs) 184 | 185 | for output in outputs: 186 | self.tap.write(output) 187 | 188 | if cmd_args.verbose: 189 | sys.stdout.write(output) 190 | 191 | self._outputq.task_done() 192 | finished += 1 193 | 194 | log.warning("Finished %s out of %s tests", finished, total) 195 | 196 | self._print_timestamp_to_tap() 197 | 198 | if not self._parallelq.empty() or not self._serialq.empty(): 199 | raise RuntimeError( 200 | "Something went wrong, not all tests were ran. {0} " 201 | "remaining.".format(self._get_remaining_tests())) 202 | 203 | def show_report(self): 204 | self.tap.flush() 205 | sys.stdout.flush() 206 | sys.stderr.flush() 207 | 208 | log.debug("Calling 'problems --summary' for report") 209 | return run(CMD + [os.path.abspath("problems"), "--summary", cmd_args.tapfile]).returncode 210 | 211 | 212 | def parse_args(): 213 | parser = argparse.ArgumentParser(description="Run tests") 214 | parser.add_argument('--verbose', '-v', action="store_true", 215 | help="Also send TAP output to stdout") 216 | parser.add_argument('--logging', '-l', action="count", 217 | default=0, 218 | help="Logging level. -lll is the highest level") 219 | parser.add_argument('--serial', action="store_true", 220 | help="Do not run tests in parallel") 221 | parser.add_argument('--tapfile', default="all.log", 222 | help="File to use for TAP output") 223 | return parser.parse_args() 224 | 225 | 226 | def main(): 227 | runner = TestRunner() 228 | runner.start() 229 | 230 | # Propagate the return code 231 | return runner.show_report() 232 | 233 | 234 | if __name__ == "__main__": 235 | cmd_args = parse_args() 236 | 237 | if cmd_args.logging == 1: 238 | level = logging.WARN 239 | elif cmd_args.logging == 2: 240 | level = logging.INFO 241 | elif cmd_args.logging >= 3: 242 | level = logging.DEBUG 243 | else: 244 | level = logging.ERROR 245 | 246 | logging.basicConfig( 247 | format="# !!! %(asctime)s - %(levelname)s - %(message)s", 248 | level=level, 249 | ) 250 | log = logging.getLogger(__name__) 251 | 252 | log.debug("Parsed commandline arguments: %s", cmd_args) 253 | 254 | try: 255 | sys.exit(main()) 256 | except Exception as e: 257 | log.exception(e) 258 | sys.exit(1) 259 | 260 | # vim: ai sts=4 et sw=4 261 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 43 | 44 | 45 |
Alejandro Amo
Alejandro Amo

💻 📦
Alex
Alex

📦
Anthropohedron
Anthropohedron

💻 📦
Bryan
Bryan

💻
Cat Lee Ball
Cat Lee Ball

📖
Christoph Schulz
Christoph Schulz

🐛 💻 📖 💡 🚧 ⚠️ 📓
Cédric Levasseur
Cédric Levasseur

💻
Erich Seifert
Erich Seifert

💻 📖
Florian Levis
Florian Levis

💻
George L. Yermulnik
George L. Yermulnik

💻 📖
Manuel
Manuel

💻
Nick Koptelov
Nick Koptelov

🐛
Renato Alves
Renato Alves

️️️️♿️ 💬 🐛 💻 🖋 🔣 🎨 📖 💡 🤔 🚇 🚧 🧑‍🏫 📦 🔌 📆 📣 🔬 👀 🛡️ ⚠️ 🔧 📓
Stefan Weil
Stefan Weil

️️️️♿️
Thomas Merz
Thomas Merz

🚇
Török Edwin
Török Edwin

💻 📖
embeddedc
embeddedc

🐛
go9girl
go9girl

️️️️♿️ 🤔
longforrich
longforrich

💻 🤔
rma-x
rma-x

🤔
sedrubal
sedrubal

💻 ⚠️
39 | 40 | Add your contributions 41 | 42 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | ## Third-party credit 53 | 54 | As third party developers often go uncredited, we'd also like to extend our 55 | thanks to: 56 | 57 | Wilhelm Schuermann (@wbsch) - bash_tap 58 | Paul Beckingham (@taskwarrior) - test scripts 59 | Hubert Kario - pointed out the solution for v59 profile changes 60 | -------------------------------------------------------------------------------- /tests/csv.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import unittest 5 | from simpletap.fdecrypt import lib 6 | 7 | 8 | class TestCSV(unittest.TestCase): 9 | def setUp(self): 10 | self.test = lib.get_test_data() 11 | self.pwd = lib.get_password() 12 | 13 | def validate_one(self, userkey, grepkey, output): 14 | expected = lib.get_user_data(userkey) 15 | matches = lib.grep(grepkey, output) 16 | 17 | self.assertEqual(matches, expected) 18 | 19 | def validate_default(self, out): 20 | self.validate_one("header_csv_default", '"password"', out) 21 | self.validate_one("doesntexist_csv_default", "doesntexist", out) 22 | self.validate_one("onemore_csv_default", "onemore", out) 23 | self.validate_one("complex_csv_default", "cömplex", out) 24 | self.validate_one("jamie_csv_default", "jãmïe", out) 25 | 26 | def validate_tabular(self, out): 27 | self.validate_one("header_csv_tabular", "'password'", out) 28 | self.validate_one("doesntexist_tabular", "doesntexist", out) 29 | self.validate_one("onemore_tabular", "onemore", out) 30 | self.validate_one("complex_tabular", "cömplex", out) 31 | self.validate_one("jamie_tabular", "jãmïe", out) 32 | 33 | def validate_semicol(self, out): 34 | self.validate_one("header_csv_semicol_singlequot", "'password'", out) 35 | self.validate_one("doesntexist_csv_semicol_singlequot", "doesntexist", out) 36 | self.validate_one("onemore_csv_semicol_singlequot", "onemore", out) 37 | self.validate_one("complex_csv_semicol_singlequot", "cömplex", out) 38 | self.validate_one("jamie_csv_semicol_singlequot", "jãmïe", out) 39 | 40 | def validate_vertbar(self, out): 41 | self.validate_one("header_csv_tab_vertbar", r"\|password\|", out) 42 | self.validate_one("doesntexist_csv_tab_vertbar", "doesntexist", out) 43 | self.validate_one("onemore_csv_tab_vertbar", "onemore", out) 44 | self.validate_one("complex_csv_tab_vertbar", "cömplex", out) 45 | self.validate_one("jamie_csv_tab_vertbar", "jãmïe", out) 46 | 47 | @unittest.skipIf(lib.platform == "Windows", 48 | "Windows DLL isn't backwards compatible") 49 | def test_firefox_20_default(self): 50 | test = os.path.join(self.test, "test_profile_firefox_20") 51 | 52 | cmd = lib.get_script() + [test, "--format", "csv"] 53 | output = lib.run(cmd, stdin=self.pwd) 54 | self.validate_default(output) 55 | 56 | @unittest.skipIf(lib.platform == "Windows", 57 | "Windows DLL isn't backwards compatible") 58 | def test_firefox_20_tabular(self): 59 | test = os.path.join(self.test, "test_profile_firefox_20") 60 | 61 | cmd = lib.get_script() + [test, "--format", "tabular"] 62 | output = lib.run(cmd, stdin=self.pwd) 63 | self.validate_tabular(output) 64 | 65 | @unittest.skipIf(lib.platform == "Windows", 66 | "Windows DLL isn't backwards compatible") 67 | def test_firefox_20_semicol(self): 68 | test = os.path.join(self.test, "test_profile_firefox_20") 69 | 70 | cmd = lib.get_script() + [test, "--format", "csv", "--csv-delimiter", ";", "--csv-quotechar", "'"] 71 | output = lib.run(cmd, stdin=self.pwd) 72 | self.validate_semicol(output) 73 | 74 | @unittest.skipIf(lib.platform == "Windows", 75 | "Windows DLL isn't backwards compatible") 76 | def test_firefox_20_vertbar(self): 77 | test = os.path.join(self.test, "test_profile_firefox_20") 78 | 79 | cmd = lib.get_script() + [test, "--format", "csv", "--csv-delimiter", "\t", "--csv-quotechar", "|"] 80 | output = lib.run(cmd, stdin=self.pwd) 81 | self.validate_vertbar(output) 82 | 83 | @unittest.skipIf(lib.platform == "Windows", 84 | "Windows DLL isn't backwards compatible") 85 | def test_firefox_46_default(self): 86 | test = os.path.join(self.test, "test_profile_firefox_46") 87 | 88 | cmd = lib.get_script() + [test, "--format", "csv"] 89 | output = lib.run(cmd, stdin=self.pwd) 90 | self.validate_default(output) 91 | 92 | @unittest.skipIf(lib.platform == "Windows", 93 | "Windows DLL isn't backwards compatible") 94 | def test_firefox_46_tabular(self): 95 | test = os.path.join(self.test, "test_profile_firefox_46") 96 | 97 | cmd = lib.get_script() + [test, "--format", "tabular"] 98 | output = lib.run(cmd, stdin=self.pwd) 99 | self.validate_tabular(output) 100 | 101 | @unittest.skipIf(lib.platform == "Windows", 102 | "Windows DLL isn't backwards compatible") 103 | def test_firefox_46_semicol(self): 104 | test = os.path.join(self.test, "test_profile_firefox_46") 105 | 106 | cmd = lib.get_script() + [test, "--format", "csv", "--csv-delimiter", ";", "--csv-quotechar", "'"] 107 | output = lib.run(cmd, stdin=self.pwd) 108 | self.validate_semicol(output) 109 | 110 | @unittest.skipIf(lib.platform == "Windows", 111 | "Windows DLL isn't backwards compatible") 112 | def test_firefox_46_vertbar(self): 113 | test = os.path.join(self.test, "test_profile_firefox_46") 114 | 115 | cmd = lib.get_script() + [test, "--format", "csv", "--csv-delimiter", "\t", "--csv-quotechar", "|"] 116 | output = lib.run(cmd, stdin=self.pwd) 117 | self.validate_vertbar(output) 118 | 119 | def test_firefox_59_default(self): 120 | test = os.path.join(self.test, "test_profile_firefox_59") 121 | 122 | cmd = lib.get_script() + [test, "--format", "csv"] 123 | output = lib.run(cmd, stdin=self.pwd) 124 | self.validate_default(output) 125 | 126 | def test_firefox_59_tabular(self): 127 | test = os.path.join(self.test, "test_profile_firefox_59") 128 | 129 | cmd = lib.get_script() + [test, "--format", "tabular"] 130 | output = lib.run(cmd, stdin=self.pwd) 131 | self.validate_tabular(output) 132 | 133 | def test_firefox_59_semicol(self): 134 | test = os.path.join(self.test, "test_profile_firefox_59") 135 | 136 | cmd = lib.get_script() + [test, "--format", "csv", "--csv-delimiter", ";", "--csv-quotechar", "'"] 137 | output = lib.run(cmd, stdin=self.pwd) 138 | self.validate_semicol(output) 139 | 140 | def test_firefox_59_vertbar(self): 141 | test = os.path.join(self.test, "test_profile_firefox_59") 142 | 143 | cmd = lib.get_script() + [test, "--format", "csv", "--csv-delimiter", "\t", "--csv-quotechar", "|"] 144 | output = lib.run(cmd, stdin=self.pwd) 145 | self.validate_vertbar(output) 146 | 147 | def test_firefox_144_default(self): 148 | test = os.path.join(self.test, "test_profile_firefox_144") 149 | 150 | cmd = lib.get_script() + [test, "--format", "csv"] 151 | output = lib.run(cmd, stdin=self.pwd) 152 | self.validate_default(output) 153 | 154 | def test_firefox_144_tabular(self): 155 | test = os.path.join(self.test, "test_profile_firefox_144") 156 | 157 | cmd = lib.get_script() + [test, "--format", "tabular"] 158 | output = lib.run(cmd, stdin=self.pwd) 159 | self.validate_tabular(output) 160 | 161 | def test_firefox_144_semicol(self): 162 | test = os.path.join(self.test, "test_profile_firefox_144") 163 | 164 | cmd = lib.get_script() + [test, "--format", "csv", "--csv-delimiter", ";", "--csv-quotechar", "'"] 165 | output = lib.run(cmd, stdin=self.pwd) 166 | self.validate_semicol(output) 167 | 168 | def test_firefox_144_vertbar(self): 169 | test = os.path.join(self.test, "test_profile_firefox_144") 170 | 171 | cmd = lib.get_script() + [test, "--format", "csv", "--csv-delimiter", "\t", "--csv-quotechar", "|"] 172 | output = lib.run(cmd, stdin=self.pwd) 173 | self.validate_vertbar(output) 174 | 175 | @unittest.skipIf(lib.platform == "Windows", 176 | "Windows DLL isn't backwards compatible") 177 | def test_firefox_nopassword_46_default(self): 178 | test = os.path.join(self.test, "test_profile_firefox_nopassword_46") 179 | 180 | cmd = lib.get_script() + [test, "-n", "--format", "csv"] 181 | output = lib.run(cmd) 182 | self.validate_default(output) 183 | 184 | @unittest.skipIf(lib.platform == "Windows", 185 | "Windows DLL isn't backwards compatible") 186 | def test_firefox_nopassword_46_tabular(self): 187 | test = os.path.join(self.test, "test_profile_firefox_nopassword_46") 188 | 189 | cmd = lib.get_script() + [test, "-n", "--format", "tabular"] 190 | output = lib.run(cmd) 191 | self.validate_tabular(output) 192 | 193 | @unittest.skipIf(lib.platform == "Windows", 194 | "Windows DLL isn't backwards compatible") 195 | def test_firefox_nopassword_46_semicol(self): 196 | test = os.path.join(self.test, "test_profile_firefox_nopassword_46") 197 | 198 | cmd = lib.get_script() + [test, "-n", "--format", "csv", "--csv-delimiter", ";", "--csv-quotechar", "'"] 199 | output = lib.run(cmd) 200 | self.validate_semicol(output) 201 | 202 | @unittest.skipIf(lib.platform == "Windows", 203 | "Windows DLL isn't backwards compatible") 204 | def test_firefox_nopassword_46_vertbar(self): 205 | test = os.path.join(self.test, "test_profile_firefox_nopassword_46") 206 | 207 | cmd = lib.get_script() + [test, "-n", "--format", "csv", "--csv-delimiter", "\t", "--csv-quotechar", "|"] 208 | output = lib.run(cmd) 209 | self.validate_vertbar(output) 210 | 211 | def test_firefox_nopassword_59_default(self): 212 | test = os.path.join(self.test, "test_profile_firefox_nopassword_59") 213 | 214 | cmd = lib.get_script() + [test, "-n", "--format", "csv"] 215 | output = lib.run(cmd) 216 | self.validate_default(output) 217 | 218 | def test_firefox_nopassword_59_tabular(self): 219 | test = os.path.join(self.test, "test_profile_firefox_nopassword_59") 220 | 221 | cmd = lib.get_script() + [test, "-n", "--format", "tabular"] 222 | output = lib.run(cmd) 223 | self.validate_tabular(output) 224 | 225 | def test_firefox_nopassword_59_semicol(self): 226 | test = os.path.join(self.test, "test_profile_firefox_nopassword_59") 227 | 228 | cmd = lib.get_script() + [test, "-n", "--format", "csv", "--csv-delimiter", ";", "--csv-quotechar", "'"] 229 | output = lib.run(cmd) 230 | self.validate_semicol(output) 231 | 232 | def test_firefox_nopassword_59_vertbar(self): 233 | test = os.path.join(self.test, "test_profile_firefox_nopassword_59") 234 | 235 | cmd = lib.get_script() + [test, "-n", "--format", "csv", "--csv-delimiter", "\t", "--csv-quotechar", "|"] 236 | output = lib.run(cmd) 237 | self.validate_vertbar(output) 238 | 239 | 240 | if __name__ == "__main__": 241 | from simpletap import TAPTestRunner 242 | unittest.main(testRunner=TAPTestRunner(buffer=True), exit=False) 243 | 244 | # vim: ai sts=4 et sw=4 245 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Firefox Decrypt 2 | 3 | ![GitHub Actions status](https://github.com/unode/firefox_decrypt/actions/workflows/main.yml/badge.svg) 4 | [![Gitmoji badge](https://img.shields.io/badge/gitmoji-%20😜%20😍-FFDD67.svg?style=flat-square)](https://gitmoji.dev) 5 | 6 | As of 1.0.0 Python 3.9+ is required. Python 2 is no longer supported. 7 | If you encounter a problem, try the latest [release](https://github.com/unode/firefox_decrypt/releases) or check open issues for ongoing work. 8 | 9 | If you definitely need to use Python 2, [Firefox Decrypt 0.7.0](https://github.com/unode/firefox_decrypt/releases/tag/0.7.0) is your best bet, although no longer supported. 10 | 11 | ### Table of contents 12 | 13 | * [About](#about) 14 | * [Usage](#usage) 15 | * [Advanced Usage](#advanced-usage) 16 | * [Non-Interactive mode](#non-interactive-mode) 17 | * Ouput formats 18 | * [CSV/Tabular](#format-csv) 19 | * [Pass - Password Store](#format-pass---passwordstore) 20 | * [Troubleshooting](#troubleshooting) 21 | * [Windows](#windows) 22 | * [MacOSX](#macosdarwin) 23 | * [Testing](#testing) 24 | * [Derived works](#spin-off-derived-and-related-works) 25 | 26 | #### About 27 | 28 | **Firefox Decrypt** is a tool to extract passwords from profiles of Mozilla (Fire/Water)fox™, Thunderbird®, SeaMonkey® and derivates. 29 | 30 | It can be used to recover passwords from a profile protected by a Master Password as long as the latter is known. 31 | If a profile is not protected by a Master Password, passwords are displayed without prompt. 32 | 33 | This tool does not try to crack or brute-force the Master Password in any way. 34 | If the Master Password is not known it will simply fail to recover any data. 35 | 36 | It requires access to libnss3, included with most Mozilla products. 37 | The script is usually able to find a compatible library but may in some cases 38 | load an incorrect/incompatible version. If you encounter this situation please file a bug report. 39 | 40 | Alternatively, you can install libnss3 (Debian/Ubuntu) or nss (Arch/Gentoo/…). 41 | libnss3 is part of https://developer.mozilla.org/docs/Mozilla/Projects/NSS 42 | 43 | If you need to decode passwords from Firefox 3 or older, although not officially supported, 44 | there is a patch in [this pull request](https://github.com/unode/firefox_decrypt/pull/36). 45 | 46 | **NOTE** :warning: : Firefox 144 introduced a new encryption algorithm for its password store while breaking direct use of the bundled libnss3.so lib (see issue [#120](https://github.com/unode/firefox_decrypt/issues/120)). 47 | If you are trying to use `firefox_decrypt` on Linux to access a Firefox 144+ profile, please install `libnss3` version 3.113 or newer. 48 | 49 | #### Usage 50 | 51 | Run: 52 | 53 | ``` 54 | python firefox_decrypt.py 55 | ``` 56 | 57 | The tool will present a numbered list of profiles. Enter the relevant number. 58 | 59 | Then, a prompt to enter the *master password* for the profile: 60 | 61 | - if no password was set, no master password will be asked. 62 | - if a password was set and is known, enter it and hit key Return or Enter 63 | - if a password was set and is no longer known, you can not proceed 64 | 65 | #### Advanced usage 66 | 67 | If your profiles are at an unusual path, you can call the script with: 68 | 69 | ``` 70 | python firefox_decrypt.py /folder/containing/profiles.ini/ 71 | ``` 72 | 73 | If you don't want to display all passwords on the screen you can use: 74 | 75 | ``` 76 | python firefox_decrypt.py | grep -C2 keyword 77 | ``` 78 | where `keyword` is part of the expected output (URL, username, email, password …) 79 | 80 | You can also choose from one of the supported formats with `--format`: 81 | 82 | * `human` - a format displaying one record for every 3 lines 83 | * `csv` - a spreadsheet-like format. See also `--csv-*` options for additional control. 84 | * `tabular` - similar to csv but producing a tab-delimited (`tsv`) file instead. 85 | * `json` - a machine compatible format - see [JSON](https://en.wikipedia.org/wiki/JSON) 86 | * `pass` - a special output format that directly calls to the [passwordstore.org](https://www.passwordstore.org) command to export passwords (*). See also `--pass-*` options. 87 | 88 | (*) `pass` can produce unintended consequences. Make sure to backup your password store before using this option. 89 | 90 | ##### Specify NSS library location 91 | 92 | In order to decode your passwords, Firefox Decrypt uses a series of heuristics to try to locate a compatible [NSS library](https://developer.mozilla.org/docs/Mozilla/Projects/NSS) on your system. 93 | As this approach can sometimes fail, starting with version 1.1.1 of Firefox Decrypt you can now define the `NSS_LIB_PATH` environment variable to manually specify the location of the library. 94 | This location will be prioritized and if no compatible library is found, the script will continue with the built-in heuristics. 95 | 96 | ``` 97 | # On Linux it will look for libnss3.so in /opt/nss/lib/ 98 | # On Mac it will look for libnss3.dylib 99 | NSS_LIB_PATH=/opt/nss/lib/ python firefox_decrypt.py 100 | 101 | # On Windows it will look for nss3.dll 102 | set NSS_LIB_PATH=D:\NSS\lib\ && python firefox_decrypt.py 103 | ``` 104 | 105 | You can confirm if this was successful by running the script in high-verbosity mode (`-vv`) and look for the `Loaded NSS` message after `Loading NSS`: 106 | 107 | ``` 108 | (...) DEBUG - Loading NSS library from /opt/nss/lib/libnss3.so 109 | (...) DEBUG - Loaded NSS library from /opt/nss/lib/libnss3.so 110 | ``` 111 | 112 | ##### Non-interactive mode 113 | 114 | A non-interactive mode which bypasses all prompts, including profile choice and master password, can be enabled with `-n/--no-interactive`. 115 | If you have multiple Mozilla profiles, make sure to also indicate your profile choice by passing `-c/--choice N` where N is the number of the profile you wish to decrypt (starting from **1**). 116 | 117 | You can list all available profiles with `-l/--list` (to stdout). 118 | 119 | Your master password is read from stdin. 120 | 121 | $ python firefox_decrypt.py --list 122 | 1 -> l1u1xh65.default 123 | 2 -> vuhdnx5b.YouTube 124 | 3 -> 1d8vcool.newdefault 125 | 4 -> ekof2ces.SEdu 126 | 5 -> 8a52xmtt.Fresh 127 | 128 | $ read -sp "Master Password: " PASSWORD 129 | Master Password: 130 | 131 | $ echo $PASSWORD | python firefox_decrypt.py --no-interactive --choice 4 132 | Website: https://login.example.com 133 | Username: 'john.doe' 134 | Password: '1n53cur3' 135 | 136 | Website: https://example.org 137 | Username: 'max.mustermann' 138 | Password: 'Passwort1234' 139 | 140 | Website: https://github.com 141 | Username: 'octocat' 142 | Password: 'qJZo6FduRcHw' 143 | 144 | [...snip...] 145 | 146 | $ echo $PASSWORD | python firefox_decrypt.py -nc 1 147 | Website: https://git-scm.com 148 | Username: 'foo' 149 | Password: 'bar' 150 | 151 | Website: https://gitlab.com 152 | Username: 'whatdoesthefoxsay' 153 | Password: 'w00fw00f' 154 | 155 | [...snip...] 156 | 157 | $ # Unset Password 158 | $ PASSWORD= 159 | 160 | ##### Format CSV 161 | 162 | Passwords may be exported in CSV format using the `--format` flag. 163 | 164 | ``` 165 | python firefox_decrypt.py --format csv 166 | ``` 167 | 168 | Additionally, `--csv-delimiter` and `--csv-quotechar` flags can specify which characters to use as delimiters and quote characters in the CSV output. 169 | 170 | ##### Format Pass - Passwordstore 171 | 172 | Stored passwords can be exported to [`pass`](http://passwordstore.org) (from passwordstore.org) using: 173 | 174 | ``` 175 | python firefox_decrypt.py --format pass 176 | ``` 177 | 178 | **All** existing passwords will be exported after the pattern `web/
[:]`. 179 | If multiple credentials exist for the same website `/` is appended. 180 | By `pass` convention, the password will be on the first and the username on the second line. 181 | 182 | To prefix the username with `login: ` for compatibility with the [browserpass](https://github.com/dannyvankooten/browserpass) extension, you can use: 183 | ``` 184 | python firefox_decrypt.py --format pass --pass-username-prefix 'login: ' 185 | ``` 186 | 187 | There is currently no way to selectively export passwords. 188 | 189 | Exporting will overwrite existing passwords without warning. Ensure you have a backup or are using the `pass git` functionality. 190 | 191 | ##### Non fatal password decryption 192 | 193 | By default, encountering a corrupted username or password will abort decryption. 194 | Since version `1.1.0` there is now `--non-fatal-decryption` that tolerates individual failures. 195 | 196 | $ python firefox_decrypt.py --non-fatal-decryption 197 | (...) 198 | Website: https://github.com 199 | Username: '*** decryption failed ***' 200 | Password: '*** decryption failed ***' 201 | 202 | which can also be combined with any of the above `--format` options. 203 | 204 | #### Troubleshooting 205 | 206 | If a problem occurs, please try `firefox_decrypt` in high verbosity mode by calling it with: 207 | 208 | ``` 209 | python firefox_decrypt.py -vvv 210 | ``` 211 | 212 | If the output does not help you to identify the cause and a solution to the problem, file a bug report including the verbose output. **Beware**: 213 | 214 | - your profile password, as well as other passwords, may be visible in the output – so **please remove any sensitive data** before sharing the output. 215 | 216 | ##### Silencing error messages 217 | 218 | Logging messages above warning level are included in the standard error output by default as these can be useful to troubleshoot failures. 219 | If you wish to omit this information append ` 2>/dev/null` to your command on UNIX and ` 2> nul` on Windows. 220 | 221 | ##### Windows 222 | 223 | Both Python and Firefox must be either 32-bit or 64-bit. 224 | 225 | If you mix architectures the code will fail. More information on issue [#8](https://github.com/unode/firefox_decrypt/issues/8). 226 | 227 | `cmd.exe` is not supported due to it's poor UTF-8 support. 228 | Use [Microsoft Terminal](https://github.com/microsoft/terminal) and install [UTF-8 compatible fonts](https://www.google.com/get/noto/). 229 | Depending on the Terminal settings, the Windows version and the language of your system, 230 | you may also need to force Python to run in `UTF-8` mode with `PYTHONUTF8=1 python firefox_decrypt.py`. 231 | 232 | 233 | ##### MacOS/Darwin 234 | 235 | If you get the error described in [#14](https://github.com/unode/firefox_decrypt/issues/14) when loading `libnss3`, consider installing `nss` using [Homebrew](https://brew.sh/) or an alternative package manager. 236 | 237 | While not supported, you may find that `DYLD_LIBRARY_PATH=. python3 firefox_decrypt.py` will work in some configurations. 238 | 239 | 240 | #### Testing 241 | 242 | If you wish to run the test suite locally, chdir into `tests/` and run `./run_all` 243 | 244 | If any test fails on your system, please ensure `libnss` is installed. 245 | 246 | If tests continue to fail, re-run with `./run_all -v` then please file a bug report including: 247 | 248 | - the output 249 | - information about your system (e.g. Linux distribution, version of libnss/firefox …). 250 | 251 | It is much appreciated. 252 | 253 | ### Contributors 254 | 255 | 256 | [![All Contributors](https://img.shields.io/github/all-contributors/unode/firefox_decrypt?color=ee8449&style=flat-square)](CONTRIBUTORS.md#contributors) 257 | 258 | 259 | See [CONTRIBUTORS.md](CONTRIBUTORS.md) for a complete list of contributions 260 | 261 | ### Spin-off, derived and related works 262 | 263 | * [firepwned](https://github.com/christophetd/firepwned#how-it-works) - check if your passwords have been involved in a known data leak 264 | * [FF Password Exporter](https://github.com/kspearrin/ff-password-exporter) - Firefox AddOn for exporting passwords. 265 | 266 | ---- 267 | 268 | Firefox is a trademark of the Mozilla Foundation in the U.S. and other countries. 269 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /firefox_decrypt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | # Based on original work from: www.dumpzilla.org 18 | 19 | from __future__ import annotations 20 | 21 | import argparse 22 | import csv 23 | import ctypes as ct 24 | import json 25 | import logging 26 | import locale 27 | import os 28 | import platform 29 | import sqlite3 30 | import sys 31 | import shutil 32 | from base64 import b64decode 33 | from getpass import getpass 34 | from itertools import chain 35 | from subprocess import run, PIPE, DEVNULL 36 | from urllib.parse import urlparse 37 | from configparser import ConfigParser 38 | from typing import Optional, Iterator, Any 39 | 40 | LOG: logging.Logger 41 | VERBOSE = False 42 | SYSTEM = platform.system() 43 | SYS64 = sys.maxsize > 2**32 44 | DEFAULT_ENCODING = "utf-8" 45 | 46 | PWStore = list[dict[str, str]] 47 | 48 | # NOTE: In 1.0.0-rc1 we tried to use locale information to encode/decode 49 | # content passed to NSS. This was an attempt to address the encoding issues 50 | # affecting Windows. However after additional testing Python now also defaults 51 | # to UTF-8 for encoding. 52 | # Some of the limitations of Windows have to do with poor support for UTF-8 53 | # characters in cmd.exe. Terminal - https://github.com/microsoft/terminal or 54 | # a Bash shell such as Git Bash - https://git-scm.com/downloads are known to 55 | # provide a better user experience and are therefore recommended 56 | 57 | 58 | def get_version() -> str: 59 | """Obtain version information from git if available otherwise use 60 | the internal version number 61 | """ 62 | 63 | def internal_version(): 64 | return ".".join(map(str, __version_info__[:3])) + "".join(__version_info__[3:]) 65 | 66 | try: 67 | p = run(["git", "describe", "--tags"], stdout=PIPE, stderr=DEVNULL, text=True) 68 | except FileNotFoundError: 69 | return internal_version() 70 | 71 | if p.returncode: 72 | return internal_version() 73 | else: 74 | return p.stdout.strip() 75 | 76 | 77 | __version_info__ = (1, 1, 1, "+git") 78 | __version__: str = get_version() 79 | 80 | 81 | class NotFoundError(Exception): 82 | """Exception to handle situations where a credentials file is not found""" 83 | 84 | pass 85 | 86 | 87 | class Exit(Exception): 88 | """Exception to allow a clean exit from any point in execution""" 89 | 90 | CLEAN = 0 91 | ERROR = 1 92 | MISSING_PROFILEINI = 2 93 | MISSING_SECRETS = 3 94 | BAD_PROFILEINI = 4 95 | LOCATION_NO_DIRECTORY = 5 96 | BAD_SECRETS = 6 97 | BAD_LOCALE = 7 98 | 99 | FAIL_LOCATE_NSS = 10 100 | FAIL_LOAD_NSS = 11 101 | FAIL_INIT_NSS = 12 102 | FAIL_NSS_KEYSLOT = 13 103 | FAIL_SHUTDOWN_NSS = 14 104 | BAD_PRIMARY_PASSWORD = 15 105 | NEED_PRIMARY_PASSWORD = 16 106 | DECRYPTION_FAILED = 17 107 | 108 | PASSSTORE_NOT_INIT = 20 109 | PASSSTORE_MISSING = 21 110 | PASSSTORE_ERROR = 22 111 | 112 | READ_GOT_EOF = 30 113 | MISSING_CHOICE = 31 114 | NO_SUCH_PROFILE = 32 115 | 116 | UNKNOWN_ERROR = 100 117 | KEYBOARD_INTERRUPT = 102 118 | 119 | def __init__(self, exitcode): 120 | self.exitcode = exitcode 121 | 122 | def __unicode__(self): 123 | return f"Premature program exit with exit code {self.exitcode}" 124 | 125 | 126 | class Credentials: 127 | """Base credentials backend manager""" 128 | 129 | def __init__(self, db): 130 | self.db = db 131 | 132 | LOG.debug("Database location: %s", self.db) 133 | if not os.path.isfile(db): 134 | raise NotFoundError(f"ERROR - {db} database not found\n") 135 | 136 | LOG.info("Using %s for credentials.", db) 137 | 138 | def __iter__(self) -> Iterator[tuple[str, str, str, int]]: 139 | pass 140 | 141 | def done(self): 142 | """Override this method if the credentials subclass needs to do any 143 | action after interaction 144 | """ 145 | pass 146 | 147 | 148 | class SqliteCredentials(Credentials): 149 | """SQLite credentials backend manager""" 150 | 151 | def __init__(self, profile): 152 | db = os.path.join(profile, "signons.sqlite") 153 | 154 | super(SqliteCredentials, self).__init__(db) 155 | 156 | self.conn = sqlite3.connect(db) 157 | self.c = self.conn.cursor() 158 | 159 | def __iter__(self) -> Iterator[tuple[str, str, str, int]]: 160 | LOG.debug("Reading password database in SQLite format") 161 | self.c.execute( 162 | "SELECT hostname, encryptedUsername, encryptedPassword, encType " 163 | "FROM moz_logins" 164 | ) 165 | for i in self.c: 166 | # yields hostname, encryptedUsername, encryptedPassword, encType 167 | yield i 168 | 169 | def done(self): 170 | """Close the sqlite cursor and database connection""" 171 | super(SqliteCredentials, self).done() 172 | 173 | self.c.close() 174 | self.conn.close() 175 | 176 | 177 | class JsonCredentials(Credentials): 178 | """JSON credentials backend manager""" 179 | 180 | def __init__(self, profile): 181 | db = os.path.join(profile, "logins.json") 182 | 183 | super(JsonCredentials, self).__init__(db) 184 | 185 | def __iter__(self) -> Iterator[tuple[str, str, str, int]]: 186 | with open(self.db) as fh: 187 | LOG.debug("Reading password database in JSON format") 188 | data = json.load(fh) 189 | 190 | try: 191 | logins = data["logins"] 192 | except Exception: 193 | LOG.error(f"Unrecognized format in {self.db}") 194 | raise Exit(Exit.BAD_SECRETS) 195 | 196 | for i in logins: 197 | try: 198 | yield ( 199 | i["hostname"], 200 | i["encryptedUsername"], 201 | i["encryptedPassword"], 202 | i["encType"], 203 | ) 204 | except KeyError: 205 | # This should handle deleted passwords that still maintain 206 | # a record in the JSON file - GitHub issue #99 207 | LOG.info(f"Skipped record {i} due to missing fields") 208 | 209 | 210 | def find_nss(locations: list[str], nssname: str) -> ct.CDLL: 211 | """Locate nss is one of the many possible locations""" 212 | fail_errors: list[tuple[str, str]] = [] 213 | 214 | OS = ("Windows", "Darwin") 215 | sublocations = ("firefox", "thunderbird", "") 216 | 217 | for loc in locations: 218 | for subloc in sublocations: 219 | nsslib = os.path.join(loc, subloc, nssname) 220 | LOG.debug("Loading NSS library from %s", nsslib) 221 | 222 | if SYSTEM in OS: 223 | # On windows in order to find DLLs referenced by nss3.dll 224 | # we need to have those locations on PATH 225 | os.environ["PATH"] = ";".join([loc, os.environ["PATH"]]) 226 | LOG.debug("PATH is now %s", os.environ["PATH"]) 227 | # However this doesn't seem to work on all setups and needs to be 228 | # set before starting python so as a workaround we chdir to 229 | # Firefox's nss3.dll/libnss3.dylib location 230 | if loc: 231 | if not os.path.isdir(loc): 232 | # No point in trying to load from paths that don't exist 233 | continue 234 | 235 | workdir = os.getcwd() 236 | os.chdir(loc) 237 | 238 | try: 239 | nss: ct.CDLL = ct.CDLL(nsslib) 240 | except OSError as e: 241 | fail_errors.append((nsslib, str(e))) 242 | else: 243 | LOG.debug("Loaded NSS library from %s", nsslib) 244 | return nss 245 | finally: 246 | if SYSTEM in OS and loc: 247 | # Restore workdir changed above 248 | os.chdir(workdir) 249 | 250 | else: 251 | LOG.error( 252 | "Couldn't find or load '%s'. This library is essential " 253 | "to interact with your Mozilla profile.", 254 | nssname, 255 | ) 256 | LOG.error( 257 | "If you are seeing this error please perform a system-wide " 258 | "search for '%s' and file a bug report indicating any " 259 | "location found. Thanks!", 260 | nssname, 261 | ) 262 | LOG.error( 263 | "Alternatively you can try launching firefox_decrypt " 264 | "from the location where you found '%s'. " 265 | "That is 'cd' or 'chdir' to that location and run " 266 | "firefox_decrypt from there.", 267 | nssname, 268 | ) 269 | 270 | LOG.error( 271 | "Please also include the following on any bug report. " 272 | "Errors seen while searching/loading NSS:" 273 | ) 274 | 275 | for target, error in fail_errors: 276 | LOG.error("Error when loading %s was %s", target, error) 277 | 278 | raise Exit(Exit.FAIL_LOCATE_NSS) 279 | 280 | 281 | def load_libnss(): 282 | """Load libnss into python using the CDLL interface""" 283 | 284 | locations: list[str] = [ 285 | os.environ.get("NSS_LIB_PATH", "."), 286 | ] 287 | 288 | if SYSTEM == "Windows": 289 | nssname = "nss3.dll" 290 | if not SYS64: 291 | locations += [ 292 | "C:\\Program Files (x86)\\Mozilla Firefox", 293 | "C:\\Program Files (x86)\\Firefox Developer Edition", 294 | "C:\\Program Files (x86)\\Mozilla Thunderbird", 295 | "C:\\Program Files (x86)\\Nightly", 296 | "C:\\Program Files (x86)\\SeaMonkey", 297 | "C:\\Program Files (x86)\\Waterfox", 298 | "", # Current directory or system lib finder 299 | ] 300 | 301 | locations += [ 302 | os.path.expanduser("~\\AppData\\Local\\Mozilla Firefox"), 303 | os.path.expanduser("~\\AppData\\Local\\Firefox Developer Edition"), 304 | os.path.expanduser("~\\AppData\\Local\\Mozilla Thunderbird"), 305 | os.path.expanduser("~\\AppData\\Local\\Nightly"), 306 | os.path.expanduser("~\\AppData\\Local\\SeaMonkey"), 307 | os.path.expanduser("~\\AppData\\Local\\Waterfox"), 308 | "C:\\Program Files\\Mozilla Firefox", 309 | "C:\\Program Files\\Firefox Developer Edition", 310 | "C:\\Program Files\\Mozilla Thunderbird", 311 | "C:\\Program Files\\Nightly", 312 | "C:\\Program Files\\SeaMonkey", 313 | "C:\\Program Files\\Waterfox", 314 | "", # Current directory or system lib finder 315 | ] 316 | 317 | # If either of the supported software is in PATH try to use it 318 | software = ["firefox", "thunderbird", "waterfox", "seamonkey"] 319 | for binary in software: 320 | location: Optional[str] = shutil.which(binary) 321 | if location is not None: 322 | nsslocation: str = os.path.join(os.path.dirname(location), nssname) 323 | locations.append(nsslocation) 324 | 325 | elif SYSTEM == "Darwin": 326 | nssname = "libnss3.dylib" 327 | locations += [ 328 | "/usr/local/lib/nss", 329 | "/usr/local/lib", 330 | "/opt/local/lib/nss", 331 | "/sw/lib/firefox", 332 | "/sw/lib/mozilla", 333 | "/usr/local/opt/nss/lib", # nss installed with Brew on Darwin 334 | "/opt/homebrew/lib", # nss installed with Brew on Darwin/Apple Silicon 335 | "/opt/pkg/lib/nss", # installed via pkgsrc 336 | "/Applications/Firefox.app/Contents/MacOS", # default manual install location 337 | "/Applications/Thunderbird.app/Contents/MacOS", 338 | "/Applications/SeaMonkey.app/Contents/MacOS", 339 | "/Applications/Waterfox.app/Contents/MacOS", 340 | "", # Current directory or system lib finder 341 | ] 342 | 343 | else: 344 | nssname = "libnss3.so" 345 | if SYS64: 346 | locations += [ 347 | "/usr/lib64", 348 | "/usr/lib64/nss", 349 | "/usr/lib", 350 | "/usr/lib/nss", 351 | "/usr/local/lib", 352 | "/usr/local/lib/nss", 353 | "/opt/local/lib", 354 | "/opt/local/lib/nss", 355 | os.path.expanduser("~/.nix-profile/lib"), 356 | "", # Current directory or system lib finder 357 | ] 358 | else: 359 | locations += [ 360 | "/usr/lib", 361 | "/usr/lib/nss", 362 | "/usr/lib32", 363 | "/usr/lib32/nss", 364 | "/usr/lib64", 365 | "/usr/lib64/nss", 366 | "/usr/local/lib", 367 | "/usr/local/lib/nss", 368 | "/opt/local/lib", 369 | "/opt/local/lib/nss", 370 | os.path.expanduser("~/.nix-profile/lib"), 371 | "", # Current directory or system lib finder 372 | ] 373 | 374 | # If this succeeds libnss was loaded 375 | return find_nss(locations, nssname) 376 | 377 | 378 | class c_char_p_fromstr(ct.c_char_p): 379 | """ctypes char_p override that handles encoding str to bytes""" 380 | 381 | def from_param(self): 382 | return self.encode(DEFAULT_ENCODING) 383 | 384 | 385 | class NSSProxy: 386 | class SECItem(ct.Structure): 387 | """struct needed to interact with libnss""" 388 | 389 | _fields_ = [ 390 | ("type", ct.c_uint), 391 | ("data", ct.c_char_p), # actually: unsigned char * 392 | ("len", ct.c_uint), 393 | ] 394 | 395 | def decode_data(self): 396 | _bytes = ct.string_at(self.data, self.len) 397 | return _bytes.decode(DEFAULT_ENCODING) 398 | 399 | class PK11SlotInfo(ct.Structure): 400 | """Opaque structure representing a logical PKCS slot""" 401 | 402 | def __init__(self, non_fatal_decryption=False): 403 | # Locate libnss and try loading it 404 | self.libnss = load_libnss() 405 | self.non_fatal_decryption = non_fatal_decryption 406 | 407 | SlotInfoPtr = ct.POINTER(self.PK11SlotInfo) 408 | SECItemPtr = ct.POINTER(self.SECItem) 409 | 410 | self._set_ctypes(ct.c_int, "NSS_Init", c_char_p_fromstr) 411 | self._set_ctypes(ct.c_int, "NSS_Shutdown") 412 | self._set_ctypes(SlotInfoPtr, "PK11_GetInternalKeySlot") 413 | self._set_ctypes(None, "PK11_FreeSlot", SlotInfoPtr) 414 | self._set_ctypes(ct.c_int, "PK11_NeedLogin", SlotInfoPtr) 415 | self._set_ctypes( 416 | ct.c_int, "PK11_CheckUserPassword", SlotInfoPtr, c_char_p_fromstr 417 | ) 418 | self._set_ctypes( 419 | ct.c_int, "PK11SDR_Decrypt", SECItemPtr, SECItemPtr, ct.c_void_p 420 | ) 421 | self._set_ctypes(None, "SECITEM_ZfreeItem", SECItemPtr, ct.c_int) 422 | 423 | # for error handling 424 | self._set_ctypes(ct.c_int, "PORT_GetError") 425 | self._set_ctypes(ct.c_char_p, "PR_ErrorToName", ct.c_int) 426 | self._set_ctypes(ct.c_char_p, "PR_ErrorToString", ct.c_int, ct.c_uint32) 427 | 428 | def _set_ctypes(self, restype, name, *argtypes): 429 | """Set input/output types on libnss C functions for automatic type casting""" 430 | res = getattr(self.libnss, name) 431 | res.argtypes = argtypes 432 | res.restype = restype 433 | 434 | # Transparently handle decoding to string when returning a c_char_p 435 | if restype == ct.c_char_p: 436 | 437 | def _decode(result, func, *args): 438 | try: 439 | return result.decode(DEFAULT_ENCODING) 440 | except AttributeError: 441 | return result 442 | 443 | res.errcheck = _decode 444 | 445 | setattr(self, "_" + name, res) 446 | 447 | def initialize(self, profile: str): 448 | # The sql: prefix ensures compatibility with both 449 | # Berkley DB (cert8) and Sqlite (cert9) dbs 450 | profile_path = "sql:" + profile 451 | LOG.debug("Initializing NSS with profile '%s'", profile_path) 452 | err_status: int = self._NSS_Init(profile_path) 453 | LOG.debug("Initializing NSS returned %s", err_status) 454 | 455 | if err_status: 456 | self.handle_error( 457 | Exit.FAIL_INIT_NSS, 458 | "Couldn't initialize NSS, maybe '%s' is not a valid profile?", 459 | profile, 460 | ) 461 | 462 | def shutdown(self): 463 | err_status: int = self._NSS_Shutdown() 464 | 465 | if err_status: 466 | self.handle_error( 467 | Exit.FAIL_SHUTDOWN_NSS, 468 | "Couldn't shutdown current NSS profile", 469 | ) 470 | 471 | def authenticate(self, profile, interactive): 472 | """Unlocks the profile if necessary, in which case a password 473 | will prompted to the user. 474 | """ 475 | LOG.debug("Retrieving internal key slot") 476 | keyslot = self._PK11_GetInternalKeySlot() 477 | 478 | LOG.debug("Internal key slot %s", keyslot) 479 | if not keyslot: 480 | self.handle_error( 481 | Exit.FAIL_NSS_KEYSLOT, 482 | "Failed to retrieve internal KeySlot", 483 | ) 484 | 485 | try: 486 | if self._PK11_NeedLogin(keyslot): 487 | password: str = ask_password(profile, interactive) 488 | 489 | LOG.debug("Authenticating with password '%s'", password) 490 | err_status: int = self._PK11_CheckUserPassword(keyslot, password) 491 | 492 | LOG.debug("Checking user password returned %s", err_status) 493 | 494 | if err_status: 495 | self.handle_error( 496 | Exit.BAD_PRIMARY_PASSWORD, 497 | "Primary password is not correct", 498 | ) 499 | 500 | else: 501 | LOG.info("No Primary Password found - no authentication needed") 502 | finally: 503 | # Avoid leaking PK11KeySlot 504 | self._PK11_FreeSlot(keyslot) 505 | 506 | def handle_error(self, exitcode: int, *logerror: Any): 507 | """If an error happens in libnss, handle it and print some debug information""" 508 | if logerror: 509 | LOG.error(*logerror) 510 | else: 511 | LOG.debug("Error during a call to NSS library, trying to obtain error info") 512 | 513 | code = self._PORT_GetError() 514 | name = self._PR_ErrorToName(code) 515 | name = "NULL" if name is None else name 516 | # 0 is the default language (localization related) 517 | text = self._PR_ErrorToString(code, 0) 518 | 519 | LOG.debug("%s: %s", name, text) 520 | 521 | raise Exit(exitcode) 522 | 523 | def decrypt(self, data64): 524 | data = b64decode(data64) 525 | inp = self.SECItem(0, data, len(data)) 526 | out = self.SECItem(0, None, 0) 527 | 528 | err_status: int = self._PK11SDR_Decrypt(inp, out, None) 529 | LOG.debug("Decryption of data returned %s", err_status) 530 | try: 531 | if err_status: # -1 means password failed, other status are unknown 532 | error_msg = ( 533 | "Username/Password decryption failed. " 534 | "Credentials damaged or cert/key file mismatch." 535 | ) 536 | 537 | if self.non_fatal_decryption: 538 | raise ValueError(error_msg) 539 | else: 540 | self.handle_error(Exit.DECRYPTION_FAILED, error_msg) 541 | 542 | res = out.decode_data() 543 | finally: 544 | # Avoid leaking SECItem 545 | self._SECITEM_ZfreeItem(out, 0) 546 | 547 | return res 548 | 549 | 550 | class MozillaInteraction: 551 | """ 552 | Abstraction interface to Mozilla profile and lib NSS 553 | """ 554 | 555 | def __init__(self, non_fatal_decryption=False): 556 | self.profile = None 557 | self.proxy = NSSProxy(non_fatal_decryption) 558 | 559 | def load_profile(self, profile): 560 | """Initialize the NSS library and profile""" 561 | self.profile = profile 562 | self.proxy.initialize(self.profile) 563 | 564 | def authenticate(self, interactive): 565 | """Authenticate the the current profile is protected by a primary password, 566 | prompt the user and unlock the profile. 567 | """ 568 | self.proxy.authenticate(self.profile, interactive) 569 | 570 | def unload_profile(self): 571 | """Shutdown NSS and deactivate current profile""" 572 | self.proxy.shutdown() 573 | 574 | def decrypt_passwords(self) -> PWStore: 575 | """Decrypt requested profile using the provided password. 576 | Returns all passwords in a list of dicts 577 | """ 578 | credentials: Credentials = self.obtain_credentials() 579 | 580 | LOG.info("Decrypting credentials") 581 | outputs: PWStore = [] 582 | 583 | url: str 584 | user: str 585 | passw: str 586 | enctype: int 587 | for url, user, passw, enctype in credentials: 588 | # enctype informs if passwords need to be decrypted 589 | if enctype: 590 | try: 591 | LOG.debug("Decrypting username data '%s'", user) 592 | user = self.proxy.decrypt(user) 593 | LOG.debug("Decrypting password data '%s'", passw) 594 | passw = self.proxy.decrypt(passw) 595 | except (TypeError, ValueError) as e: 596 | LOG.warning( 597 | "Failed to decode username or password for entry from URL %s", 598 | url, 599 | ) 600 | LOG.debug(e, exc_info=True) 601 | user = "*** decryption failed ***" 602 | passw = "*** decryption failed ***" 603 | 604 | LOG.debug( 605 | "Decoded username '%s' and password '%s' for website '%s'", 606 | user, 607 | passw, 608 | url, 609 | ) 610 | 611 | output = {"url": url, "user": user, "password": passw} 612 | outputs.append(output) 613 | 614 | if not outputs: 615 | LOG.warning("No passwords found in selected profile") 616 | 617 | # Close credential handles (SQL) 618 | credentials.done() 619 | 620 | return outputs 621 | 622 | def obtain_credentials(self) -> Credentials: 623 | """Figure out which of the 2 possible backend credential engines is available""" 624 | credentials: Credentials 625 | try: 626 | credentials = JsonCredentials(self.profile) 627 | except NotFoundError: 628 | try: 629 | credentials = SqliteCredentials(self.profile) 630 | except NotFoundError: 631 | LOG.error( 632 | "Couldn't find credentials file (logins.json or signons.sqlite)." 633 | ) 634 | raise Exit(Exit.MISSING_SECRETS) 635 | 636 | return credentials 637 | 638 | 639 | class OutputFormat: 640 | def __init__(self, pwstore: PWStore, cmdargs: argparse.Namespace): 641 | self.pwstore = pwstore 642 | self.cmdargs = cmdargs 643 | 644 | def output(self): 645 | pass 646 | 647 | 648 | class HumanOutputFormat(OutputFormat): 649 | def output(self): 650 | for output in self.pwstore: 651 | record: str = ( 652 | f"\nWebsite: {output['url']}\n" 653 | f"Username: '{output['user']}'\n" 654 | f"Password: '{output['password']}'\n" 655 | ) 656 | sys.stdout.write(record) 657 | 658 | 659 | class JSONOutputFormat(OutputFormat): 660 | def output(self): 661 | sys.stdout.write(json.dumps(self.pwstore, indent=2)) 662 | # Json dumps doesn't add the final newline 663 | sys.stdout.write("\n") 664 | 665 | 666 | class CSVOutputFormat(OutputFormat): 667 | def __init__(self, pwstore: PWStore, cmdargs: argparse.Namespace): 668 | super().__init__(pwstore, cmdargs) 669 | self.delimiter = cmdargs.csv_delimiter 670 | self.quotechar = cmdargs.csv_quotechar 671 | self.header = cmdargs.csv_header 672 | 673 | def output(self): 674 | csv_writer = csv.DictWriter( 675 | sys.stdout, 676 | fieldnames=["url", "user", "password"], 677 | lineterminator="\n", 678 | delimiter=self.delimiter, 679 | quotechar=self.quotechar, 680 | quoting=csv.QUOTE_ALL, 681 | ) 682 | if self.header: 683 | csv_writer.writeheader() 684 | 685 | for output in self.pwstore: 686 | csv_writer.writerow(output) 687 | 688 | 689 | class TabularOutputFormat(CSVOutputFormat): 690 | def __init__(self, pwstore: PWStore, cmdargs: argparse.Namespace): 691 | super().__init__(pwstore, cmdargs) 692 | self.delimiter = "\t" 693 | self.quotechar = "'" 694 | 695 | 696 | class PassOutputFormat(OutputFormat): 697 | def __init__(self, pwstore: PWStore, cmdargs: argparse.Namespace): 698 | super().__init__(pwstore, cmdargs) 699 | self.prefix = cmdargs.pass_prefix 700 | self.cmd = cmdargs.pass_cmd 701 | self.username_prefix = cmdargs.pass_username_prefix 702 | self.always_with_login = cmdargs.pass_always_with_login 703 | 704 | def output(self): 705 | self.test_pass_cmd() 706 | self.preprocess_outputs() 707 | self.export() 708 | 709 | def test_pass_cmd(self) -> None: 710 | """Check if pass from passwordstore.org is installed 711 | If it is installed but not initialized, initialize it 712 | """ 713 | LOG.debug("Testing if password store is installed and configured") 714 | 715 | try: 716 | p = run([self.cmd, "ls"], capture_output=True, text=True) 717 | except FileNotFoundError as e: 718 | if e.errno == 2: 719 | LOG.error("Password store is not installed and exporting was requested") 720 | raise Exit(Exit.PASSSTORE_MISSING) 721 | else: 722 | LOG.error("Unknown error happened.") 723 | LOG.error("Error was '%s'", e) 724 | raise Exit(Exit.UNKNOWN_ERROR) 725 | 726 | LOG.debug("pass returned:\nStdout: %s\nStderr: %s", p.stdout, p.stderr) 727 | 728 | if p.returncode != 0: 729 | if 'Try "pass init"' in p.stderr: 730 | LOG.error("Password store was not initialized.") 731 | LOG.error("Initialize the password store manually by using 'pass init'") 732 | raise Exit(Exit.PASSSTORE_NOT_INIT) 733 | else: 734 | LOG.error("Unknown error happened when running 'pass'.") 735 | LOG.error("Stdout: %s\nStderr: %s", p.stdout, p.stderr) 736 | raise Exit(Exit.UNKNOWN_ERROR) 737 | 738 | def preprocess_outputs(self): 739 | # Format of "self.to_export" should be: 740 | # {"address": {"login": "password", ...}, ...} 741 | self.to_export: dict[str, dict[str, str]] = {} 742 | 743 | for record in self.pwstore: 744 | url = record["url"] 745 | user = record["user"] 746 | passw = record["password"] 747 | 748 | # Keep track of web-address, username and passwords 749 | # If more than one username exists for the same web-address 750 | # the username will be used as name of the file 751 | address = urlparse(url) 752 | 753 | if address.netloc not in self.to_export: 754 | self.to_export[address.netloc] = {user: passw} 755 | 756 | else: 757 | self.to_export[address.netloc][user] = passw 758 | 759 | def export(self): 760 | """Export given passwords to password store 761 | 762 | Format of "to_export" should be: 763 | {"address": {"login": "password", ...}, ...} 764 | """ 765 | LOG.info("Exporting credentials to password store") 766 | if self.prefix: 767 | prefix = f"{self.prefix}/" 768 | else: 769 | prefix = self.prefix 770 | 771 | LOG.debug("Using pass prefix '%s'", prefix) 772 | 773 | for address in self.to_export: 774 | for user, passw in self.to_export[address].items(): 775 | # When more than one account exist for the same address, add 776 | # the login to the password identifier 777 | if self.always_with_login or len(self.to_export[address]) > 1: 778 | passname = f"{prefix}{address}/{user}" 779 | else: 780 | passname = f"{prefix}{address}" 781 | 782 | LOG.info("Exporting credentials for '%s'", passname) 783 | 784 | data = f"{passw}\n{self.username_prefix}{user}\n" 785 | 786 | LOG.debug("Inserting pass '%s' '%s'", passname, data) 787 | 788 | # NOTE --force is used. Existing passwords will be overwritten 789 | cmd: list[str] = [ 790 | self.cmd, 791 | "insert", 792 | "--force", 793 | "--multiline", 794 | passname, 795 | ] 796 | 797 | LOG.debug("Running command '%s' with stdin '%s'", cmd, data) 798 | 799 | p = run(cmd, input=data, capture_output=True, text=True) 800 | 801 | if p.returncode != 0: 802 | LOG.error( 803 | "ERROR: passwordstore exited with non-zero: %s", p.returncode 804 | ) 805 | LOG.error("Stdout: %s\nStderr: %s", p.stdout, p.stderr) 806 | raise Exit(Exit.PASSSTORE_ERROR) 807 | 808 | LOG.debug("Successfully exported '%s'", passname) 809 | 810 | 811 | def get_sections(profiles): 812 | """ 813 | Returns hash of profile numbers and profile names. 814 | """ 815 | sections = {} 816 | i = 1 817 | for section in profiles.sections(): 818 | if section.startswith("Profile"): 819 | sections[str(i)] = profiles.get(section, "Path") 820 | i += 1 821 | else: 822 | continue 823 | return sections 824 | 825 | 826 | def print_sections(sections, textIOWrapper=sys.stderr): 827 | """ 828 | Prints all available sections to an textIOWrapper (defaults to sys.stderr) 829 | """ 830 | for i in sorted(sections): 831 | textIOWrapper.write(f"{i} -> {sections[i]}\n") 832 | textIOWrapper.flush() 833 | 834 | 835 | def ask_section(sections: ConfigParser): 836 | """ 837 | Prompt the user which profile should be used for decryption 838 | """ 839 | # Do not ask for choice if user already gave one 840 | choice = "ASK" 841 | while choice not in sections: 842 | sys.stderr.write("Select the Mozilla profile you wish to decrypt\n") 843 | print_sections(sections) 844 | try: 845 | choice = input() 846 | except EOFError: 847 | LOG.error("Could not read Choice, got EOF") 848 | raise Exit(Exit.READ_GOT_EOF) 849 | 850 | try: 851 | final_choice = sections[choice] 852 | except KeyError: 853 | LOG.error("Profile No. %s does not exist!", choice) 854 | raise Exit(Exit.NO_SUCH_PROFILE) 855 | 856 | LOG.debug("Profile selection matched %s", final_choice) 857 | 858 | return final_choice 859 | 860 | 861 | def ask_password(profile: str, interactive: bool) -> str: 862 | """ 863 | Prompt for profile password 864 | """ 865 | passwd: str 866 | passmsg = f"\nPrimary Password for profile {profile}: " 867 | 868 | if sys.stdin.isatty() and interactive: 869 | passwd = getpass(passmsg) 870 | else: 871 | sys.stderr.write("Reading Primary password from standard input:\n") 872 | sys.stderr.flush() 873 | # Ability to read the password from stdin (echo "pass" | ./firefox_...) 874 | passwd = sys.stdin.readline().rstrip("\n") 875 | 876 | return passwd 877 | 878 | 879 | def read_profiles(basepath): 880 | """ 881 | Parse Firefox profiles in provided location. 882 | If list_profiles is true, will exit after listing available profiles. 883 | """ 884 | profileini = os.path.join(basepath, "profiles.ini") 885 | 886 | LOG.debug("Reading profiles from %s", profileini) 887 | 888 | if not os.path.isfile(profileini): 889 | LOG.warning("profile.ini not found in %s", basepath) 890 | raise Exit(Exit.MISSING_PROFILEINI) 891 | 892 | # Read profiles from Firefox profile folder 893 | profiles = ConfigParser() 894 | profiles.read(profileini, encoding=DEFAULT_ENCODING) 895 | 896 | LOG.debug("Read profiles %s", profiles.sections()) 897 | 898 | return profiles 899 | 900 | 901 | def get_profile( 902 | basepath: str, interactive: bool, choice: Optional[str], list_profiles: bool 903 | ): 904 | """ 905 | Select profile to use by either reading profiles.ini or assuming given 906 | path is already a profile 907 | If interactive is false, will not try to ask which profile to decrypt. 908 | choice contains the choice the user gave us as an CLI arg. 909 | If list_profiles is true will exits after listing all available profiles. 910 | """ 911 | try: 912 | profiles: ConfigParser = read_profiles(basepath) 913 | 914 | except Exit as e: 915 | if e.exitcode == Exit.MISSING_PROFILEINI: 916 | LOG.warning("Continuing and assuming '%s' is a profile location", basepath) 917 | profile = basepath 918 | 919 | if list_profiles: 920 | LOG.error("Listing single profiles not permitted.") 921 | raise 922 | 923 | if not os.path.isdir(profile): 924 | LOG.error("Profile location '%s' is not a directory", profile) 925 | raise 926 | else: 927 | raise 928 | else: 929 | if list_profiles: 930 | LOG.debug("Listing available profiles...") 931 | print_sections(get_sections(profiles), sys.stdout) 932 | raise Exit(Exit.CLEAN) 933 | 934 | sections = get_sections(profiles) 935 | 936 | if len(sections) == 1: 937 | section = sections["1"] 938 | 939 | elif choice is not None: 940 | try: 941 | section = sections[choice] 942 | except KeyError: 943 | LOG.error("Profile No. %s does not exist!", choice) 944 | raise Exit(Exit.NO_SUCH_PROFILE) 945 | 946 | elif not interactive: 947 | LOG.error( 948 | "Don't know which profile to decrypt. " 949 | "We are in non-interactive mode and -c/--choice wasn't specified." 950 | ) 951 | raise Exit(Exit.MISSING_CHOICE) 952 | 953 | else: 954 | # Ask user which profile to open 955 | section = ask_section(sections) 956 | 957 | section = section 958 | profile = os.path.join(basepath, section) 959 | 960 | if not os.path.isdir(profile): 961 | LOG.error( 962 | "Profile location '%s' is not a directory. Has profiles.ini been tampered with?", 963 | profile, 964 | ) 965 | raise Exit(Exit.BAD_PROFILEINI) 966 | 967 | return profile 968 | 969 | 970 | # From https://bugs.python.org/msg323681 971 | class ConvertChoices(argparse.Action): 972 | """Argparse action that interprets the `choices` argument as a dict 973 | mapping the user-specified choices values to the resulting option 974 | values. 975 | """ 976 | 977 | def __init__(self, *args, choices, **kwargs): 978 | super().__init__(*args, choices=choices.keys(), **kwargs) 979 | self.mapping = choices 980 | 981 | def __call__(self, parser, namespace, value, option_string=None): 982 | setattr(namespace, self.dest, self.mapping[value]) 983 | 984 | 985 | def parse_sys_args() -> argparse.Namespace: 986 | """Parse command line arguments""" 987 | 988 | if SYSTEM == "Windows": 989 | profile_path = os.path.join(os.environ["APPDATA"], "Mozilla", "Firefox") 990 | elif os.uname()[0] == "Darwin": 991 | profile_path = "~/Library/Application Support/Firefox" 992 | else: 993 | profile_path = "~/.mozilla/firefox" 994 | 995 | parser = argparse.ArgumentParser( 996 | description="Access Firefox/Thunderbird profiles and decrypt existing passwords" 997 | ) 998 | parser.add_argument( 999 | "profile", 1000 | nargs="?", 1001 | default=profile_path, 1002 | help=f"Path to profile folder (default: {profile_path})", 1003 | ) 1004 | 1005 | format_choices = { 1006 | "human": HumanOutputFormat, 1007 | "json": JSONOutputFormat, 1008 | "csv": CSVOutputFormat, 1009 | "tabular": TabularOutputFormat, 1010 | "pass": PassOutputFormat, 1011 | } 1012 | 1013 | parser.add_argument( 1014 | "-f", 1015 | "--format", 1016 | action=ConvertChoices, 1017 | choices=format_choices, 1018 | default=HumanOutputFormat, 1019 | help="Format for the output.", 1020 | ) 1021 | parser.add_argument( 1022 | "-d", 1023 | "--csv-delimiter", 1024 | action="store", 1025 | default=";", 1026 | help="The delimiter for csv output", 1027 | ) 1028 | parser.add_argument( 1029 | "-q", 1030 | "--csv-quotechar", 1031 | action="store", 1032 | default='"', 1033 | help="The quote char for csv output", 1034 | ) 1035 | parser.add_argument( 1036 | "--no-csv-header", 1037 | action="store_false", 1038 | dest="csv_header", 1039 | default=True, 1040 | help="Do not include a header in CSV output.", 1041 | ) 1042 | parser.add_argument( 1043 | "--pass-username-prefix", 1044 | action="store", 1045 | default="", 1046 | help=( 1047 | "Export username as is (default), or with the provided format prefix. " 1048 | "For instance 'login: ' for browserpass." 1049 | ), 1050 | ) 1051 | parser.add_argument( 1052 | "-p", 1053 | "--pass-prefix", 1054 | action="store", 1055 | default="web", 1056 | help="Folder prefix for export to pass from passwordstore.org (default: %(default)s)", 1057 | ) 1058 | parser.add_argument( 1059 | "-m", 1060 | "--pass-cmd", 1061 | action="store", 1062 | default="pass", 1063 | help="Command/path to use when exporting to pass (default: %(default)s)", 1064 | ) 1065 | parser.add_argument( 1066 | "--pass-always-with-login", 1067 | action="store_true", 1068 | help="Always save as / (default: only when multiple accounts per domain)", 1069 | ) 1070 | parser.add_argument( 1071 | "-n", 1072 | "--no-interactive", 1073 | action="store_false", 1074 | dest="interactive", 1075 | default=True, 1076 | help="Disable interactivity.", 1077 | ) 1078 | parser.add_argument( 1079 | "--non-fatal-decryption", 1080 | action="store_true", 1081 | default=False, 1082 | help="If set, corrupted entries will be skipped instead of aborting the process.", 1083 | ) 1084 | parser.add_argument( 1085 | "-c", 1086 | "--choice", 1087 | help="The profile to use (starts with 1). If only one profile, defaults to that.", 1088 | ) 1089 | parser.add_argument( 1090 | "-l", "--list", action="store_true", help="List profiles and exit." 1091 | ) 1092 | parser.add_argument( 1093 | "-e", 1094 | "--encoding", 1095 | action="store", 1096 | default=DEFAULT_ENCODING, 1097 | help="Override default encoding (%(default)s).", 1098 | ) 1099 | parser.add_argument( 1100 | "-v", 1101 | "--verbose", 1102 | action="count", 1103 | default=0, 1104 | help="Verbosity level. Warning on -vv (highest level) user input will be printed on screen", 1105 | ) 1106 | parser.add_argument( 1107 | "--version", 1108 | action="version", 1109 | version=__version__, 1110 | help="Display version of firefox_decrypt and exit", 1111 | ) 1112 | 1113 | args = parser.parse_args() 1114 | 1115 | # understand `\t` as tab character if specified as delimiter. 1116 | if args.csv_delimiter == "\\t": 1117 | args.csv_delimiter = "\t" 1118 | 1119 | return args 1120 | 1121 | 1122 | def setup_logging(args) -> None: 1123 | """Setup the logging level and configure the basic logger""" 1124 | if args.verbose == 1: 1125 | level = logging.INFO 1126 | elif args.verbose >= 2: 1127 | level = logging.DEBUG 1128 | else: 1129 | level = logging.WARN 1130 | 1131 | logging.basicConfig( 1132 | format="%(asctime)s - %(levelname)s - %(message)s", 1133 | level=level, 1134 | ) 1135 | 1136 | global LOG 1137 | LOG = logging.getLogger(__name__) 1138 | 1139 | 1140 | def identify_system_locale() -> str: 1141 | encoding: Optional[str] = locale.getpreferredencoding() 1142 | 1143 | if encoding is None: 1144 | LOG.error( 1145 | "Could not determine which encoding/locale to use for NSS interaction. " 1146 | "This configuration is unsupported.\n" 1147 | "If you are in Linux or MacOS, please search online " 1148 | "how to configure a UTF-8 compatible locale and try again." 1149 | ) 1150 | raise Exit(Exit.BAD_LOCALE) 1151 | 1152 | return encoding 1153 | 1154 | 1155 | def main() -> None: 1156 | """Main entry point""" 1157 | args = parse_sys_args() 1158 | 1159 | setup_logging(args) 1160 | 1161 | global DEFAULT_ENCODING 1162 | 1163 | if args.encoding != DEFAULT_ENCODING: 1164 | LOG.info( 1165 | "Overriding default encoding from '%s' to '%s'", 1166 | DEFAULT_ENCODING, 1167 | args.encoding, 1168 | ) 1169 | 1170 | # Override default encoding if specified by user 1171 | DEFAULT_ENCODING = args.encoding 1172 | 1173 | LOG.info("Running firefox_decrypt version: %s", __version__) 1174 | LOG.debug("Parsed commandline arguments: %s", args) 1175 | encodings = ( 1176 | ("stdin", sys.stdin.encoding), 1177 | ("stdout", sys.stdout.encoding), 1178 | ("stderr", sys.stderr.encoding), 1179 | ("locale", identify_system_locale()), 1180 | ) 1181 | 1182 | LOG.debug( 1183 | "Running with encodings: %s: %s, %s: %s, %s: %s, %s: %s", *chain(*encodings) 1184 | ) 1185 | 1186 | for stream, encoding in encodings: 1187 | if encoding.lower() != DEFAULT_ENCODING: 1188 | LOG.warning( 1189 | "Running with unsupported encoding '%s': %s" 1190 | " - Things are likely to fail from here onwards", 1191 | stream, 1192 | encoding, 1193 | ) 1194 | 1195 | # Load Mozilla profile and initialize NSS before asking the user for input 1196 | moz = MozillaInteraction(args.non_fatal_decryption) 1197 | 1198 | basepath = os.path.expanduser(args.profile) 1199 | 1200 | # Read profiles from profiles.ini in profile folder 1201 | profile = get_profile(basepath, args.interactive, args.choice, args.list) 1202 | 1203 | # Start NSS for selected profile 1204 | moz.load_profile(profile) 1205 | # Check if profile is password protected and prompt for a password 1206 | moz.authenticate(args.interactive) 1207 | # Decode all passwords 1208 | outputs = moz.decrypt_passwords() 1209 | 1210 | # Export passwords into one of many formats 1211 | formatter = args.format(outputs, args) 1212 | formatter.output() 1213 | 1214 | # Finally shutdown NSS 1215 | moz.unload_profile() 1216 | 1217 | 1218 | def run_ffdecrypt(): 1219 | try: 1220 | main() 1221 | except KeyboardInterrupt: 1222 | print("Quit.") 1223 | sys.exit(Exit.KEYBOARD_INTERRUPT) 1224 | except Exit as e: 1225 | sys.exit(e.exitcode) 1226 | 1227 | 1228 | if __name__ == "__main__": 1229 | run_ffdecrypt() 1230 | --------------------------------------------------------------------------------