Fixes https://github.com/lesspass/lesspass/issues/529tags/mobile-v9.5.3
@@ -2,7 +2,6 @@ import argparse | |||
import os | |||
from lesspass import version | |||
from lesspass import name | |||
from lesspass import description | |||
EXAMPLES = """ | |||
@@ -27,6 +26,12 @@ copyright: | |||
""" | |||
def _get_config_path(): | |||
DEFAULT_XDG_CONFIG_HOME = os.path.join(os.path.expanduser("~"), ".config") | |||
data_home_path = os.environ.get("XDG_CONFIG_HOME", DEFAULT_XDG_CONFIG_HOME) | |||
return os.path.join(data_home_path, "lesspass") | |||
def range_type(value_string): | |||
value = int(value_string) | |||
if value not in range(5, 35 + 1): | |||
@@ -83,7 +88,9 @@ def parse_args(args): | |||
help="copy the password to clipboard", | |||
) | |||
parser.add_argument( | |||
"--exclude", default=None, help="exclude char from generated password", | |||
"--exclude", | |||
default=None, | |||
help="exclude char from generated password", | |||
) | |||
parser.add_argument( | |||
"--no-fingerprint", | |||
@@ -91,6 +98,34 @@ def parse_args(args): | |||
action="store_true", | |||
help="hide visual fingerprint of the master password when you type", | |||
) | |||
config_home_path = _get_config_path() | |||
backup_file = os.path.join(config_home_path, "profiles.json") | |||
parser.add_argument( | |||
"--save", | |||
dest="save_path", | |||
nargs="?", | |||
const=backup_file, | |||
default=None, | |||
help=f"[beta] Save your password profiles. /!\ File not encrypted. Use carefully. (default: {backup_file})", | |||
) | |||
parser.add_argument( | |||
"--load", | |||
dest="load_path", | |||
default=None, | |||
help="[beta] Load your password profiles file", | |||
) | |||
parser.add_argument( | |||
"--config-home-path", | |||
dest="config_home_path", | |||
default=config_home_path, | |||
help=argparse.SUPPRESS, | |||
) | |||
parser.add_argument( | |||
"--url", | |||
dest="url", | |||
default="https://api.lesspass.com/", | |||
help="[beta] LessPass Database URL used by --save and --load command", | |||
) | |||
lowercase_group = parser.add_mutually_exclusive_group() | |||
lowercase_group.add_argument( | |||
"-l", | |||
@@ -0,0 +1,102 @@ | |||
import getpass | |||
import json | |||
import os | |||
import sys | |||
import requests | |||
from lesspass.password import generate_password | |||
def _login(config_home_path, url): | |||
os.makedirs(config_home_path, exist_ok=True) | |||
config_path = os.path.join(config_home_path, "config.json") | |||
tokens = None | |||
if os.path.exists(config_path): | |||
with open(config_path, encoding="utf-8") as f: | |||
tokens = json.load(f) | |||
if tokens: | |||
refresh_tokens = requests.post( | |||
f"{url}auth/jwt/refresh/", | |||
json=tokens, | |||
) | |||
if refresh_tokens.status_code == 200: | |||
token_refreshed = refresh_tokens.json() | |||
with open(config_path, "w", encoding="utf-8") as f: | |||
json.dump(token_refreshed, f, ensure_ascii=False, indent=4) | |||
return token_refreshed["access"] | |||
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) | |||
default_lesspass_profile = { | |||
"site": "lesspass.com", | |||
"login": email, | |||
"lowercase": True, | |||
"uppercase": True, | |||
"digits": True, | |||
"symbols": True, | |||
"length": 16, | |||
"counter": 1, | |||
} | |||
encrypted_password = generate_password(default_lesspass_profile, master_password) | |||
r = requests.post( | |||
f"{url}auth/jwt/create/", | |||
json={"email": email, "password": encrypted_password}, | |||
) | |||
if r.status_code != 200: | |||
print("Wrong email and/or master password") | |||
sys.exit(1) | |||
tokens = r.json() | |||
with open(config_path, "w", encoding="utf-8") as f: | |||
json.dump(tokens, f, ensure_ascii=False, indent=4) | |||
print(f"Access and refresh tokens saved in {config_path}") | |||
return tokens["access"] | |||
def save_password_profiles(config_home_path, url, backup_path): | |||
token = _login(config_home_path, url) | |||
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) | |||
print(f"Password profiles saved in {backup_path}") | |||
def load_password_profiles(config_home_path, url, backup_path): | |||
token = _login(config_home_path, url) | |||
with open(backup_path, encoding="utf-8") as f: | |||
data = json.load(f) | |||
for password_profile in data["results"]: | |||
get_password_profiles = requests.get( | |||
f"{url}passwords/{password_profile['id']}/", | |||
headers={"Authorization": "JWT %s" % token}, | |||
) | |||
if get_password_profiles.status_code == 200: | |||
print( | |||
f"Password profile for site {password_profile['site']} and login {password_profile['login']} already exists. Skipping." | |||
) | |||
elif get_password_profiles.status_code == 404: | |||
create_password = requests.post( | |||
f"{url}passwords/", | |||
json=password_profile, | |||
headers={"Authorization": "JWT %s" % token}, | |||
) | |||
print(create_password.text) | |||
create_password.raise_for_status() | |||
print( | |||
f"Password profile for site {password_profile['site']} and login {password_profile['login']} successfully imported." | |||
) | |||
else: | |||
get_password_profiles.raise_for_status() | |||
get_password_profiles = requests.get( | |||
f"{url}passwords/", headers={"Authorization": "JWT %s" % token} | |||
) | |||
with open(backup_path, "w", encoding="utf-8") as f: | |||
json.dump(get_password_profiles.json(), f, ensure_ascii=False, indent=4) |
@@ -11,6 +11,7 @@ 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 | |||
signal.signal(signal.SIGINT, lambda s, f: sys.exit(0)) | |||
@@ -24,6 +25,12 @@ def main(args=sys.argv[1:]): | |||
) | |||
sys.exit(3) | |||
if args.save_path: | |||
return save_password_profiles(args.config_home_path, args.url, args.save_path) | |||
if args.load_path: | |||
return load_password_profiles(args.config_home_path, args.url, args.load_path) | |||
if args.prompt: | |||
if not args.site: | |||
args.site = getpass.getpass("Site: ") | |||
@@ -1 +1 @@ | |||
__version__ = "10.0.2" | |||
__version__ = "10.1.0" |
@@ -1 +1 @@ | |||
. |
@@ -16,7 +16,7 @@ setuptools.setup( | |||
description="LessPass stateless password generator", | |||
long_description=long_description, | |||
long_description_content_type="text/markdown", | |||
install_requires=[], | |||
install_requires=["requests"], | |||
entry_points=""" | |||
[console_scripts] | |||
lesspass=lesspass.core:main | |||
@@ -118,3 +118,34 @@ class TestParseArgs(unittest.TestCase): | |||
def test_parse_no_fingerprint(self): | |||
self.assertTrue(parse_args(["site", "--no-fingerprint"]).no_fingerprint) | |||
self.assertFalse(parse_args(["site"]).no_fingerprint) | |||
def test_parse_args_save_path(self): | |||
self.assertEqual( | |||
parse_args(["--save", "/tmp/profiles.json"]).save_path, "/tmp/profiles.json" | |||
) | |||
self.assertEqual(parse_args(["site"]).save_path, None) | |||
with patch.dict("os.environ", {"XDG_CONFIG_HOME": "/tmp"}): | |||
self.assertEqual( | |||
parse_args(["--save"]).save_path, "/tmp/lesspass/profiles.json" | |||
) | |||
def test_parse_args_load_path(self): | |||
self.assertEqual( | |||
parse_args(["--load", "/tmp/profiles.json"]).load_path, "/tmp/profiles.json" | |||
) | |||
self.assertEqual(parse_args(["site"]).load_path, None) | |||
def test_parse_args_config_home_path(self): | |||
self.assertTrue(parse_args([]).config_home_path.endswith("/.config/lesspass")) | |||
def test_parse_args_XDG_CONFIG_HOME_env_variable(self): | |||
with patch.dict("os.environ", {"XDG_CONFIG_HOME": "/tmp"}): | |||
self.assertEqual(parse_args([]).config_home_path, "/tmp/lesspass") | |||
def test_parse_args_default_base_url(self): | |||
self.assertEqual(parse_args([]).url, "https://api.lesspass.com/") | |||
def test_parse_args_default_base_url(self): | |||
self.assertEqual( | |||
parse_args(["--url", "https://example.org/"]).url, "https://example.org/" | |||
) |