[Fun] Help me improve my password strength algorithm

JavaScript and client side scripting.

Moderator: General Moderators

User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

[Fun] Help me improve my password strength algorithm

Post 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="" />
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post 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="" />
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post 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? :)
User avatar
Weirdan
Moderator
Posts: 5978
Joined: Mon Nov 03, 2003 6:13 pm
Location: Odessa, Ukraine

Post by Weirdan »

perhaps scoring based on length should not be linear, for example I wouldn't consider password 'c73y' a strong one.
User avatar
MarK (CZ)
Forum Contributor
Posts: 239
Joined: Tue Apr 13, 2004 12:51 am
Location: Prague (CZ) / Vienna (A)
Contact:

Post 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
User avatar
Weirdan
Moderator
Posts: 5978
Joined: Mon Nov 03, 2003 6:13 pm
Location: Odessa, Ukraine

Post by Weirdan »

d11 penalizes you for repetition :)
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post 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.
User avatar
Weirdan
Moderator
Posts: 5978
Joined: Mon Nov 03, 2003 6:13 pm
Location: Odessa, Ukraine

Post 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'];
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post 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? :)
User avatar
Weirdan
Moderator
Posts: 5978
Joined: Mon Nov 03, 2003 6:13 pm
Location: Odessa, Ukraine

Post 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'.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post 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.
User avatar
RobertGonzalez
Site Administrator
Posts: 14293
Joined: Tue Sep 09, 2003 6:04 pm
Location: Fremont, CA, USA

Post 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?
User avatar
Weirdan
Moderator
Posts: 5978
Joined: Mon Nov 03, 2003 6:13 pm
Location: Odessa, Ukraine

Post 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.
Roja
Tutorials Group
Posts: 2692
Joined: Sun Jan 04, 2004 10:30 pm

Post 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.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post 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
Post Reply