Page 1 of 2

[Fun] Help me improve my password strength algorithm

Posted: Thu Jul 13, 2006 11:45 am
by Chris Corbyn
Totally unfinished but once I start to get meaninful values out of passwordChecker.check() I can do the rest really quickly.

Basically it's supposed to score a password based upon length, case changes, numbers/letters, special chars. It would then update a little gauge as the user types in the password box.

In my check() method I increment the value the passwordChecker.points by multiplying the points given in the criteria by a factor determined by the frequency at which they occur in the string. It's halfway to working but still a little erratic.

Anybody fancy chipping in some ideas? :)

Code: Select all

<script type="text/javascript">
<!-- Hide me from older browsers

/**
 * Checks the strength of a password
 * based upon it's length and combination of characters
 */
function strengthChecker(outputId)
{
	/**
	 * Id of the node we want to show output in
	 * @var string ID
	 * @private
	 */
	var elementId = outputId;
	/**
	 * An internal timeout.  Gets used in loading
	 * ... possibly pointless.
	 * @var timeout
	 * @private
	 */
	var tm = 0;
	/**
	 * Passwords are given a strength score
	 * @var float score
	 * @private
	 */
	var score = false;
	/**
	 * The actual DOM node for the guage we append
	 * to elementId
	 * @var node gauge
	 * @private
	 */
	var gaugeNode;
	/**
	 * The weight given for the various strength enhancers
	 * @var array (float) points
	 * @private
	 */
	var criteriaPoints = new Array();
	criteriaPoints['Case'] = 2;
	criteriaPoints['Length'] = 0.5;
	criteriaPoints['SpecialChars'] = 7;
	criteriaPoints['AlphaNum'] = 2;
	
	/**
	 * Does the initial loading of the password gauge
	 * upon instantiation if the page is idle
	 * @return void
	 * @private
	 */
	var load = function()
	{
		//The DOM tree may not have finished building yet
		if (document.getElementById(elementId))
		{
			if (score === false) updateOutput();
			if (tm) window.clearTimeout(tm);
		}
		else //If the element doesn't exist, look again in 200ms
		{
			tm = window.setTimeout(function() { load(); }, 200);
		}
	}
	/**
	 * Update what is displayed for the gauge at elementId
	 * @return void
	 * @private
	 */
	var updateOutput = function()
	{
		//
	}
	/**
	 * Reads a new password and then gives it a score
	 * The password gauge is then updated
	 * @param string password value
	 * @return float score
	 */
	this.check = function(v)
	{
		score = 0;
		//Score based upon length
		score += v.length * criteriaPoints['Length'];
		
		var lower = 0;
		var upper = 0;
		var numbers = 0;
		var specialChars = 0;
		var lettersOnly = '';
		var numbersOnly = '';
		var charsOnly = '';
		for (var i = 0; i < v.length; i++)
		{
			var letter = v.substr(i, 1);
			if (letter.match(/[a-z]/))
			{
				lettersOnly += letter;
				lower++;
			}
			else if (letter.match(/[A-Z]/))
			{
				lettersOnly += letter;
				upper++;
			}
			else if (letter.match(/\d/))
			{
				numbersOnly += letter;
				numbers++;
			}
			else if (letter.match(/[\W\-\. ]/))
			{
				chars += letter;
				specialChars++;
			}
		}
		//Points based upon case change
		var caseDiff = Math.abs(upper - lower);
		score += parseFloat((lettersOnly.length - caseDiff) * criteriaPoints['Case']);
		//Alpha Numeric Points
		var alphaNumDiff = Math.abs(upper+lower - numbers);
		score += parseFloat(((lettersOnly.length + numbersOnly.length) - alphaNumDiff) * criteriaPoints['AlphaNum']);
		//Special Character Points
		score += parseFloat(specialChars * criteriaPoints['SpecialChars']);
		
		//Now update the gauge
		updateOutput();
		return score;
	}
	/**
	 * Allow the user to change the points weightings
	 * @param string criteria (See criteriaPoints)
	 * @param float points
	 * @return bool successful
	 */
	this.setPoints = function(criteria, pnts)
	{
		if (criteriaPoints[criteria])
		{
			criteriaPoints[criteria] = parseFloat(pnts);
			return true;
		}
		else return false;
	}
	
	//At end of instantiation load the gauge
	load();
}

