In the previous part, we implemented quite a few checks for password strength. Now, we're going to implement a Dictionary Check. This basically checks for any words in the password that can be found in the dictionary. This is important because most password-guessing attacks use a dictionary. So in order to be safe, you probably want to check the given password for such vulnerabilities.
There's a catch, though. Unless you're willing to host an entire dictionary database on your server, you'll have to use an API. The good news is, the Internet is full of these APIs. The one we're going to use today is at
Oxford Dictionaries. We are going to use an AJAX call to this URL, passing in a series of strings that might be words, and the returned result will determine if any of the strings you sent, are
actually dictionary words.
So, first, register an account and obtain a key.
Next, create a file,
validate.php. In there, obtain the POST variables,
url and
words, and assign them to local variables.
words will be a JSON array, so we need to run it through the
json_decode() function.
Then declare two variables,
wordsFound (which defaults to
false) and
wordToValidate. This file will print out an array,
result, which contains
wordsFound in JSON format.
validate.php
<?php
$url = $_POST["url"];
$words = json_decode($_POST["words"]);
$wordsFound = false;
$wordToValidate = "";
$result = array("wordsFound" =>$wordsFound);
echo json_encode($result);
?>
Next, implement a
For loop to iterate through the words array. For each iteration, set wordToValidate to the current element of the
words array.
validate.php
<?php
$url = $_POST["url"];
$words = json_decode($_POST["words"]);
$wordsFound = false;
$wordToValidate = "";
for ($i = 0; $i < sizeof($words); $i++)
{
$wordToValidate = $words[$i];
}
$result = array("wordsFound" =>$wordsFound);
echo json_encode($result);
?>
We'll use
cURL to access the URL. First, we have to initialize an object using the
curl_init() function. Then we need to disable verification. Note that this is not recommended in a live environment - you
need to verify stuff to ensure it's safe. But for the purposes of this exercise, sure, disable it.
validate.php
<?php
$url = $_POST["url"];
$words = json_decode($_POST["words"]);
$wordsFound = false;
$wordToValidate = "";
for ($i = 0; $i < sizeof($words); $i++)
{
$wordToValidate = $words[$i];
$curl = curl_init();
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
}
$result = array("wordsFound" =>$wordsFound, "word" => $wordToValidate);
echo json_encode($result);
?>
Now we use the
curl_setopt_array() function, passing in the
curl object and an array as arguments. And after that, we execute the
curl_close() function on the
curl object.
validate.php
<?php
$url = $_POST["url"];
$words = json_decode($_POST["words"]);
$wordsFound = false;
$wordToValidate = "";
for ($i = 0; $i < sizeof($words); $i++)
{
$wordToValidate = $words[$i];
$curl = curl_init();
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt_array($curl, array()));
curl_close($curl);
}
$result = array("wordsFound" =>$wordsFound);
echo json_encode($result);
?>
In the array, we include key-value pairs that will work as configuration. CURLOPT_URL will be set to the URL and the current word in the words array. The rest is pretty straightforward until you get to the last part, which is yet another array, CURLOPT_HTTPHEADER. This is where your id and key for the API are to be passed in. This is important. The API host is not going to let you use its services without some form of verification. No, my id and key aren't "xxxxx". Just get your own, already.
validate.php
<?php
$url = $_POST["url"];
$words = json_decode($_POST["words"]);
$wordsFound = false;
$wordToValidate = "";
for ($i = 0; $i < sizeof($words); $i++)
{
$wordToValidate = $words[$i];
$curl = curl_init();
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt_array($curl, array(
CURLOPT_URL => $url . $wordToValidate,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => array(
"app_id: xxxxx",
"app_key: xxxxx"
),
));
curl_close($curl);
}
$result = array("wordsFound" =>$wordsFound);
echo json_encode($result);
?>
Now, run the function
curl_exec() on the
curl object, and return the result to the variable
response.
validate.php
<?php
$url = $_POST["url"];
$words = json_decode($_POST["words"]);
$wordsFound = false;
$wordToValidate = "";
for ($i = 0; $i < sizeof($words); $i++)
{
$wordToValidate = $words[$i];
$curl = curl_init();
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt_array($curl, array(
CURLOPT_URL => $url . $wordToValidate,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => array(
"app_id: xxxxx",
"app_key: xxxxx"
),
));
$response = curl_exec($curl);
curl_close($curl);
}
$result = array("wordsFound" =>$wordsFound);
echo json_encode($result);
?>
The response will contain a "404" if no matches are found, and an array of matches otherwise. Now, we don't actually care how many matches there are - we just need to know that there is at least
one match. So, if there's no "404" in the result, wordsFound is true. Not
exactly ironclad logic, but it'll do for now.
validate.php
<?php
$url = $_POST["url"];
$words = json_decode($_POST["words"]);
$wordsFound = false;
$wordToValidate = "";
for ($i = 0; $i < sizeof($words); $i++)
{
$wordToValidate = $words[$i];
$curl = curl_init();
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt_array($curl, array(
CURLOPT_URL => $url . $wordToValidate,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => array(
"app_id: xxxxx",
"app_key: xxxxx"
),
));
$response = curl_exec($curl);
if (strpos($response, "404") === false) $wordsFound = true;
curl_close($curl);
}
$result = array("wordsFound" =>$wordsFound, "word" => $wordToValidate);
echo json_encode($result);
?>
Now, we'll want to work on the JavaScript some more. Let's start by dissecting the entered password and retrieving all the possible words of five letters and above from it. For this, we'll use the
getWords() function which we'll create later. We'll pass in the scope variable
enteredPassword, and the result will be the value of a new variable,
possibleWords.
js/main.js
if (/[0-9]/g.test($scope.enteredPassword))
{
pts ++;
}
else
{
$scope.strengthMessage += "Try including numbers in your password.\n";
}
var possibleWords = getWords($scope.enteredPassword)
Let's create the
getWords() function. In this function, we declare the variable
newArr as an empty array. At the end of the function, we will return
newArr. We'll also create another array,
arr, splitting the password by any non-alphanumeric characters. Thus, if you enter in a password like "teochew-thunder3pass@word", you'll get an array containing "teochew", "thunder", "pass" and "word".
js/main.js
function getWords(password)
{
var arr = password.split(/[^A-Za-z]/);
var newArr = [];
return newArr;
}
function getCode(pts)
{
if (pts <= 0) return "weak";
if (pts == 1) return "moderate";
if (pts == 2) return "strong";
if (pts >= 3) return "excellent";
}
However, we only want words that are 5 characters and above. Because searching for words 4 letters and below would return too many positives. Thus, we iterate through the
arr array, and push any result greater than 4 characters, into the
newArr array. We also convert the pushed string to lowercase. Thus, if the array contained "teochew", "thunder", "pass" and "word", only "teochew" and "thunder" would get through.
js/main.js
function getWords(password)
{
var arr = password.split(/[^A-Za-z]/);
var newArr = [];
for (var i = 0; i <arr.length; i++)
{
if (arr[i].length > 4)
{
newArr.push(arr[i].toLowerCase());
}
}
return newArr;
}
Next, we're going to get all possible words within the strings in
newArr. Declare another variable,
tempArr, as an empty array. Iterate through
newArr with a
For loop. The operation will only affect those strings that are longer than 5 characters, so put in a conditional for that.
js/main.js
function getWords(password)
{
var arr = password.split(/[^A-Za-z]/);
var newArr = [];
for (var i = 0; i <arr.length; i++)
{
if (arr[i].length > 4)
{
newArr.push(arr[i].toLowerCase());
}
}
var tempArr = [];
for (var i = 0; i < newArr.length; i++)
{
if (newArr[i].length > 5)
{
}
}
return newArr;
}
Now, this is how we process the strings that are greater than 5 characters. Say, for the string "teochew", we will derive the strings "teoch", "teoche", "teochew", "eoche", "eochew" and "ochew".
Next up is another nested
For loop. The outer loop will start the search from strings beginning with the first three letters of the string "teochew", because starting the string with anything after those first three letters will net you a string of less than 5 characters.
The inner loop will process strings of 5 characters and above, to the maximum allowed by the length of the string "teochew".
js/main.js
function getWords(password)
{
var arr = password.split(/[^A-Za-z]/);
var newArr = [];
for (var i = 0; i <arr.length; i++)
{
if (arr[i].length > 4)
{
newArr.push(arr[i].toLowerCase());
}
}
var tempArr = [];
for (var i = 0; i < newArr.length; i++)
{
if (newArr[i].length > 5)
{
for (var j = 0; j < newArr[i].length - 4; j++)
{
for (var k = 5; k < newArr[i].length; k++)
{
}
}
}
}
return newArr;
}
Thus, the sub-string is represented by
newArr[i].substr(j, k). We then check if it's already in
tempArr before pushing it in.
js/main.js
function getWords(password)
{
var arr = password.split(/[^A-Za-z]/);
var newArr = [];
for (var i = 0; i <arr.length; i++)
{
if (arr[i].length > 4)
{
newArr.push(arr[i].toLowerCase());
}
}
var tempArr = [];
for (var i = 0; i < newArr.length; i++)
{
if (newArr[i].length > 5)
{
for (var j = 0; j < newArr[i].length - 4; j++)
{
for (var k = 5; k < newArr[i].length; k++)
{
if (j + k <= newArr[i].length)
{
if (tempArr.indexOf(newArr[i].substr(j, k).toLowerCase()) == -1)
tempArr.push(newArr[i].substr(j, k).toLowerCase());
}
}
}
}
}
return newArr;
}
After that, we use another
For loop to iterate through
tempArr. If
newArr does not already contain the element in
tempArr (this is possible because you may be processing multiple identical strings, for example, if the password was "teochew-teochew3pass@word"), then push that element into
newArr.
js/main.js
function getWords(password)
{
var arr = password.split(/[^A-Za-z]/);
var newArr = [];
for (var i = 0; i <arr.length; i++)
{
if (arr[i].length > 4)
{
newArr.push(arr[i].toLowerCase());
}
}
var tempArr = [];
for (var i = 0; i < newArr.length; i++)
{
if (newArr[i].length > 5)
{
for (var j = 0; j < newArr[i].length - 4; j++)
{
for (var k = 5; k < newArr[i].length; k++)
{
if (j + k <= newArr[i].length)
{
if (tempArr.indexOf(newArr[i].substr(j, k).toLowerCase()) == -1)
tempArr.push(newArr[i].substr(j, k).toLowerCase());
}
}
}
}
}
for (var i = 0; i < tempArr.length; i++)
{
if (newArr.indexOf(tempArr[i].toLowerCase()) == -1)
newArr.push(tempArr[i].toLowerCase());
}
return newArr;
}
Now back to the
processPassword() scope function! Wrap the call to the
getCode() function in an
If statement. If the
getWords() function had returned a non-empty array, we'll do more processing. If not, we end the function there with a return statement.
js/main.js
var possibleWords = getWords($scope.enteredPassword)
if (possibleWords.length > 0)
{
}
else
{
$scope.strengthCode = getCode(pts);
return;
}
To process a non-empty array containing all the words you want to validate against the dictionary, call some AJAX. Everything's pretty standard here. The file you are sending the POST to, is the
validate.php file we wrote earlier. You pass in arguments such as a JSON string of the words, and the URL where your API resides.
js/main.js
var possibleWords = getWords($scope.enteredPassword)
if (possibleWords.length > 0)
{
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function()
{
if (this.readyState == 4 && this.status == 200)
{
}
};
xmlhttp.open("POST", "validate.php", true);
xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xmlhttp.send("words=" + JSON.stringify(possibleWords) + "&url=https://od-api.oxforddictionaries.com/api/v1/entries/en/");
}
else
{
$scope.strengthCode = getCode(pts);
return;
}
Next, you use the
parse() method of the JSON object on the returned response. If
wordsFound is
true, then decrement
pts, set the
strengthMessage and
strengthCode variables, and return.
js/main.js
var possibleWords = getWords($scope.enteredPassword)
if (possibleWords.length > 0)
{
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function()
{
if (this.readyState == 4 && this.status == 200)
{
var result = JSON.parse(this.responseText);
if (result.wordsFound)
{
pts --;
$scope.strengthMessage += "Avoid using dictionary words. (" + result.word + ")\n";
$scope.strengthCode = getCode(pts);
return;
}
}
};
xmlhttp.open("POST", "validate.php", true);
xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xmlhttp.send("words=" + JSON.stringify(possibleWords) + "&url=https://od-api.oxforddictionaries.com/api/v1/entries/en/");
}
else
{
$scope.strengthCode = getCode(pts);
return;
}
Try it!
Something seems to have gone wrong... and I'm getting a warning from oxforddictionaries.com!
Yeah, that's to be expected. If you're a cheap bastard like me, you got the free version of the API, which means you can only make that many calls to the API within one minute. If you tested with an extra long password, that would have sent a gazillion requests to the API.
But the logic for this validation is sound... I think.
Till we meet again, stay STRONG.
T___T