Browse Source

Add icons fingerprint

closes: #402
closes: #442
pull/544/head
Chandler DeLoach 5 years ago
committed by Guillaume Vincent
parent
commit
9226608f10
4 changed files with 356 additions and 3 deletions
  1. +14
    -0
      cli/lesspass/cli.py
  2. +8
    -3
      cli/lesspass/core.py
  3. +281
    -0
      cli/lesspass/visual_fingerprint.py
  4. +53
    -0
      cli/tests/test_visual_fingerprint.py

+ 14
- 0
cli/lesspass/cli.py View File

@@ -86,6 +86,20 @@ def parse_args(args):
default=None,
help="exclude char from generated password",
)
parser.add_argument(
"-f",
"--fingerprint",
dest="fingerprint",
action="store_true",
help="show visual fingerprint of password as you type it"
)
parser.add_argument(
"-f",
"--fingerprint",
dest="fingerprint",
action="store_true",
help="show visual fingerprint of password as you type it"
)

lowercase_group = parser.add_mutually_exclusive_group()
lowercase_group.add_argument(


+ 8
- 3
cli/lesspass/core.py View File

@@ -10,6 +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

signal.signal(signal.SIGINT, lambda s, f: sys.exit(0))

@@ -29,14 +30,18 @@ def main(args=sys.argv[1:]):
if not args.login:
args.login = getpass.getpass("Login: ")

if not args.master_password:
args.master_password = getpass.getpass("Master Password: ")

if not args.site:
print("error: argument SITE is required but was not provided.")
sys.exit(4)

if not args.master_password:
prompt = "Master Password: "
if args.fingerprint:
args.master_password = getpass_with_visual_fingerprint(prompt)
else:
args.master_password = getpass.getpass(prompt)

if not args.master_password:
print("error: argument MASTER_PASSWORD is required but was not provided")
sys.exit(5)



+ 281
- 0
cli/lesspass/visual_fingerprint.py View File

@@ -0,0 +1,281 @@
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: ")

+ 53
- 0
cli/tests/test_visual_fingerprint.py View File

@@ -0,0 +1,53 @@
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"
}
]

Loading…
Cancel
Save