var pw = new strengthChecker('pw_gauge');

//I'm just using this to debug
function displayValue(v)
{
	document.getElementById('dummy').value = v;
}

// -->
</script>

Password: <input type="password" name="pass" onkeyup="displayValue(pw.check(this.value));" />
<div id="pw_gauge"></div>

<!-- Debug only! -->
<br />
Score: <input type="text" id="dummy" value="" />

Posted: Thu Jul 13, 2006 12:29 pm
by Chris Corbyn
Doh! I wasn't even penalising for lack of certain things. I was just being over generous on points.

This seems to work pretty well, just need to tweak the numbers I think :) (Doesn't have to be critically accurate, it's just a guide for the user).

Code: Select all

<script type="text/javascript">
<!-- Hide me from older browsers

/**
 * Checks the strength of a password
 * based upon it's length and combination of characters
 */
function strengthChecker(outputId)
{
	/**
	 * Id of the node we want to show output in
	 * @var string ID
	 * @private
	 */
	var elementId = outputId;
	/**
	 * An internal timeout.  Gets used in loading
	 * ... possibly pointless.
	 * @var timeout
	 * @private
	 */
	var tm = 0;
	/**
	 * Passwords are given a strength score
	 * @var float score
	 * @private
	 */
	var score = false;
	/**
	 * The actual DOM node for the guage we append
	 * to elementId
	 * @var node gauge
	 * @private
	 */
	var gaugeNode;
	/**
	 * The weight given for the various strength enhancers
	 * @var array (float) points
	 * @private
	 */
	var criteriaPoints = new Array();
	criteriaPoints['CaseChange'] = 3;
	criteriaPoints['Length'] = 1;
	criteriaPoints['SpecialChars'] = 6;
	criteriaPoints['AlphaNum'] = 3;
	criteriaPoints['NoNumbers'] = -0.5;
	criteriaPoints['NoSpecialChars'] = -0.4;
	criteriaPoints['NoLetters'] = -1;
	criteriaPoints['NoCaseChange'] = -0.5;
	criteriaPoints['Duplicates'] = -3;
	
	/**
	 * Does the initial loading of the password gauge
	 * upon instantiation if the page is idle
	 * @return void
	 * @private
	 */
	var load = function()
	{
		//The DOM tree may not have finished building yet
		if (document.getElementById(elementId))
		{
			if (score === false) updateOutput();
			if (tm) window.clearTimeout(tm);
		}
		else //If the element doesn't exist, look again in 200ms
		{
			tm = window.setTimeout(function() { load(); }, 200);
		}
	}
	/**
	 * Update what is displayed for the gauge at elementId
	 * @return void
	 * @private
	 */
	var updateOutput = function()
	{
		//
	}
	/**
	 * Reads a new password and then gives it a score
	 * The password gauge is then updated
	 * @param string password value
	 * @return float score
	 */
	this.check = function(v)
	{
		score = 0;
		//Score based upon length
		score += v.length * criteriaPoints['Length'];
		
		var collected = new Array();
		var lower = 0;
		var upper = 0;
		var numbers = 0;
		var specialChars = 0;
		var duplicates = 0;
		var lettersOnly = '';
		var numbersOnly = '';
		var charsOnly = '';
		for (var i = 0; i < v.length; i++)
		{
			var letter = v.substr(i, 1);
			if (collected.hasValue(letter)) duplicates++;
			
			collected.push(letter);
			if (letter.match(/[a-z]/))
			{
				lettersOnly += letter;
				lower++;
			}
			else if (letter.match(/[A-Z]/))
			{
				lettersOnly += letter;
				upper++;
			}
			else if (letter.match(/\d/))
			{
				numbersOnly += letter;
				numbers++;
			}
			else if (letter.match(/[\W\-\. ]/))
			{
				chars += letter;
				specialChars++;
			}
		}
		//Points based upon case change
		var caseDiff = Math.abs(upper - lower);
		score += parseFloat((lettersOnly.length - caseDiff) * criteriaPoints['CaseChange']);
		//Alpha Numeric Points
		var alphaNumDiff = Math.abs(upper+lower - numbers);
		score += parseFloat(((lettersOnly.length + numbersOnly.length) - alphaNumDiff) * criteriaPoints['AlphaNum']);
		//Special Character Points
		score += parseFloat(specialChars * criteriaPoints['SpecialChars']);
		//Penalise for lack of numbers
		if (!numbers)
		{
			score += parseFloat(v.length * criteriaPoints['NoNumbers']);
		}
		//Penalise for lack of letters
		if (!lower && !upper)
		{
			score += parseFloat(v.length * criteriaPoints['NoLetters']);
		}
		//Penalise for lack of special chars
		if (!specialChars)
		{
			score += parseFloat(v.length * criteriaPoints['NoSpecialChars']);
		}
		//Penalise for lack of changing case
		if ((upper || lower) && (!upper || !lower))
		{
			score += parseFloat(v.length * criteriaPoints['NoCaseChange']);
		}
		//Penalise for duplicate chars
		score += parseFloat(duplicates * criteriaPoints['Duplicates']);
		
		//Now update the gauge
		updateOutput();
		return score;
	}
	/**
	 * Allow the user to change the points weightings
	 * @param string criteria (See criteriaPoints)
	 * @param float points
	 * @return bool successful
	 */
	this.setPoints = function(criteria, pnts)
	{
		if (criteriaPoints[criteria])
		{
			criteriaPoints[criteria] = parseFloat(pnts);
			return true;
		}
		else return false;
	}
	
	//At end of instantiation load the gauge
	load();
	
	//Just comes in useful
	Array.prototype.hasValue = function(v)
	{
		for (var i in this)
		{
			if (this[i] == v) return true;
		}
		return false;
	}
}

