#!/usr/bin/perl

# by tim ellis - very different from the script it was based off of,
# and I don't think the original script had author attribution, so.
#
# released under terms of GPL v2 and no later. share and share alike, people.

# defaults
my $opt_w = "/usr/share/dict/words";
my $opt_n = 1;
my $opt_f = 4;
my $opt_a = 0;
my $opt_v = 0;
my $opt_l = "";
my $opt_r = "";

&Getopts("rlavhn:o:f:w:") || die("USAGE: $0 [-h] [-n number] \n");
 
if (defined($opt_h)) {
    print "common usage: $0 [-n number] [-l]\n";
    print "\n";
    print "Generate random passords. The subjective measure of WORST..BEST is based on\n";
    print "ease of memorization for a human. It's safe to say all passwords generated by\n";
    print "this script have a good amount of entropy.\n";
    print "\n";
    print "Recommended Options:\n";
    print "   -n NNN      generate NNN random passwords default=1\n";
    print "   -l          longer passwords - harder to remember and type\n";
    print "   -r          ridiculously-long passwords (implies and extends -l)\n";
    print "Additional Options:\n";
    print "   -a          all lowercase - harder to remember, easier to type\n";
    print "   -f 1        (BAD)  word fragments separated by symbol with random numbers appended\n";
    print "   -f 2        (WORST) same as 1 but with up to 4 letters capitalised\n";
    print "   -f 3        (OKAY) random word fragments with random digits prepended/appended\n";
    print "   -f 4        (BEST) random word fragments\n";
    print "   -w file     word file to load instead of /usr/share/dict/words\n";
    print "   -v          verbose mode\n";
    print "   -h          show this help\n";
 
    exit(0);
}

my $wordfile = $opt_w;
my $count = $opt_n;
my $desiredFormat = $opt_f;

# first thing, read all words longer than 4 chars into memory
open (WORDS, "$wordfile") || die "can't open wordfile";
while (my $inline = <WORDS>) {
    chomp $inline;

    if (length($inline) > 4) {
        $inline =~ s/[^A-Za-z]//g;
        $words[$wordnum++] = $inline;
    }
}
close (WORDS);

# here are words someone in the world really hates
my @badwords = split("/", "fuck/fuk/fck/shit/crap/lick/poop/anus/jesus/allah/religion/suck/dung/dick/dork/jerk/cunt/cock/piss/dung/turd/terd/penis/vagina/pussy/cum/wack/hurt/dumb/tit/butt/urine/feces/hole");

if ($opt_v) { print " - got $wordnum words\n"; }

# print however many passwords they requested
for ( ; $count > 0 ; $count--) {
    if ($opt_v) { print " + password#$count: "; }

    # figure out the sort of password they want
    if ($desiredFormat == 0) { $format = int(rand 4) + 1; } else { $format = $desiredFormat; }

    if ($opt_r) { $opt_l = 1; }
    $passWord = genPassword($format);
    if ($opt_r) { $passWord .= genPassword($format); }

    print $passWord . "\n";
}
  
# we are done; exit
exit 0;

# -------------------------------------------------------
# main routine - generates a password
sub genPassword {
    my $format = shift;

    my $word1 = "";
    my $word2 = "";
    my $word = "";
    my $splitPoint = 0;

    my $badWordFound = 1;
    while ($badWordFound) {
        # the most complicated for humans to remember
        if ($format == 1 || $format == 2) {
            $splitPoint = rand(3) + 3;
            $word1 = leftWord($splitPoint);
            $word1 = addSmartLetter($word1, 2);
            $word2 = leftWord(8 - $splitPoint) ;
            $word = $word1 . randSymbol() . $word2 . randomNumber(2);
            if ($opt_l) { $word .= randSymbol(); }
        };

        # just format=1 with extra remembering-difficulty
        if ($format == 2) {
            # uppercase it more; uppercase 4 random letters
            # this might not create passwords with 4 uppercase letters
            # because it might choose an already-uppercase letter or a
            # digit or a symbol
            for (my $uppercase = 0; $uppercase < 4; $uppercase ++ ) { 
                my $offset = int(rand length($word)) + 1;
                substr($word, $offset, 1) = uc(substr($word, $offset,1));
            }
        }

        # a little easier for humans, but random digits
        if ($format == 3) {
            $splitPoint = rand (4) + 3;
            $word = randomNumber(1) . leftWord($splitPoint) . rightWord(3) . leftWord(10 - $splitPoint) . randomNumber(2);
        }

        # easiest for humans to remember
        if ($format == 4) {
            $splitPoint = rand (4) + 4;
            $word1 = leftWord($splitPoint);
            $word1 = addSmartLetter($word1, 2);
            $word = $word1 . leftWord(12 - $splitPoint) . rightWord(4) ;
        }

        if ($opt_l) {
           $splitPoint = rand (4) + 4;
           $word .= leftWord($splitPoint) . rightWord(9 - $splitPoint);
        }
        if ($opt_v) { print "format=$format: "; }

        $badWordFound = 0;
        foreach my $bword (@badwords) {
            if ( $word =~ /$bword/i ) {
                if ($opt_v) { print "Censored: $word (has $bword): "; }
                $badWordFound = 1;
                last;
            }
        }
    }

    if ($opt_a) { $word = lc ($word); }
    return $word;
}

