Browse Source

Add --export and --logout function to LessPass cli

--export generates all passwords with password profilessaved in LessPass database.
--logout remove the config.json file saved when a user login LessPass database.
tags/cli-v10.2.0
Guillaume Vincent 1 year ago
parent
commit
baa6b3f71b
9 changed files with 113 additions and 42 deletions
  1. +12
    -0
      cli/lesspass/cli.py
  2. +35
    -4
      cli/lesspass/connected.py
  3. +18
    -6
      cli/lesspass/core.py
  4. +2
    -0
      cli/lesspass/password.py
  5. +1
    -1
      cli/lesspass/profile.py
  6. +1
    -1
      cli/lesspass/version.py
  7. +9
    -0
      cli/tests/test_cli.py
  8. +12
    -0
      cli/tests/test_password_generation.py
  9. +23
    -30
      cli/tests/test_profile.py

+ 12
- 0
cli/lesspass/cli.py View File

@@ -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,


+ 35
- 4
cli/lesspass/connected.py View File

@@ -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. "
)

+ 18
- 6
cli/lesspass/core.py View File

@@ -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:


+ 2
- 0
cli/lesspass/password.py View File

@@ -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]
]


+ 1
- 1
cli/lesspass/profile.py View File

@@ -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

+ 1
- 1
cli/lesspass/version.py View File

@@ -1 +1 @@
__version__ = "10.1.1"
__version__ = "10.2.0"

+ 9
- 0
cli/tests/test_cli.py View File

@@ -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)

+ 12
- 0
cli/tests/test_password_generation.py View File

@@ -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(),


+ 23
- 30
cli/tests/test_profile.py View File

@@ -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"])


Loading…
Cancel
Save