tracman-server/static/js/mellt.js

210 lines
9.3 KiB
JavaScript

'use strict';
/**
* Mellt
*
* Tests the strength of a password by calculating how long it would take to
* brute force it.
*
* @version 0.1.0
* @link http://mel.lt/ The homepage for this script.
* @link http://www.hammerofgod.com/passwordcheck.aspx Much of this is based
* on the description of Thor's Godly Privacy password strength checker,
* however the actual code below is all my own.
* @link http://xato.net/passwords/more-top-worst-passwords/ The included
* common passwords list is from Mark Burnett's password collection (which
* is excellent). You can of course use your own password file instead.
*/
var Mellt = function() {
/**
* @var integer HashesPerSecond The number of attempts per second you expect
* an attacker to be able to attempt. Set to 1 billion by default.
*/
this.HashesPerSecond = 1000000000;
/**
* @var string CommonPasswords A variable containing an array of common
* passwords to check against. If you include common-passwords.js in your
* HTML after including Mellt.js, the contents of that file will be used
* if this isn't set.
* Set this to null (and don't include common-passwords.js) to skip
* checking common passwords.
*/
this.CommonPasswords = null;
/**
* @var array $CharacterSets An array of strings, each string containing a
* character set. These should proceed in the order of simplest (0-9) to most
* complex (all characters). More complex = more characters.
*/
this.CharacterSets = [
// We're making some guesses here about human nature (again much of this is
// based on the TGP password strength checker, and Timothy "Thor" Mullen
// deserves the credit for the thinking behind this). Basically we're combining
// what we know about users (SHIFT+numbers are more common than other
// punctuation for example) combined with how an attacker will attack a
// password (most common letters first, expanding outwards).
//
// If you want to support passwords that use non-english characters, and
// your attacker knows this (for example, a Russian site would be expected
// to contain passwords in Russian characters) add your characters to one of
// the sets below, or create new sets and insert them in the right places.
"0123456789",
"abcdefghijklmnopqrstuvwxyz",
"abcdefghijklmnopqrstuvwxyz0123456789",
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-=_+",
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-=_+[]\"{}|;':,./<>?`~"
];
};
Mellt.prototype = {
CommonPasswords: null,
/**
* Tests password strength by simulating how long it would take a cracker to
* brute force your password.
*
* Also optionally tests against a list of common passwords (contained in an
* external file) to weed out things like "password", which from a pure brute
* force perspective would be harder to break if it wasn't so common.
*
* The character sets being used in this checker assume English (ASCII)
* characters (no umlauts for example). If you run a non-english site, and you
* suspect the crackers will realize this, you may want to modify the
* character set to include the characters in your language.
*
* @param string $password The password to test the strength of
* @return integer Returns an integer specifying how many days it would take
* to brute force the password (at 1 billion checks a second) or -1 to
* indicate the password was found in the common passwords file. Obviously if
* they don't have direct access to the hashed passwords this time would be
* longer, and even then most computers (at the time of this writing) won't be
* able to test 1 billion hashes a second, but this function measures worst
* case scenario, so... I would recommend you require at least 30 days to brute
* force a password, obviously more if you're a bank or other secure system.
* @throws Exception If an error is encountered.
*/
CheckPassword: function(password) {
// First check passwords in the common password file if available.
// We do this because "password" takes 129 seconds, but is the first
// thing an attacker will try.
if (!this.CommonPasswords && Mellt.prototype.CommonPasswords) {
this.CommonPasswords = Mellt.prototype.CommonPasswords;
}
if (this.CommonPasswords) {
var text = password.toLowerCase();
for (var t=0; t<this.CommonPasswords.length; t++) {
if (this.CommonPasswords[t]==text) {
// If their password exists in the common file, then it's
// zero time to crack this terrible password.
return -1;
}
}
}
// Figure out which character set the password is using (based on the most
// "complex" character in it).
var base = '';
var baseKey = null;
for (var t=0; t<password.length; t++) {
var char = password[t];
var foundChar = false;
for (var characterSetKey=0; characterSetKey<this.CharacterSets.length; characterSetKey++) {
var characterSet = this.CharacterSets[characterSetKey];
if (baseKey<=characterSetKey && characterSet.indexOf(char)>-1) {
baseKey = characterSetKey;
base = characterSet;
foundChar = true;
break;
}
}
// If the character we were looking for wasn't anywhere in any of the
// character sets, assign the largest (last) character set as default.
if (!foundChar) {
base = this.CharacterSets[this.CharacterSets.length-1];
break;
}
}
// Starting at the first character, figure out it's position in the character set
// and how many attempts will take to get there. For example, say your password
// was an integer (a bank card PIN number for example):
// 0 (or 0000 if you prefer) would be the very first password they attempted by the attacker.
// 9999 would be the last password they attempted (assuming 4 characters).
// Thus a password/PIN of 6529 would take 6529 attempts until the attacker found
// the proper combination. The same logic words for alphanumeric passwords, just
// with a larger number of possibilities for each position in the password. The
// key thing to note is the attacker doesn't need to test the entire range (every
// possible combination of all characters) they just need to get to the point in
// the list of possibilities that is your password. They can (in this example)
// ignore anything between 6530 and 9999. Using this logic, 'aaa' would be a worse
// password than 'zzz', because the attacker would encounter 'aaa' first.
var attempts = 0;
var charactersInBase = base.length;
var charactersInPassword = password.length;
for (var position=0; position<charactersInPassword; position++) {
// We power up to the reverse position in the string. For example, if we're trying
// to hack the 4 character PING code in the example above:
// First number * (number of characters possible in the charset ^ length of password)
// ie: 6 * (10^4) = 6000
// then add that same equation for the second number:
// 5 * (10^3) = 500
// then the third numbers
// 2 * (10^2) = 20
// and add on the last number
// 9
// Totals: 6000 + 500 + 20 + 9 = 6529 attempts before we encounter the correct password.
var powerOf = charactersInPassword - position - 1;
// Character position within the base set. We add one on because strpos is base
// 0, we want base 1.
var charAtPosition = base.indexOf(password[position])+1;
// If we're at the last character, simply add it's position in the character set
// this would be the "9" in the pin code example above.
if (powerOf==0) {
attempts = attempts + charAtPosition;
}
// Otherwise we need to iterate through all the other characters positions to
// get here. For example, to find the 5 in 25 we can't just guess 2 and then 5
// (even though Hollywood seems to insist this is possible), we need to try 0,1,
// 2,3...15,16,17...23,24,25 (got it).
else {
// This means we have to try every combination of values up to this point for
// all previous characters. Which means we need to iterate through the entire
// character set, X times, where X is our position -1. Then we need to multiply
// that by this character's position.
// Multiplier is the (10^4) or (10^3), etc in the pin code example above.
var multiplier = Math.pow(charactersInBase,powerOf);
// New attempts is the number of attempts we're adding for this position.
var newAttempts = charAtPosition * multiplier;
// Add that on to our existing number of attempts.
attempts = attempts + newAttempts;
}
}
// We can (worst case) try a billion passwords a second. Calculate how many days it
// will take us to get to the password.
var perDay = this.HashesPerSecond*60*60*24;
// This allows us to calculate a number of days to crack. We use days because anything
// that can be cracked in less than a day is basically useless, so there's no point in
// having a smaller granularity (hours for example).
var days = attempts / perDay;
// If it's going to take more than a billion days to crack, just return a billion. This
// helps when code outside this function isn't using bcmath. Besides, if the password
// can survive 2.7 million years it's probably ok.
if (days>1000000000) {
return 1000000000;
}
return Math.round(days);
}
};