PDA

View Full Version : [Beta] Ext.ux.SecurePass



vincentc
16 Jul 2007, 5:41 AM
Hello,

First, thanks to the team and specifically Jack S to this very great Extjs. =D>

I'm currently writing my first extension, that add a meter to evaluate the strength of an input password. It is based on the one of hotmail. Following the code needed:

scripts/secure-pass.js:

Ext.namespace('Ext.ux');

Ext.ux.SecurePass = function(config) {
Ext.ux.SecurePass.superclass.constructor.call(this, config);
}

Ext.extend(Ext.ux.SecurePass, Ext.form.TextField, {
/**
* @cfg {String/Object} errors A Error spec, or true for a default spec (defaults to
* {
* PwdEmpty: "Please type a password, and then retype it to confirm.",
* PwdDifRPwd: "The new password and the confirmation password don't match. Please type the same password in both boxes.",
* PwdShort: "Your password must be at least 6 characters long. Please type a different password.",
* PwdLong: "Your password can't contain more than 16 characters. Please type a different password.",
* PwdBadChar: "The password contains characters that aren't allowed. Please type a different password.",
* IDInPwd: "Your password can't include the part of your ID. Please type a different password.",
* FNInPwd: "Your password can't contain your first name. Please type a different password.",
* LNInPwd: "Your password can't contain your last name. Please type a different password."
* })
*/
// private
errors : {
PwdEmpty: "Please type a password, and then retype it to confirm.",
PwdDifRPwd: "The new password and the confirmation password don't match. Please type the same password in both boxes.",
PwdShort: "Your password must be at least 6 characters long. Please type a different password.",
PwdLong: "Your password can't contain more than 16 characters. Please type a different password.",
PwdBadChar: "The password contains characters that aren't allowed. Please type a different password.",
IDInPwd: "Your password can't include the part of your ID. Please type a different password.",
FNInPwd: "Your password can't contain your first name. Please type a different password.",
LNInPwd: "Your password can't contain your last name. Please type a different password."
},

/**
* @cfg {String/Object} Label for the strength meter (defaults to
* 'Password strength:')
*/
// private
meterLabel : 'Password strength:',

/**
* @cfg {String/Object} pwdStrengths A pwdStrengths spec, or true for a default spec (defaults to
* ['Weak', 'Medium', 'Strong'])
*/
// private
pwdStrengths : ['Weak', 'Medium', 'Strong'],

// private
strength : 0,

// private
_lastPwd : null,

// private
kCapitalLetter : 0,
kSmallLetter : 1,
kDigit : 2,
kPunctuation : 3,

// private
initEvents : function(){
Ext.ux.SecurePass.superclass.initEvents.call(this);
this.el.on('keyup', this.checkStrength, this, {buffer:50});
},

// private
onRender : function(ct, position){
Ext.ux.SecurePass.superclass.onRender.call(this, ct, position);
this.wrap = this.el.wrap({cls: "x-form-field-wrap"});
this.trigger = this.wrap.createChild({tag: "div", cls: "StrengthMeter "+this.triggerClass});
if(this.meterLabel != ''){
this.trigger.createChild({tag: "label", html: this.meterLabel});
}
this.trigger.createChild({tag: "div", cls: "PwdMeterBase", html: '<div class="PwdBack"><div class="PwdMeter" id="PwdMeter"></div></div>'});
if(this.hideTrigger){
this.trigger.setDisplayed(false);
}
this.setSize(this.width||'', this.height||'');
},

// private
onDestroy : function(){
if(this.trigger){
this.trigger.removeAllListeners();
this.trigger.remove();
}
if(this.wrap){
this.wrap.remove();
}
Ext.form.TriggerField.superclass.onDestroy.call(this);
},

// private
checkStrength : function(){
var pwd = this.el.getValue();
if (pwd == this._lastPwd) {
return;
}

var strength;
if (this.ClientSideStrongPassword(pwd)) {
strength = 3;
} else if(this.ClientSideMediumPassword(pwd)) {
strength = 2;
} else if(this.ClientSideWeakPassword(pwd)) {
strength = 1;
} else {
strength = 0;
}

document.getElementById('PwdMeter').style.width = 82 * strength +'px';
if(this.pwdStrengths != null && strength > 0) {
document.getElementById('PwdMeter').innerHTML = '&nbsp;'+ this.pwdStrengths[strength - 1];
} else {
document.getElementById('PwdMeter').innerHTML = '';
}

this._lastPwd = pwd;
},

// private
validateValue : function(value){
if(!Ext.form.NumberField.superclass.validateValue.call(this, value)){
return false;
}
if(value.length < 1){ // if it's blank and textfield didn't flag it then it's valid
return true;
}
if(value.length == 0) {
this.markInvalid(this.errors.PwdEmpty);
return false;
}
if("[\x21-\x7e]*".match(value)) {
this.markInvalid(this.errors.PwdBadChar);
return false;
}
if(value.length < 6) {
this.markInvalid(this.errors.PwdShort);
return false;
}
if(value.length > 16) {
this.markInvalid(this.errors.PwdLong);
return false;
}
/*if(value.length > 0 && this.iRPwd != 'undefined' && Ext.get(this.iRPwd) != null && value != Ext.get(this.iRPwd).getValue()) {
this.markInvalid(this.errors.PwdDifRPwd);
return false;
}*/
return true;
},

// private
CharacterSetChecks : function(type){
this.type = type;
this.fResult = false;
},

// private
isctype : function(character, type){
switch (type) { //why needed break after return in js ? very odd bug
case this.kCapitalLetter: if (character >= 'A' && character <= 'Z') { return true; } break;
case this.kSmallLetter: if (character >= 'a' && character <= 'z') { return true; } break;
case this.kDigit: if (character >= '0' && character <= '9') { return true; } break;
case this.kPunctuation: if ("[email protected]#$%^&*()_+-='\";:[{]}|.>,</?`~".indexOf(character) >= 0) { return true; } break;
default: return false;
}
},

// private
IsLongEnough : function(pwd, size){
return !(pwd == null || isNaN(size) || pwd.length < size);
},

// private
SpansEnoughCharacterSets : function(word, nb){
if (!this.IsLongEnough(word, nb))
{
return false;
}

var characterSetChecks = new Array(
new this.CharacterSetChecks(this.kCapitalLetter), new this.CharacterSetChecks(this.kSmallLetter),
new this.CharacterSetChecks(this.kDigit), new this.CharacterSetChecks(this.kPunctuation));
for (var index = 0; index < word.length; ++index) {
for (var nCharSet = 0; nCharSet < characterSetChecks.length; ++nCharSet) {
if (!characterSetChecks[nCharSet].fResult && this.isctype(word.charAt(index), characterSetChecks[nCharSet].type)) {
characterSetChecks[nCharSet].fResult = true;
break;
}
}
}

var nCharSets = 0;
for (var nCharSet = 0; nCharSet < characterSetChecks.length; ++nCharSet) {
if (characterSetChecks[nCharSet].fResult) {
++nCharSets;
}
}

if (nCharSets < nb) {
return false;
}
return true;
},

// private
ClientSideStrongPassword : function(pwd){
return this.IsLongEnough(pwd, 8) && this.SpansEnoughCharacterSets(pwd, 3);
},

// private
ClientSideMediumPassword : function(pwd){
return this.IsLongEnough(pwd, 7) && this.SpansEnoughCharacterSets(pwd, 2);
},

// private
ClientSideWeakPassword : function(pwd){
return this.IsLongEnough(pwd, 6) || !this.IsLongEnough(pwd, 0);
}
})