var pw = new strengthChecker('pw_gauge');

//I'm just using this to debug
function displayValue(v)
{
	document.getElementById('dummy').value = v;
}

// -->
</script>

Password: <input type="password" name="pass" onkeyup="displayValue(pw.check(this.value));" />
<div id="pw_gauge"></div>

<!-- Debug only! -->
<br />
Score: <input type="text" id="dummy" value="" />

Posted: Thu Jul 13, 2006 1:09 pm
by Chris Corbyn
*Fairly* happy with it now although I may be being a little harsh but that can be configured:

http://www.w3style.co.uk/~d11wtq/password_checker.html

Comments? :)

Posted: Thu Jul 13, 2006 1:25 pm
by Weirdan
perhaps scoring based on length should not be linear, for example I wouldn't consider password 'c73y' a strong one.

Posted: Thu Jul 13, 2006 1:30 pm
by MarK (CZ)
Strange thing: try writing for ex. 'a1.a' and it says STRONG but then, try adding 'aa's and with every addition, the level of security is lower.
I find it strange that longer password is less secure than a short one (composed of the same chars), although it's a stupid pass :D

I haven't read through the code though, maybe you want it like that - I'm no pro in security :D

Posted: Thu Jul 13, 2006 1:33 pm
by Weirdan
d11 penalizes you for repetition :)

Posted: Thu Jul 13, 2006 1:37 pm
by Chris Corbyn
Weirdan wrote:perhaps scoring based on length should not be linear, for example I wouldn't consider password 'c73y' a strong one.
Yeah, I'll re-think how scoring based on length works. My other idea was to make length a factor of some number that the final score gets divided by.
Weirdan wrote:d11 penalizes you for repetition :)
Indeed I do.

EDIT | FYI Mark I've told it to not treat spaces, dots and dashes as special characters since they are common and more likely to be used by brute force bots.

Posted: Thu Jul 13, 2006 1:49 pm
by Weirdan
Yeah, I'll re-think how scoring based on length works.
My idea was to use something like that:

Code: Select all

score += (v.length * v.length) * criteriaPoints['Length'] - 5;
// or even
score += ((v.length * v.length) - 5) * criteriaPoints['Length'];

Posted: Thu Jul 13, 2006 2:25 pm
by Chris Corbyn
Weirdan wrote:
Yeah, I'll re-think how scoring based on length works.
My idea was to use something like that:

