Work the Shell: Generating Good Passwords

Dave works on a new method for generating secure passwords with the help of 1Password. By Dave Taylor

A while back I shared a script concept that would let you enter a proposed password for an account and evaluate whether it was very good (well, maybe "secure" would be a better word to describe the set of tests to ensure that the proposed password included uppercase, lowercase, a digit and a punctuation symbol to make it more unguessable).

Since then, however, I've really been trying personally to move beyond mnemonic passwords of any sort to those that look more like gobbledygook. You know what I mean—passwords like fRz3li,4qDP? that turn out to be essentially random and, therefore, impossible to crack using any sort of dictionary attack.

Aiding me with this is the terrific password manager 1Password. You can learn more about it here, but the key feature I'm using is a combination of having it securely store my passwords for hundreds of websites and having a simple and straightforward password generator feature (Figure 1).

Password screen

Figure 1. 1Password Password Generation System

If I'm working on the command line, however, why pop out to the program to get a good password? Instead, a script can do the same thing, particularly if I again tap into the useful $RANDOM shortcut for generating random numbers.

Generating Secure Passwords

The easiest way to fulfill this task is to have a general-purpose approach to generating a random element from a specific set of possibilities. So, a random uppercase letter might be generated like this:


uppers="ABCDEFGHIJKLMNOPQRSTUVWXYZ"

letter=${uppers:$(( $RANDOM % 26 )):1}

The basic notational convention used here is the super handy Bash shell variable slicing syntax of:


${variable:startpoint:charcount}

To get the first character only of a variable, for example, you can simply reference it as:


${variable:1:1}

That's easy enough. Instead of a fixed reference number, however, I'm using $(( $RANDOM % 26 )) as a way to generate a value between 0–25 that's different each time.

Add strings that contain all the major character classes you seek and you've got a good start:


lowers="abcdefghijklmnopqrstuvwxyz"
digits="0123456789"
punct="()./?;:[{]}|=+-_*&^%$#@!~"  # skip quotes

To get even fancier, there's another notation ${#variable} that returns the number of characters in a variable, so the following shows that there are 24 characters in that particular string:


