Fixes https://github.com/lesspass/lesspass/issues/529tags/mobile-v9.5.3
@@ -2,7 +2,6 @@ import argparse | |||||
import os | import os | ||||
from lesspass import version | from lesspass import version | ||||
from lesspass import name | |||||
from lesspass import description | from lesspass import description | ||||
EXAMPLES = """ | 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): | def range_type(value_string): | ||||
value = int(value_string) | value = int(value_string) | ||||
if value not in range(5, 35 + 1): | if value not in range(5, 35 + 1): | ||||
@@ -83,7 +88,9 @@ def parse_args(args): | |||||
help="copy the password to clipboard", | help="copy the password to clipboard", | ||||
) | ) | ||||
parser.add_argument( | parser.add_argument( | ||||
"--exclude", default=None, help="exclude char from generated password", | |||||
"--exclude", | |||||
default=None, | |||||
help="exclude char from generated password", | |||||
) | ) | ||||
parser.add_argument( | parser.add_argument( | ||||
"--no-fingerprint", | "--no-fingerprint", | ||||
@@ -91,6 +98,34 @@ def parse_args(args): | |||||
action="store_true", | action="store_true", | ||||
help="hide visual fingerprint of the master password when you type", | 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 = parser.add_mutually_exclusive_group() | ||||
lowercase_group.add_argument( | lowercase_group.add_argument( | ||||
"-l", | "-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.password import generate_password | ||||
from lesspass.clipboard import copy, get_system_copy_command | from lesspass.clipboard import copy, get_system_copy_command | ||||
from lesspass.fingerprint import getpass_with_fingerprint | 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)) | signal.signal(signal.SIGINT, lambda s, f: sys.exit(0)) | ||||
@@ -24,6 +25,12 @@ def main(args=sys.argv[1:]): | |||||
) | ) | ||||
sys.exit(3) | 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 args.prompt: | ||||
if not args.site: | if not args.site: | ||||
args.site = getpass.getpass("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", | description="LessPass stateless password generator", | ||||
long_description=long_description, | long_description=long_description, | ||||
long_description_content_type="text/markdown", | long_description_content_type="text/markdown", | ||||
install_requires=[], | |||||
install_requires=["requests"], | |||||
entry_points=""" | entry_points=""" | ||||
[console_scripts] | [console_scripts] | ||||
lesspass=lesspass.core:main | lesspass=lesspass.core:main | ||||
@@ -118,3 +118,34 @@ class TestParseArgs(unittest.TestCase): | |||||
def test_parse_no_fingerprint(self): | def test_parse_no_fingerprint(self): | ||||
self.assertTrue(parse_args(["site", "--no-fingerprint"]).no_fingerprint) | self.assertTrue(parse_args(["site", "--no-fingerprint"]).no_fingerprint) | ||||
self.assertFalse(parse_args(["site"]).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/" | |||||
) |