--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
@@ -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, | |||
@@ -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. " | |||
) |
@@ -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: | |||
@@ -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] | |||
] | |||
@@ -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 @@ | |||
__version__ = "10.1.1" | |||
__version__ = "10.2.0" |
@@ -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) |
@@ -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(), | |||
@@ -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"]) | |||