Code: Select all

score += (v.length * v.length) * criteriaPoints['Length'] - 5;
// or even
score += ((v.length * v.length) - 5) * criteriaPoints['Length'];
I did this (before I read your post)

Code: Select all

		var lengthPoints = criteriaPoints['Length'];
		for (i = 0; i < v.length; i++) //Non-linear
		{
			score += lengthPoints;
			lengthPoints *= 0.8;
		}
		//Use this as a factor in subsequent point additions
		var multiplier = lengthPoints;
Criteria now:

* Gain points for length, losing significance as length increases
* Gain points for using letters and numbers, the more of a mixture the more points
* Gain points for using uppercase/lowercase, the more of a mix the better
* Gain points for using special chars, the more seen the more points
* Lose points for duplicating characters, the more frequent, the heavier the penalization
* Lose points for password being less than a defined length (8 default), the closer is it to the min length, the less points you lose
* Lose points for not having any letters at all
* Lose points for not having any numbers at all
* Lose points for not having any special chars
* Lose points for password being all the same case

I won't post the newer code since it's on this page:

http://www.w3style.co.uk/~d11wtq/password_checker.html

Better? :)

Posted: Thu Jul 13, 2006 2:46 pm
by Weirdan
Gain points for length, losing significance as length increases
Seems like it's wrong behaviour... consider the following password:

Code: Select all

the quick brown fox jumpt over the moon while trying to beat d11's fuzzword strength checking ulgarytm
In real world it's excellent... yet your checker rates it as mere 'good'.

Posted: Thu Jul 13, 2006 2:51 pm
by Chris Corbyn
Weirdan wrote:
Gain points for length, losing significance as length increases
Seems like it's wrong behaviour... consider the following password:

Code: Select all

the quick brown fox jumpt over the moon while trying to beat d11's fuzzword strength checking ulgarytm
In real world it's excellent... yet your checker rates it as mere 'good'.
Bleh :P

I think that's possibly down to how my penalization works. The length of the string is used in a multiplier in order to generate a number of points to deduct.

Let me have a fiddle.

Grrr.... this is hard :lol:

EDIT | Hmm.... ok hang on, how about increasing as length of string increases.... that would solve that issue.

Posted: Thu Jul 13, 2006 3:07 pm
by RobertGonzalez
Maybe if your points were proportional to the length. As an example, an 8 Char password 1Ws2aQ(% is a very strong password (it is Very Strong at 6 chars). Maybe set your points so that there is a proportional value of upper case, lower case, numerics and special chars. That way long strings can still be very strong based on the contet of the string.

Does that make sense?

Posted: Thu Jul 13, 2006 3:34 pm
by Weirdan
That way long strings can still be very strong based on the contet of the string.
using sentences with typos within usually produce very strong passwords even if there's only alphabetical chars of the same case.

d11, perhaps you should define (in plain english) what constitutes strong password.

Posted: Thu Jul 13, 2006 3:42 pm
by Roja
Weirdan wrote:d11, perhaps you should define (in plain english) what constitutes strong password.
To be fair, its a very hard challenge.

For example, a password of 10 characters is stronger than 9. But if those 9 characters are a dictionary word, for example, "dictionary", it could be easily guessed by a dictionary attack.

Similarly, a password containing a number (in addition to letters) is stronger than one that does not. However, if the number simply replaces a common letter in a dictionary word, again, attackers know that trick. From a pure brute force point of view, letters (A-Z, 26 choices) are stronger than numbers (0-9, 10 choices), except when you consider dictionary attacks.

Then there is the discussion of passphrases, which might not contain any special characters, but could be near impossible to brute force. ("I love the Arkansas Razorbacks").

Its a very difficult problem to define.

Posted: Thu Jul 13, 2006 4:10 pm
by Chris Corbyn
Well sadly I can't do dictionary checks clinet-side alone.... I'd need AJAX at the very least. Like I say, this is just supposed to be "guide", not something we'd rely on.

I've been fiddling with it to the point my head is all over the place now and I'm not sure if I've made it worse :(

http://www.w3style.co.uk/~d11wtq/password_checker.html