styles/secure-pass.css:

@charset "utf-8";

.PwdMeterBase{
margin-bottom:10px;
width:248px;
}

.PwdBack{
height:9px;
background-image:url(../images/password_meter_grey.gif);
background-position:center center;
background-repeat: no-repeat;
}

.PwdMeter{
width:0;
height:9px;
background-image:url(../images/password_meter.gif);
background-position:center center;
background-repeat: no-repeat;
font-size:9px;
}


and a sample html:
[html]<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Secure Pass</title>
<script type="text/javascript" src="library/yui-utilities.js"></script>
<script type="text/javascript" src="library/ext-yui-adapter.js"></script>
<script type="text/javascript" src="library/ext-all.js"></script>
<script type="text/javascript" src="library/ext-lang-fr_CA.js"></script>
<script type="text/javascript" src="scripts/secure-pass.js"></script>
<script type="text/javascript">
Ext.onReady(function() {
sPwd = new Ext.ux.SecurePass({
iRPwd : 'iRetypePwd',
pwdStrengths : ['Faible', 'Moyen', 'Fort'],
errors : {
PwdEmpty: 'Tapez un mot de passe, puis retapez-le pour confirmation.',
PwdDifRPwd: 'Le nouveau mot de passe et le mot de passe de confirmation ne correspondent pas. Tapez le m

brian.moeskau
16 Jul 2007, 6:01 AM
Cool! Care to make it official?

http://extjs.com/learn/Ext_Extensions

trbs
16 Jul 2007, 6:30 AM
Yeah looks great, cannot wait to try this out :)

vincentc
17 Jul 2007, 1:47 AM
Hello,

Please find here the final version:

Strong passwords contain 8-16 characters, do not include other fields, and combine three of these character types: uppercase letters, lowercase letters, numbers, or symbols.
Medium passwords contain 7 characters, do not other fields, and combine two of the character types.
Weak passwords contain 6 characters minimum, do not include other fields.

scripts/secure-pass.js


Ext.namespace('Ext.ux.form');

Ext.ux.form.SecurePass = function(config) {
Ext.ux.form.SecurePass.superclass.constructor.call(this, config);
}

Ext.extend(Ext.ux.form.SecurePass, Ext.form.TextField, {
/**
* @cfg {String/Object} errors A Error spec, or true for a default spec (defaults to
* {
* PwdEmpty: "Please type a password, and then retype it to confirm.",
* PwdShort: "Your password must be at least 6 characters long. Please type a different password.",
* PwdLong: "Your password can't contain more than 16 characters. Please type a different password.",
* PwdBadChar: "The password contains characters that aren't allowed. Please type a different password.",
* IDInPwd: "Your password can't include the part of your ID. Please type a different password.",
* FNInPwd: "Your password can't contain your first name. Please type a different password.",
* LNInPwd: "Your password can't contain your last name. Please type a different password."
* })
*/
// private
errors : {
PwdEmpty: "Please type a password, and then retype it to confirm.",
PwdShort: "Your password must be at least 6 characters long. Please type a different password.",
PwdLong: "Your password can't contain more than 16 characters. Please type a different password.",
PwdBadChar: "The password contains characters that aren't allowed. Please type a different password.",
IDInPwd: "Your password can't include the part of your ID. Please type a different password.",
FNInPwd: "Your password can't contain your first name. Please type a different password.",
LNInPwd: "Your password can't contain your last name. Please type a different password."
},

/**
* @cfg {String/Object} Label for the strength meter (defaults to
* 'Password strength:')
*/
// private
meterLabel : 'Password strength:',

/**
* @cfg {String/Object} pwdStrengths A pwdStrengths spec, or true for a default spec (defaults to
* ['Weak', 'Medium', 'Strong'])
*/
// private
pwdStrengths : ['Weak', 'Medium', 'Strong'],

/**
* @cfg {String/Object} fieldsFilter A fieldsFilter spec, as [['field_name', 'error_id'], ...]
*/
// private
fieldsFilter : null,

// private
strength : 0,

// private
_lastPwd : null,

// private
kCapitalLetter : 0,
kSmallLetter : 1,
kDigit : 2,
kPunctuation : 3,

// private
initEvents : function(){
Ext.ux.form.SecurePass.superclass.initEvents.call(this);
this.el.on('keyup', this.checkStrength, this, {buffer:50});
},

// private
onRender : function(ct, position){
Ext.ux.form.SecurePass.superclass.onRender.call(this, ct, position);
this.wrap = this.el.wrap({cls: "x-form-field-wrap"});
this.trigger = this.wrap.createChild({tag: "div", cls: "StrengthMeter "+this.triggerClass});
if(this.meterLabel != ''){
this.trigger.createChild({tag: "label", html: this.meterLabel});
}
this.trigger.createChild({tag: "div", cls: "PwdMeterBase", html: '<div class="PwdBack"><div class="PwdMeter" id="PwdMeter"></div></div>'});
if(this.hideTrigger){
this.trigger.setDisplayed(false);
}
this.setSize(this.width||'', this.height||'');
},

// private
onDestroy : function(){
if(this.trigger){
this.trigger.removeAllListeners();
this.trigger.remove();
}
if(this.wrap){
this.wrap.remove();
}
Ext.form.TriggerField.superclass.onDestroy.call(this);
},

// private
checkStrength : function(){
var pwd = this.el.getValue();
if (pwd == this._lastPwd) {
return;
}

var strength;
if (this.ClientSideStrongPassword(pwd)) {
strength = 3;
} else if(this.ClientSideMediumPassword(pwd)) {
strength = 2;
} else if(this.ClientSideWeakPassword(pwd)) {
strength = 1;
} else {
strength = 0;
}

document.getElementById('PwdMeter').style.width = 82 * strength +'px';
if(this.pwdStrengths != null && strength > 0) {
document.getElementById('PwdMeter').innerHTML = '&nbsp;'+ this.pwdStrengths[strength - 1];
} else {
document.getElementById('PwdMeter').innerHTML = '';
}

this._lastPwd = pwd;
},

// private
validateValue : function(value){
if (!Ext.form.TextField.superclass.validateValue.call(this, value)){
return false;
}
if (value.length == 0) {
this.markInvalid(this.errors.PwdEmpty);
return false;
}
if ("[\x21-\x7e]*".match(value)) {
this.markInvalid(this.errors.PwdBadChar);
return false;
}
if (value.length < 6) {
this.markInvalid(this.errors.PwdShort);
return false;
}
if (value.length > 16) {
this.markInvalid(this.errors.PwdLong);
return false;
}
for (var index = 0; index < this.fieldsFilter.length; ++index) {
filter = document.getElementById(this.fieldsFilter[index][0]).value;
if (filter != '')
{
re = new RegExp(filter);
if (re.test(value)) {
this.markInvalid(eval('this.errors.'+ this.fieldsFilter[index][1]));
return false;
}
}
}
return true;
},

// private
CharacterSetChecks : function(type){
this.type = type;
this.fResult = false;
},

// private
isctype : function(character, type){
switch (type) { //why needed break after return in js ? very odd bug
case this.kCapitalLetter: if (character >= 'A' && character <= 'Z') { return true; } break;
case this.kSmallLetter: if (character >= 'a' && character <= 'z') { return true; } break;
case this.kDigit: if (character >= '0' && character <= '9') { return true; } break;
case this.kPunctuation: if ("[email protected]#$%^&*()_+-='\";:[{]}|.>,</?`~".indexOf(character) >= 0) { return true; } break;
default: return false;
}
},

// private
IsLongEnough : function(pwd, size){
return !(pwd == null || isNaN(size) || pwd.length < size);
},

// private
SpansEnoughCharacterSets : function(word, nb){
if (!this.IsLongEnough(word, nb))
{
return false;
}

var characterSetChecks = new Array(
new this.CharacterSetChecks(this.kCapitalLetter), new this.CharacterSetChecks(this.kSmallLetter),
new this.CharacterSetChecks(this.kDigit), new this.CharacterSetChecks(this.kPunctuation));
for (var index = 0; index < word.length; ++index) {
for (var nCharSet = 0; nCharSet < characterSetChecks.length; ++nCharSet) {
if (!characterSetChecks[nCharSet].fResult && this.isctype(word.charAt(index), characterSetChecks[nCharSet].type)) {
characterSetChecks[nCharSet].fResult = true;
break;
}
}
}

var nCharSets = 0;
for (var nCharSet = 0; nCharSet < characterSetChecks.length; ++nCharSet) {
if (characterSetChecks[nCharSet].fResult) {
++nCharSets;
}
}

if (nCharSets < nb) {
return false;
}
return true;
},

// private
ClientSideStrongPassword : function(pwd){
return this.IsLongEnough(pwd, 8) && this.SpansEnoughCharacterSets(pwd, 3);
},

// private
ClientSideMediumPassword : function(pwd){
return this.IsLongEnough(pwd, 7) && this.SpansEnoughCharacterSets(pwd, 2);
},

// private
ClientSideWeakPassword : function(pwd){
return this.IsLongEnough(pwd, 6) || !this.IsLongEnough(pwd, 0);
}
})


styles/secure-pass.css


@charset "utf-8";

.PwdMeterBase{
margin-bottom:10px;
width:248px;
}
.PwdBack{
height:9px;
background-image:url(../images/password_meter_grey.gif);
background-position:center center;
background-repeat: no-repeat;
}
.PwdMeter{
width:0;
height:9px;
background-image:url(../images/password_meter.gif);
background-position:center center;
background-repeat: no-repeat;
font-size:9px;
}


and a html sample...


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Secure Pass</title>
<script type="text/javascript" src="library/yui-utilities.js"></script>
<script type="text/javascript" src="library/ext-yui-adapter.js"></script>
<script type="text/javascript" src="library/ext-all.js"></script>
<script type="text/javascript" src="scripts/secure-pass.js"></script>
<script type="text/javascript">
Ext.onReady(function() {
sPwd = new Ext.ux.form.SecurePass({
fieldsFilter : [['lastname', 'LNInPwd']]
});
sPwd.applyTo('iPwd');
});
</script>
<style>
@import 'styles/ext-all.css';
@import 'styles/secure-pass.css';
</style>
</head>
<body>
<div class="x-form-element">
<label for="lastname">*Last name:</label>
<input id="lastname" name="lastname" maxlength="32" type="text">
</div>
<div class="x-form-element">
<label for="iPwd">*Type password:</label>
<input id="iPwd" name="iPwd" maxlength="16" type="password">
</div>
</body>
</html>


Possible evolutions :

manage retype password
dictionnary of common words
custom strength functions
custom minimum / maximum password length


Again, thanks to Ext team for their work.

Hope you find this usefull.
Enjoy !

JeffHowden
17 Jul 2007, 9:30 PM
I think this is a great start. However, like pretty much everything Ext, develop this to the nth degree and combine all the right decisions from the various players in the field, the whole time, doing it overall better. With that, here's some of what's currently in the market:

http://www.certainkey.com/demos/password/
Notice that it calculates entropy and detects dictionary words.

http://www.codeandcoffee.com/2007/06/27/how-to-make-a-password-strength-meter-like-google/
An example based off Google's approach

Additionally, the checker should "canonicalize" (normalize or convert to all-letter form) the password prior to checks against the dictionary. This will catch common combinations of using 5 for s, 1 for l or i, 2 for z, 8 for b or B, etc.

Here's some good advice regarding passwords:

http://geodsoft.com/howto/password/password_advice.htm

Herm
21 Jul 2007, 1:36 AM
Or you could do password strength checking 'in the cloud' and provide an Ext UI to the Google checker: https://www.google.com/accounts/RatePassword?Passwd=amI2Strong? which has been written about here http://www.codeproject.com/useritems/GooglePasswordStrength.asp .

DigitalSkyline
21 Jul 2007, 4:22 PM
No offence but using a third party to determine the strength of your password? Why not just send them the keys to your house while your at it?

JeffHowden
21 Jul 2007, 9:34 PM
I briefly considered that too, but I highly dislike the notion of a) sending the password over the wire, b) using an external service that has no documentation on their practice of traffic logging on that address, and c) there being literally no documentation as to how the strength checking works (ie, is it just length, is it a count of different character types, does it do dictionary checks, does it include any sort of "other field" checking to ensure things like username, first name, last name, email, etc. aren't in the password).

Just a guess, but from the following scoring a 4, I'd say their strength checking really isn't very full-featured:

https://www.google.com/accounts/RatePassword?Passwd=iamastrongpassword
https://www.google.com/accounts/RatePassword?Passwd=mypassword
https://www.google.com/accounts/RatePassword?Passwd=iphone
https://www.google.com/accounts/RatePassword?Passwd=extjs
https://www.google.com/accounts/RatePassword?Passwd=jscript