Last active
January 22, 2021 18:48
-
-
Save buckleyc/c4aab5fe963c117170dad78a3ca2cfc2 to your computer and use it in GitHub Desktop.
PasswordGana - Creates secure passwords using user-friendly syllables
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
""" | |
User-friendly password, with hiragana style phrasing | |
- based on idea from https://github.com/asakura42/passgen | |
Generates a user-readable/friendly password based on phonetic syllables (ala hiragana), | |
with some numbers and special characters for spice. | |
Checking these passwords on https://password.kaspersky.com/ shows bruteforce >= Centuries. | |
""" | |
# Futures | |
from __future__ import unicode_literals | |
# Generic/Built-in | |
import sys | |
from typing import Annotated, IO | |
import logging | |
import secrets | |
import string | |
import clipboard | |
# Other Libs | |
# from typing import Any, Union | |
# from colorama import Fore, Style | |
# Owned | |
# from {path} import {class} | |
__author__ = "Buckley Collum" | |
__copyright__ = "Copyright 2021, QuoinWorks" | |
__credits__ = ["Buckley Collum"] | |
__license__ = "GNU General Public License v3.0" | |
__version__ = "0.3.9" | |
__maintainer__ = "Buckley Collum" | |
__email__ = "[email protected]" | |
__status__ = "Dev" | |
# logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) | |
alphabet = string.ascii_lowercase | |
ignore = "" | |
vowel = list("aeiou") | |
digraph = "bl br ch dr fl fr gl gr kl kr pl pr sc sh sl st th tr" | |
special = string.punctuation # ",./-=;[]'`" | |
endings = "d l n r s t ck cy ly nd ng nt rt ny sh sm st ment ness ship tion ful less" | |
# Ignore selected characters | |
base = [x for x in alphabet if x not in ignore.split(" ")] | |
# Remove the Vowels to get the list of Consonants | |
consonant = [x for x in base if x not in vowel] | |
consonant.append("") # Add empty consonant as a choice | |
# Add in some Digraphs (e.g., 'ch') to make password even stronger, while still being readable | |
consonant.extend(digraph.split(" ")) | |
# List of suffix | |
suffix = list(endings.split(" ")) | |
IS_VERBOSE = False | |
USE_ENGLISH = True | |
WORD = 2 | |
SYLLABLE = 2 | |
DIGIT = 4 | |
NEED = 1 | |
def _supports_color(stream: Annotated[IO, "output stream"] = sys.stdout): | |
""" | |
Returns True if the current platform supports | |
coloring terminal output using this method. Returns False otherwise. | |
""" | |
if not stream.isatty(): | |
return False # auto color only on TTYs | |
try: | |
from curses import tigetnum, error, setupterm | |
except ImportError: | |
return False | |
else: | |
try: | |
try: | |
return tigetnum("colors") > 2 | |
except error: | |
setupterm() | |
return tigetnum("colors") > 2 | |
except: | |
# guess false in case of error; yes, it is broad. | |
return False | |
def mora() -> Annotated[str, "return a mora"]: | |
"""Create a mora/syllable using Consonant & Vowel, and occasional Capitalization, return this string.""" | |
from secrets import choice | |
pho = f"{choice(consonant)}{choice(vowel)}" | |
return pho.capitalize() if choice([True, False]) else pho | |
def appendix(length: Annotated[int, "length"] = DIGIT) -> Annotated[str, "number with length-digits"]: | |
"""Given a number for length, return a string of digits.""" | |
from secrets import randbelow | |
return f"{str(randbelow((10 ** length) - 1)).zfill(length)}" | |
def report(pwlist: Annotated[list, "list of passwords"], args) -> None: | |
"""Print a report of the passwords and their strength, return nothing.""" | |
proc = { | |
"inteli7": {"model": "single core of Intel i7 CPU", "pwpersec": 5e07}, | |
"cuda": {"model": "dedicated CUDA node", "pwpersec": 7e08}, | |
} | |
pwperyear = proc["cuda"]["pwpersec"] * (365 * 24 * 60 * 60) | |
# Determine the number of guesses required for a character-walk brute force attack | |
chars = "".join([string.ascii_letters, string.digits, string.punctuation]) | |
charset = len(chars) | |
for pw in pwlist: | |
pw_print([pw], args.color) | |
# Determine the number of guesses if the cracker knows the probable scheme | |
possibilities = (len(consonant) * 2 + len(vowel)) ** ( | |
args.words * args.syllables | |
) | |
possibilities *= len(special) ** args.words | |
if args.english: | |
possibilities *= len(suffix) ** args.words | |
possibilities *= 10 ** args.digits | |
# print(f"{possibilities:.2g}") | |
print( | |
f" Smart Guessing: {possibilities / pwperyear:.3n} years to bruteforce on {proc['cuda']['model']}" | |
) | |
logging.info( | |
"\tc:%x v:%x w:%x s:%x sp:%x => %.2e possibilities", | |
len(consonant), | |
len(vowel), | |
args.words, | |
args.syllables, | |
len(special), | |
possibilities, | |
) | |
possibilities = 0 | |
for i in range(len(pw)): | |
# each loop counts the number of possibilities for the len(pw)-1 guessing | |
possibilities += charset ** i | |
# each loop counts the number of possibilities for the len(pw) guessing | |
possibilities += chars.index(pw[i]) * (len(chars) ** (len(pw) - i - 1)) | |
debug_str = ( | |
f"{i}: {possibilities} {pw[i]}:{chars.index(pw[i])} {len(chars)} {len(pw) - i - 1} " | |
f"{chars.index(pw[i]) * (len(chars) ** (len(pw) - i - 1))}" | |
) | |
logging.debug(debug_str) | |
print( | |
f" Character Walking: {int(possibilities/(pwperyear * 100)):.2g} centuries to bruteforce " | |
f"on {proc['cuda']['model']}" | |
) | |
logging.info( | |
"\tpassword length = {} => {:.2e} possibilities".format( | |
len(pw), possibilities | |
) | |
) | |
return | |
def pw_print(pwlist: Annotated[list, "list of passwords"], colorful: Annotated[bool, "use color"]) -> None: | |
for pw in pwlist: | |
entpw = entropy(pw) | |
if colorful: | |
from sty import fg, rs | |
pw_color = conv_ent_rgb(entpw) | |
c_on, c_off = fg(*pw_color), rs.fg | |
else: | |
c_on, c_off = "", "" | |
my_str = [f"{c_on}{pw}{c_off}\t{c_on}{int(entpw)} bits{c_off} of entropy"] | |
if entpw < 100: | |
my_str.append(f" * {c_on}Not Strong!{c_off} *") | |
print("".join(my_str)) | |
return | |
def translate(value, from_min: int, from_max: int, to_min: int, to_max: int) -> int: | |
"""for color: Given an input value and the ranges for source and destination ranges, | |
return the interpolated result.""" | |
if value <= from_min: | |
return to_min | |
elif value >= from_max: | |
return to_max | |
# Figure out how 'wide' each range is | |
from_span = from_max - from_min | |
to_span = to_max - to_min | |
# Convert the left range into a 0-1 range (float) | |
value_scaled = float(value - from_min) / float(from_span) | |
# Convert the 0-1 range into a value in the right range. | |
return int(to_min + (value_scaled * to_span)) | |
def conv_ent_rgb(ent: Annotated[float, "entropy"]) -> Annotated[tuple, "rgb as a tuple"]: | |
"""Given a entropy value, generate an RGB color triplet based on the 'strength' of password entropy, | |
return tuple.""" | |
if ent <= 50: | |
# Below 50 is very weak, thus Red | |
red, green, blue = 255, 0, 0 | |
elif ent >= 120: | |
# Above 120 is very strong, thus Blue | |
red, green, blue = 0, 128, 255 | |
elif ent >= 100: | |
# Above 100 is Strong, thus Green | |
red, green, blue = 0, 255, 0 | |
else: | |
# Between 50-100 is Fair, thus ramp from Red to Yellow to Green | |
red = translate(ent, 75, 100, 255, 0) | |
green = translate(ent, 50, 75, 0, 255) | |
blue = 0 | |
# print(f" {ent:.1f} : {red},{green},{blue}") | |
return red, green, blue | |
def entropy( | |
password: Annotated[str, "password to check"] | |
) -> Annotated[float, "return float representing entropy"]: | |
"""Calculate the entropy for a password, return the value as a float.""" | |
from string import printable | |
from math import log2 | |
return log2((len(printable) - 5) ** len(password)) | |
def random_capital( | |
target: Annotated[str, "word to randomly capitalize"] | |
) -> Annotated[str, "randomized string"]: | |
"""Given a string, return the string with random capitalization.""" | |
from random import choice | |
return "".join(choice((c.upper(), c, c)) for c in target) | |
def random_line( | |
fname: Annotated[str, "filename for wordlist"] | |
) -> Annotated[str, "random line from file"]: | |
"""Given a filename, return a random line.""" | |
from random import choice | |
lines = open(fname).read().splitlines() | |
return random_capital(choice(lines)) | |
def parse_args(): | |
import argparse | |
parser = argparse.ArgumentParser( | |
description="Generate semi-memorable secure passwords based on mora." | |
) | |
parser.add_argument( | |
"-c", | |
"--color", | |
default=_supports_color(), | |
action=argparse.BooleanOptionalAction, | |
help="make output colorful", | |
) | |
parser.parse_args(["--no-color"]) | |
parser.add_argument( | |
"-e", | |
"--english", | |
default=USE_ENGLISH, | |
action=argparse.BooleanOptionalAction, | |
help="make word English-y with endings", | |
) | |
parser.parse_args(["--no-english"]) | |
parser.add_argument( | |
"-s", | |
"--syllables", | |
type=int, | |
nargs="?", | |
default=SYLLABLE, | |
help="choose number of syllables", | |
) | |
parser.add_argument( | |
"-w", | |
"--words", | |
type=int, | |
nargs="?", | |
default=WORD, | |
help="choose number of words", | |
) | |
parser.add_argument( | |
"-d", | |
"--digits", | |
type=int, | |
nargs="?", | |
default=DIGIT, | |
help="choose number of digits", | |
) | |
parser.add_argument( | |
"-W", | |
"--wordlist", | |
default=False, | |
action=argparse.BooleanOptionalAction, | |
help="use EFF wordlist", | |
) | |
parser.add_argument( | |
"-n", | |
"--needed", | |
type=int, | |
nargs="?", | |
default=NEED, | |
help="choose number of passwords to generate", | |
) | |
parser.add_argument( | |
"-v", | |
"--verbose", | |
dest="verbosity", | |
action="count", | |
default=IS_VERBOSE, | |
help="verbose output (repeat for increased verbosity)", | |
) | |
parser.add_argument( | |
"-q", | |
"--quiet", | |
dest="verbosity", | |
action="store_const", | |
const=-1, | |
default=0, | |
help="quiet output (show errors only)", | |
) | |
args = parser.parse_args() | |
setup_logging(args.verbosity) | |
return args | |
def setup_logging(verbosity: int) -> None: | |
from os import getenv | |
import logging | |
# logging.debug(getenv('LOGLEVEL', 'WARNING').upper()) | |
base_loglevel = getattr(logging, (getenv("LOGLEVEL", "ERROR")).upper()) | |
verbosity = min(verbosity, 2) | |
loglevel = base_loglevel - (verbosity * 10) | |
logging.basicConfig(level=loglevel, format=" • %(message)s") | |
return | |
def main(args) -> int: | |
logging.debug(" ARGS = {}".format(args)) | |
passwordlist = [] | |
while len(passwordlist) < args.needed: | |
pw_pieces = [] | |
for w in range(args.words): | |
if args.wordlist: | |
"""Use Electronic Frontier Foundation (EFF) wordlist | |
via https://www.eff.org/files/2016/09/08/eff_short_wordlist_2_0.txt""" | |
eff_wordlist = "eff_short_wordlist_2_0.txt" | |
num, word = random_line(eff_wordlist).split("\t") | |
pw_pieces.append(word) | |
else: | |
"""Generate a word""" | |
for s in range(args.syllables): | |
"""Generate a syllable""" | |
pw_pieces.append(mora()) # Get a syllable | |
if args.english: | |
"""Generate an English ending""" | |
pw_pieces.append(secrets.choice(suffix)) | |
pw_pieces.append(secrets.choice(special)) | |
pw_pieces.append(appendix(args.digits)) # Add a random appendix | |
password = "".join(pw_pieces) | |
uppers = [cl for cl in password if cl.isupper()] | |
specials = [cl for cl in password if not cl.isalnum()] | |
if uppers and specials: | |
"""password must have some capital letters and special characters""" | |
passwordlist.append(password) | |
logging.debug("Password approved: {}".format(password)) | |
if args.verbosity: | |
report(passwordlist, args) | |
else: | |
pw_print(passwordlist, args.color) | |
clipboard.copy(passwordlist[-1]) | |
return 0 | |
if __name__ == "__main__": | |
try: | |
ret = main(parse_args()) # main(sys.argv[1:]) | |
sys.exit(0 if ret is None else ret) | |
except Exception as e: | |
logging.error(str(e)) | |
logging.debug("", exc_info=True) | |
try: | |
sys.exit(str(e)) | |
except AttributeError: | |
sys.exit(1) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
""" | |
User-friendly password, with hiragana style phrasing | |
- based on idea from https://github.com/asakura42/passgen | |
Generates a user-readable/friendly password based on phonetic syllables (ala hiragana), | |
with some numbers and special characters for spice. | |
Checking these passwords on https://password.kaspersky.com/ shows bruteforce >= Centuries. | |
""" | |
# Futures | |
from __future__ import unicode_literals | |
# Generic/Built-in | |
import sys | |
import random | |
import secrets | |
# Other Libs | |
# from typing import Any, Union | |
# from colorama import Fore, Style | |
# Owned | |
# from {path} import {class} | |
__author__ = "Buckley Collum" | |
__copyright__ = "Copyright 2020, QuoinWorks" | |
__credits__ = ["Buckley Collum"] | |
__license__ = "GNU General Public License v3.0" | |
__version__ = "0.1.0" | |
__maintainer__ = "Buckley Collum" | |
__email__ = "[email protected]" | |
__status__ = "Dev" | |
alphabet = "a b c d e f g h i j k l m n o p q r s t u v w x y z" | |
ignore = "y" | |
vowels = "a e i o u" | |
digraph = "ch sh th br dr kr tr kl pl pr st sl" | |
special = ",./-=;" | |
# Get the Vowels first | |
vowel = list(vowels.split(" ")) | |
# Ignore selected characters | |
base = [x for x in alphabet.split(" ") if x not in ignore.split(" ")] | |
# Remove the Vowels to get the list of Consonants | |
consonant = [x for x in base if x not in vowel] | |
# Add in some Digraphs (e.g., 'ch') to make password even stronger, while still being readable | |
consonant.extend(digraph.split(" ")) | |
def mora(): | |
"""Create a mora/syllable using Consonant & Vowel, and occasional Capitalization""" | |
case = random.randint(0, 2) | |
pho = f"{secrets.choice(consonant)}{secrets.choice(vowel)}" | |
return pho.capitalize() if case == 0 else pho | |
def suffix(): | |
"""A three digit number, to be used as a suffix""" | |
return f"{random.randint(0, 999):03d}" | |
def is_integer(n): | |
try: | |
float(n) | |
except ValueError: | |
return False | |
else: | |
return float(n).is_integer() | |
def main(syllable=4): | |
if len(sys.argv) > 1: | |
"""Is the only argument an integer, to be used for the number of syllables.""" | |
val = sys.argv[1] | |
if is_integer(val): | |
syllable = int(val) | |
# print(f"Continuing using {syllable} syllables.") | |
else: | |
sys.exit(f"Input is not an integer. input = {val}") | |
password = "" | |
spacer = random.randint(1, syllable - 2) | |
for i in range(syllable): | |
password += mora() # Get a syllable | |
if i == spacer: # Add a special character | |
password += secrets.choice(special) | |
password += suffix() # Add a suffix | |
print(password) | |
sys.exit(0) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment