Skip to content

Instantly share code, notes, and snippets.

@buckleyc
Last active January 22, 2021 18:48
Show Gist options
  • Save buckleyc/c4aab5fe963c117170dad78a3ca2cfc2 to your computer and use it in GitHub Desktop.
Save buckleyc/c4aab5fe963c117170dad78a3ca2cfc2 to your computer and use it in GitHub Desktop.
PasswordGana - Creates secure passwords using user-friendly syllables
#!/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)
#!/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