249 lines
8.2 KiB
Python
Executable File
249 lines
8.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import hashlib
|
|
import os
|
|
import random
|
|
import re
|
|
import string
|
|
import sys
|
|
import termios
|
|
import tty
|
|
|
|
__version__ = "20251126.a"
|
|
|
|
|
|
class bc:
|
|
m = "\033[35m"
|
|
b = "\033[34m"
|
|
g = "\033[32m"
|
|
y = "\033[33m"
|
|
r = "\033[31m"
|
|
c = "\033[36m"
|
|
k = "\033[30m\033[40m"
|
|
M = "\033[95m"
|
|
B = "\033[94m"
|
|
G = "\033[92m"
|
|
Y = "\033[93m"
|
|
R = "\033[91m"
|
|
C = "\033[96m"
|
|
bold = "\033[1m"
|
|
z = "\033[0m"
|
|
noblink = "\033[?25l"
|
|
blink = "\033[?25h"
|
|
|
|
def disable(self):
|
|
for x in "rgbcmyzkRGBCMY":
|
|
setattr(self, x, "")
|
|
setattr(self, "bold", "")
|
|
|
|
def format(self, s):
|
|
format_dict = {x: getattr(self, x) for x in "rgbcmyzkRGBCMY"}
|
|
return s.format(**format_dict)
|
|
|
|
|
|
class QAsk:
|
|
def __init__(self):
|
|
self.opts = self.get_opts()
|
|
self.c = bc()
|
|
if self.opts.no_color:
|
|
self.c.disable()
|
|
self.user_input = ""
|
|
self.eq = "▁▂▃▄▅▆▇█▇▆▅▄▃▂"
|
|
self.limits = (
|
|
(self.c.r, 2, "r"),
|
|
(self.c.y, 4, "y"),
|
|
(self.c.B, 7, "B"),
|
|
(self.c.c, 9, "c"),
|
|
(self.c.g, 12, "g"),
|
|
(self.c.G, 15, "G"),
|
|
)
|
|
self.color_hash = ((self.c.r, "r"), (self.c.g, "g"), (self.c.B, "B"), (self.c.c, "c"), (self.c.y, "y"))
|
|
self.no_pw = "•"
|
|
self.is_correct = "♥"
|
|
self.rand_chr = "■" # "╩╦═╬╧╤╪"
|
|
|
|
def get_opts(self):
|
|
parser = argparse.ArgumentParser(
|
|
description="Colorful password dialog",
|
|
epilog="Dialog printed to stderr, user input echoed to stdout, i.e. input=$( qaskpass )",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--title",
|
|
"-t",
|
|
action="store",
|
|
default=None,
|
|
help="Title for dialog. Use {G} to change color to bright green. Valid codes: rgbcmyzkRGBCMY",
|
|
)
|
|
parser.add_argument(
|
|
"-w", action="store", type=int, default=3, help="Width of display area. 0 to disable display."
|
|
)
|
|
parser.add_argument("--no-color", action="store_true", default=False, help="Disable colors")
|
|
parser.add_argument("--measure", action="store_true", default=False, help="Measure password goodness")
|
|
parser.add_argument("--eq", action="store_true", default=False, help="Visual aid EQ mode.")
|
|
parser.add_argument(
|
|
"--export",
|
|
action="store_true",
|
|
default=False,
|
|
help="output the --title to be used with given password, instead of the password",
|
|
)
|
|
parser.add_argument(
|
|
"--expect-sha256",
|
|
action="store",
|
|
default=None,
|
|
help="Show green dots when string matches the sha256 sum. Note: strip newlines before calculating!. Exitcode = 10 if checksum does not match.",
|
|
)
|
|
parser.add_argument("--version", action="version", version="%(prog)s {version}".format(version=__version__))
|
|
args = parser.parse_args()
|
|
return args
|
|
|
|
def pwscore(self):
|
|
if not self.opts.measure:
|
|
return random.randint(0, 18)
|
|
score = len(set(self.user_input)) / 5
|
|
simple = False
|
|
if re.search("[A-Z]", self.user_input):
|
|
score *= 2
|
|
simple = True
|
|
if re.search("[a-z]", self.user_input):
|
|
score *= 1.5
|
|
simple = True
|
|
if re.search("[0-9]", self.user_input):
|
|
score *= 1.5
|
|
simple = True
|
|
if not simple:
|
|
score *= 2
|
|
return score
|
|
|
|
def get_color_hash(self, i):
|
|
return self.color_hash[int(self.sha256[(1 + i * 2) % len(self.sha256)], 16) % len(self.color_hash)]
|
|
|
|
def animchar(self, pos):
|
|
"""Returns one character, based on pwscore or pw hash"""
|
|
i = len(self.user_input) - pos
|
|
if self.opts.measure:
|
|
if i < 0:
|
|
return self.c.r + " ▁ "[random.randint(0, 2)]
|
|
rheight = random.randint(-1, 1)
|
|
clr = self.limits[0][0]
|
|
score = self.pwscore()
|
|
for limit in self.limits:
|
|
if score > limit[1] - 1 + 2 * random.random():
|
|
clr = limit[0]
|
|
else:
|
|
break
|
|
return clr + self.eq[int(rheight + (i / self.opts.w)) % 14]
|
|
|
|
else: # no measure
|
|
rheight = random.randint(-1, 1)
|
|
height = int(self.sha256[i * 2 % len(self.sha256)], 16) / 2
|
|
clr = self.get_color_hash(i)[0]
|
|
if self.opts.eq:
|
|
if len(self.user_input) == 0:
|
|
rheight = random.randint(0, 1)
|
|
height = 0
|
|
clr = self.color_hash[0][0]
|
|
else:
|
|
pass
|
|
return clr + self.eq[int(rheight + height) % len(self.eq)]
|
|
if len(self.user_input) == 0:
|
|
clr = self.color_hash[0][0]
|
|
return clr + self.no_pw
|
|
else:
|
|
return clr + self.rand_chr[int(rheight + height) % len(self.rand_chr)]
|
|
|
|
def get_export(self):
|
|
title = ["-t 'Password hint: "]
|
|
for pos in range(self.opts.w):
|
|
i = len(self.user_input) - pos
|
|
animchar = self.animchar(i)
|
|
if self.opts.eq:
|
|
pass
|
|
else:
|
|
a = self.get_color_hash(i)[1]
|
|
title.append("{" + a + "}" + animchar[-1])
|
|
title.append("'\n")
|
|
|
|
return "".join(title)
|
|
|
|
def pquit(self, s="", e=0):
|
|
print(self.c.blink, file=sys.stderr, end="")
|
|
print(self.c.z, file=sys.stderr)
|
|
print(s, end="", file=sys.stdout if e == 0 else sys.stderr)
|
|
sys.exit(e)
|
|
|
|
def check_sha256(self):
|
|
|
|
self.sha256 = hashlib.sha256(self.user_input.encode("utf-8")).hexdigest()
|
|
if self.opts.expect_sha256:
|
|
if self.sha256 == self.opts.expect_sha256:
|
|
self.dot_color = self.c.G
|
|
self.dot = self.is_correct
|
|
self.enter_exitcode = 0
|
|
else:
|
|
self.enter_exitcode = 10
|
|
|
|
def get_chr(self):
|
|
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)
|
|
return ch
|
|
|
|
def ask(self):
|
|
|
|
print(self.c.noblink, file=sys.stderr, end="")
|
|
self.dot = self.no_pw
|
|
if self.opts.title:
|
|
title = self.c.format(self.opts.title)
|
|
print(f"{self.c.Y}{title}{self.c.z}", file=sys.stderr)
|
|
|
|
while True:
|
|
try:
|
|
self.enter_exitcode = 0
|
|
if self.opts.w > 0:
|
|
self.dot_color = self.c.m
|
|
self.dot = self.no_pw
|
|
self.check_sha256()
|
|
display = "".join(
|
|
[
|
|
f"\r{self.c.z}{self.dot_color}{self.dot*4} ",
|
|
*[self.animchar(i) for i in range(self.opts.w)],
|
|
f" {self.dot_color}{self.dot*4}{self.c.z}\r{self.c.k}",
|
|
]
|
|
)
|
|
print(display, file=sys.stderr, end="")
|
|
sys.stderr.flush()
|
|
key = self.get_chr()
|
|
if ord(key) == 3: # ctrl-c
|
|
self.pquit(e=1)
|
|
elif ord(key) == 13: # enter
|
|
if self.opts.export:
|
|
msg = self.get_export()
|
|
else:
|
|
msg = self.user_input
|
|
self.pquit(msg, e=self.enter_exitcode)
|
|
elif ord(key) == 27: # esc (also starts control characters
|
|
key = self.get_chr()
|
|
if ord(key) == 27:
|
|
self.pquit(e=1)
|
|
continue
|
|
elif ord(key) == 127: # backspace
|
|
self.user_input = self.user_input[0:-1]
|
|
else:
|
|
if key in string.printable:
|
|
self.user_input += key
|
|
|
|
except Exception as e:
|
|
raise (e)
|
|
self.pquit(s=str(e), e=1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
q = QAsk()
|
|
q.ask()
|