Here’s how I pick good passwords

By | April 9, 2021

A billion people have written about this before, but I like my approach, so I’m sharing it in case someone else finds it useful.

Most of my passwords go into a password manager; those are long and random and generated by the password manager and I don’t care whether they’re easy to type or memorable or whatever. However, there are a few passwords I have to type by hand, e.g., the one I use to unlock my computer and my password manager’s master password. I want those to have a decent length, be reasonably memorable, be reasonably easy for a touch-typist to type, and be complex enough to satisfy the complexity requirements of any policies that might be imposed upon me.

To accomplish that, I have a password generator script which prints out ten randomly generated passphrases — multiple words separated by hyphens — satisfying my requirements as outlined below. When I need to pick a new password, I run the script in a terminal window that isn’t logging its output anywhere, scan the list, pick the first one that I think I’ll be able to remember, and use it. If I don’t like any of the ten passwords the script prints, I just keep running it until I like one.

Here’s what the script does to generate the passwords:

  • Suck up the contents of /usr/share/dict/words to get a list of words from which passwords can be built.
  • For each password being generated, start by randomly concatenating words together, separated by hyphens, until the password hits the minimum length (I use 12 characters).
  • If the password already has a capital letter in it, then just use that; otherwise create variants of the password by separately capitalizing each of its words.
  • If the password already has a number in it (some words in /usr/share/dict/words have numbers in them), then just use that; otherwise create variants of the password by separately translating each of the characters abegis to 438915 (l88t!).
  • Estimate how slow it is for a touch-typist to type each variant of the password based on how many repeat letters it has, how many keystrokes are required, and how often adjacent keystrokes are on the same hand. Keep the variant with the lowest typing complexity score.
  • If the resulting complexity score is too high, throw away the password.

Once the script finds ten passwords with low enough complexity scores, it prints them out, sorted by complexity score, with the score for each password next to it.

These algorithm gives me passwords with at least one lower-case, upper-case, symbol (the hyphen!), and digit, ensuring that they will pass pretty much all password complexity policies.

Here is the script (yes, it’s a Perl script; when I wrote the first version of it, Python didn’t exist). Let me know (comment below or send me email) if you find it useful!

#!/usr/bin/env perl
# Memorable, typeable password generator. See
# By Jonathan Kamens <>.
# This script is in the public domain. You are welcome to do whatever you want
# with it, though it would be nice if you'd give me credit somehow or at least
# send me email and let me know how you're using it.
# Change the following constants as appropriate.
$min_length = 14;
$max_score = 20;
$num_passwords = 10;
$words_file = '/usr/share/dict/words';
open(WORDS, '<', $words_file) or die;
while (<WORDS>) {
next if (/\W/);
next if (length($_) < 4);
push(@words, $_);
sub typing_score {
local($_) = @_;
my($left_lower) = "~12345qwertasdfgzxcvb";
my($left_upper) = "~!\@#$%QWERTASDFGZXCVB";
my($right_lower) = "67890-=yuiop[]\\hjkl;'nm,./";
my($right_upper) = "^&*()_+YUIOP{}|HJKL:\"NM<>?";
my(%left, %right, $lower, $upper);
map($left{$_}++, split(//, $left_lower), split(//, $left_upper));
map($right{$_}++, split(//, $right_lower), split(//, $right_upper));
map($lower{$_}++, split(//, $left_lower), split(//, $right_lower));
map($upper{$_}++, split(//, $left_upper), split(//, $right_upper));
my $score = 0;
# Repeat letters add a point
my $last_letter = undef;
foreach my $letter (split(//)) {
$score++ if ($letter eq $last_letter);
$last_letter = $letter;
foreach my $letter (split(//)) {
if ($upper{$letter}) {
push(@hands, $left{$letter} ? 'right' : 'left', 'both');
else {
push(@hands, $left{$letter} ? 'left' : 'right');
# A point for each keystroke
$score += scalar @hands;
# A point for each consecutive letter on the same hand
my $last_hand = undef;
foreach my $hand (@hands) {
$score++ if ($last_hand eq $hand or $last_hand eq 'both');
$last_hand = $hand;
return $score;
sub random_word {
return $words[int(rand(scalar @words))];
sub random_password {
my $pw = &random_word;
my $l = 0;
while ($l < $min_length) {
$pw .= '' . &random_word;
$l = length($pw);
return $pw;
sub variants {
local($_) = @_;
# One of the words has to be capitalized and one of the letters has to be
# replaced by a number.
if (/[A-Z]/) {
push(@cap_options, $_);
else {
my(@fragments) = split(/\b([a-z])/);
shift @fragments if ! $fragments[0];
for (my $i = 0; $i < @fragments; $i += 2) {
my $pw = '';
for (my $j = 0; $j < $i; $j++) {
$pw .= $fragments[$j];
$pw .= uc $fragments[$i];
$pw .= $fragments[$i+1];
for (my $j = $i + 2; $j < @fragments; $j++) {
$pw .= $fragments[$j];
push(@cap_options, $pw);
if (/[0-9]/) {
for (@cap_options) {
my(@fragments) = grep($_, split(/([abegis])/));
for (my $i = 0; $i < @fragments; $i++) {
my $char = $fragments[$i];
next if $char !~ /^[abegis]$/;
$char =~ tr/abegis/483915/;
my $pw = '';
for (my $j = 0; $j < $i; $j++) {
$pw .= $fragments[$j];
$pw .= $char;
for (my $j = $i + 1; $j < @fragments; $j++) {
$pw .= $fragments[$j];
push(@num_options, $pw);
while (scalar keys %passwords < $num_passwords) {
my $pw = &random_password;
my(@variants) = &variants($pw);
next if not @variants;
my $best_score = 999;
for (@variants) {
my $score = &typing_score($_);
if ($score < $best_score) {
$best_score = $score;
$pw = $_;
next if $best_score > $max_score;
$passwords{$pw} = $best_score;
foreach my $pw (sort { $passwords{$a} <=> $passwords{$b} } keys %passwords) {
print("$pw ($passwords{$pw})\n");

Print Friendly, PDF & Email

Leave a Reply

Your email address will not be published. Required fields are marked *