| 1 | #!/usr/bin/python3 |
|---|
| 2 | import os |
|---|
| 3 | import sys |
|---|
| 4 | import time |
|---|
| 5 | import glob |
|---|
| 6 | import random |
|---|
| 7 | import math |
|---|
| 8 | import argparse |
|---|
| 9 | import subprocess |
|---|
| 10 | |
|---|
| 11 | |
|---|
| 12 | TARGET_DIR = os.path.join(os.path.dirname(sys.argv[0]), 'wordlists') |
|---|
| 13 | |
|---|
| 14 | |
|---|
| 15 | def clone_wordlists(): |
|---|
| 16 | """Clone a copy of the wordlists if needed. Update the wordlist if it |
|---|
| 17 | hasn't been in a while. |
|---|
| 18 | """ |
|---|
| 19 | if os.path.exists(TARGET_DIR): |
|---|
| 20 | try: |
|---|
| 21 | last_modified = os.stat(os.path.join(TARGET_DIR, '.git/FETCH_HEAD')).st_mtime |
|---|
| 22 | except FileNotFoundError: |
|---|
| 23 | last_modified = 0 |
|---|
| 24 | if last_modified < time.time() - 7 * 24 * 60 * 60: # Check for updates weekly |
|---|
| 25 | subprocess.check_call(['git', 'pull'], cwd=TARGET_DIR) |
|---|
| 26 | else: |
|---|
| 27 | subprocess.check_call(['git', 'clone', 'https://github.com/imsky/wordlists.git'], |
|---|
| 28 | cwd=os.path.dirname(TARGET_DIR)) |
|---|
| 29 | |
|---|
| 30 | |
|---|
| 31 | def load(part_of_speech): |
|---|
| 32 | """Load all unique words of a given part of speech from the wordlists. |
|---|
| 33 | """ |
|---|
| 34 | words = set() |
|---|
| 35 | for filename in glob.glob(os.path.join(TARGET_DIR, part_of_speech, '*.txt')): |
|---|
| 36 | words.update(w.strip() for w in open(filename, encoding='utf-8') if w.strip()) |
|---|
| 37 | return words |
|---|
| 38 | |
|---|
| 39 | |
|---|
| 40 | def main(argv=None): |
|---|
| 41 | """Generate a random-but-memorable username. |
|---|
| 42 | """ |
|---|
| 43 | if argv is None: |
|---|
| 44 | argv = sys.argv |
|---|
| 45 | parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, |
|---|
| 46 | description=main.__doc__) |
|---|
| 47 | parser.add_argument("--adjectives", "-a", default=1, type=int, |
|---|
| 48 | help="number of adjectives") |
|---|
| 49 | parser.add_argument("--divider", default='', |
|---|
| 50 | help="character or string between words") |
|---|
| 51 | parser.add_argument("--bits", default=0, type=int, |
|---|
| 52 | help="minimum bits of entropy") |
|---|
| 53 | parser.add_argument("--verbose", action='store_true', |
|---|
| 54 | help="be chatty") |
|---|
| 55 | args = parser.parse_args(argv[1:]) |
|---|
| 56 | |
|---|
| 57 | clone_wordlists() |
|---|
| 58 | # load() returns a set, but we need a subscriptable type, so convert to |
|---|
| 59 | # lists. But there's no reason to sort them, since we're only taking |
|---|
| 60 | # random picks from the lists. |
|---|
| 61 | nouns = list(load('nouns')) |
|---|
| 62 | adjectives = list(load('adjectives')) |
|---|
| 63 | |
|---|
| 64 | adjective_count = args.adjectives |
|---|
| 65 | # Determine the number of bits of randomness given the adjectives count |
|---|
| 66 | bits = math.log2(len(nouns)) + adjective_count * math.log2(len(adjectives)) |
|---|
| 67 | # Increase the number of adjectives as needed to achieve the minimum bits |
|---|
| 68 | # of randomness |
|---|
| 69 | while bits < args.bits: |
|---|
| 70 | adjective_count += 1 |
|---|
| 71 | bits = math.log2(len(nouns)) + adjective_count * math.log2(len(adjectives)) |
|---|
| 72 | |
|---|
| 73 | if args.verbose: |
|---|
| 74 | sys.stderr.write(f"Found {len(nouns)} nouns and {len(adjectives)} adjectives;" |
|---|
| 75 | f" using {adjective_count} adjective{adjective_count != 1 and 's' or ''} " |
|---|
| 76 | f"and one noun giving {bits :0.2f} bits of entropy.\n") |
|---|
| 77 | |
|---|
| 78 | # Use the best source of randomness available. |
|---|
| 79 | prng = random.SystemRandom() |
|---|
| 80 | words = [] |
|---|
| 81 | for i in range(adjective_count): |
|---|
| 82 | words.append(prng.choice(adjectives)) |
|---|
| 83 | # We don't remove used adjectives from the list of choices, so that |
|---|
| 84 | # means it's possible to have repeats, such as "wise-small-wise-ant" |
|---|
| 85 | words.append(prng.choice(nouns)) |
|---|
| 86 | result = args.divider.join(words) |
|---|
| 87 | sys.stdout.write(f"{result}\n") |
|---|
| 88 | return 0 |
|---|
| 89 | |
|---|
| 90 | |
|---|
| 91 | if __name__ == '__main__': |
|---|
| 92 | sys.exit(main()) |
|---|