
Perl solution to Event 5 in the 2008 Winter Scripting Games.
Solutions are also available for VBScript and Windows PowerShell.
The Scripting Guys don’t know much about Perl, but what we do know is that the language is especially adept at working with string values. That made event 5 of special interest; that’s because Event 5 is all about working with string values, including regular expressions, the tr and s functions, even the chomp function. As newcomers to the language, the Scripting Guys enjoyed the fun – and challenge – of working with string values in a language known for its string-handling capabilities.
And yes, the Scripting Guys really did have fun working with Perl and its string-handling capabilities. That’s what happens when you start to get old: your idea of fun begins to change, dramatically.
At any rate, here’s the solution we managed to come up with. We hope you enjoy it reading it half as much as we enjoyed writing it:
$intScore = 13;
$blnDuplicate = $False;
$strPassword = $ARGV[0];
open (WordList, "C:\\Scripts\\WordList.txt");
@arrWordList = <WordList>;
close (WordList);
$intLength = @arrWordList;
for ($i = 0; $i < $intLength; $i ++)
{
$strValue = @arrWordList[$i];
chomp($strValue);
@arrWordList[$i] = $strValue;
}
$strWordList = join(" ", @arrWordList);
$strWord = " " . $strPassword . " ";
$stWord =~ tr/A-Z/a-z/;
$result = index($strWordList, $strWord);
if ($result != -1)
{
print "Password found in the dictionary.\n";
$intScore --;
}
$intLength = length ($strPassword);
$substring = substr($strPassword, 1, $intLength - 1);
$strWord = " " . $substring . " ";
$strWord =~ tr/A-Z/a-z/;
$result = index($strWordList, $strWord);
if ($result != -1)
{
print "Password, minus the first letter, found in the dictionary.\n";
$intScore --;
}
$intLength = length ($strPassword);
$substring = substr($strPassword, 0, $intLength - 1);
$strWord = " " . $substring . " ";
$strWord =~ tr/A-Z/a-z/;
$result = index($strWordList, $strWord);
if ($result != -1)
{
print "Password, minus the last letter letter, found in the dictionary.\n";
$intScore --;
}
if ( $strPassword =~ /[0]/ )
{
$strWord = " " . $strPassword . " ";
$strWord =~ tr/A-Z/a-z/;
$strWord =~ s/0/o/g;
$result = index($strWordList, $strWord);
if ($result != -1)
{
print "Improper letter substitution (0 for o).\n";
$intScore --;
}
}
if ( $strPassword =~ /[1]/ )
{
$strWord = " " . $strPassword . " ";
$strWord =~ tr/A-Z/a-z/;
$strWord =~ s/1/l/g;
$result = index($strWordList, $strWord);
if ($result != -1)
{
print "Improper letter substitution (1 for l).\n";
$intScore --;
}
}
if ($intLength < 10 || $intlength > 20 )
{
print "Improper password length.\n";
$intScore --;
}
if ( $strPassword !~ /[0-9]/ )
{
print "No numbers in password. \n";
$intScore --;
}
if ( $strPassword !~ /[A-Z]/ )
{
print "No uppercase letters in password. \n";
$intScore --;
}
if ( $strPassword !~ /[a-z]/ )
{
print "No lowercase letters in password. \n";
$intScore --;
}
if ( $strPassword !~ /[^A-Za-z0-9]/ )
{
print "No symbols in password. \n";
$intScore --;
}
if ( $strPassword =~ /[a-z]{4}/ )
{
print "Four consecutive lowercase letters in password. \n";
$intScore --;
}
if ( $strPassword =~ /[A-Z]{4}/ )
{
print "Four consecutive uppercase letters in password. \n";
$intScore --;
}
%dictionary = ();
$intLength = length $strPassword;
for ($i = 0; $i < $intLength; $i ++)
{
$strLetter = substr($strPassword, $i, 1);
$blnCheck = exists($dictionary{$strLetter});
if ($blnCheck == 1)
{$blnDuplicate = True;}
else
{$dictionary{$strLetter} = $strLetter;}
}
if ($blnDuplicate eq True )
{
print "Duplicate letters in password. \n";
$intScore --;
}
print "\n";
if ($intScore <= 6)
{print "A password score of $intScore indicates a weak password."}
elsif ($intScore < 11)
{print "A password score of $intScore indicates a moderately-strong password."}
else
{print "A password score of $intScore indicates a strong password."}
There’s quite a bit of code here, so let’s start walking through the script and see if we can figure out how it works. To begin with, we assign values to two variables. We assign a value of 13 to $intScore, which just happens to be the best score a proposed password can attain. Meanwhile, we assign the value False ($False) to $blnDuplicate; we’ll use this variable when we check the password for duplicate characters. After initializing these variables we then use the following line of code to retrieve the first command-line argument used when we started the script; this argument, of course, represents the password to be tested:
$strPassword = $ARGV[0];
Next, we need to read in the contents of our list of official words; that’s what this block of code is for:
open (WordList, "C:\\Scripts\\WordList.txt"); @arrWordList = <WordList>; close (WordList);
As we’ve noted before, when the Scripting Games began neither of the Scripting Guys knew the first thing about Perl. (And yes, in typical Scripting Guys fashion, rather than learn Perl and then compete in the Advanced division of the Scripting Games we opted to do things the other way around.) Did our lack of knowledge hurt us from time-to-time? You bet. For example, when you read in a text file using Perl, the information from that file is automatically stored as an array (in this case, an array named @arrWordList). We need to search this text file to see if, for example, our password happens to be a real word. How do you search an array in Perl? To be honest, we had no idea. Therefore, we decided to convert the array to a string value named $strWordList instead:
$intLength = @arrWordList;
for ($i = 0; $i < $intLength; $i ++)
{
$strValue = @arrWordList[$i];
chomp($strValue);
@arrWordList[$i] = $strValue;
}
$strWordList = join(" ", @arrWordList);
We’re under no illusions that this is the best way to tackle the problem. On the other hand, it was relatively easy to script, and – more importantly – it worked.
At this point we’re ready to start running some password tests. To begin with, we add blank spaces to the beginning and end of our password:
$strWord = " " . $strPassword . " ";
Why do we do that? Well, suppose we supplied a password like whistl. Is whistl a real word? No. However, the string whistl can be found in our word list; after al, those are the first 6 characters in the word whistle. By bracketing our password with blank spaces we help guarantee that, if we find anything at all, it will be a complete word, and not just part of a word.
To avoid possible problems with letter casing we then use the tr function to convert all the characters in the password to their lowercase equivalent:
$stWord =~ tr/A-Z/a-z/;
That’s probably not required but, like they say, better safe than sorry.
And now, at long last, we’re ready to run one of our password checks. In the following block of code we check to see if the supplied password can be found anywhere in our official word list:
$result = index($strWordList, $strWord);
if ($result != -1)
{
print "Password found in the dictionary.\n";
$intScore --;
}
What we’re doing here is using the index function to determine the starting character position of the target string $strWord (the password) in our word list. If the target string cannot be found then index returns the value -1; if the target strong can be found then index returns an integer value representing the character position where the string begins. To a certain extent, however, that doesn’t matter. What really matters is that any value other than -1 means that our password is a real word. In that case, we echo back a message stating that the password was found in the dictionary, then use the – operator ($intScore --) to subtract 1 from our password score.
Our next task is to remove the final character from the password to see if the user came up with a password simply by sticking a character to the front of a real word (e.g., 2whistle). We can extract all but the first character in a string by using these two lines of code:
$intLength = length ($strPassword); $substring = substr($strPassword, 1, $intLength - 1);
In line 1 we’re using the Length function to determine the number of characters in the password. In line 2, we’re using the substr method to return just a portion of the string: we’re starting with the second character (character 1) and continuing through to the end of the string.
And no, that’s not a misprint; the second character in the string really is character 1.. Why? Because the first character in a string is character 0 (and the last character in a string is the number of characters minus 1). Our syntax looks a little odd, but it effectively skips the first character in the string, then returns all the other characters. In other words; everything except the first character.
After adding our blank spaces we then check to see if the password, minus the first letter, can be found in the dictionary:
$result = index($strWordList, $strWord);
if ($result != -1)
{
print "Password, minus the first letter, found in the dictionary.\n";
$intScore --;
}
If it can, then we echo back the appropriate message and subtract 1 from $intScore. We then repeat this same process, this time removing the last character from the password and then checking to see if the remaining characters make up a real word.
That brings us to this block of code:
if ( $strPassword =~ /[0]/ )
{
$strWord = " " . $strPassword . " ";
$strWord =~ tr/A-Z/a-z/;
$strWord =~ s/0/o/g;
$result = index($strWordList, $strWord);
if ($result != -1)
{
print "Improper letter substitution (0 for o).\n";
$intScore --;
}
}
What’s all this for? Well, after tacking on the blank spaces we next check to see if the password contains a 0 (zero). Why? Well, another common thing people do when choosing a password is simply replacing the letter O with a zero; in other words, a password like reboot becomes reb00t. That’s not considered very good practice, to say the least. Therefore, we’re checking to see if the password contains any zeroes. If it does, we then use this line of code to replace those zeroes with the letter ):
$strWord =~ s/0/o/g;
We then check to see if this modified version of the word (with all the zeroes replaced by the letter O) happens to be a real word. If it is, we echo back a message to that effect and, again, decrement $intScore.
We then perform a similar check to see if the user simply replaced any L’s in the password with the number 1.
Once we’ve done that we’re finished with all the check-to-see-if-this-is-a-real-word tests. So what’s next? Well, next we need to verify that the password has at least 10 characters, but no more than 20 characters. That’s what this block of code is for:
if ($intLength < 10 || $intlength > 20 )
{
print "Improper password length.\n";
$intScore --;
}
Yes, that was easy, wasn’t it?
Well, once you know the syntax, that is.
Now we’re ready to check our password for the presence of various characters. In rapid-fire succession we’re going to run a series of regular expression tests:
| • | if ( $strPassword !~ /[0-9]/ ). Here we’re trying to ensure that the password includes at least one number; that is, one character in the range 0-9. |
| • | if ( $strPassword !~ /[A-Z]/ ). Here we’re trying to ensure that the password includes at least uppercase letter; that is, one character in the range A-Z. Note that we use the –cmatch operator here. That enables us to do a case-sensitive match, a match that views the letters a and A as being separate and distinct. |
| • | if ( $strPassword !~ /[a-z]/ ). Here we’re trying to ensure that the password includes at least one lowercase letter; that is, one character in the range a-z. If the password does not contain a lowercase letter we’ll echo back a message and decrement $intScore by 1. Again, we use the –cmatch operator. |
| • | if ( $strPassword !~ /[^A-Za-z0-9]/ ). Here we’re trying to ensure that the password contains at least one symbol; for purposes of this event we defined “symbol” as being anything that isn’t an uppercase letter (A-Z), a lowercase letter (a-z), or a number (0-9). The caret symbol (^) is a negation operator: it tells the script to look for a character that isn’t a member of the specified character sets. |
| • | if ( $strPassword =~ /[a-z]{4}/ ). Here we’re trying to ensure that the password does not include 4 lowercase letters in succession. The [a-z] means “look for a lowercase letter;” the {4} means “look for 4 of those lowercase letters, one right after the other.” |
| • | if ( $strPassword =~ /[A-Z]{4}/ ). Here we’re trying to ensure that the password does not include 4 uppercase letters in succession. The [A-Z] means “look for a uppercase letter;” the {4} means “look for 4 of those uppercase letters, one right after the other.” |
If our password fails any of these tests we echo back the appropriate message and decrement the value of $intScore by 1.
All that’s left now is to determine whether or not the password includes any duplicate characters. That’s what this block of code does:
%dictionary = ();
$intLength = length $strPassword;
for ($i = 0; $i < $intLength; $i ++)
{
$strLetter = substr($strPassword, $i, 1);
$blnCheck = exists($dictionary{$strLetter});
if ($blnCheck == 1)
{$blnDuplicate = True;}
else
{$dictionary{$strLetter} = $strLetter;}
}
What are we doing here? Well, in the first line of code we’re setting up an empty hash table named %dictionary. After that we set up a for loop that loops through all the characters in the password, one-by-one. Inside that loop we grab the first character, then use this line of code to determine whether or not our hash table already contains an entry for that character:
$blnCheck = exists($dictionary{$strLetter});
If $blnCheck is False, we add the character to the hash table:
{$dictionary{$strLetter} = $strLetter;}
If $blnCheck is True that means that we have a duplicate character; in other words, we’ve found a character that’s already in the hash table. In turn, we use this line of code to set the value of $blnDuplicate to True:
{$blnDuplicate = True;}
Once we’ve finished looping through all the characters in the password we then check the value of $blnDuplicate. If $blnDuplicate is True we echo back the fact that a duplicate character was found and, as usual, subtract 1 from $intScore:
if ($blnDuplicate eq True )
{
print "Duplicate letters in password. \n";
$intScore --;
}
All that’s left now is to take a look at the value of $intScore and issue our official password analysis report:
if ($intScore <= 6)
{print "A password score of $intScore indicates a weak password."}
elsif ($intScore < 11)
{print "A password score of $intScore indicates a moderately-strong password."}
else
{print "A password score of $intScore indicates a strong password."}
As you can see, if $intScore is less than or equal to 6 the password is rated as weak. If $intScore is greater than 6 but less than 11 the password is rated as moderately-strong. And if $intScore is 11 or more, the password is rated as strong.
All in all, that wasn’t too bad. And it probably would have been even easier had we had the slightest idea what we were doing!