#!/usr/bin/perl -T

# Password generator based on http://xkcd.com/936/
# A.Daviel October 2012

$now = localtime() ;
$method = $ENV{'REQUEST_METHOD'} ;
$query = $ENV{'QUERY_STRING'} ;
$ENV{'PATH'} = '/usr/bin' ;

$maxstep = 4 ; # for cracklib avoidance
$maxp = 10 ;

print<<EOT;
Content-type: text/html

<html><head><title>Random Word Password Generator</title></head>
<body><h2>Random Word Password Generator</h2>
<form>
Approximate length: <input type="text" name="length" size="2" value="31">
joined with <input type="text" name="join" size="2" value="/">
<input type="checkbox" name="cracklib" value="1" title="assume cracklib" checked>
from corpus size <input type="text" name="ncorpus" size="5" value="50000">
<input type="submit" value="Generate" title="Click to generate a selection of possible passwords">
</form>
EOT
if ($method eq 'GET' && $query =~ /=/) {
  @pairs = split(/&/, $query);
  foreach $pair (@pairs) {
    ($name, $value) = split(/=/, $pair);
    $value =~ tr/+/ /;
    $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
    $F{$name} = $value ;
  }
  if ($F{'join'} =~ /(.{1,4})/) { $join = $1 ; } else { $join = '.' ; }
  if ($F{'ncorpus'} =~ /([\d]{1,6})/) { $ncorp = $1 ; }
  if ($F{'length'} =~ /([\d]{1,70})/) {
    $len = $1 ;
    $| = 1 ;
    print "Please wait .. loading dictionary ...<p>\n" ;
#    open (IN,"aspell -l canadian dump master|") or die "$0 Cannot run aspell\n" ;
    open (IN,"data/aspell.byfreq.txt") or die "$0 Cannot read wordlist\n" ;
    $i = 0 ;
    while (<IN>) {
      if (/^[\d]+[\s]([\w]+)/) {
        $_ = $1 ;
        chomp ;
        if (length($_) < $len/5 or length($_) > $len/3) { next ; }
        unless (/^[a-z]+$/) { next ; }
        $word[$i] = $_ ; $i++ ;
        if ($i > $ncorp) { last ; }
      }
    }
    $N = $i ;
    $e = log($N)/log(2) * 4 ;
    $strength = "Very Weak" ;
    if ($e > 28) { $strength = "Weak" ; }
    if ($e > 36) { $strength = "Reasonable" ; }
    if ($e > 60) { $strength = "Strong" ; }
    if ($e > 128) { $strength = "Very Strong" ; }

    printf ("Target length %d; %d unique words; %.1f bits of entropy (for 4-word phrase) - <b>$strength</b><p>\n",$len,$N,$e) ;

    print"<pre>\n" ;
    #srand(1) ;
    $np = 0 ; while ($np < $maxp ) {
      $p = &generatePassword($len) ;
      if ($F{'cracklib'}) {
        $nn = 0 ;
        for ($j=0;$j<length($p)-1;$j++) {
          $c0 = substr($p,$j,1) ;
          $c1 = substr($p,$j+1,1) ;
          if (ord($c1) == ord($c0) + 1 or ord($c1) == ord($c0) - 1) { $nn ++ ; }
        }
        if ($nn > $maxstep) { next ; }
      }
      $p = &html_escape($p) ;
      print "$p\n" ; $np++ ;
    }
    print "</pre>\n" ;
  } 
}
print<<EOT;
<p>
Password generator modelled after
<a href="http://xkcd.com/936/">xkcd.com/936 &quot;Password Strength&quot;</a>
(<a href="http://forums.xkcd.com/viewtopic.php?f=7&t=73384">discussion</a>)
<p>
Entropy calculated per <a href="http://en.wikipedia.org/wiki/Password_strength#Entropy_as_a_measure_of_password_strength">information entropy formula</a>
(taking into account the fact  that only dictionary words are used, not random letters)
<p>
Words are taken from Linux <a href="http://aspell.net/">aspell</a> English master dictionary,
sorted by frequency of occurrence in
<a href="http://books.google.com/ngrams/datasets">Google Books</a>.<br>
Selecting a smaller corpus size thus generates less uncommon words, but makes for weaker passwords.<br>
Check the "assume cracklib" box to avoid generating more than 4 pairs of adjacent letters
such as "ab" or "gf" - which make Linux say a password is "simplistic".
<p>
<img src="/password-entropy.png"><br>
<b>Distribution of generated password entropy as a function of target length</b>
<br>(for 50,000 word corpus)
<p>
<i>These passwords are generated on the server using a pseudo-random number generator.
But I can't tell which one you pick, if any. I don't log generated passwords or attempt to 
figure out who you are from browser fingerprinting, but then you only have my word for that...</i>

<p>
See also:
<ul>
<li><a href="mkpasswd.pl">random character password generator</a>
<li><a href="http://rumkin.com/tools/password/passchk.php">Password strength test</a> (Rumkin.com)
<li><a href="http://xkcd.com/792/">xkcd &quot;Password Reuse&quot;</a>
(<a href="http://forums.xkcd.com/viewtopic.php?f=7&t=64172">discussion</a>)
</ul>

</body>
</html>
EOT

sub html_escape {
  $_ = $_[0] ;
  s/\&/\&amp;/g ;
  s/\</\&lt;/g ;
  s/\>/\&gt;/g ;
  return $_ ;
}


sub generatePassword {
  $minlen = shift ; 
  my $password = $word[int(rand($N))] ;
  while (length($password) < $minlen) {
    $password .= $join ;
    $password .= $word[int(rand($N))] ;
  }
  return $password
} 