# ----------------------------------------------------------------------
# return the left N letters of a random word minimum N chars long
sub leftWord {
    $numletters = shift;
    $randWord = randomWord($numletters);
    return substr($randWord,0,$numletters - 1);
}
# ----------------------------------------------------------------------
# return the right N letters of a random word minimum N chars long
sub rightWord {
   my $numletters = shift;
   my $randWord = randomWord($numletters);
   my $offset = length($randWord) - $numletters;
   return substr($randWord,$offset,$numletters);
}
# ----------------------------------------------------------------------
# random word from N to N+4 letters long
sub randomWord {
    my $numletters = shift;
    my $guess = int(rand $wordnum);
    while (length($words[$guess]) < $numletters || length($words[$guess]) > $numletters + 4) {
        $guess++;
        if ($guess > $wordnum) {
            $guess = 0;
        }
    }
    my $retWord = $words[$guess];
    unless ($opt_a) { substr($retWord, 0, 1) = uc (substr($retWord, 0, 1)); }
    return $retWord; 
}
# ----------------------------------------------------------------------
# add N letters to end of word trying to maintain some pronounceability
sub addSmartLetter {
    my $inWord = shift;
    my $numletters = shift;
    for (my $i=0; $i < $numletters; $i++) {
        if ( is_end_in_vowel($inWord) ) {
            $inWord .= randomConsonant();
        } else {
            $inWord .= randomVowel();
        }
    }
    return $inWord;
}

# ----------------------------------------------------------------------
# subroutines to return random somethingerothers
# ----------------------------------------------------------------------
sub randSymbol {
    my $possiblechars = "/~_-+=!#\$\%^*():/\"'";
    my $offset = int (rand length($possiblechars));
    return substr($possiblechars, $offset, 1);
}
sub randomNumber {
    my $numDigits = shift;
    my $possiblechars = "1234567890";
    my $retNum = "";
    for (my $i = 0; $i < $numDigits; $i++) {
        my $offset = int (rand length($possiblechars));
        $retNum .= substr($possiblechars, $offset, 1);
    }
    return $retNum;
}
sub randomLetter {
    my $possiblechars = "aabcdeefghijklmnoopqrssttuvwxyz";
    my $offset = int (rand length($possiblechars));
    return substr($possiblechars, $offset, 1);
}
sub randomVowel {
    my $possiblechars = "aaaeeeiiiooouuy";
    my $offset = int (rand length($possiblechars));
    return substr($possiblechars, $offset, 1);
}
sub randomConsonant {
    my $possiblechars = "bcdffghjkllmnpqrssttvwxz";
    my $offset = int (rand length($possiblechars));
    return substr($possiblechars, $offset, 1);
}
# ----------------------------------------------------------------------


# this will return if the last char in a string is vowel
sub is_end_in_vowel {
    local $instring = shift;
    local ($lastchar) = $instring =~ /(.)$/; 

    my $vowel_list = "aeiouy";
    if ( $vowel_list =~ /$lastchar/i ) {
        return 1;
    } else {
        return 0;
    }
}



# ----------------------------------------------------------------------
# This code was adapted (read that, stolen) from perl's getopt.pl library.
sub Getopts {
    local($argumentative) = @_;
    local(@args,$_,$first,$rest,$errs);

    @args = split( / */, $argumentative );
    while(($_ = $ARGV[0]) =~ /^-(.)(.*)/) {
        ($first,$rest) = ($1,$2);
        $pos = index($argumentative,$first);
        if($pos >= $[) {
            if($args[$pos+1] eq ':') {
                shift(@ARGV);
                if($rest eq '') {
                    $rest = shift(@ARGV);
                }
                eval "\$opt_$first = \$rest;";
            }
            else {
                eval "\$opt_$first = 1";
                if($rest eq '') {
                    shift(@ARGV);
                }
                else {
                    $ARGV[0] = "-$rest";
                }
            }
        }
        else {
            print STDERR "Unknown option: $first\n";
            ++$errs;
            if($rest ne '') {
                $ARGV[0] = "-$rest";
            }
            else {
                shift(@ARGV);
            }
        }
    }
    $errs == 0;
}