From 485a8d19b4e55f8a0883b8e003e687f08189268e Mon Sep 17 00:00:00 2001 From: lmcclell <48396038+lmcclell@users.noreply.github.com> Date: Sun, 31 Jan 2021 09:47:14 +1100 Subject: [PATCH] Added unit tests (#58) * Add unit testing for key decryption * Add functional tests --- .github/workflows/testing.yaml | 58 +++++++++++++++++ .gitignore | 4 ++ README.md | 14 ++--- ffpass/__init__.py | 107 ++++++++++++++++++-------------- tests/firefox-70/key4.db | Bin 0 -> 294912 bytes tests/firefox-70/logins.json | 1 + tests/firefox-84/key4.db | Bin 0 -> 294912 bytes tests/firefox-84/logins.json | 1 + tests/firefox-mp-70/key4.db | Bin 0 -> 294912 bytes tests/firefox-mp-70/logins.json | 1 + tests/firefox-mp-84/key4.db | Bin 0 -> 294912 bytes tests/firefox-mp-84/logins.json | 1 + tests/test_key.py | 38 ++++++++++++ tests/test_run.py | 49 +++++++++++++++ 14 files changed, 220 insertions(+), 54 deletions(-) create mode 100644 .github/workflows/testing.yaml create mode 100644 tests/firefox-70/key4.db create mode 100644 tests/firefox-70/logins.json create mode 100644 tests/firefox-84/key4.db create mode 100644 tests/firefox-84/logins.json create mode 100644 tests/firefox-mp-70/key4.db create mode 100644 tests/firefox-mp-70/logins.json create mode 100644 tests/firefox-mp-84/key4.db create mode 100644 tests/firefox-mp-84/logins.json create mode 100644 tests/test_key.py create mode 100644 tests/test_run.py diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml new file mode 100644 index 0000000..259fae8 --- /dev/null +++ b/.github/workflows/testing.yaml @@ -0,0 +1,58 @@ +name: ffpass + +on: [push, pull_request_target] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install . + + - name: Test with pytest + run: | + pip install pytest + pip install pytest-cov + python -m pytest tests --junit-xml pytest.xml + + - name: Lint with flake8 + run: | + pip install flake8 + flake8 --ignore=E741,E501 . + + - name: Upload Unit Test Results + if: always() + uses: actions/upload-artifact@v2 + with: + name: Unit Test Results + path: pytest.xml + + publish-test-results: + name: "Publish Unit Tests Results" + needs: test + runs-on: ubuntu-latest + if: success() || failure() + + steps: + - name: Download Artifacts + uses: actions/download-artifact@v2 + with: + path: artifacts + + - name: Publish Unit Test Results + uses: EnricoMi/publish-unit-test-result-action@v1.7 + with: + check_name: Unit Test Results + github_token: ${{ secrets.GITHUB_TOKEN }} + files: "artifacts/Unit Test Results/pytest.xml" diff --git a/.gitignore b/.gitignore index 746645f..948d392 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ __pycache__/ build/ dist/ *.egg-info/ +junit/ +.vscode +.coverage +pytest.xml diff --git a/README.md b/README.md index 498e245..d551c4e 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ pip install ffpass ``` bash ffpass export > passwords.csv -ffpass export -t passwords.csv -ffpass export --to passwords.csv +ffpass export -f passwords.csv +ffpass export --file passwords.csv ``` ### Usage @@ -47,7 +47,7 @@ ffpass export --to passwords.csv optional arguments: -h, --help show this help message and exit - -t TO_FILE, --to TO_FILE + -f FILE, --file FILE file to export password (defaults to stdout) -d DIRECTORY, --directory DIRECTORY, --dir DIRECTORY Firefox profile directory -v, --verbose @@ -57,7 +57,7 @@ ffpass export --to passwords.csv ``` bash ffpass import < passwords.csv ffpass import -f passwords.csv -ffpass import --from passwords.csv +ffpass import --file passwords.csv ``` By default, it works with the passwords exported from Google Chrome. @@ -70,7 +70,7 @@ By default, it works with the passwords exported from Google Chrome. optional arguments: -h, --help show this help message and exit - -f FROM_FILE, --from FROM_FILE + -f FILE, --file FILE file to import from (defaults to stdin) -d DIRECTORY, --directory DIRECTORY, --dir DIRECTORY [Firefox profile directory](https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data#w_how-do-i-find-my-profile) -v, --verbose @@ -114,7 +114,7 @@ Now, Firefox can more accurately import logins saved in Chrome/Chromium on Windo ``` bash -ffpass import --from passwords.csv +ffpass import --file passwords.csv ``` Restart Firefox, making sure it didn't leave any process still open. @@ -129,7 +129,7 @@ Restart Firefox, making sure it didn't leave any process still open. ``` bash -ffpass export --to passwords.csv +ffpass export --file passwords.csv ``` ### Import in Google Chrome diff --git a/ffpass/__init__.py b/ffpass/__init__.py index 2e80314..e01bb2d 100644 --- a/ffpass/__init__.py +++ b/ffpass/__init__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 - """ The MIT License (MIT) Copyright (c) 2018 Louis Abraham @@ -11,10 +10,9 @@ ffpass can import and export passwords from Firefox Quantum. \x1B[0m\033[1m\033[F\033[F example of usage: + ffpass export --file passwords.csv - ffpass export --to passwords.csv - - ffpass import --from passwords.csv + ffpass import --file passwords.csv \033[0m\033[1;32m\033[F\033[F @@ -38,6 +36,7 @@ from datetime import datetime from urllib.parse import urlparse import sqlite3 import os.path +import logging from pyasn1.codec.der.decoder import decode as der_decode from pyasn1.codec.der.encoder import encode as der_encode @@ -62,14 +61,8 @@ class WrongPassword(Exception): pass -def _err(message): - print(f'error: {message}', file=sys.stderr) - - -def _msg(message): - if not args.verbose: - return - print(message, file=sys.stderr) +class NoProfile(Exception): + pass def getKey(directory: Path, masterPassword=""): @@ -104,7 +97,7 @@ def getKey(directory: Path, masterPassword=""): if clearText != b"password-check\x02\x02": raise WrongPassword() - _msg("password checked") + logging.info("password checked") # decrypt 3des key to decrypt "logins.json" content c.execute(""" @@ -131,7 +124,7 @@ def getKey(directory: Path, masterPassword=""): cipherT = decodedA11[1].asOctets() key = decrypt3DES(globalSalt, masterPassword, entrySalt, cipherT) - _msg("{}: {}".format(encryption_method, key.hex())) + logging.info("{}: {}".format(encryption_method, key.hex())) return key[:24] @@ -171,7 +164,7 @@ def decrypt3DES(globalSalt, masterPassword, entrySalt, encryptedData): k = k1 + k2 iv = k[-8:] key = k[:24] - _msg("key={} iv={}".format(key.hex(), iv.hex())) + logging.info("key={} iv={}".format(key.hex(), iv.hex())) return DES3.new(key, DES3.MODE_CBC, iv).decrypt(encryptedData) @@ -212,7 +205,7 @@ def dumpJsonLogins(directory, jsonLogins): def exportLogins(key, jsonLogins): if "logins" not in jsonLogins: - _err("no 'logins' key in logins.json") + logging.error("no 'logins' key in logins.json") return [] logins = [] for row in jsonLogins["logins"]: @@ -228,17 +221,18 @@ def exportLogins(key, jsonLogins): return logins -def lower_header(from_file): - it = iter(from_file) +def lower_header(csv_file): + it = iter(csv_file) yield next(it).lower() yield from it -def readCSV(from_file): +def readCSV(csv_file): logins = [] - reader = csv.DictReader(lower_header(from_file)) + reader = csv.DictReader(lower_header(csv_file)) for row in reader: logins.append((rawURL(row["url"]), row["username"], row["password"])) + logging.info(f'read {len(logins)} logins') return logins @@ -250,7 +244,9 @@ def rawURL(url): def addNewLogins(key, jsonLogins, logins): nextId = jsonLogins["nextId"] timestamp = int(datetime.now().timestamp() * 1000) + logging.info('adding logins') for i, (url, username, password) in enumerate(logins, nextId): + logging.debug(f'adding {url} {username}') entry = { "id": i, "hostname": url, @@ -280,24 +276,27 @@ def guessDir(): } if sys.platform not in dirs: - _msg(f"Automatic profile selection is not supported for {sys.platform}") - return + logging.error(f"Automatic profile selection is not supported for {sys.platform}") + logging.error("Please specify a profile to parse (-d path/to/profile)") + raise NoProfile paths = Path(dirs[sys.platform]).expanduser() profiles = [path.parent for path in paths.glob(os.path.join("*", "logins.json"))] + logging.debug(f"Paths: {paths}") + logging.debug(f"Profiles: {profiles}") if len(profiles) == 0: - _err("Cannot find any Firefox profiles") - return + logging.error("Cannot find any Firefox profiles") + raise NoProfile if len(profiles) > 1: - _msg("More than one profile detected. Please specify a profile to parse (-d path/to/profile)") - _msg("valid profiles:\n\t" + '\n\t'.join(map(str, profiles))) - return + logging.error("More than one profile detected. Please specify a profile to parse (-d path/to/profile)") + logging.error("valid profiles:\n\t\t" + '\n\t\t'.join(map(str, profiles))) + raise NoProfile profile_path = profiles[0] - _msg(f"Using profile: {profile_path}") + logging.info(f"Using profile: {profile_path}") return profile_path @@ -321,29 +320,29 @@ def main_export(args): return jsonLogins = getJsonLogins(args.directory) logins = exportLogins(key, jsonLogins) - writer = csv.writer(args.to_file) + writer = csv.writer(args.file) writer.writerow(["url", "username", "password"]) writer.writerows(logins) def main_import(args): - if args.from_file == sys.stdin: + if args.file == sys.stdin: try: key = getKey(args.directory) except WrongPassword: # it is not possible to read the password # if stdin is used for input - _err("Password is not empty. You have to specify FROM_FILE.") + logging.error("Password is not empty. You have to specify FROM_FILE.") sys.exit(1) else: key = askpass(args.directory) jsonLogins = getJsonLogins(args.directory) - logins = readCSV(args.from_file) + logins = readCSV(args.file) addNewLogins(key, jsonLogins, logins) dumpJsonLogins(args.directory, jsonLogins) -def makeParser(required_dir): +def makeParser(): parser = argparse.ArgumentParser( prog="ffpass", description=__doc__, @@ -362,15 +361,15 @@ def makeParser(required_dir): parser_import.add_argument( "-f", - "--from", - dest="from_file", + "--file", + dest="file", type=argparse.FileType("r", encoding="utf-8"), default=sys.stdin, ) parser_export.add_argument( - "-t", - "--to", - dest="to_file", + "-f", + "--file", + dest="file", type=argparse.FileType("w", encoding="utf-8"), default=sys.stdout, ) @@ -381,11 +380,11 @@ def makeParser(required_dir): "--directory", "--dir", type=Path, - required=required_dir, default=None, help="Firefox profile directory", ) sub.add_argument("-v", "--verbose", action="store_true") + sub.add_argument("--debug", action="store_true") parser_import.set_defaults(func=main_import) parser_export.set_defaults(func=main_export) @@ -393,19 +392,33 @@ def makeParser(required_dir): def main(): - global args - args = makeParser(False).parse_args() + logging.basicConfig(level=logging.ERROR, format="%(levelname)s: %(message)s") + + parser = makeParser() + args = parser.parse_args() + + if args.verbose: + log_level = logging.INFO + elif args.debug: + log_level = logging.DEBUG + else: + log_level = logging.ERROR + + logging.getLogger().setLevel(log_level) + if args.directory is None: - guessed_dir = guessDir() - if guessed_dir is None: - args = makeParser(True).parse_args() - else: - args.directory = guessed_dir + try: + args.directory = guessDir() + except NoProfile: + print("") + parser.print_help() + parser.exit() args.directory = args.directory.expanduser() + try: args.func(args) except NoDatabase: - _err("Firefox password database is empty. Please create it from Firefox.") + logging.error("Firefox password database is empty. Please create it from Firefox.") if __name__ == "__main__": diff --git a/tests/firefox-70/key4.db b/tests/firefox-70/key4.db new file mode 100644 index 0000000000000000000000000000000000000000..9dcdd64821c476dbbbb75da6f465e744ce8300ae GIT binary patch literal 294912 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 Y1PBlyu&)FTmX3`+*WTK?zIk=+AJYN#fdBvi literal 0 HcmV?d00001 diff --git a/tests/firefox-70/logins.json b/tests/firefox-70/logins.json new file mode 100644 index 0000000..83a6ed8 --- /dev/null +++ b/tests/firefox-70/logins.json @@ -0,0 +1 @@ +{"nextId":2,"logins":[{"id":1,"hostname":"http://www.stealmylogin.com","httpRealm":null,"formSubmitURL":"https://example.com","usernameField":"username","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECKnaoufEsN+EBAglQGuxstdAVw==","encryptedPassword":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECEvvw+bkhjriBAgjXBr8vnzt6A==","guid":"{417f43f0-09fb-1341-abe4-adf0045a5750}","encType":1,"timeCreated":1610850335449,"timeLastUsed":1610850335449,"timePasswordChanged":1610850335449,"timesUsed":1}],"potentiallyVulnerablePasswords":[],"dismissedBreachAlertsByLoginGUID":{},"version":2} \ No newline at end of file diff --git a/tests/firefox-84/key4.db b/tests/firefox-84/key4.db new file mode 100644 index 0000000000000000000000000000000000000000..36f2e37e8133e139680ffe986776dfbd3930ffcf GIT binary patch literal 294912 zcmeI*Z){y<9S87p+PmA{!W!)`)yyS#s7U94bN{sKP*S?Cu;SRCvIPgk_O`vu!p5NO z7;H-E62}|=1dO7I223O&CQ6LP7fKM&#Kk{vNTx)MArLi!L=g-#F2r-5d+TLR5@UEH z`#d@K^moqlJm32~pL4pt+%-G5&Mwr8Hy)VVUt1_vqPwFwjxH}2qbTZ3KZW$O-12FW zTfRtt#;>d2sO*d`zVhLfizlPb!{gD%bBjMb@`EFXj`SaX>G1gB$KPm@`veFOAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB=EFmcYJ_j@~tE;-wEP)TZ{;_tzI{n`;ZT<)57+ zB$iRJu>(sEVU zl+X9}u8pq`1Ez1T&Cc91Kfhyc_V(IBy`#Cfv2B}2KT-^x*%Dd>ANDA?_Iq%o@gxl;M7g^>4jFtiqkUY8{KE5l#){4NPE8b zUCXbvJdaYygkwee?0!Xs-1w5qmdRO7L=?*BIrZg|u7 z@m=eSwIuzd&#hKYKb5QwH?ljW?ABm*Ihb7zhRbZoU^P4SXQzSev>`hUW~YtWsg|9l z($mHel{C&FG08GYN#lF6lv0w#l#(o`lw?7rBugqKSyU;>vPwx7R!XL`b8~r(^@Zee zqYlaCkX#PQ<&azs$>oq-4#|};s}g2a!mLV|RSB~OLhnH69SFSxp?9Fs8!`t%=7umb z_>=-rIqbBYWOK`5r{%EPYLd2cDZAY)v)oD;T?wNrVRYIVhBO9dW2!-_nq=Qy4N}!0 zRSn--4c}S~&!s=i?GI`FA+0~mov!y+s+ImMvRP)i%`)3qv&_DsS!P>omf1d=Wd_(R zGsQ+3P?}|HTHtBkZA}|ko;J{0PFgN6CoPwkla|ZNNz3Kjv|L_JS}y0NsK*%bDq~B{Ne&d@y^qNnp>$Rx1PH zy)AF-tF+~NuUz{^{{*&vS0_rXx;_1y)0zF?uLnAx%0Zad!nP&(>HNmw7xsEs5^>!dQR_* zqO2?H`QKYl$D?EE>|7i_6kV4r-J9&s7uFrya_rTW@rsqnWHQ{kJU6N4inspwwaNQS zKl}1$A9!Sc*S?*mZ4okCvwH}|5$tB8^6BbUFnE;@nn=98UX?X2oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5IE}uI@)uEo{mSyy3(D;i>IRW&IRW&#w*k7w!6L)E#8ox zGy((&5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&Ucyk5f!ke4-Z)r>K>s+y> zdi1V9R_Po8;c&oAdT@A&rU!+-e2rN?jEa?>T>Uw8k--btuS?RoH;|rBbqVcXC5(Ro%(cV^rR6WK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB=EFlE9kwp?H3F@5IgZJ11t!$%d)v>c)vA Wv{I?#-1ar`t+o02LkH$&I{yhoOlQLY literal 0 HcmV?d00001 diff --git a/tests/firefox-84/logins.json b/tests/firefox-84/logins.json new file mode 100644 index 0000000..83a6ed8 --- /dev/null +++ b/tests/firefox-84/logins.json @@ -0,0 +1 @@ +{"nextId":2,"logins":[{"id":1,"hostname":"http://www.stealmylogin.com","httpRealm":null,"formSubmitURL":"https://example.com","usernameField":"username","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECKnaoufEsN+EBAglQGuxstdAVw==","encryptedPassword":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECEvvw+bkhjriBAgjXBr8vnzt6A==","guid":"{417f43f0-09fb-1341-abe4-adf0045a5750}","encType":1,"timeCreated":1610850335449,"timeLastUsed":1610850335449,"timePasswordChanged":1610850335449,"timesUsed":1}],"potentiallyVulnerablePasswords":[],"dismissedBreachAlertsByLoginGUID":{},"version":2} \ No newline at end of file diff --git a/tests/firefox-mp-70/key4.db b/tests/firefox-mp-70/key4.db new file mode 100644 index 0000000000000000000000000000000000000000..9be3138733915e9f9d3f9ba10ae7181697f89314 GIT binary patch literal 294912 zcmeI*OKe&7CNeU*_5FtzBuIv)ZorkB6)QBXmOI_SNOd@Dmm~re>E=f~j zhlB($K_alI*s!3ggjj_|SFmFd@sx^0cC6U2DFQZ8#beQcvf!LEGfb^05@Ln$cXa3a zfA2ZxchC9mI9}${rlfCyI?r^rI{lxc^UDiIPcPI? zA3e6TQ0w2VO>eB#PA)ILaCG^_+NTy?terW&cJ1?vKXI$44vG`N=%ncy(?4>gE^Eht8GG<*ke9dZnMgQtww|$10U-bt?Z{ z8gTXtosG3uH#bjS+W1muYkj1*xW(hoEPS?>I^z-s18UQqdeN9UGMq9#lQPbCR@cuD zhE#TBq|OI2nlsOqE7gxo{p{8tV|Yi#X1Du+lzLI0nJ-nU2l~6&*xbCl zerXU>-VxL7eIRA7KJ%@o^O5S*RQ{d$^vVVSgKDT3)qQP)`~S&{J9_NI^67)MP7yxg zy>%Ml(~Nbx5%1LFt+{wP7cb}1WgIfsil^ClYRA)4@iZ4thvKOdPpjc{C`A?Bb4n~? zM!o1hC#KYk7*j7|PQ8dh^&%$Kix^ceVphF~VfEr{JolE@U0+IWbnBGdNXdvzlpEGtFwIS`Urs3d%L!$FIic+5hO)n$Q1){}`M%uvorw<)cjFhQSH^EouZ&-zUb!|X z*9Yaff0yZt&>g!vSk7u(&f09JSX)ivbMf64i9L>OHQVWX+c-4S9I8}5IrXrA0tdgV zEA>HL3V(A(u>(>$G`p4%Wwa&I+W+-+L3De z&hHlRRHo5R(&|M}}zzV_(GU#i3(oKfm(9!{2=JjhBD<)8gr3y7I^q zH=n!t-rjuAa9%17l=t8H-dErJ!=0a0zWeU)O7DF5@a6qyzJGTp&&wb9+tu68KmME7 z-~HIk>z{Zu-}uIO*m=JF_beP50RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N Z0t5&U*i8a^N|X6(oz2ZF7cZ?1{R{4~^Tq%G literal 0 HcmV?d00001 diff --git a/tests/firefox-mp-70/logins.json b/tests/firefox-mp-70/logins.json new file mode 100644 index 0000000..83a6ed8 --- /dev/null +++ b/tests/firefox-mp-70/logins.json @@ -0,0 +1 @@ +{"nextId":2,"logins":[{"id":1,"hostname":"http://www.stealmylogin.com","httpRealm":null,"formSubmitURL":"https://example.com","usernameField":"username","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECKnaoufEsN+EBAglQGuxstdAVw==","encryptedPassword":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECEvvw+bkhjriBAgjXBr8vnzt6A==","guid":"{417f43f0-09fb-1341-abe4-adf0045a5750}","encType":1,"timeCreated":1610850335449,"timeLastUsed":1610850335449,"timePasswordChanged":1610850335449,"timesUsed":1}],"potentiallyVulnerablePasswords":[],"dismissedBreachAlertsByLoginGUID":{},"version":2} \ No newline at end of file diff --git a/tests/firefox-mp-84/key4.db b/tests/firefox-mp-84/key4.db new file mode 100644 index 0000000000000000000000000000000000000000..51017f14f12ac0f093cf39a55e405681cbae3c63 GIT binary patch literal 294912 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 Yk0qg%N*$-RuZ(Z5&CVX&yMHSGPk+T_bN~PV literal 0 HcmV?d00001 diff --git a/tests/firefox-mp-84/logins.json b/tests/firefox-mp-84/logins.json new file mode 100644 index 0000000..83a6ed8 --- /dev/null +++ b/tests/firefox-mp-84/logins.json @@ -0,0 +1 @@ +{"nextId":2,"logins":[{"id":1,"hostname":"http://www.stealmylogin.com","httpRealm":null,"formSubmitURL":"https://example.com","usernameField":"username","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECKnaoufEsN+EBAglQGuxstdAVw==","encryptedPassword":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECEvvw+bkhjriBAgjXBr8vnzt6A==","guid":"{417f43f0-09fb-1341-abe4-adf0045a5750}","encType":1,"timeCreated":1610850335449,"timeLastUsed":1610850335449,"timePasswordChanged":1610850335449,"timesUsed":1}],"potentiallyVulnerablePasswords":[],"dismissedBreachAlertsByLoginGUID":{},"version":2} \ No newline at end of file diff --git a/tests/test_key.py b/tests/test_key.py new file mode 100644 index 0000000..db102e3 --- /dev/null +++ b/tests/test_key.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +import ffpass +from pathlib import Path +import pytest + +TEST_KEY = b'\xbfh\x13\x1a\xda\xb5\x9d\xe3X\x10\xe0\xa8\x8a\xc2\xe5\xbcE\xf2I\r\xa2pm\xf4' +MASTER_PASSWORD = 'test' + + +def test_firefox_key(): + key = ffpass.getKey(Path('tests/firefox-84')) + assert key == TEST_KEY + + +def test_firefox_mp_key(): + key = ffpass.getKey(Path('tests/firefox-mp-84'), MASTER_PASSWORD) + assert key == TEST_KEY + + +def test_firefox_wrong_masterpassword_key(): + with pytest.raises(ffpass.WrongPassword): + ffpass.getKey(Path('tests/firefox-mp-84'), 'wrongpassword') + + +def test_legacy_firefox_key(): + key = ffpass.getKey(Path('tests/firefox-70')) + assert key == TEST_KEY + + +def test_legacy_firefox_mp_key(): + key = ffpass.getKey(Path('tests/firefox-mp-70'), MASTER_PASSWORD) + assert key == TEST_KEY + + +def test_legacy_firefox_wrong_masterpassword_key(): + with pytest.raises(ffpass.WrongPassword): + ffpass.getKey(Path('tests/firefox-mp-70'), 'wrongpassword') diff --git a/tests/test_run.py b/tests/test_run.py new file mode 100644 index 0000000..a244a83 --- /dev/null +++ b/tests/test_run.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +import subprocess + +MASTER_PASSWORD = 'test' +HEADER = 'url,username,password\n' +IMPORT_CREDENTIAL = 'http://www.example.com,foo,bar\n' +EXPECTED_EXPORT_OUTPUT = f'{HEADER}http://www.stealmylogin.com,test,test\n' +EXPECTED_IMPORT_OUTPUT = EXPECTED_EXPORT_OUTPUT + IMPORT_CREDENTIAL + + +def run_ffpass(mode, path): + command = ["ffpass", mode, "-d", path] + if mode == 'import': + ffpass_input = HEADER + IMPORT_CREDENTIAL + else: + ffpass_input = None + + return subprocess.run(command, stdout=subprocess.PIPE, input=ffpass_input, encoding='utf-8') + + +def test_legacy_firefox_export(): + r = run_ffpass('export', 'tests/firefox-70') + r.check_returncode() + assert r.stdout == EXPECTED_EXPORT_OUTPUT + + +def test_firefox_export(): + r = run_ffpass('export', 'tests/firefox-84') + r.check_returncode() + assert r.stdout == EXPECTED_EXPORT_OUTPUT + + +def test_legacy_firefox(): + r = run_ffpass('import', 'tests/firefox-70') + r.check_returncode() + + r = run_ffpass('export', 'tests/firefox-70') + r.check_returncode() + assert r.stdout == EXPECTED_IMPORT_OUTPUT + + +def test_firefox(): + r = run_ffpass('import', 'tests/firefox-84') + r.check_returncode() + + r = run_ffpass('export', 'tests/firefox-84') + r.check_returncode() + assert r.stdout == EXPECTED_IMPORT_OUTPUT -- 2.52.0