Anders Bergh reverse-engineered the OS X Password Assistant and wrote a command-line utility which does the same job. From its source you can find out that all of the password generation is done by a proprietary undocumented library function SFPWAPasswordSuggest()
from the SecurityFoundation framework.
Though the function is proprietary and no official Apple documentation exists, you can play with either the function itself or the command-line utility around it to figure out what it's producing. Allister Banks already did that in 2015, assumingly for OS X Yosemite, and came up with a code snippet which produces pretty much the same results (except for a few strange author's preferences, like limiting the range of available random numbers).
In a nutshell, SFPWAPasswordSuggest()
picks two random words from a vocabulary and inserts a random number and one special character (in that order) between them. The random number length is such that the overall string length will be exactly as required, and there will always be a single special character. Regarding the vocabulary, OS X from Yosemite to High Sierra use /System/Library/Frameworks/SecurityInterface.framework/Resources/pwa_dict_en.gz
, 287 kb in size and with 83935 English words of length between 2 and 24, for that:
$ gzcat pwa_dict_en.gz | python2 -c 'import sys
> sys.stdin.read(512)
> word_counts = dict()
> for num_of_letters_index in range(64):
> value = int(sys.stdin.read(8).strip(), 16)
> if value:
> word_counts[num_of_letters_index] = value
> print "Distribution:", word_counts'
Distribution: {1: 26, 2: 191, 3: 1229, 4: 3170, 5: 5591, 6: 8913, 7: 12452, 8: 13462, 9: 12163, 10: 9820, 11: 7007, 12: 4516, 13: 2704, 14: 1429, 15: 691, 16: 329, 17: 150, 18: 61, 19: 21, 20: 5, 21: 3, 22: 1, 24: 1}
$
Here's how you can parse the file. In other OS X versions, the function may be designed somewhat differently; use dtruss ./sf-pwgen
to figure out.
Note that GUI limits the length of your generated password with number 31. As far as I can see, the function doesn't produce an error for longer requests, yet is not guaranteed to work for passwords longer than 31 characters. Actually, for passwords above 67 characters, it's almost guaranteed to fail; as you can spot above the vocabulary doesn't feature enough words of sufficient length, and the generating algorithm doesn't handle this case well, simply returning a short passphrase consisting of only numbers and a special character:
$ ./sf-pwgen -c 1 -l 68
2814154076!
$
EDIT 24.01.2018: on behalf of Alex Recuenco:
Calculating entropy
Following @ximaera solution.
import math
x = {1: 26, 2: 191, 3: 1229, 4: 3170, 5: 5591, 6: 8913, 7: 12452, 8: 13462, 9: 12163, 10: 9820, 11: 7007, 12: 4516, 13: 2704, 14: 1429, 15: 691, 16: 329, 17: 150, 18: 61, 19: 21, 20: 5, 21: 3, 22: 1, 24: 1}
def entropy(pass_length, n_symbols = 30):
combinations = 0
for key, value in x.items():
for key2, value2 in x.items():
if (key + key2) < (pass_length - 1):
combinations += value * value2 * n_symbols * (10 ** (pass_length - key - key2 - 1))
# last value
return {'combinations': combinations, 'entropy': math.log2(combinations)}
print(entropy(31))
Which when you run it:
> {'combinations': 1124445877165765109161692550890600, 'entropy': 109.79284135298234}
110 bits of entropy, maximum... I thought it would be better for some reason. The entropy of a password of just numeric characters of length 30 is approximately 100