#!/usr/bin/perl
#
# $Id: pwMarkov.pl,v 1.1 2010/11/23 20:51:49 rdilley Exp $
#
# author: ron dilley
#
# desc: this perl script generates passwords using markov chains
#
# Copyright (C) 2010  Ron A. Dilley
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; version 2
# of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
# ron.dilley@uberadmin.com
#
############################################################################

#
# modules
#
use Getopt::Std;
use FileHandle;

#
# pragmas
#
use strict;

#
# set environment
#
$ENV{PATH} = "/usr/bin:/bin:/usr/sbin:/sbin:/usr/ucb";

#
# turn on autoflush
#
select STDERR; $| = 1;
select STDOUT; $| = 1;

#
# defines
#
$::TRUE = 1;
$::FALSE = 0;
$::FAILED = -1;

$::VERSION = '$Id: pwMarkov.pl,v 1.1 2010/11/23 20:51:49 rdilley Exp $';
$::PROGNAME = "pwMarkov.pl";

$::NONWORD = '\n';

%::Config = ();
$::Config{'debug'} = $::FALSE;
$::Config{'MAX_OUT_COUNT'} = 10000;
$::Config{'DEPTH'} = 2;
$::Config{'MIN_PW_LEN'} = 6;
$::Config{'MAX_PW_LEN'} = 15;

#
# main routine
#
if ( &main() != $::TRUE ) {
  exit( 1 );
}

exit( 0 );

############################################################################
#
# sub-routines
#

#
# main routine
#
sub main {
  my $arg;

  #
  # display script banner
  #
  &show_banner();

  #
  # parse command-line
  #
  &parse_command_line();

  # process args that are left
  while( $arg = shift( @::ARGV ) ) {
    &processFile( $arg );
  }

  # make some passwords
  &generatePasswords();

  # done
  return $::TRUE;
}

#
# display banner info
#
sub show_banner {
  print stderr "$::PROGNAME v$::VERSION\n";
  print stderr "By: Ron Dilley\n";
  print stderr "\n";
  print stderr "$::PROGNAME comes with ABSOLUTELY NO WARRANTY.\n";
  print stderr "This is free software, and you are welcome\n";
  print stderr "to redistribute it under certain conditions;\n";
  print stderr "See the GNU General Public License for details.\n";
  print stderr "\n";

  return $::TRUE;
}

#
# display help info
#
sub show_help {
  print stderr "Syntax:\n";
  print stderr "\n";
  print stderr "$::PROGNAME [options] {dir} [{dir} ...]\n";
  print stderr "\n";
  print stderr "-1         Set depth to 1\n";
  print stderr "-2         Set depth to 2\n";
  print stderr "-3         Set depth to 3\n";
  print stderr "-c {num}   Number of passwords to generate\n";
  print stderr "-d {0-9}   Display debug information during program run\n";
  print stderr "\n";

  return $::TRUE;
}

#
# parse command-line arguments
#
sub parse_command_line {
  no strict 'vars';

  if ( getopts( 'd:c:123l:m:' ) == $::FALSE ) {
    &show_help();
    return $::FAILED;
  }
  if ( defined $opt_d ) {
    if ( $opt_d > 0 ) {
      # set debug mode
      $::Config{'debug'} = $opt_d;
    }
  }
  if ( defined $opt_c ) {
    if ( $opt_c > 0 ) {
      $::Config{'MAX_OUT_COUNT'} = $opt_c;
    }
  }
  if ( defined $opt_l ) {
    if ( $opt_l > 0 ) {
      # set minimum password length
      $::Config{'MIN_PW_LEN'} = $opt_l;
    }
  }
  if ( defined $opt_m ) {
    if ( $opt_m > 0 ) {
      # set maximum password length
      $::Config{'MAX_PW_LEN'} = $opt_m;
    }
  }
  if ( defined $opt_1 ) {
    $::Config{'DEPTH'} = 1;
  }
  if ( defined $opt_2 ) {
    $::Config{'DEPTH'} = 2;
  }
  if ( defined $opt_3 ) {
    $::Config{'DEPTH'} = 3;
  }
  return $::TRUE;
}

