In the password generating tool from my last
article,
at its most simple, you specify the number of characters you want in the
password, and each is then chosen randomly from a pool of acceptable values.
With the built-in RANDOM
in the Linux shell, that's super easy to do:
okay="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
okay="${okay}0123456789<>/?,>;:[{]}\|=+-_)(^%$#@!~
length=10
ltrs=${#okay}
while [ $length -ge 0 ]
do
letter="${okay:$RANDOM % $ltrs:1}"
result="$result$letter"
length=$(( $length - 1 ))
done
echo "Result: $result"
In the actual script, I set okay
to a single value rather than
build it in
two steps; this is just for formatting here online. Otherwise,
ltrs
is set to
the length of $okay
as a speedy shortcut, and the result is built up by using
the string slicing syntax of:
${variable:indexlocation:length}
To extract just the fourth character of a string, for example,
${string:4:1}
, this
works fine and is easy. The result speaks for itself:
$ sh lazy-passwords.sh
Result: Ojkr9>|}dMr
And, a few more:
Result: Mi8]TfJKVaH
Result: >MWvF2D/R?r
Result: h>J6\p4eNPH
Result: KixhCFZaesr
Where this becomes a more complex challenge is when you decide you don't want to have things randomly selected but instead want to weight the results so that you have more letters than digits, or no more than a few punctuation characters, even on a 15–20 character password.
Which is, of course, exactly what I've been building.
I have to admit that there's a certain lure to making something complex, if nothing else than just to see if it can be done and work properly.
As a result, the simple few lines above changed to this in my last article:
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
In the above code, I'm assigning probability that a given letter will be one of four classes: uppercase, lowercase, punctuation or digits. The first three are each 2:7 chances, and punctuation is 1:7, or half as likely to be produced.
A run of four iterations of the above algorithm produces these results:
q.x0bAPmZb
P}aWX2N-U]
5jdI&ep7rt
-k:TA[1I!3
Since random is, well, random, in both situations, you actually can end up with a password that includes no punctuation. So, how do you force a specific number of occurrences of punctuation symbols? One solution is to check after the fact to see if you met your target count.
To do that, you'll need two things: goals and counters. Let's add
the former as startup options in a typical getops
block:
while getopts "l:d:p:" arg
do
case "$arg" in
l) length=$OPTARG ;;
d) digitGoal=$OPTARG ;;
p) punctGoal=$OPTARG ;;
*) echo "Valid -l length, -d digits, -p punctuation"
exit 1 ;;
esac
done
The counters are also easy; every time a digit or punctuation condition is met in the case statement (shown earlier), you increment the counter by one.
Finally, at the end, you can compare the two and see if you met your weighted randomization goals:
if [ $digitsAdded -lt $digitGoal ] ; then
echo "Didn't add enough digits. [goal = $digitGoal,
inserted = $digitsAdded]"
exit 1
elif [ $punctAdded -lt $punctGoal ] ; then
echo "Didn't add enough punctuation. [goal = $punctGoal,
inserted = $punctAdded]"
exit 1
fi
If the total length of the requested password is reasonable compared to the random chance that a digit or punctuation character will be added, this will work fine. A 15-character password with at least two punctuation characters will be generated without a hiccup almost every single time.
Although once in a while:
$ sh makepw.sh -l 15 -p 4
Didn't add enough punctuation. [goal = 4, inserted = 1]
This begs the most important question of the script algorithm: what do you do once you realize that you haven't met your digit and punctuation character goals?
One solution is simply to try again, but if the user sets up an impossible situation, like a six-character password with four digits and four punctuation characters, or even four and two, that's no bueno.
Another possibility is to step through the generated password, replacing unconstrained values (such as upper and lowercase) with the specific value required. This has the consequence that if you ask for a lot of punctuation or digits, you're going to end up having those requested characters front-loaded, which isn't exactly random.
What if, instead of producing a random password, you split it into two steps? The first step is to generate the required number of random digits and random punctuation characters, add completely random values to add up to the desired length, then "shuffle" the result to produce the final password.
I know, you're almost done with this program, but that's a really interesting solution that sidesteps a lot of problems, so let's just retrench and start over!
Actually, it's not that bad, because most of the work's already been done. This will just make it simpler:
while [ ${#password} -lt $length ] ; do
if [ $digitsAdded -lt $digitGoal ] ; then
letter=${digits:$(( $RANDOM % ${#digits} )):1}
digitsAdded=$(( $digitsAdded +1 ))
elif [ $punctAdded -lt $punctGoal ] ; then
letter=${punct:$(( $RANDOM % ${#punct} )):1}
punctAdded=$(( $punctAdded +1 ))
else
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}
punctAdded=$(( $punctAdded + 1 )) ;;
6) letter=${digits:$(( $RANDOM % ${#digits} )):1}
digitsAdded=$(( $digitsAdded +1 )) ;;
esac
fi
password="${password}$letter"
done
Without the final password-scrambler code, here's what you get with a couple invocations for a 15-character password with at least four digits and at least four punctuation characters:
$ sh makepw.sh -l 15 -p 4 -d 4
Interim password generated: 8119?:)@_g&rw%=
$ sh makepw.sh -l 15 -p 4 -d 4
Interim password generated: 7599}(|&l*4KFY/
You clearly can see how these are front-loaded, with the digits required, the punctuation required and then "everything else".
There are a lot of ways to shuffle the letters in a word within a shell
script, ranging from invoking Perl or Awk to using the Linux
shuf
command to
solving it yourself. I'm going to leave this as an exercise for the
reader, because with that small added step, you've got a fully functional
password generator that's ready to take on your hundreds of system
users.