User-friendly passwords

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

View source for the content of this page.