#
# process file
#
sub processFile {
  my ( $fname ) = @_;
  my $fHandle = new FileHandle;
  my $line;
  my @tokens;
  my $token;
  my $tok1;
  my $tok2;
  my $tok3;

  print stderr "Opening [$fname] for read\n";

  if ( $fname eq "-" ) {
    # read from stdin
    if ( ! defined open( $fHandle, "<&STDIN" ) ) {
      print stderr "ERROR - Unable to dup stdin\n";
      return $::FAILED;
    }
  } else {
    if ( ! defined open( $fHandle, "< $fname" ) ) {
      print stderr "ERROR - Unable to open [$fname]\n";
      return $::FAILED;
    }
  }

  while ( $line = <$fHandle> ) {
    chomp( $line );
    #print "$line\n";

    # restart at the begining
    $tok1 = $tok2 = $tok3 = $::NONWORD;
    foreach $token ( split( //, $line )  ) {
      if ( $::Config{'DEPTH'} == 1 ) {
      	push( @{$::stateTable{$tok1}}, $token );
      	$tok1 = $token; # shift one token      
      } elsif ( $::Config{'DEPTH'} == 2 ) {
      	push( @{$::stateTable{$tok1}{$tok2}}, $token );
      	( $tok1, $tok2 ) = ( $tok2, $token ); # shift one token
      } elsif ( $::Config{'DEPTH'} == 3 ) {
        push( @{$::stateTable{$tok1}{$tok2}{$tok3}}, $token );
        ( $tok1, $tok2, $tok3 ) = ( $tok2, $tok3, $token );
      }
    }
    # end of the token sequence
    if ( $::Config{'DEPTH'} == 1 ) {
      push( @{$::stateTable{$tok1}}, $::NONWORD );
    } elsif ( $::Config{'DEPTH'} == 2 ) {
      push( @{$::stateTable{$tok1}{$tok2}}, $::NONWORD );
  	} elsif ( $::Config{'DEPTH'} == 3 ) {
  	  push( @{$::stateTable{$tok1}{$tok2}{$tok3}}, $::NONWORD );
  	}
  }

  close( $fHandle );

  return $::TRUE;
}

#
# generate passwords
#
sub generatePasswords {
  my @tokens;
  my $token;
  my $tok1;
  my $tok2;
  my $tok3;
  my $aPtr;
  my $rTok;
  my $i;
  my $done;
  my $pos;

  for ( $i = 0; $i < $::Config{'MAX_OUT_COUNT'}; $i++ ) {
    $tok1 = $tok2 = $tok3 = $::NONWORD;
    $done = $::FALSE;
    $pos = 0;
    while ( ! $done ) {
      if ( $::Config{'DEPTH'} == 1 ) {
        $aPtr = $::stateTable{$tok1};      
      } elsif ( $::Config{'DEPTH'} == 2 ) {
        $aPtr = $::stateTable{$tok1}{$tok2};
      } elsif ( $::Config{'DEPTH'} == 3 ) {
        $aPtr = $::stateTable{$tok1}{$tok2}{$tok3};      
      }
      $rTok = int( rand( scalar( @$aPtr ) ) );
      if ( ( $token = $aPtr->[$rTok] ) eq $::NONWORD ) {
	$tok1 = $tok2 = $tok3 = $::NONWORD;
	if ( $pos >= $::Config{'MIN_PW_LEN'} ) {
	  print "\n";
	  $done = $::TRUE;
	  $pos = 0;
	}
      } else {
	$pos++;
	print "$token";
	if ( $pos == $::Config{'MAX_PW_LEN'} ) {
	  print "\n";
	  $tok1 = $tok2 = $tok3 = $::NONWORD;
	  $done = $::TRUE;
	  $pos = 0;
	}
      }
      if ( $::Config{'DEPTH'} == 1 ) {
        $tok1 = $token;
      } elsif ( $::Config{'DEPTH'} == 2 ) {
        ( $tok1, $tok2 ) = ( $tok2, $token ); # shift
      } elsif ( $::Config{'DEPTH'} == 3 ) {
        ( $tok1, $tok2, $tok3 ) = ( $tok2, $tok3, $token );
      }
    }
  }
}
