aboutsummaryrefslogtreecommitdiffstats
path: root/makepass.pl
blob: df8ad109ed283d93daf7239d45a8bbdbdc2e5d0c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#!/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 );