aboutsummaryrefslogblamecommitdiffstats
path: root/makepass.pl
blob: df8ad109ed283d93daf7239d45a8bbdbdc2e5d0c (plain) (tree)
1
2
3
4
5
6
7
8




                                       
                        
 
                                                       
































































































                                                                              

                                                               































                                                                                

                                                                 



                                                       
#!/usr/bin/env perl
#
# Author  : Dennis Eriksen <d@ennis.no>
# 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 = <FILE> ) 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 );