diff --git a/cli/lesspass/cli.py b/cli/lesspass/cli.py index 9ceac6f..5f3cd21 100644 --- a/cli/lesspass/cli.py +++ b/cli/lesspass/cli.py @@ -101,6 +101,12 @@ def parse_args(args): config_home_path = _get_config_path() backup_file = os.path.join(config_home_path, "profiles.json") parser.add_argument( + "--logout", + dest="logout", + action="store_true", + help=f"Remove {os.path.join(config_home_path, 'config.json')} file", + ) + parser.add_argument( "--save", dest="save_path", nargs="?", @@ -115,6 +121,12 @@ def parse_args(args): help="[beta] Load your password profiles file", ) parser.add_argument( + "--export", + dest="export_file_path", + default=None, + help="Export all your passwords from LessPass database with your master password. /!\ Please note that your passwords will be saved in clear text.", + ) + parser.add_argument( "--config-home-path", dest="config_home_path", default=config_home_path, diff --git a/cli/lesspass/connected.py b/cli/lesspass/connected.py index 5b32bc3..f9a2875 100644 --- a/cli/lesspass/connected.py +++ b/cli/lesspass/connected.py @@ -1,3 +1,4 @@ +import contextlib import getpass import json import os @@ -8,7 +9,7 @@ import requests from lesspass.password import generate_password -def _login(config_home_path, url): +def _login(config_home_path, url, master_password): os.makedirs(config_home_path, exist_ok=True) config_path = os.path.join(config_home_path, "config.json") tokens = None @@ -29,7 +30,6 @@ def _login(config_home_path, url): print("LessPass login") email = input("Email: ") - master_password = getpass.getpass("Master Password: ") if not email or not master_password: print("Email and Master Password are mandatory") sys.exit(1) @@ -58,8 +58,20 @@ def _login(config_home_path, url): return tokens["access"] +def logout(config_home_path): + config_path = os.path.join(config_home_path, "config.json") + try: + with contextlib.suppress(FileNotFoundError): + os.remove(config_path) + print("Logout successful") + except Exception as e: + print(f"Can't remove {config_path}. Error was:") + print(e) + + def save_password_profiles(config_home_path, url, backup_path): - token = _login(config_home_path, url) + master_password = getpass.getpass("Master Password: ") + token = _login(config_home_path, url, master_password) r = requests.get(f"{url}passwords/", headers={"Authorization": "JWT %s" % token}) with open(backup_path, "w", encoding="utf-8") as f: json.dump(r.json(), f, ensure_ascii=False, indent=4) @@ -68,7 +80,8 @@ def save_password_profiles(config_home_path, url, backup_path): def load_password_profiles(config_home_path, url, backup_path): - token = _login(config_home_path, url) + master_password = getpass.getpass("Master Password: ") + token = _login(config_home_path, url, master_password) with open(backup_path, encoding="utf-8") as f: data = json.load(f) @@ -100,3 +113,21 @@ def load_password_profiles(config_home_path, url, backup_path): ) with open(backup_path, "w", encoding="utf-8") as f: json.dump(get_password_profiles.json(), f, ensure_ascii=False, indent=4) + + +def export_passwords(config_home_path, url, export_file_path): + master_password = getpass.getpass("Master Password: ") + token = _login(config_home_path, url, master_password) + r = requests.get(f"{url}passwords/", headers={"Authorization": "JWT %s" % token}) + r.raise_for_status() + with open(export_file_path, "w", encoding="utf-8") as f: + f.write("name,url,username,password\n") + for password_profile in r.json()["results"]: + password = generate_password(password_profile, master_password) + print(f"{password_profile['site']} exported") + f.write( + f"{password_profile['site']},https://{password_profile['site']},{password_profile['login']},{password}\n" + ) + print( + f"Passwords exported in {export_file_path}. /!\ Be careful all your passwords are in clear text. " + ) diff --git a/cli/lesspass/core.py b/cli/lesspass/core.py index 236df11..6743fe3 100644 --- a/cli/lesspass/core.py +++ b/cli/lesspass/core.py @@ -11,7 +11,12 @@ from lesspass.profile import create_profile from lesspass.password import generate_password from lesspass.clipboard import copy, get_system_copy_command from lesspass.fingerprint import getpass_with_fingerprint -from lesspass.connected import save_password_profiles, load_password_profiles +from lesspass.connected import ( + save_password_profiles, + load_password_profiles, + logout, + export_passwords, +) signal.signal(signal.SIGINT, lambda s, f: sys.exit(0)) @@ -31,6 +36,12 @@ def main(args=sys.argv[1:]): if args.load_path: return load_password_profiles(args.config_home_path, args.url, args.load_path) + if args.export_file_path: + return export_passwords(args.config_home_path, args.url, args.export_file_path) + + if args.logout: + return logout(args.config_home_path) + if args.prompt: if not args.site: args.site = getpass.getpass("Site: ") @@ -41,18 +52,19 @@ def main(args=sys.argv[1:]): print("error: argument SITE is required but was not provided.") sys.exit(4) - if not args.master_password: + master_password = args.master_password + if not master_password: prompt = "Master Password: " if args.no_fingerprint: - args.master_password = getpass.getpass(prompt) + master_password = getpass.getpass(prompt) else: - args.master_password = getpass_with_fingerprint(prompt) + master_password = getpass_with_fingerprint(prompt) - if not args.master_password: + if not master_password: print("error: argument MASTER_PASSWORD is required but was not provided") sys.exit(5) - profile, master_password = create_profile(args) + profile = create_profile(args) try: generated_password = generate_password(profile, master_password) except exceptions.ExcludeAllCharsAvailable: diff --git a/cli/lesspass/password.py b/cli/lesspass/password.py index 44b5bd7..56495db 100644 --- a/cli/lesspass/password.py +++ b/cli/lesspass/password.py @@ -92,6 +92,8 @@ def _get_one_char_per_rule(entropy, rules, exclude=""): def _get_configured_rules(password_profile): rules = ["lowercase", "uppercase", "digits", "symbols"] + if "numbers" in password_profile: + password_profile["digits"] = password_profile["numbers"] return [ rule for rule in rules if rule in password_profile and password_profile[rule] ] diff --git a/cli/lesspass/profile.py b/cli/lesspass/profile.py index 1c92d19..e0ba184 100644 --- a/cli/lesspass/profile.py +++ b/cli/lesspass/profile.py @@ -15,4 +15,4 @@ def create_profile(args): profile["uppercase"] = args.u profile["digits"] = args.d profile["symbols"] = args.s - return profile, args.master_password + return profile diff --git a/cli/lesspass/version.py b/cli/lesspass/version.py index 7864303..27960b7 100644 --- a/cli/lesspass/version.py +++ b/cli/lesspass/version.py @@ -1 +1 @@ -__version__ = "10.1.1" +__version__ = "10.2.0" diff --git a/cli/tests/test_cli.py b/cli/tests/test_cli.py index c423e61..e910748 100644 --- a/cli/tests/test_cli.py +++ b/cli/tests/test_cli.py @@ -154,3 +154,12 @@ class TestParseArgs(unittest.TestCase): self.assertEqual( parse_args(["--url", "https://example.org"]).url, "https://example.org/" ) + + def test_export(self): + self.assertEqual( + parse_args(["--export", "/tmp/export.csv"]).export_file_path, + "/tmp/export.csv", + ) + + def test_logout(self): + self.assertTrue(parse_args(["--logout"]).logout) diff --git a/cli/tests/test_password_generation.py b/cli/tests/test_password_generation.py index 2655093..7a74720 100644 --- a/cli/tests/test_password_generation.py +++ b/cli/tests/test_password_generation.py @@ -129,6 +129,18 @@ class TestPassword(unittest.TestCase): password._get_configured_rules(password_profile), ["uppercase", "symbols"] ) + def test_get_configured_rules_use_numbers_as_digits(self): + password_profile = { + "lowercase": False, + "uppercase": False, + "numbers": True, + "symbols": False, + } + + self.assertListEqual( + password._get_configured_rules(password_profile), ["digits"] + ) + def test_get_set_of_characters_without_rule(self): self.assertEqual( password._get_set_of_characters(), diff --git a/cli/tests/test_profile.py b/cli/tests/test_profile.py index b36e199..198de30 100644 --- a/cli/tests/test_profile.py +++ b/cli/tests/test_profile.py @@ -6,7 +6,7 @@ from lesspass.profile import create_profile class TestProfile(unittest.TestCase): def test_create_profile_default(self): - profile, master_password = create_profile(parse_args(["site", "login"])) + profile = create_profile(parse_args(["site", "login"])) self.assertTrue(profile["lowercase"]) self.assertTrue(profile["uppercase"]) self.assertTrue(profile["digits"]) @@ -16,154 +16,147 @@ class TestProfile(unittest.TestCase): self.assertEqual(profile["site"], "site") self.assertEqual(profile["login"], "login") self.assertEqual(profile["exclude"], "") - self.assertIsNone(master_password) def test_create_profile_login(self): - profile, _ = create_profile(parse_args(["site"])) + profile = create_profile(parse_args(["site"])) self.assertEqual(profile["login"], "") def test_create_profile_length(self): - profile, _ = create_profile(parse_args(["site", "--length", "8"])) + profile = create_profile(parse_args(["site", "--length", "8"])) self.assertEqual(profile["length"], 8) def test_create_profile_counter(self): - profile, _ = create_profile(parse_args(["site", "--counter", "2"])) + profile = create_profile(parse_args(["site", "--counter", "2"])) self.assertEqual(profile["counter"], 2) - def test_create_profile_master_password(self): - _, master_password = create_profile( - parse_args(["site", "login", "master_password"]) - ) - self.assertEqual(master_password, "master_password") - def test_create_profile_l(self): - profile, _ = create_profile(parse_args(["site", "-l"])) + profile = create_profile(parse_args(["site", "-l"])) self.assertTrue(profile["lowercase"]) self.assertFalse(profile["uppercase"]) self.assertFalse(profile["digits"]) self.assertFalse(profile["symbols"]) def test_create_profile_u(self): - profile, _ = create_profile(parse_args(["site", "-u"])) + profile = create_profile(parse_args(["site", "-u"])) self.assertFalse(profile["lowercase"]) self.assertTrue(profile["uppercase"]) self.assertFalse(profile["digits"]) self.assertFalse(profile["symbols"]) def test_create_profile_d(self): - profile, _ = create_profile(parse_args(["site", "-d"])) + profile = create_profile(parse_args(["site", "-d"])) self.assertFalse(profile["lowercase"]) self.assertFalse(profile["uppercase"]) self.assertTrue(profile["digits"]) self.assertFalse(profile["symbols"]) def test_create_profile_s(self): - profile, _ = create_profile(parse_args(["site", "-s"])) + profile = create_profile(parse_args(["site", "-s"])) self.assertFalse(profile["lowercase"]) self.assertFalse(profile["uppercase"]) self.assertFalse(profile["digits"]) self.assertTrue(profile["symbols"]) def test_create_profile_lu(self): - profile, _ = create_profile(parse_args(["site", "-lu"])) + profile = create_profile(parse_args(["site", "-lu"])) self.assertTrue(profile["lowercase"]) self.assertTrue(profile["uppercase"]) self.assertFalse(profile["digits"]) self.assertFalse(profile["symbols"]) def test_create_profile_ld(self): - profile, _ = create_profile(parse_args(["site", "-ld"])) + profile = create_profile(parse_args(["site", "-ld"])) self.assertTrue(profile["lowercase"]) self.assertFalse(profile["uppercase"]) self.assertTrue(profile["digits"]) self.assertFalse(profile["symbols"]) def test_create_profile_ls(self): - profile, _ = create_profile(parse_args(["site", "-ls"])) + profile = create_profile(parse_args(["site", "-ls"])) self.assertTrue(profile["lowercase"]) self.assertFalse(profile["uppercase"]) self.assertFalse(profile["digits"]) self.assertTrue(profile["symbols"]) def test_create_profile_ud(self): - profile, _ = create_profile(parse_args(["site", "-ud"])) + profile = create_profile(parse_args(["site", "-ud"])) self.assertFalse(profile["lowercase"]) self.assertTrue(profile["uppercase"]) self.assertTrue(profile["digits"]) self.assertFalse(profile["symbols"]) def test_create_profile_us(self): - profile, _ = create_profile(parse_args(["site", "-us"])) + profile = create_profile(parse_args(["site", "-us"])) self.assertFalse(profile["lowercase"]) self.assertTrue(profile["uppercase"]) self.assertFalse(profile["digits"]) self.assertTrue(profile["symbols"]) def test_create_profile_ds(self): - profile, _ = create_profile(parse_args(["site", "-ds"])) + profile = create_profile(parse_args(["site", "-ds"])) self.assertFalse(profile["lowercase"]) self.assertFalse(profile["uppercase"]) self.assertTrue(profile["digits"]) self.assertTrue(profile["symbols"]) def test_create_profile_lud(self): - profile, _ = create_profile(parse_args(["site", "-lud"])) + profile = create_profile(parse_args(["site", "-lud"])) self.assertTrue(profile["lowercase"]) self.assertTrue(profile["uppercase"]) self.assertTrue(profile["digits"]) self.assertFalse(profile["symbols"]) def test_create_profile_lus(self): - profile, _ = create_profile(parse_args(["site", "-lus"])) + profile = create_profile(parse_args(["site", "-lus"])) self.assertTrue(profile["lowercase"]) self.assertTrue(profile["uppercase"]) self.assertFalse(profile["digits"]) self.assertTrue(profile["symbols"]) def test_create_profile_uds(self): - profile, _ = create_profile(parse_args(["site", "-uds"])) + profile = create_profile(parse_args(["site", "-uds"])) self.assertFalse(profile["lowercase"]) self.assertTrue(profile["uppercase"]) self.assertTrue(profile["digits"]) self.assertTrue(profile["symbols"]) def test_create_profile_luds(self): - profile, _ = create_profile(parse_args(["site", "-luds"])) + profile = create_profile(parse_args(["site", "-luds"])) self.assertTrue(profile["lowercase"]) self.assertTrue(profile["uppercase"]) self.assertTrue(profile["digits"]) self.assertTrue(profile["symbols"]) def test_create_profile_suld(self): - profile, _ = create_profile(parse_args(["site", "-suld"])) + profile = create_profile(parse_args(["site", "-suld"])) self.assertTrue(profile["lowercase"]) self.assertTrue(profile["uppercase"]) self.assertTrue(profile["digits"]) self.assertTrue(profile["symbols"]) def test_create_profile_nl(self): - profile, _ = create_profile(parse_args(["site", "--no-lowercase"])) + profile = create_profile(parse_args(["site", "--no-lowercase"])) self.assertFalse(profile["lowercase"]) self.assertTrue(profile["uppercase"]) self.assertTrue(profile["digits"]) self.assertTrue(profile["symbols"]) def test_create_profile_nu(self): - profile, _ = create_profile(parse_args(["site", "--no-uppercase"])) + profile = create_profile(parse_args(["site", "--no-uppercase"])) self.assertTrue(profile["lowercase"]) self.assertFalse(profile["uppercase"]) self.assertTrue(profile["digits"]) self.assertTrue(profile["symbols"]) def test_create_profile_nd(self): - profile, _ = create_profile(parse_args(["site", "--no-digits"])) + profile = create_profile(parse_args(["site", "--no-digits"])) self.assertTrue(profile["lowercase"]) self.assertTrue(profile["uppercase"]) self.assertFalse(profile["digits"]) self.assertTrue(profile["symbols"]) def test_create_profile_ns(self): - profile, _ = create_profile(parse_args(["site", "--no-symbols"])) + profile = create_profile(parse_args(["site", "--no-symbols"])) self.assertTrue(profile["lowercase"]) self.assertTrue(profile["uppercase"]) self.assertTrue(profile["digits"])