$ echo ${#punct}
24

To be maximally flexible, every reference can include that too, allowing me to add or delete specific punctuation marks at will:


${punct:$(( $RANDOM % ${#punct} )):1}

It's starting to look a bit like my cat ran over the keyboard, but that's why I add all the spaces here. Many script writers tend to eschew all those spaces and use shorter variable names, but in my opinion, something like this is definitely harder to read and understand:


${p:$(($RANDOM % ${#p})):1}

In fact, it reminds me of an old programming language called APL where it was generally accepted that it was easier to rewrite code than understand what someone else had done within a program. Yikes.

This solves the challenge of producing a random character in a specific charset, I'll defer the next piece of the script until my next column, at which point I'll show how to build a sequence of these random characters to create a string of the desired length and complexity.

Add lowercase and a constrained set of punctuation and some rules on how many of each you want, and you can make some pretty complicated passwords. To start, let's just focus on a random sequence of n uppercase letters.

That's easily done:


while [ ${#password} -lt $length ] ; do
   letter=${uppers:$(( $RANDOM % ${#uppers} )):1}
   password="${password}$letter"
done

Remember that the ${#var} notation produces the length of the current value of that variable, so this is an easy way to build up the $password variable until it's equal to the target length as specified in $length.

Here's a quick test run or two:


$ sh makepw.sh
password generated = HDBYPMVETY
password generated = EQKIQRCCZT
password generated = DNCJMMXNHM

Looks great! Now the bigger challenge is to pick randomly from a set of choices. There are a couple ways to do it, but let's use a case statement, like this:


while [ ${#password} -lt $length ] ; do
  case $(( $RANDOM % 4 )) in
     0 ) letter=${uppers:$(( $RANDOM % ${#uppers} )):1}  ;;
     1 ) letter=${lowers:$(( $RANDOM % ${#lowers} )):1}  ;;
     2 ) letter=${punct:$((  $RANDOM % ${#punct}  )):1}  ;;
     3 ) letter=${digits:$(( $RANDOM % ${#digits} )):1}  ;;
  esac
  password="${password}$letter"
done

Since you're basically weighing upper, lower, digits and punctuation the same, it's not a huge surprise that the resultant passwords are rather punctuation-heavy:


$ sh makepw.sh
password generated = 8t&4n=&b(B
password generated = 5=B]9?CEqQ
password generated = |1O|*;%&A;

These are all great passwords, impossible to guess algorithmically (and, yeah, hard to remember too, but that's an inevitable side effect of this kind of password algorithm).

But let's say that you'd rather have it be biased toward letters and digits than punctuation, because it's so much easier to type. That can be done by simply expanding the random number choice and assigning more than one value to those options you want to have appear more frequently, like this:


while [ ${#password} -lt $length ] ; do
   case $(( $RANDOM % 7 )) in
     0|1 ) letter=${uppers:$(( $RANDOM % ${#uppers})):1} ;;
     2|3 ) letter=${lowers:$(( $RANDOM % ${#lowers})):1} ;;
     4|5 ) letter=${punct:$(( $RANDOM % ${#punct}  )):1} ;;
     6 ) letter=${digits:$(( $RANDOM % ${#digits}  )):1} ;;
   esac
  password="${password}$letter"
done

This works better, and the results are a bit less like a cat running across your keyboard:


$ sh makepw.sh
password generated = /rt?7D8QxR
password generated = us&*gpyB*-
password generated = rB}?2:)eJM
password generated = PC34jOD_}2

Next time, maybe I'll switch things around and let the user specify desired length and probability of punctuation being added to the password produced. Stay secure until then!

Revisiting Last Month's Script: Randomly Switching Upper and Lowercase

Last time, I talked about what's known informally as l33t-speak, a series of letter and letter-pair substitutions that marks the jargon of the hacker elite (or some subset of hacker elite, because I'm pretty sure that real computer security experts don't need to substitute vowels with digits to sound cool and hip).

Still, it was an interesting exercise as a shell-scripting problem, because it's surprisingly simply to adapt a set of conversion rules into a sequence of commands. I sidestepped one piece of it, however, and that's what I want to poke around with: changing uppercase and lowercase letters somewhat randomly.

This is where "Linux Journal" might become "LiNUx jOurNAl", for example. Why? Uhm, because it's a puzzle to solve. Jeez, you ask such goofy questions of me!

Breaking Down a Line Letter by Letter

The first and perhaps most difficult task is to take a line of input and break it down letter by letter so each can be analyzed and randomly transliterated. There are lots of ways to accomplish this in Linux (of course), but I'm going to use the built-in Bash substring variable reference sequence. It looks like this:


${variable:index:length}

So to get just the ninth character of variable input, for example, I could use ${input:9:1}. Bash also has another handy variable reference that produces the length of the value of a particular variable: ${#variable}. Put the two together, and here's the basic initialization and loop:


input="$*"
length="${#input}"

while [ $charindex -lt $length ]
do
    char="${input:$charindex:1}"
    # conversion occurs here
    newstring="${newstring}$char"
    charindex=$(( $charindex + 1 ))
done

Keep in mind that charindex is initialized to 0, and newstring is initialized to "", so you can see how this quickly steps through every character, adding it to newstring. "Conversion occurs here" is not very exciting, but that's the placeholder you need.

Lower, Meet Upper, and Vice Versa

Last time I also showed a quick and easy way to choose a number 1–10 randomly, so you can sometimes have something happen and other times not happen. In this command:


doit=$(( $RANDOM % 10 ))       # random virtual coin flip

Let's say there's only a 30% chance that an uppercase letter will convert to lowercase, but a 50% chance that a lowercase letter will become uppercase. How do you code that? To start, let's get the basic tests:


if [ -z "$(echo "$char" | sed -E 's/[[:lower:]]//')" ]
then
   # it's a lowercase character
elif [ -z "$(echo "$char" | sed -E 's/[[:upper:]]//')" ]
then
   # it's uppercase
fi

This is a classic shell-script trick: to ascertain if a character is a member of a class, replace it with null, then test to see if the resultant string is null (the -Z test).

The last bit's easy. Generate the random number, then if it's below the threshold, transliterate the char; otherwise, do nothing. Thus:


if [ -z "$(echo "$char" | sed -E 's/[[:lower:]]//')" ]
then
  # lowercase. 50% chance we'll change it
  if [ $doit -lt 5 ] ; then
    char="$(echo $char | tr '[[:lower:]]' '[[:upper:]]')"
  fi
elif [ -z "$(echo "$char" | sed -E 's/[[:upper:]]//')" ]
then
  # uppercase. 30% chance we'll change it
  if [ $doit -lt 3 ] ; then
    char="$(echo $char | tr '[[:upper:]]' '[[:lower:]]')"
  fi
fi

Put it all together and you have this Frankenstein's monster of a script:


$ sh changecase.sh Linux Journal is a great read.
LiNuX JoURNal is a GrEaT ReAd.
$ !!
LINuX journAl IS a gREat rEAd
$

Now you're ready for writing some ransom notes, it appears!

About the Author

Dave Taylor has been hacking shell scripts on UNIX and Linux systems for a really long time. He's the author of Learning Unix for Mac OS X and Wicked Cool Shell Scripts. You can find him on Twitter as @DaveTaylor, and you can reach him through his tech Q&A site: Ask Dave Taylor.

Dave Taylor