#!/usr/bin/env python3 import argparse import hashlib import os import random import re import string import sys import termios import tty __version__ = "20251013.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), (self.c.y, 4), (self.c.B, 7), (self.c.c, 9), (self.c.g, 12), (self.c.G, 15)) 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( "--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 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 if self.opts.eq: if len(self.user_input) == 0: rheight = random.randint(0, 1) height = 0 clr = self.limits[0][0] else: rheight = random.randint(-1, 1) height = int(self.sha256[i * 2 % len(self.sha256)], 16) / 2 clr = self.limits[int(self.sha256[(1 + i * 2) % len(self.sha256)], 16) % len(self.limits)][0] return clr + self.eq[int(rheight + height) % 14] if len(self.user_input) == 0: clr = self.limits[0][0] return clr + self.no_pw else: rheight = random.randint(-1, 1) height = int(self.sha256[i * 2 % len(self.sha256)], 16) / 2 clr = self.limits[int(self.sha256[(1 + i * 2) % len(self.sha256)], 16) % len(self.limits)][0] return clr + self.rand_chr[int(rheight + height) % len(self.rand_chr)] 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 self.pquit(self.user_input, 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()