@@ -1,2 +1,2 @@ | |||
name = "lesspass" | |||
description = "Lesspass is a stateless password manager." | |||
description = "LessPass is a stateless password manager." |
@@ -5,8 +5,7 @@ from lesspass import version | |||
from lesspass import name | |||
from lesspass import description | |||
examples = \ | |||
""" | |||
EXAMPLES = """ | |||
examples: | |||
# no symbols | |||
lesspass site login masterpassword --no-symbols | |||
@@ -21,84 +20,107 @@ examples: | |||
LESSPASS_MASTER_PASSWORD="masterpassword" lesspass site login | |||
""" | |||
copyright = \ | |||
""" | |||
COPYRIGHT = """ | |||
copyright: | |||
Copyright © 2018 Guillaume Vincent <contact@lesspass.com>. License GPLv3: GNU GPL version 3 <https://gnu.org/licenses/gpl.html>. | |||
This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law | |||
""" | |||
def parse_args(args): | |||
parser = argparse.ArgumentParser( | |||
# we override usage here to match original help output | |||
# and to indicate SITE as a required argument, either via | |||
# cli or via the prompt flag | |||
usage="lesspass SITE [LOGIN] [MASTER_PASSWORD] [OPTIONS]", | |||
description=description, | |||
epilog=examples+copyright, | |||
formatter_class=argparse.RawDescriptionHelpFormatter) | |||
parser.add_argument("-v", "--version", action="version", | |||
version=version.__version__) | |||
# technically this is required, but we can't require it here because | |||
# the user can still provide this via --prompt | |||
parser.add_argument("site", | |||
nargs="?", | |||
help="site used in the password generation. " + | |||
"(required)") | |||
parser.add_argument("login", nargs="?", | |||
help="login used in the password generation." + | |||
" Default to '' if not provided") | |||
epilog=EXAMPLES + COPYRIGHT, | |||
formatter_class=argparse.RawDescriptionHelpFormatter, | |||
) | |||
parser.add_argument( | |||
"-v", "--version", action="version", version=version.__version__ | |||
) | |||
parser.add_argument("site", help="site used in the password generation (required)") | |||
parser.add_argument( | |||
"login", nargs="?", help="login used in the password generation. Default to ''." | |||
) | |||
parser.add_argument( | |||
"master_password", | |||
default=os.environ.get("LESSPASS_MASTER_PASSWORD", None), | |||
nargs="?", | |||
help="master password used in password generation. Default " + | |||
"to LESSPASS_MASTER_PASSWORD env variable or prompt." | |||
) | |||
parser.add_argument("-L", "--length", default=16, type=int, | |||
help="password length (default: 16, max: 35)") | |||
parser.add_argument("-C", "--counter", default=1, type=int, | |||
help="password counter (default: 1)") | |||
parser.add_argument("-p", "--prompt", dest="prompt", | |||
action="store_true", | |||
help="prompt for values interactively") | |||
help="master password used in password generation. Default to LESSPASS_MASTER_PASSWORD env variable or prompt.", | |||
) | |||
parser.add_argument( | |||
"-c", "--copy", "--clipboard", dest="clipboard", action="store_true", | |||
help="attempt to copy to password to clipboard" | |||
"-L", | |||
"--length", | |||
default=16, | |||
type=int, | |||
help="password length (default: 16, max: 35)", | |||
) | |||
parser.add_argument( | |||
"-C", "--counter", default=1, type=int, help="password counter (default: 1)" | |||
) | |||
parser.add_argument( | |||
"-p", | |||
"--prompt", | |||
dest="prompt", | |||
action="store_true", | |||
help="prompt for values interactively", | |||
) | |||
parser.add_argument( | |||
"-c", | |||
"--copy", | |||
dest="clipboard", | |||
action="store_true", | |||
help="copy to password to clipboard", | |||
) | |||
lowercase_excl = parser.add_mutually_exclusive_group() | |||
lowercase_excl.add_argument("-l", "--lowercase", | |||
help="add lowercase in password", | |||
dest="l", | |||
action="store_true") | |||
lowercase_excl.add_argument("--no-lowercase", | |||
help="remove lowercase from password", | |||
dest="nl", | |||
action="store_true") | |||
uppercase_excl = parser.add_mutually_exclusive_group() | |||
uppercase_excl.add_argument("-u", "--uppercase", dest="u", | |||
help="add uppercase in password", | |||
action="store_true") | |||
uppercase_excl.add_argument("--no-uppercase", dest="nu", | |||
help="remove uppercase from password", | |||
action="store_true") | |||
lowercase_group = parser.add_mutually_exclusive_group() | |||
lowercase_group.add_argument( | |||
"-l", | |||
"--lowercase", | |||
help="add lowercase in password", | |||
dest="l", | |||
action="store_true", | |||
) | |||
lowercase_group.add_argument( | |||
"--no-lowercase", | |||
help="remove lowercase from password", | |||
dest="nl", | |||
action="store_true", | |||
) | |||
digits_excl = parser.add_mutually_exclusive_group() | |||
uppercase_group = parser.add_mutually_exclusive_group() | |||
uppercase_group.add_argument( | |||
"-u", | |||
"--uppercase", | |||
dest="u", | |||
help="add uppercase in password", | |||
action="store_true", | |||
) | |||
uppercase_group.add_argument( | |||
"--no-uppercase", | |||
dest="nu", | |||
help="remove uppercase from password", | |||
action="store_true", | |||
) | |||
digits_excl.add_argument("-d", "--digits", dest="d", | |||
help="add digits in password", | |||
action="store_true") | |||
digits_excl.add_argument("--no-digits", dest="nd", | |||
help="remove digits from password", | |||
action="store_true") | |||
digits_group = parser.add_mutually_exclusive_group() | |||
digits_group.add_argument( | |||
"-d", "--digits", dest="d", help="add digits in password", action="store_true" | |||
) | |||
digits_group.add_argument( | |||
"--no-digits", | |||
dest="nd", | |||
help="remove digits from password", | |||
action="store_true", | |||
) | |||
symbols_excl = parser.add_mutually_exclusive_group() | |||
symbols_excl.add_argument("-s", "--symbols", dest="s", | |||
help="add symbols in password", | |||
action="store_true") | |||
symbols_excl.add_argument("--no-symbols", dest="ns", | |||
help="remove symbols from password", | |||
action="store_true") | |||
symbols_group = parser.add_mutually_exclusive_group() | |||
symbols_group.add_argument( | |||
"-s", "--symbols", dest="s", help="add symbols in password", action="store_true" | |||
) | |||
symbols_group.add_argument( | |||
"--no-symbols", | |||
dest="ns", | |||
help="remove symbols from password", | |||
action="store_true", | |||
) | |||
return parser.parse_args(args) |
@@ -36,6 +36,7 @@ commands = { | |||
"xclip": ["xclip", "-selection", "clipboard"], | |||
} | |||
def copy(text): | |||
command = get_system_copy_command() | |||
if command is None: | |||
@@ -12,11 +12,14 @@ from lesspass.clipboard import copy, get_system_copy_command | |||
signal.signal(signal.SIGINT, lambda s, f: sys.exit(0)) | |||
def main(args=sys.argv[1:]): | |||
args = parse_args(args) | |||
if args.clipboard and not get_system_copy_command(): | |||
print("ERROR To use the option -c (--copy) you need pbcopy " + | |||
"on OSX, xsel or xclip on Linux, and clip on Windows") | |||
print( | |||
"ERROR To use the option -c (--copy) you need pbcopy " | |||
+ "on OSX, xsel or xclip on Linux, and clip on Windows" | |||
) | |||
sys.exit(3) | |||
if args.prompt: | |||
@@ -24,15 +27,12 @@ def main(args=sys.argv[1:]): | |||
args.login = getpass.getpass("Login: ") | |||
if not args.master_password: | |||
args.master_password = getpass.getpass("Master Password: ") | |||
# if by this point we don't have SITE or the master password, | |||
# we should stop. | |||
if not args.site: | |||
print("ERROR argument SITE is required but was not provided.") | |||
sys.exit(4) | |||
if not args.master_password: | |||
print("ERROR argument MASTER_PASSWORD is required but " + | |||
"was not provided") | |||
print("ERROR argument MASTER_PASSWORD is required but was not provided") | |||
sys.exit(5) | |||
profile, master_password = create_profile(args) | |||
@@ -54,5 +54,6 @@ def main(args=sys.argv[1:]): | |||
else: | |||
print(generated_password) | |||
if __name__ == '__main__': | |||
if __name__ == "__main__": | |||
main() |
@@ -2,8 +2,8 @@ def create_profile(args): | |||
profile = { | |||
"lowercase": False if args.nl else True, | |||
"uppercase": False if args.nu else True, | |||
"digits": False if args.nd else True, | |||
"symbols": False if args.ns else True, | |||
"digits": False if args.nd else True, | |||
"symbols": False if args.ns else True, | |||
"length": args.length, | |||
"counter": args.counter, | |||
"site": args.site, | |||
@@ -1 +1 @@ | |||
__version__ = '6.1.0' | |||
__version__ = "7.0.0" |
@@ -3,9 +3,10 @@ import pexpect | |||
import signal | |||
import time | |||
class TestInteraction(unittest.TestCase): | |||
def test_keyboard_interrupt(self): | |||
p = pexpect.spawn('python3 lesspass/core.py --prompt') | |||
p = pexpect.spawn("python3 lesspass/core.py --prompt") | |||
p.expect("Site: ") | |||
p.kill(signal.SIGINT) | |||
p.expect(pexpect.EOF) | |||
@@ -89,10 +89,11 @@ class TestPassword(unittest.TestCase): | |||
"login": "contact@example.org", | |||
"counter": 1, | |||
} | |||
master_password = 'password' | |||
master_password = "password" | |||
self.assertEqual( | |||
password._calc_entropy(password_profile, master_password), b'dc33d431bce2b01182c613382483ccdb0e2f66482cbba5e9d07dab34acc7eb1e' | |||
password._calc_entropy(password_profile, master_password), | |||
b"dc33d431bce2b01182c613382483ccdb0e2f66482cbba5e9d07dab34acc7eb1e", | |||
) | |||
def test_get_configured_rules_empty_when_no_rules_in_profile(self): | |||
@@ -108,67 +109,92 @@ class TestPassword(unittest.TestCase): | |||
"symbols": True, | |||
} | |||
self.assertListEqual(password._get_configured_rules(password_profile), ['uppercase', 'symbols']) | |||
self.assertListEqual( | |||
password._get_configured_rules(password_profile), ["uppercase", "symbols"] | |||
) | |||
def test_get_set_of_characters_without_rule(self): | |||
self.assertEqual(password._get_set_of_characters(), 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~') | |||
self.assertEqual( | |||
password._get_set_of_characters(), | |||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", | |||
) | |||
def test_get_set_of_characters_with_single_rule(self): | |||
self.assertEqual(password._get_set_of_characters(["lowercase"]), "abcdefghijklmnopqrstuvwxyz") | |||
self.assertEqual(password._get_set_of_characters(["uppercase"]), "ABCDEFGHIJKLMNOPQRSTUVWXYZ") | |||
self.assertEqual( | |||
password._get_set_of_characters(["lowercase"]), "abcdefghijklmnopqrstuvwxyz" | |||
) | |||
self.assertEqual( | |||
password._get_set_of_characters(["uppercase"]), "ABCDEFGHIJKLMNOPQRSTUVWXYZ" | |||
) | |||
self.assertEqual(password._get_set_of_characters(["digits"]), "0123456789") | |||
self.assertEqual(password._get_set_of_characters(["symbols"]), "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") | |||
self.assertEqual( | |||
password._get_set_of_characters(["symbols"]), | |||
"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", | |||
) | |||
def test_get_set_of_characters_with_several_rules(self): | |||
self.assertEqual(password._get_set_of_characters(["lowercase", "digits"]), "abcdefghijklmnopqrstuvwxyz0123456789") | |||
self.assertEqual( | |||
password._get_set_of_characters(["lowercase", "digits"]), | |||
"abcdefghijklmnopqrstuvwxyz0123456789", | |||
) | |||
def test_consume_entropy(self): | |||
entropy = b'dc33d431bce2b01182c613382483ccdb0e2f66482cbba5e9d07dab34acc7eb1e' | |||
entropy = b"dc33d431bce2b01182c613382483ccdb0e2f66482cbba5e9d07dab34acc7eb1e" | |||
password_value, password_entropy = password._consume_entropy( | |||
generated_password="", | |||
quotient=int(entropy, 16), | |||
set_of_characters="abcdefghijklmnopqrstuvwxyz0123456789", | |||
max_length=12 | |||
password_value, password_entropy = password._consume_entropy( | |||
generated_password="", | |||
quotient=int(entropy, 16), | |||
set_of_characters="abcdefghijklmnopqrstuvwxyz0123456789", | |||
max_length=12, | |||
) | |||
self.assertEqual(password_value, 'gsrwvjl3d0sn') | |||
self.assertEqual(password_entropy, 21019920789038193790619410818194537836313158091882651458040) | |||
self.assertEqual(password_value, "gsrwvjl3d0sn") | |||
self.assertEqual( | |||
password_entropy, | |||
21019920789038193790619410818194537836313158091882651458040, | |||
) | |||
def test_get_one_char_per_rule_without_rules(self): | |||
self.assertListEqual( | |||
password._get_one_char_per_rule( entropy=21019920789038193790619410818194537836313158091882651458040, rules=[] ), | |||
['', 21019920789038193790619410818194537836313158091882651458040] | |||
password._get_one_char_per_rule( | |||
entropy=21019920789038193790619410818194537836313158091882651458040, | |||
rules=[], | |||
), | |||
["", 21019920789038193790619410818194537836313158091882651458040], | |||
) | |||
def test_get_one_char_per_rule_with_several_rules(self): | |||
self.assertListEqual( | |||
password._get_one_char_per_rule( | |||
entropy=21019920789038193790619410818194537836313158091882651458040, | |||
rules=['lowercase', 'digits'] | |||
entropy=21019920789038193790619410818194537836313158091882651458040, | |||
rules=["lowercase", "digits"], | |||
), | |||
['a0', 80845849188608437656228503146902068601204454199548659454] | |||
["a0", 80845849188608437656228503146902068601204454199548659454], | |||
) | |||
def test_insert_string_pseudo_randomly(self): | |||
self.assertEqual( | |||
password._insert_string_pseudo_randomly( | |||
generated_password='gsrwvjl3d0sn', | |||
entropy=80845849188608437656228503146902068601204454199548659454, | |||
string='a0' | |||
), 'gsrwvjl03d0asn' | |||
generated_password="gsrwvjl3d0sn", | |||
entropy=80845849188608437656228503146902068601204454199548659454, | |||
string="a0", | |||
), | |||
"gsrwvjl03d0asn", | |||
) | |||
def test_render_password(self): | |||
password_profile = { | |||
password_profile = { | |||
"site": "example.org", | |||
"login": "contact@example.org", | |||
"digits": True, | |||
"lowercase": True, | |||
"length": 14, | |||
"counter": 1 | |||
"counter": 1, | |||
} | |||
master_password = 'password' | |||
master_password = "password" | |||
entropy = password._calc_entropy(password_profile, master_password) | |||
self.assertEqual(password._render_password(entropy, password_profile), 'gsrwvjl03d0asn') | |||
self.assertEqual( | |||
password._render_password(entropy, password_profile), "gsrwvjl03d0asn" | |||
) | |||
@@ -1,59 +0,0 @@ | |||
# import unittest | |||
# from lesspass.cli import parse_args | |||
# from lesspass.validator import validate_args | |||
# class TestValidateArgs(unittest.TestCase): | |||
# def test_validate_args_no_opposite_rules_lowercase(self): | |||
# error, message = validate_args(parse_args(["site", "-l", "--no-lowercase"])) | |||
# self.assertTrue(error) | |||
# self.assertTrue( | |||
# "Can't have -l (--lowercase) and --no-lowercase at the same time" in message | |||
# ) | |||
# def test_validate_args_no_opposite_rules_uppercase(self): | |||
# error, message = validate_args(parse_args(["site", "-u", "--no-uppercase"])) | |||
# self.assertTrue(error) | |||
# self.assertTrue( | |||
# "Can't have -u (--uppercase) and --no-uppercase at the same time" in message | |||
# ) | |||
# def test_validate_args_no_opposite_rules_digits(self): | |||
# error, message = validate_args(parse_args(["site", "-d", "--no-digits"])) | |||
# self.assertTrue(error) | |||
# self.assertTrue( | |||
# "Can't have -d (--digits) and --no-digits at the same time" in message | |||
# ) | |||
# def test_validate_args_no_opposite_rules_symbols(self): | |||
# error, message = validate_args(parse_args(["site", "-s", "--no-symbols"])) | |||
# self.assertTrue(error) | |||
# self.assertTrue( | |||
# "Can't have -s (--symbols) and --no-symbols at the same time" in message | |||
# ) | |||
# def test_validate_args_concat_errors(self): | |||
# _, message = validate_args( | |||
# parse_args(["site", "-u", "--no-uppercase", "-l", "--no-lowercase"]) | |||
# ) | |||
# self.assertTrue( | |||
# "Can't have -l (--lowercase) and --no-lowercase at the same time" in message | |||
# ) | |||
# self.assertTrue( | |||
# "Can't have -u (--uppercase) and --no-uppercase at the same time" in message | |||
# ) | |||
# def test_validate_args_no_site(self): | |||
# error, message = validate_args(parse_args([])) | |||
# self.assertTrue(error) | |||
# self.assertTrue( | |||
# "SITE is a required argument" in message | |||
# ) | |||
# def test_validate_args_site_optional_with_prompt(self): | |||
# error, message = validate_args(parse_args(["--prompt"])) | |||
# self.assertFalse(error) | |||
# self.assertTrue( | |||
# "SITE is a required argument" not in message | |||
# ) |