Created
December 2, 2022 17:14
-
-
Save yunruse/53774958ae69b875766892e549168240 to your computer and use it in GitHub Desktop.
Phonetic password schema, a la iCloud
This file contains 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
''' | |
Small tool that generates a phonetic password with a schema | |
identical to that used in iCloud's password generators. | |
Pronouncable-ish with 20 characters and ~70 bits of entropy. | |
Useful for password managers or sysadmins. | |
''' | |
# Example password from schema: fivhAp-riznux-0mizhi | |
# Let V be the set of all lowercase vowels "aiueoy". | |
# Let C be the set of all lowercase consonants in the Latin (English) alphabet. | |
# A password is three blocks separated by a "-" character. | |
# Two blocks are CVCCVC; | |
# one (randomly positioned) is CVCCV with a digit (0-9) prepended or appended. | |
# One letter, randomly selected, is capitalised. | |
# I have my own personal criticisms of the schema. | |
# semivowels Y and W are a bit awkward sometimes in English | |
# and there is a chance of capitalising I which is sometimes ambiguous. | |
# The random capital placement also makes it annoying to pronounce. | |
# The ability to more easily communicate over a clear line / store in short term memory | |
# is worth losing 4 bits of entropy, in my mind. | |
# That said, it's close enough to the phonotactics of most languages | |
# that I must admit it is very charming. | |
# It's no xkcd 936 but it's a lot nicer than a random string. | |
from secrets import choice as r, randbelow | |
from string import ascii_lowercase, digits as D | |
__all__ = ('password', ) | |
V = tuple('aeiouy') | |
C = tuple(set(ascii_lowercase) - set(V)) | |
def _block(digit: bool) -> str: | |
block = r(C)+r(V)+r(C)+r(C)+r(V) | |
if digit: | |
if randbelow(2): # <- half and half | |
return r(D) + block | |
else: | |
return block + r(D) | |
else: | |
return block + r(C) | |
def password() -> str: | |
'''Generate an iCloud-like password.''' | |
blocks = [_block(False), _block(False)] | |
blocks.insert(randbelow(3), _block(True)) | |
password = list('-'.join(blocks)) | |
# this is a slightly silly but relatively simple method. | |
# password generating isn't subject to timing attacks | |
# or at least I'm quite confident they really shouldn't be... | |
i = 6 # "-" | |
while password[i] not in ascii_lowercase: | |
i = randbelow(len(password)) | |
password[i] = password[i].upper() | |
return ''.join(password) | |
def _bits_of_entropy(): | |
from math import log2 | |
N_vowels = 6 | |
N_consonants = 11 | |
return( | |
log2(len(V)) * N_vowels | |
+ log2(len(C)) * N_consonants | |
+ log2(3) # places to insert numeric block | |
+ log2(2) # places to insert number | |
+ log2(17) # letters, one of which is capitalised | |
) | |
if __name__ == '__main__': | |
from argparse import ArgumentParser | |
parser = ArgumentParser(description=__doc__) | |
parser.add_argument('-n', '-N', type=int, nargs='?', default=1, help='Number of passwords to generate (default 1)') | |
args = parser.parse_args() | |
for i in range(args.n): | |
print(password()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment