A good password should be randomly generated, have a certain level of randomness, and not be too difficult to type. The generator below takes as argument a pattern, like ‘000‘ for three decimal digits, ‘lll0‘ for three letters followed by a decimal digit, or ‘Cwc0‘ for an uppercase consonant, a lowercase wowel, a lowercase consonant and a decimal digit. A second argument is a repeat count, four by default.
To generate passwords of 16 decimal digits in four groups, say:
$ password-generator.pl 0000 4 |
8058460983365774 8058 4609 8336 5774 |
4609833657742695 4609 8336 5774 2695 |
8336577426958145 8336 5774 2695 8145 |
5774269581454664 5774 2695 8145 4664 |
2695814546642111 2695 8145 4664 2111 |
8145466421116990 8145 4664 2111 6990 |
4664211169900297 4664 2111 6990 0297 |
2111699002979514 2111 6990 0297 9514 |
6990029795149225 6990 0297 9514 9225 |
0297951492251091 0297 9514 9225 1091 |
9514922510914616 9514 9225 1091 4616 |
9225109146163033 9225 1091 4616 3033 |
About 53.2 bits of randomness (16.0 decimal digits, 8.1 ASCII-94 characters) |
The 0000 indicates four digits, and 4 is the number of repetitions. The left half is the “raw” password, the right half is the same password, with spaces printed between the groups. It should come as no suprise that the program reports 16 decimal digits of randomness. ASCII-94 refers to all the printable ASCII character, character codes 33 to 126.
Only pick one password from the list, since nearby passwords are related. For each new password, the leftmost group is dropped and replaced by a new group to the right. Look at the ‘5774‘ at the far right of the first password, and see how it moves to the left for each new password. After making an appearance at the far left, it is dropped.
To generate passwords of 12 characters in three groups, where each group is three letters and a digit, say:
$ password-generator.pl aaa0 4 |
fhj3sfl0djt2 fhj3 sfl0 djt2 |
sfl0djt2vqh6 sfl0 djt2 vqh6 |
djt2vqh6iec4 djt2 vqh6 iec4 |
vqh6iec4wyu1 vqh6 iec4 wyu1 |
iec4wyu1pqa3 iec4 wyu1 pqa3 |
wyu1pqa3kyl3 wyu1 pqa3 kyl3 |
pqa3kyl3jdq7 pqa3 kyl3 jdq7 |
kyl3jdq7nmx7 kyl3 jdq7 nmx7 |
jdq7nmx7evh8 jdq7 nmx7 evh8 |
nmx7evh8wzu7 nmx7 evh8 wzu7 |
evh8wzu7zee6 evh8 wzu7 zee6 |
About 52.3 bits of randomness (15.7 decimal digits, 8.0 ASCII-94 characters) |
While these passwords are shorter, they have about the same randomness as the previous 16-digit passwords. This is because there are more letters than digits, creating about the same number of different passwords.
And you can have an equally strong, but even shorter, password by using the full ASCII-94 character set:
password-generator.pl @@@@ 2 |
,]D1Ko+~ ,]D1 Ko+~ |
Ko+~j8He Ko+~ j8He |
j8Hee4&h j8He e4&h |
e4&hdl"3 e4&h dl"3 |
dl"3ZD[[ dl"3 ZD[[ |
ZD[[ws#G ZD[[ ws#G |
ws#GhWQn ws#G hWQn |
hWQneks= hWQn eks= |
eks=T,#O eks= T,#O |
T,#OK@=9 T,#O K@=9 |
K@=9If.r K@=9 If.r |
About 52.4 bits of randomness (15.8 decimal digits, 8.0 ASCII-94 characters) |
Passwords such as these may become more readable using the ‘-w‘ (wide) option:
password-generator.pl -w @@@@ 2 |
/|h>R5zt /|h> R5zt / | h > R 5 z t |
R5ztM6m$ R5zt M6m$ R 5 z t M 6 m $ |
M6m$J-KN M6m$ J-KN M 6 m $ J - K N |
J-KN'!%E J-KN '!%E J - K N ' ! % E |
'!%E=4=n '!%E =4=n ' ! % E = 4 = n |
=4=nGw7s =4=n Gw7s = 4 = n G w 7 s |
Gw7sF:(W Gw7s F:(W G w 7 s F : ( W |
F:(WBi~F F:(W Bi~F F : ( W B i ~ F |
Bi~Fw'WO Bi~F w'WO B i ~ F w ' W O |
w'WOL?|5 w'WO L?|5 w ' W O L ? | 5 |
L?|5%LCJ L?|5 %LCJ L ? | 5 % L C J |
%LCJliR` %LCJ liR` % L C J l i R ` |
liR`nFuW liR` nFuW l i R ` n F u W |
nFuWW/,# nFuW W/,# n F u W W / , # |
W/,#X>0a W/,# X>0a W / , # X > 0 a |
About 52.4 bits of randomness (15.8 decimal digits, 8.0 ASCII-94 characters) |
To generate passwords of 16 characters in four groups, where each group is consonant + wovel + consonant + digit, say:
$ password-generator.pl cwc0 4 |
zyr0zih6qat0kus2 zyr0 zih6 qat0 kus2 |
zih6qat0kus2xyr0 zih6 qat0 kus2 xyr0 |
qat0kus2xyr0saj4 qat0 kus2 xyr0 saj4 |
kus2xyr0saj4gan9 kus2 xyr0 saj4 gan9 |
xyr0saj4gan9tif6 xyr0 saj4 gan9 tif6 |
saj4gan9tif6hyq1 saj4 gan9 tif6 hyq1 |
gan9tif6hyq1naq2 gan9 tif6 hyq1 naq2 |
tif6hyq1naq2fav6 tif6 hyq1 naq2 fav6 |
hyq1naq2fav6xoc8 hyq1 naq2 fav6 xoc8 |
naq2fav6xoc8bod8 naq2 fav6 xoc8 bod8 |
fav6xoc8bod8fus4 fav6 xoc8 bod8 fus4 |
About 58.2 bits of randomness (17.5 decimal digits, 8.9 ASCII-94 characters) |
Here are all the characters that can go into a pattern:
+ | Punctuation and special characters |
0 | Digits |
H | Hexadecimal digits, uppercase |
h | Hexadecimal digits, lowercase |
@ | All printable ASCII characters |
A | Digits and uppercase letters (alphanumeric) |
L | Uppercase letters (alphabetic) |
C | Uppercase consonants |
W | Uppercase wowels |
a | Digits and lowercase letters (alphanumeric) |
l | Lowercase letters |
c | Lowercase consonants |
w | Lowercase wowels |
z | Upper- and lower-case letters |
Using patterns decreases the randomness, but that can be countered by using longer passwords.
For most uses, such as logging in to a web site, the password need not be very long. There is an interval of several seconds between attempts, and the number of attemps is often limited to just a few. With four digits and three attempts, an attacker has a 3 in 10,000 chance. This is what most banks and credit card companies deem safe, although they are probably under pressure from customers, who do not want six or eight digit PIN codes.
Using ‘cwc0‘ generates passwords that are almost pronouncable. With a repeat count of just two, there are almost 631 miljon passwords, giving the attacker a 1 in 210 miljon chance.
Download
There is no download; just copy and paste the source.
The source code
#!/usr/bin/perl use strict; use warnings; use Getopt::Std; use Digest::SHA; my $urandom = "/dev/urandom"; our $opt_h = 0; # Help our $opt_w = 0; # Wide format my %codes = ( '0' => "0123456789", 'L' => "ABCDEFGHIJKLMNOPQRSTUVWXYZ", # Letters 'C' => "BCDFGHJKLMNPQRSTVWXZ", # Consonants 'W' => "AEIOUY", # Wovels 'A' => "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", # Alphanumeric 'H' => "0123456789ABCDEF", # Hexadecimal 'z' => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", '+' => '!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~', '@' => join('', map(chr($_), (33..126))) # Printable ); # # Called if an unknown character is used in the pattern # sub help() { my $progname = ($0 =~ s,.*/,,r); print(<<EOF); Usage: $progname [-h] [-l] [pattern-or-count]... A count is one or more digits, first digit not zero. Anything else is a pattern. Options: -h Help (this message) -w Wide output Pattern characters: EOF sub p($) { $_ = shift; print("\t$_ => ${codes{$_}}\n"); } map p($_), (sort grep { $_ =~ /[A-Za-z0-9]/ } keys %codes); # Letters and digits map p($_), (sort grep { $_ =~ /[^A-Za-z0-9]/ } keys %codes); # Special characters } # # Run various commands, and return their output # sub seed() { return join("", ` date +%s.%N ls -lR --full-time /var/log 2> /dev/null date +%s.%N # tracepath -m8 theguardian.com ifconfig -a date +%s.%N `); } # # Return a list of 64 random 32-bit unsigned integers # The complicated implementation is just programmer fun. # Using the standard rand() function would suffice. # sub get_rand() { my $buf = ""; if (open(RAND, "<$urandom")) { my $nread = read(RAND, $buf, 256); # 256 bytes = 2048 bits 256 == $nread || die "Read($urandom) failed ($!)"; } else { my $s = seed(); $buf = Digest::SHA::sha512(rand().$s); $buf .= Digest::SHA::sha512(rand().$s); $buf .= Digest::SHA::sha512(rand().$s); $buf .= Digest::SHA::sha512(rand().$s); } return unpack('L*', $buf); # 2048 bits => 64 unsigned integers } sub main() { # # Generate lowercase codes # for my $ch ('A'..'Z') { $codes{lc($ch)} = lc($codes{$ch}) if (exists($codes{$ch})); } if ($opt_h) { help(); exit(0); } # # Set default values for the pattern and number of groups. # Then process the argument list. # my @pattern = split(//, "cwc0"); #--- Default pattern my $n_groups = 4; #--- Default nr of groups while (@ARGV) { my $arg = shift @ARGV; if ($arg =~ /^[1-9]\d*$/ && $arg) { # Digits only, no $n_groups = int($arg); # leading zero } else { @pattern = split(//, $arg); # Anything else } } my $plen = @pattern; my $pwd_len = $plen * $n_groups; my $randness = 0; # log() of nr of password combinations my $count = 0; # # Map each random number from get_rand() to a character, # using the pattern repeatedly. # my @tmp = map { my $key = $pattern[$count % $plen]; exists $codes{$key} || help && die "Unknown char `$key'"; my $chars = $codes{$key}; # 'h' => 0123456789abcdef my $nchars = length($chars); # => 16 $randness += log($nchars) if ($count++ < $pwd_len); substr($chars, $_ % $nchars, 1); } get_rand(); # # Join the result into a single string, then split it # into strings the length of the pattern. # my @groups = (join('', @tmp) =~ /.{$plen}/g); # # While there are at least $n_groups groups left use the # leading groups as password, then discard the leading group. # for ( ; @groups >= $n_groups; shift @groups) { my $pwd = join(' ', @groups[0..($n_groups-1)]); printf(" %s %s %s\n", ($pwd =~ s/ //gr), $pwd, $opt_w ? join(' ', split(//, $pwd)) : ""); } printf("About %.1f bits of randomness ". "(%.1f decimal digits, %.1f ASCII-94 characters)\n", $randness/log(2), $randness/log(10), $randness/log(94)); } getopts('wh'); main;
You can reach me by email at “lars dash 7 at sdu dot se” or by telephone +46 705 189090