Tuesday 19 June 2018

Web Tutorial: AngularJS Password Strength Validator (Part 2/2)

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

No comments:

Post a Comment