#!/usr/bin/env perl # # Author : Dennis Eriksen # File : makepass.pl # Created : 2023-07-27 # License : BSD-3-Clause # # Copyright (c) 2018-2022 Dennis Eriksen • d@ennis.no 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 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 LOWER => 'abcdefghijklmnopqrstuvwxyz', UPPER => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', DIGIT => '0123456789', OTHER => '!$%&#/()=?+-_,.;:<>[]{}|\@*', }; my @lower = split //, LOWER; my @upper = split //, UPPER; my @digit = split //, DIGIT; my @other = split //, OTHER; my @alpha = ( @lower, @upper ); my @alnum = ( @alpha, @digit ); my @every = ( @alnum, @other ); my $length = $ARGV[0] // $ENV{'MAKEPASS_DEFAULT_LENGTH'} // 0; my @normal = $ENV{'MAKEPASS_NORMAL'} ? split //, $ENV{'MAKEPASS_NORMAL'} : ( @alnum, '-', '_' ); my @special = $ENV{'MAKEPASS_SPECIAL'} ? split //, $ENV{'MAKEPASS_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 ); # # 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 # # Function to create random string # sub randstring (@chars) { @chars = @normal if !@chars; my $len = $length || int( rand( RANGE_MAX - RANGE_MIN ) + RANGE_MIN ); my $str; for my $i ( 1 .. $len ) { $str .= ( $i == 1 || $i == $len ) ? $alnum[ rand @alnum ] : $chars[ rand @chars ]; } return $str; } # # Print in random strings in columns # sub print_columns ( $title, $num, @chars ) { say "$title:"; 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 for my $i ( 1 .. $num ) { printf "%-${length}s%s", $strings[ $i - 1 ], ( $i % $columns == 0 ) ? "\n" : ""; print "\n" if ( $i == $num && $i % $columns > 0 ); } print "\n"; } # # Print passwords # print_columns( "Normal passwords", NORM_NUM, @normal ); print_columns( "Passwords with special characters", SPEC_NUM, @special ); # # Print Passphrases # say "Passphrases:"; open( FILE, "<", $wordlist ) or die("Can't open file"); chomp( my @wordlist = ) and close(FILE); # # Return passphrases # sub passphrase ($arrh) { # Use array-handle to avoid copying large array around my @indexes; push( @indexes, rand( @{$arrh} ) ) foreach ( 1 .. PASS_NUM ); join( '-', @{$arrh}[@indexes] ); } say passphrase( \@wordlist ) foreach ( 1 .. PASS_NUM );