diff options
Diffstat (limited to 'makepass.pl')
-rwxr-xr-x | makepass.pl | 300 |
1 files changed, 228 insertions, 72 deletions
diff --git a/makepass.pl b/makepass.pl index df8ad10..36bfcf1 100755 --- a/makepass.pl +++ b/makepass.pl @@ -10,22 +10,19 @@ use strict; use warnings; use v5.10.0; -use feature qw(signatures); -use constant { - NORM_NUM => 10, # number of normal passwords - SPEC_NUM => 6, # number of special passwords - PASS_NUM => 6, # number of passphrases +use Getopt::Std; - MAX_LENGTH => 255, # max length of passwords - RANGE_MAX => 42, # max length when using random length - RANGE_MIN => 8, # min length when using random length - PASS_WORDS => 8, # number of words in passphrases +use constant { + MAX => 255, # max length of passwords + RANGE_MAX => 42, # max length when using random length + RANGE_MIN => 8, # min length when using random length + PASS_WORDS => 8, # number of words in passphrases LOWER => 'abcdefghijklmnopqrstuvwxyz', UPPER => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', DIGIT => '0123456789', - OTHER => '!$%&#/()=?+-_,.;:<>[]{}|\@*', + OTHER => '!$%&#/()=?+-_,.;:<>[]{}|@*', }; my @lower = split //, LOWER; @@ -36,7 +33,9 @@ my @alpha = ( @lower, @upper ); my @alnum = ( @alpha, @digit ); my @every = ( @alnum, @other ); -my $length = $ARGV[0] // $ENV{'MAKEPASS_DEFAULT_LENGTH'} // 0; +my $length = $ENV{'MAKEPASS_LENGTH'} // 0; +my $number = $ENV{'MAKEPASS_NUMBER'} // 10; +my $printlen = 0; my @normal = $ENV{'MAKEPASS_NORMAL'} ? split //, $ENV{'MAKEPASS_NORMAL'} @@ -47,99 +46,256 @@ my @special = : (@every); my $wordlist = $ENV{'MAKEPASS_WORDLIST'} || '/usr/share/dict/words'; -# -# Some errorhandling -# -die "Length has to be a whole number between 0 and " - . MAX_LENGTH - . ". 0 means random length, and is the default." - if ( $length !~ /^[[:digit:]]+$/ || $length < 0 || $length > MAX_LENGTH ); - -die "The file \"$wordlist\" does not exist" if ( !-f $wordlist ); +my $col_width; +my $col_num; -# -# Seed rand with data from /dev/random -# -# This is probably not necessary, but I do this in makepass.zsh and I want the -# scripts to match -open( my $random, '<:raw', '/dev/random' ) or die $!; -read( $random, my $bytes, 8 ) and close($random); - -# See https://perldoc.perl.org/functions/pack -srand( unpack( "L", $bytes ) ); - -# # Get screen width so we can print in pretty columns -# # TODO: Native perl. Possible solutions: # - Term::ReadKey # - not a builtin module. Needs installing everywhere. # - builtin module ioctl and posix-call `TIOCGWINSZ`. # - does not work on all systemd. I.e. OpenBSD. -my $width = `tput cols` || 80; -chomp($width); # remove \n at end of line +my $xwidth = `tput cols` || 80; +chomp($xwidth); # remove \n at end of line # -# Function to create random string +# Main function! Go magic! # -sub randstring (@chars) { - @chars = @normal if !@chars; - my $len = $length || int( rand( RANGE_MAX - RANGE_MIN ) + RANGE_MIN ); - my $str; +sub main { + # + # Getopts + # - for my $i ( 1 .. $len ) { - $str .= - ( $i == 1 || $i == $len ) - ? $alnum[ rand @alnum ] - : $chars[ rand @chars ]; - } + getopts('hl:n:p'); + our ( $opt_h, $opt_l, $opt_n, $opt_p ); - return $str; + # -h = help + help() if ($opt_h); + + # -l NUM = length of passwords + # I know this check is unnecessary because we check $LENGTH below, but + # I still want it. + die "-l takes a number between 0 and " . MAX + if ( $opt_l && !int_in_range( $opt_l, 0, MAX ) ); + $length = $ARGV[0] // $opt_l // $length; + + # -n NUM = number of passwords + die "-n takes a number between 0 and " . MAX + if ( $opt_n && !int_in_range( $opt_n, 1, MAX ) ); + $number = $opt_n // $number; + + # -p = print length of passwords + $printlen = 1 if $opt_p; + + # + # Some errorhandling + # + die "only one argument" if ( scalar(@ARGV) > 1 ); + + die "length must be a number between 0 and " . MAX + unless ( int_in_range( $length, 0, MAX ) ); + + die "number-argument must be between 1 and " . MAX + unless ( int_in_range( $number, 1, MAX ) ); + + # + # Some other stuff + # + + # Seed rand() with bits from /dev/random. Because. + open( my $random, '<:raw', '/dev/random' ) or die $!; + read( $random, my $bytes, 8 ) and close($random); + + # See https://perldoc.perl.org/functions/pack + srand( unpack( "L", $bytes ) ); + + $printlen = $length < 100 ? 2 : 3 if $printlen; + + $col_width = ( $length ? $length : RANGE_MAX ) + 2; + $col_num = + int( $xwidth / ( $col_width + ( $printlen ? $printlen + 1 : 0 ) ) ) || 1; + + # + # Print passwords + # + print_columns( "Normal passwords", $number, @normal ); + print "\n"; + print_columns( "Passwords with special characters", + int( $number / 3 * 2 + 1 ), @special ); + + # + # Print Passphrases + # + # But only if we can read the wordlist + if ( -r $wordlist ) { + print "\n"; + say "Passphrases:"; + open( FILE, "<", $wordlist ) or die("Can't open file"); + chomp( my @wordlist = <FILE> ) and close(FILE); + + say passphrase( \@wordlist ) foreach ( 1 .. int( $number / 2 ) ); + } } # # Print in random strings in columns # -sub print_columns ( $title, $num, @chars ) { - say "$title:"; - +#sub print_columns ( $title, $num, @chars ) { +sub print_columns { +say "@_"; + my $title = shift(@_); + my $num = shift(@_); + my @chars = @_; my @strings; + push( @strings, randstring(@chars) ) foreach ( 1 .. $num ); - # Calculate the number of columns that can fit on the screen. - my $length = - ( $length || RANGE_MAX ) + 2; # Add two for spacing between columns - my $columns = int( $width / $length ) || 1; # Minimum 1 col + say "$title:"; for my $i ( 1 .. $num ) { - printf "%-${length}s%s", - $strings[ $i - 1 ], - ( $i % $columns == 0 ) ? "\n" : ""; - print "\n" if ( $i == $num && $i % $columns > 0 ); + printf "%0${printlen}i ", length( $strings[ $i - 1 ] ) if ($printlen); + printf "%-${col_width}s", $strings[ $i - 1 ]; + print "\n" + if ( $i % $col_num == 0 || ( $i == $num && $i % $col_num > 0 ) ); } - print "\n"; } # -# Print passwords +# Function to create random string # -print_columns( "Normal passwords", NORM_NUM, @normal ); -print_columns( "Passwords with special characters", SPEC_NUM, @special ); +#sub randstring (@chars) { +sub randstring { + my @chars = @_; + my $str; + my $len = $length || int( rand( RANGE_MAX - RANGE_MIN ) + RANGE_MIN ); -# -# Print Passphrases -# -say "Passphrases:"; -open( FILE, "<", $wordlist ) or die("Can't open file"); -chomp( my @wordlist = <FILE> ) and close(FILE); + @chars = @normal if !@chars; + + $str .= $alpha[ rand @alpha ]; + $str .= $chars[ rand @chars ] foreach ( 1 .. $len - 2 ); + $str .= $alnum[ rand @alnum ] if ( $len >= 2 ); + + return $str; +} # # Return passphrases # -sub passphrase ($arrh) { # Use array-handle to avoid copying large array around +#sub passphrase ($arrh) { +sub passphrase { # Use array-handle to avoid copying large array around + my $arrh = shift(@_); my @indexes; - push( @indexes, rand( @{$arrh} ) ) foreach ( 1 .. PASS_NUM ); - join( '-', @{$arrh}[@indexes] ); + + push( @indexes, rand( @{$arrh} ) ) foreach ( 1 .. PASS_WORDS ); + ( my $str = join( '-', @{$arrh}[@indexes] ) ) =~ tr/A-Za-z0-9_-//cd; + return $str; +} + +# +# Check if int and in range +# +#sub int_in_range ( $num, $min, $max ) { +sub int_in_range { + my $num = shift(@_); + my $min = shift(@_); + my $max = shift(@_); + + return ( $num =~ /^[[:digit:]]+$/ && $min <= $num && $num <= $max ) ? 1 : 0; +} + +# +# Help function +# +sub help { + my $str='NAME + makepass - create several random passwords + +SYNOPSIS + makepass [OPTIONS] [NUM] + + If a NUM is provided, passwords will be NUM characters long. + + By default `makepass` will output passwords from the three following classes: + + - Normal passwords - random strings with letters (both lower and upper + case), numbers, and dashes and underscores. + + - Passwords with special characters - random strings generated from lower + and upper case letters, numbers, and the following characters: + !#$%&/()=?+-_,.;:<>[]{}|@* + + - Passphrases - if we find a dictionary, a series of eight random words + from the dictionary, separated by dashes. The number of words can not be + changed, but you do not have to use all of them. Use as mane as you want. + + The first character will always be alphabetic, and the last will always be + alphanumeric. + +DESCRIPTION + makepass has the following options: + + -h + output this help-text + -l + length of passwords. See MAKEPASS_LENGTH below + -n + number of passwords. See MAKEPASS_NUMBER below + -p + print length of number + +ENVIRONMENT + makepass examines the following environmental variables. + + MAKEPASS_LENGTH + Specifies the length of passwords. Valid values are 0-255. If 0, a + random value between 8 and 42 will be used for each password. -l + overrides this environmental variable, and the argument NUM overrides + that again. So `MAKEPASS_LENGTH=10 makepass -l 12 14` will give + passwords that are 14 characters long, even though both -l and + MAKEPASS_LENGTH also specifies a length. + + MAKEPASS_NUMBER + The number of passwords to generate. This formula is used to determine + how many passwords from each group should be generated: + - (n) normal passwords + - (n / 3 * 2 + 1) special passwords + - (n / 2) passphrases + Where n is 10 by default. Valid values for n are 1-255. Floating-poing + math is not used, so results may vary. + + MAKEPASS_PRINTLEN + If 1, print length of all passwords. If 0, don\'t. + + MAKEPASS_NORMAL + String of characters from which to generate "normal" passwords. + Defaults to: + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_ + + MAKEPASS_SPECIAL + String of characters from which to generate passwords with special + characters. Defaults to the same characters as in MAKEPASS_NORMAL, plus + these: + !#$%&/()=?+-_,.;:<>[]{}|@* + + MAKEPASS_WORDLIST + Specifies the dictionary we find words for passphrases in. If this is + unset or empty, we try "/usr/share/dict/words". If that file does not + exist, no passphrases will be provided. + +NOTES + This scripts makes use of rand() - a perl function that returns a + pseudo-random number. It is not cryptographically secure. We initially + seed the random number generator with a random 32bit integer generated from + /dev/random. This should provide enough randomnes to generate sufficiently + secure passwords. + +AUTHOR + Dennis Eriksen <https://dnns.no>'; + + print "$str\n"; + + exit(0); } -say passphrase( \@wordlist ) foreach ( 1 .. PASS_NUM ); +# Call with & so it gets the same arguments as root. (@_) +&main; |