@@ -26,9 +26,10 @@ copyright: | |||
This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law | |||
""" | |||
def range_type(value_string): | |||
value = int(value_string) | |||
if value not in range(5, 35+1): | |||
if value not in range(5, 35 + 1): | |||
raise argparse.ArgumentTypeError("%s is out of range, choose in [5-35]" % value) | |||
return value | |||
@@ -59,10 +60,10 @@ def parse_args(args): | |||
"-L", | |||
"--length", | |||
default=16, | |||
choices=range(5, 35+1), | |||
choices=range(5, 35 + 1), | |||
type=range_type, | |||
help="password length (default: 16, min: 5, max: 35)", | |||
metavar='[5-35]' | |||
metavar="[5-35]", | |||
) | |||
parser.add_argument( | |||
"-C", "--counter", default=1, type=int, help="password counter (default: 1)" | |||
@@ -82,16 +83,13 @@ 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( | |||
"-f", | |||
"--fingerprint", | |||
dest="fingerprint", | |||
"--no-fingerprint", | |||
dest="no_fingerprint", | |||
action="store_true", | |||
help="show visual fingerprint of password as you type it" | |||
help="hide visual fingerprint of the master password when you type", | |||
) | |||
parser.add_argument( | |||
"-f", | |||
@@ -10,7 +10,7 @@ from lesspass.cli import parse_args | |||
from lesspass.profile import create_profile | |||
from lesspass.password import generate_password | |||
from lesspass.clipboard import copy, get_system_copy_command | |||
from lesspass.visual_fingerprint import getpass_with_visual_fingerprint | |||
from lesspass.fingerprint import getpass_with_fingerprint | |||
signal.signal(signal.SIGINT, lambda s, f: sys.exit(0)) | |||
@@ -36,10 +36,10 @@ def main(args=sys.argv[1:]): | |||
if not args.master_password: | |||
prompt = "Master Password: " | |||
if args.fingerprint: | |||
args.master_password = getpass_with_visual_fingerprint(prompt) | |||
else: | |||
if args.no_fingerprint: | |||
args.master_password = getpass.getpass(prompt) | |||
else: | |||
args.master_password = getpass_with_fingerprint(prompt) | |||
if not args.master_password: | |||
print("error: argument MASTER_PASSWORD is required but was not provided") | |||
@@ -0,0 +1,153 @@ | |||
import hmac | |||
import hashlib | |||
import sys | |||
import os | |||
import random | |||
import tty | |||
import termios | |||
import threading | |||
if os.name == "nt": | |||
import msvcrt | |||
icon_names = [ | |||
["fa-hashtag", "#️"], | |||
["fa-heart", "❤️"], | |||
["fa-hotel", "🏨"], | |||
["fa-university", "🎓"], | |||
["fa-plug", "🔌"], | |||
["fa-ambulance", "🚑"], | |||
["fa-bus", "🚌"], | |||
["fa-car", "🚗"], | |||
["fa-plane", "✈️"], | |||
["fa-rocket", "🚀"], | |||
["fa-ship", "🚢"], | |||
["fa-subway", "🚇"], | |||
["fa-truck", "🚚"], | |||
["fa-jpy", "💴"], | |||
["fa-eur", "💶"], | |||
["fa-btc", "₿"], | |||
["fa-usd", "💵"], | |||
["fa-gbp", "💷"], | |||
["fa-archive", "🗄️"], | |||
["fa-area-chart", "📈"], | |||
["fa-bed", "🛏️"], | |||
["fa-beer", "🍺"], | |||
["fa-bell", "🔔"], | |||
["fa-binoculars", "🔭"], | |||
["fa-birthday-cake", "🎂"], | |||
["fa-bomb", "💣"], | |||
["fa-briefcase", "💼"], | |||
["fa-bug", "🐛"], | |||
["fa-camera", "📷"], | |||
["fa-cart-plus", "🛒"], | |||
["fa-certificate", "⭐"], | |||
["fa-coffee", "☕"], | |||
["fa-cloud", "☁️"], | |||
["fa-coffee", "☕"], | |||
["fa-comment", "🗨️"], | |||
["fa-cube", "📦"], | |||
["fa-cutlery", "🍴"], | |||
["fa-database", "🖥️"], | |||
["fa-diamond", "💎"], | |||
["fa-exclamation-circle", "❗"], | |||
["fa-eye", "👁️"], | |||
["fa-flag", "🏁"], | |||
["fa-flask", "⚗️"], | |||
["fa-futbol-o", "⚽"], | |||
["fa-gamepad", "🎮"], | |||
["fa-graduation-cap", "🎓"], | |||
] | |||
MAX_ICON_WIDTH = max([len(icon) for icon in icon_names]) | |||
def get_icon_name(hash_slice): | |||
index = int(hash_slice, base=16) % len(icon_names) | |||
return icon_names[index][1] | |||
def get_fingerprint(hmac_sha256): | |||
hash1, hash2, hash3 = hmac_sha256[0:6], hmac_sha256[6:12], hmac_sha256[12:18] | |||
fingerprint = [] | |||
fingerprint.append(get_icon_name(hash1)) | |||
fingerprint.append(get_icon_name(hash2)) | |||
fingerprint.append(get_icon_name(hash3)) | |||
return fingerprint | |||
def get_hmac_sha256(password_bytes): | |||
return hmac.new(password_bytes, digestmod=hashlib.sha256).hexdigest() | |||
def get_mnemonic(password): | |||
fingerprint = get_fingerprint(get_hmac_sha256(password.encode("utf-8"))) | |||
return "{fingerprint_1} {fingerprint_2} {fingerprint_3}".format( | |||
fingerprint_1=fingerprint[0], | |||
fingerprint_2=fingerprint[1], | |||
fingerprint_3=fingerprint[2], | |||
) | |||
def get_fake_mnemonic(): | |||
fake_password = "".join( | |||
chr(random.randrange(ord("a"), ord("z") + 1)) for i in range(16) | |||
) | |||
return get_mnemonic(fake_password) | |||
def getchar(): | |||
# Returns a single character from standard input | |||
# Credit for this function: (not written by file author) | |||
# jasonrdsouza & mvaganov https://gist.github.com/jasonrdsouza/1901709 | |||
ch = "" | |||
if os.name == "nt": # Windows | |||
ch = msvcrt.getch() | |||
else: | |||
fd = sys.stdin.fileno() | |||
old_settings = termios.tcgetattr(fd) | |||
try: | |||
tty.setraw(sys.stdin.fileno()) | |||
ch = sys.stdin.read(1) | |||
finally: | |||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) | |||
if ord(ch) == 3: # handle ctrl+C | |||
sys.stdout.write("\n") | |||
quit() | |||
return ch | |||
def getpass_with_fingerprint(prompt): | |||
global semaphore | |||
global stdout_lock | |||
sys.stdout.write(prompt) | |||
sys.stdout.flush() | |||
password = "" | |||
delayed_write = None | |||
while True: | |||
c = getchar() | |||
if delayed_write: | |||
delayed_write.cancel() | |||
if c == "\r": | |||
sys.stdout.write(f"\r{prompt}{get_fake_mnemonic()}\n") | |||
break | |||
elif c == "\x7f": # backspace | |||
password = password[:-1] | |||
else: | |||
password += c | |||
if len(password) != 0: | |||
delayed_write = threading.Timer( | |||
0.5, lambda: sys.stdout.write(f"\r{prompt}{get_mnemonic(password)}") | |||
) | |||
delayed_write.start() | |||
sys.stdout.write(f"\r{prompt}{get_fake_mnemonic()}") | |||
else: | |||
sys.stdout.write(f"\r{prompt}{' '*(MAX_ICON_WIDTH*3)}") | |||
return password | |||
if __name__ == "__main__": | |||
getpass_with_fingerprint("Master password: ") |
@@ -1 +1 @@ | |||
__version__ = "9.2.0" | |||
__version__ = "9.3.0" |
@@ -1,281 +0,0 @@ | |||
import hmac | |||
import hashlib | |||
import sys | |||
import os | |||
import random | |||
import tty | |||
import termios | |||
import threading | |||
if os.name == "nt": | |||
import msvcrt | |||
def user_has_icons_in_terminal(): | |||
return os.path.exists(os.path.expanduser("~/.fonts/icons-in-terminal.ttf")) | |||
colors_256 = [ | |||
"\x1b[38;5;248m", # black #000000 | |||
"\x1b[38;5;30m", # dark cyan #074750 | |||
"\x1b[38;5;37m", # mid cyan #009191 | |||
"\x1b[38;5;211m", # bright pink #FF6CB6 | |||
"\x1b[38;5;219m", # cotton candy pink #FFB5DA | |||
"\x1b[38;5;55m", # mid purple #490092 | |||
"\x1b[38;5;69m", # sky blue #006CDB | |||
"\x1b[38;5;140m", # lavendar #B66DFF | |||
"\x1b[38;5;81m", # baby blue #6DB5FE | |||
"\x1b[38;5;153m", # white blue #B5DAFE | |||
"\x1b[38;5;88m", # blood red #920000 | |||
"\x1b[38;5;94m", # burnt orange #924900 | |||
"\x1b[38;5;172m", # orange #DB6D00 | |||
"\x1b[38;5;82m", # lime green #24FE23 | |||
] | |||
unicode_colors = [ | |||
"\x1b[31;40m", # black #000000 | |||
"\x1b[36;40m", # dark cyan #074750 | |||
"\x1b[36;40m", # mid cyan #009191 | |||
"\x1b[35;40m", # bright pink #FF6CB6 | |||
"\x1b[35;40m", # cotton candy pink #FFB5DA | |||
"\x1b[35;40m", # mid purple #490092 | |||
"\x1b[34;40m", # sky blue #006CDB | |||
"\x1b[35;40m", # lavendar #B66DFF | |||
"\x1b[34;40m", # baby blue #6DB5FE | |||
"\x1b[34;40m", # white blue #B5DAFE | |||
"\x1b[31;40m", # blood red #920000 | |||
"\x1b[31;40m", # burnt orange #924900 | |||
"\x1b[33;40m", # orange #DB6D00 | |||
"\x1b[32;40m", # lime green #24FE23 | |||
] | |||
fallback_colors = [ | |||
"\x1b[37;40m", # black #000000 | |||
"\x1b[30;46m", # dark cyan #074750 | |||
"\x1b[30;46m", # mid cyan #009191 | |||
"\x1b[30;45m", # bright pink #FF6CB6 | |||
"\x1b[30;45m", # cotton candy pink #FFB5DA | |||
"\x1b[30;45m", # mid purple #490092 | |||
"\x1b[30;44m", # sky blue #006CDB | |||
"\x1b[30;45m", # lavendar #B66DFF | |||
"\x1b[30;44m", # baby blue #6DB5FE | |||
"\x1b[30;44m", # white blue #B5DAFE | |||
"\x1b[30;41m", # blood red #920000 | |||
"\x1b[30;41m", # burnt orange #924900 | |||
"\x1b[30;43m", # orange #DB6D00 | |||
"\x1b[30;42m", # lime green #24FE23 | |||
] | |||
icon_names = [ | |||
"hashtag", | |||
"heart", | |||
"hotel", | |||
"university", | |||
"plug", | |||
"ambulance", | |||
"bus", | |||
"car", | |||
"plane", | |||
"rocket", | |||
"ship", | |||
"subway", | |||
"truck", | |||
"japanese yen", | |||
"euro", | |||
"bitcoin", | |||
"U.S. dollar", | |||
"British pound", | |||
"archive", | |||
"area-chart", | |||
"bed", | |||
"beer", | |||
"bell", | |||
"binoculars", | |||
"birthday-cake", | |||
"bomb", | |||
"briefcase", | |||
"bug", | |||
"camera", | |||
"cart-plus", | |||
"certificate", | |||
"coffee", | |||
"cloud", | |||
"coffee", | |||
"comment", | |||
"cube", | |||
"cutlery", | |||
"database", | |||
"diamond", | |||
"exclamation-circle", | |||
"eye", | |||
"flag", | |||
"flask", | |||
"futbol", | |||
"gamepad", | |||
"graduation-cap", | |||
] | |||
icons_in_terminal_icons = { | |||
"hashtag": "\ue33e", | |||
"heart": "\ue0e5", | |||
"hotel": "\ue268", # NOTE: "fa-building" substituted | |||
"university": "\ue644", | |||
"plug": "\ue29d", | |||
"ambulance": "\ue1bf", | |||
"bus": "\ue2bc", | |||
"car": "\ue587", | |||
"plane": "\ue14c", | |||
"rocket": "\ue1f7", | |||
"ship": "\ue2ce", | |||
"subway": "\ue2eb", | |||
"truck": "\ue199", | |||
"japanese yen": "\uec97", # NOTE: Linea circled yen icon substituted | |||
"euro": "\ue714", | |||
"bitcoin": "\ue21a", | |||
"U.S. dollar": "\ue215", | |||
"British pound": "\uec89", # NOTE: Linea circled pound sterling icon substituted | |||
"archive": "\ue244", | |||
"area-chart": "\ue2b4", | |||
"bed": "\ue2e8", | |||
"beer": "\ue1c2", | |||
"bell": "\ue1b9", | |||
"binoculars": "\ue29c", | |||
"birthday-cake": "\ue2b3", | |||
"bomb": "\ue299", | |||
"briefcase": "\ue187", | |||
"bug": "\ue245", | |||
"camera": "\ue10e", | |||
"cart-plus": "\ue2cb", | |||
"certificate": "\ue17a", | |||
"coffee": "\ue1ba", | |||
"cloud": "\ue18b", | |||
"comment": "\ue14f", | |||
"cube": "\ue26c", | |||
"cutlery": "\ue1bb", | |||
"database": "\ue279", | |||
"diamond": "\ue2cd", | |||
"exclamation-circle": "\ue145", | |||
"eye": "\ue149", | |||
"flag": "\ue102", | |||
"flask": "\ue18c", | |||
"futbol": "\ue29a", | |||
"gamepad": "\ue1df", | |||
"graduation-cap": "\ue259", | |||
} | |||
MAX_ICON_WIDTH = max([len(icon) for icon in icon_names]) | |||
def get_list_entry(hash_slice, lookup_list): | |||
index = int(hash_slice, base=16) % len(lookup_list) | |||
return lookup_list[index] | |||
def get_color(hash_slice): | |||
if user_has_icons_in_terminal(): | |||
if "256" in os.environ["TERM"]: | |||
colors = colors_256 | |||
else: | |||
colors = unicode_colors | |||
else: | |||
colors = fallback_colors | |||
return get_list_entry(hash_slice, colors) | |||
def get_icon(hash_slice): | |||
icon_name = get_list_entry(hash_slice, icon_names) | |||
return ( | |||
icons_in_terminal_icons[icon_name] | |||
if user_has_icons_in_terminal() | |||
else icon_name | |||
) | |||
def get_fingerprint(hmac_sha256): | |||
hash1, hash2, hash3 = hmac_sha256[0:6], hmac_sha256[6:12], hmac_sha256[12:18] | |||
fingerprint = [] | |||
fingerprint.append({"color": get_color(hash1), "icon": get_icon(hash1)}) | |||
fingerprint.append({"color": get_color(hash2), "icon": get_icon(hash2)}) | |||
fingerprint.append({"color": get_color(hash3), "icon": get_icon(hash3)}) | |||
return fingerprint | |||
def get_hmac_sha256(password_bytes): | |||
return hmac.new(password_bytes, digestmod=hashlib.sha256).hexdigest() | |||
def get_fixed_width_text(fingerprint_entry): | |||
color, icon = fingerprint_entry["color"], fingerprint_entry["icon"] | |||
if user_has_icons_in_terminal(): | |||
text = f"{color}{icon} \x1b[0m" | |||
else: | |||
text = f"{color}{icon}{' '*(MAX_ICON_WIDTH-len(icon))}\x1b[0m" | |||
return text | |||
def get_mnemonic(password): | |||
fingerprint = get_fingerprint(get_hmac_sha256(password.encode("utf-8"))) | |||
return ( | |||
f"[ {get_fixed_width_text(fingerprint[0])} " | |||
f"{get_fixed_width_text(fingerprint[1])} " | |||
f"{get_fixed_width_text(fingerprint[2])} ]" | |||
) | |||
def get_fake_mnemonic(): | |||
fake_password = "".join( | |||
chr(random.randrange(ord("a"), ord("z") + 1)) for i in range(16) | |||
) | |||
return get_mnemonic(fake_password) | |||
def getchar(): | |||
# Returns a single character from standard input | |||
# Credit for this function: (not written by file author) | |||
# jasonrdsouza & mvaganov https://gist.github.com/jasonrdsouza/1901709 | |||
ch = "" | |||
if os.name == "nt": # Windows | |||
ch = msvcrt.getch() | |||
else: | |||
fd = sys.stdin.fileno() | |||
old_settings = termios.tcgetattr(fd) | |||
try: | |||
tty.setraw(sys.stdin.fileno()) | |||
ch = sys.stdin.read(1) | |||
finally: | |||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) | |||
if ord(ch) == 3: # handle ctrl+C | |||
sys.stdout.write("\n") | |||
quit() | |||
return ch | |||
def getpass_with_visual_fingerprint(prompt): | |||
global semaphore | |||
global stdout_lock | |||
sys.stdout.write(prompt) | |||
sys.stdout.flush() | |||
password = "" | |||
delayed_write = None | |||
while True: | |||
c = getchar() | |||
if delayed_write: | |||
delayed_write.cancel() | |||
if c == "\r": | |||
sys.stdout.write(f"\r{prompt}{get_fake_mnemonic()}\n") | |||
break | |||
elif c == "\x7f": # backspace | |||
password = password[:-1] | |||
else: | |||
password += c | |||
if len(password) != 0: | |||
delayed_write = threading.Timer( | |||
0.5, lambda: sys.stdout.write(f"\r{prompt}{get_mnemonic(password)}") | |||
) | |||
delayed_write.start() | |||
sys.stdout.write(f"\r{prompt}{get_fake_mnemonic()}") | |||
else: | |||
sys.stdout.write(f"\r{prompt}{' '*(MAX_ICON_WIDTH*3)}") | |||
return password | |||
if __name__ == "__main__": | |||
getpass_with_visual_fingerprint("Master password: ") |
@@ -114,3 +114,7 @@ class TestParseArgs(unittest.TestCase): | |||
def test_parse_args_exclude_single_and_double_quote(self): | |||
self.assertEqual(parse_args(["site", "--exclude", "\"'"]).exclude, "\"'") | |||
def test_parse_no_fingerprint(self): | |||
self.assertTrue(parse_args(["site", "--no-fingerprint"]).no_fingerprint) | |||
self.assertFalse(parse_args(["site"]).no_fingerprint) |
@@ -0,0 +1,7 @@ | |||
from lesspass.fingerprint import get_mnemonic | |||
def test_get_fingerprint(): | |||
assert get_mnemonic(b"password") == "⚗️🗄️🍺" | |||
assert get_mnemonic(b"Password12345") == "🚑🛏️💷" | |||
assert get_mnemonic(b"Ma$$W0rld!@#$%^&*()<gamma>") == "📈💷💷" |
@@ -1,53 +0,0 @@ | |||
from lesspass.visual_fingerprint import ( | |||
get_fingerprint, | |||
get_hmac_sha256 | |||
) | |||
def get_fingerprint_from_password(password_bytes): | |||
return get_fingerprint(get_hmac_sha256(password_bytes)) | |||
def test_get_fingerprint(): | |||
assert get_fingerprint_from_password(b'password') == [ | |||
{ | |||
"color": "\x1b[30;45m", # => #FFB5DA | |||
"icon": "flask" | |||
}, | |||
{ | |||
"color": "\x1b[30;46m", # => #009191 | |||
"icon": "archive" | |||
}, | |||
{ | |||
"color": "\x1b[30;44m", # => #B5DAFE | |||
"icon": "beer" | |||
} | |||
] | |||
assert get_fingerprint_from_password(b'Password12345') == [ | |||
{ | |||
"color": "\x1b[30;41m", # => #924900 | |||
"icon": "ambulance" | |||
}, | |||
{ | |||
"color": "\x1b[30;44m", # => #6DB5FE | |||
"icon": "bed" | |||
}, | |||
{ | |||
"color": "\x1b[30;45m", # => #FF6CB6 | |||
"icon": "British pound" | |||
} | |||
] | |||
assert get_fingerprint_from_password(b'Ma$$W0rld!@#$%^&*()<gamma>') == [ | |||
{ | |||
"color": "\x1b[30;44m", # => #B5DAFE | |||
"icon": "area-chart" | |||
}, | |||
{ | |||
"color": "\x1b[30;45m", # => #490092 | |||
"icon": "British pound" | |||
}, | |||
{ | |||
"color": "\x1b[30;41m", # => #924900 | |||
"icon": "British pound" | |||
} | |||
] |