1

I have this javascript password generating function. Right now I'm discarding passwords that don't match the selected specifications. For example if the password doesn't contain numbers, I discard it and generate a new one hopping that one will have a number in it. This however doesn't seem to be efficient performance vise, at least not to me.

Is there a better way to implement the forcing of specific characters in the generated password?

Also i'm planning to add so that the password can be forced to contain special characters. If I do this the current way, I would have to have some regex to check if the password contains special characters and if not throw it way (again doesn't seem very efficient to me).

function generatePassword(length, charset, nosimilar) {
    // default parameters
    length = (typeof length === "undefined") ? 8 : length;
    charset = (typeof charset === "undefined") ? 'abcdefghjknpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ123456789' : charset;
    nosimilar = (typeof similar === "undefined") ? true : nosimilar;

    var gen;
    retVal = "";
    for (var i = 0, n = charset.length; i < length; ++i) {
        gen = charset.charAt(Math.floor(Math.random() * n))
        if ( (retVal.charAt( retVal.length-1 ) == gen) && (nosimilar)) {
            retVal = retVal.substring(0, retVal.length - 1)
            retVal += charset.charAt(Math.floor(Math.random() * n))
            console.log('Generated character same as the last one. Trunkated and regenerated.');
        }
        retVal += gen;
    }

    // if charset contains numbers make sure we get atleast one number
    if ( (retVal.match(/\d+/g) == null) && (charset.match(/\d+/g) != null)) {
    console.log('Password generated but no numbers found. Regenerating.');
    generatePassword(length, charset, nosimilar);
    }

    return retVal;
}

if ($("#chLetters").prop('checked')) charset += 'abcdefghjknpqrstuvwxyz';
if ($("#chNumbers").prop('checked')) charset += '123456789';
if ($("#chMixedCase").prop('checked')) charset += 'ABCDEFGHJKLMNPQRSTUVWXYZ';
if ($("#chSpecial").prop('checked')) charset += '!@$%&?+*-_';

$("#passgen").text(generatePassword($("#maxLength").val(), charset, $("#chNoSimilar").prop('checked')));
user555
  • 1,564
  • 2
  • 15
  • 29
  • Your desired password can be completely random? I understood there have to be special signs but any other rules? Where in the string should special signs appear? – Grzegorz Piwowarek May 04 '13 at 09:22
  • 2
    Well, i would use different sets of characters in different steps. Example: Lowercase Set, UppercaseSet, NumberSet, SpecialSet. Then start by chosing elements from each set, putting them in random position in the string. Eg, 4 elements from lowercase, 2 from uppercase, 2 number, 1 special. – LittleSweetSeas May 04 '13 at 09:22
  • 1
    Would this question be better off at [**codereview.stackexchange.com**](http://codereview.stackexchange.com)? – Nope May 04 '13 at 09:24
  • The idea is to pass different charset to the function, depending of how the password is to look like. I was also split on if the question belongs more on codereview. Dunno, feel free to move it. – user555 May 04 '13 at 09:28
  • @user555: Though the question is a great question for a good discussion I think it may not fit into the expected SO Q&A format. Any answer could debatable with pros and cons and none could be right or wrong. – Nope May 04 '13 at 09:35
  • 1
    I think this would fit here better if the topic was changed: for example "How to ensure a generated password matches requirements" would be ok. (Not the least because the question about "inefficiency" is a bit academical: even if the function would run a thousand times before it finds a password that fits the specs, the end user would not notice any difference in speed.) – JJJ May 04 '13 at 09:48

3 Answers3

1

If you have a password n characters long and the requirement is that it has at least one letter, one number and one special character, it means that it will have between 1 and n-2 characters of each. For example (simplified):

function generatePassword( length ) {
    var letters = 'abcdefghjknpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ',
        special = '{}()#%&',
        characters = [],
        amountOfLetters = Math.floor( Math.random() * ( length - 2 ) ) + 1,
        amountOfNumbers = Math.floor( Math.random() * ( length - 1 - amountOfLetters ) ) + 1,
        amountOfSpecial = length - ( amountOfLetters + amountOfNumbers );

    // pick letters
    for (var i = 0, n = letters.length; i < amountOfLetters; ++i) {
        characters.push( letters.charAt( Math.floor( Math.random() * n ) ) );
    }

    // pick numbers
    for ( i = 0; i < amountOfNumbers; ++i) {
        characters.push( ''+( Math.floor( Math.random() * 9 ) + 1 ) );
    }

    // pick special characters
    for ( i = 0, n = special.length; i < amountOfSpecial; ++i) {
        characters.push( special.charAt( Math.floor( Math.random() * n ) ) );
    }

    // sort the array and concatenate elements into a string
    return characters.sort( function( a, b ) {
        return Math.random() - 0.5;
    } ).join( '' );
}

Demo: http://jsfiddle.net/gGwyM/

The function picks between 1 and n-2 letters, then between 1 and n-L-1 numbers (where L is the amount of letters), and the rest are special characters. This guarantees that the password contains at least one character from each group.

(Note that you should use a better function than what I have here to randomize the array, see e.g. How to randomize (shuffle) a JavaScript array?)

Community
  • 1
  • 1
JJJ
  • 32,902
  • 20
  • 89
  • 102
  • Why does your number generator not pick any zeroes? – Bergi May 04 '13 at 09:47
  • 1
    Because the OP didn't have a zero in the original set of characters. I assume it's because it's easy to confuse with capital O. (Personally I'd skip 1 as well since it looks like lower case L or capital I.) – JJJ May 04 '13 at 09:54
0

You could use something like this. Using this method allows you to create multiple key sets and by using a simple option attribute you can switch these keysets in and out of the the password generation, along with setting a length or even choosing to generate a hex password.

function isObject(value) {
  return Object.prototype.toString.call(value) === '[object Object]';
}

function assign(target, source) {
  for (var prop in source) {
    if (source.hasOwnProperty(prop)) {
      target[prop] = source[prop];
    }
  }

  return target;
}

function shuffle(obj) {
  var i = obj.length;
  var rnd, tmp;

  while (i) {
    rnd = Math.floor(Math.random() * i);
    i -= 1;
    tmp = obj[i];
    obj[i] = obj[rnd];
    obj[rnd] = tmp;
  }

  return obj;
}

function generatePassword(options) {
  var opts = isObject(options) ? assign({}, options) : {};
  var keyspace = '';
  if (opts.hex) {
    keyspace = '0123456789abcdef';
    if (opts.uppercase) {
      keyspace = keyspace.toUpperCase();
    }
  } else {
    if (opts.alpha) {
      keyspace += 'abcdefghijklmnopqrstuvwxyz';
    }

    if (opts.uppercase) {
      keyspace += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    }

    if (opts.numeric) {
      keyspace += '0123456789';
    }

    if (opts.punctuation) {
      keyspace += "`!\"?$?%^&*()_-+={[}]:;@'~#|\\<,>.?/";
    }
  }

  if (keyspace.length - 1 < 0) {
    return '';
  }

  opts.size = opts.size >>> 0 || 16;
  if (opts.size < 5) {
    opts.size = 5;
  } else if (opts.size > 100) {
    opts.size = 100;
  }

  return shuffle(keyspace.split('')).join('').slice(0, opts.size);
}

var password = generatePassword({
  alpha: true,
  uppercase: true,
  numeric: true,
  punctuation: true
});

console.log(password);

Other options are hex generation an size for length

You could modify it to take a string of special characters you want to use.

Update: Here is a more complex example that enforces a number of each type of chosen characters and works similar to the above simple example.

function isObject(value) {
  return Object.prototype.toString.call(value) === '[object Object]';
}

function assign(target, source) {
  for (var prop in source) {
    if (source.hasOwnProperty(prop)) {
      target[prop] = source[prop];
    }
  }

  return target;
}

function shuffle(obj) {
  var i = obj.length;
  var rnd, tmp;

  while (i) {
    rnd = Math.floor(Math.random() * i);
    i -= 1;
    tmp = obj[i];
    obj[i] = obj[rnd];
    obj[rnd] = tmp;
  }

  return obj;
}

function getXChars(string, number) {
  var str = typeof string === 'string' ? string : '';
  var num = typeof number === 'number' && number > 0 ? number : 0;
  var array = [];
  var i = str.length;
  var rnd;

  while (i && array.length < num) {
    rnd = Math.floor(Math.random() * i);
    i -= 1;
    array.push(str.charAt(rnd));
  }

  return array;
}

function generatePassword(opts) {
  var opts = isObject(opts) ? assign({}, opts) : {};
  var keyspace = '';
  var result = [];
  var i = 0;
  var tmp;

  if (typeof opts.hex === 'number' && opts.hex > 0) {
    i += opts.hex;
    keyspace = '0123456789abcdef';
    if (opts.uppercase === true) {
      keyspace = keyspace.toUpperCase();
    }

    result = result.concat(getXChars(keyspace, opts.hex));
  } else {
    if (typeof opts.alpha === 'number' && opts.alpha > 0) {
      i += opts.alpha;
      tmp = 'abcdefghijklmnopqrstuvwxyz';
      keyspace += tmp;
      result = result.concat(getXChars(tmp, opts.alpha));
    }

    if (typeof opts.uppercase === 'number' && opts.uppercase > 0) {
      i += opts.uppercase;
      tmp = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
      keyspace += tmp;
      result = result.concat(getXChars(tmp, opts.uppercase));
    }

    if (typeof opts.numeric === 'number' && opts.numeric > 0) {
      i += opts.numeric;
      tmp = '0123456789';
      keyspace += tmp;
      result = result.concat(getXChars(tmp, opts.numeric));
    }

    if (typeof opts.punctuation === 'number' && opts.punctuation > 0) {
      i += opts.punctuation;
      tmp = "`!\"?$?%^&*()_-+={[}]:;@'~#|\\<,>.?/";
      keyspace += tmp;
      result = result.concat(getXChars(tmp, opts.punctuation));
    }
  }

  if (keyspace.length === 0) {
    return keyspace;
  }

  opts.size = opts.size >>> 0 || 16;
  if (opts.size < 5) {
    opts.size = 5;
  } else if (opts.size > 100) {
    opts.size = 100;
  }

  result = result.concat(getXChars(keyspace, opts.size - i));

  return shuffle(result).join('');
}

var password = generatePassword({
  alpha: 1,
  uppercase: 1,
  numeric: 1,
  punctuation: 1
});

console.log(password);

Update: As promised here is a possible replacement for Math.random. If window.crypto.getRandomValues is available then it will use it, otherwise it falls back to Math.random.

function random() {
  if (window.crypto && typeof window.crypto.getRandomValues === 'function') {
    console.log('Using crypto');
    var array = new Uint32Array(1);
    window.crypto.getRandomValues(array);
    return array[0] / (Math.pow(2, 32) + 1);
  }

  console.log('Using random');
  return Math.random();
}

console.log(random());
Xotic750
  • 22,914
  • 8
  • 57
  • 79
  • Care to explain the down vote? – Xotic750 May 04 '13 at 09:31
  • Not my downvote, but: The explanation why yours is better is missing, and you shouldn't set `l=keyspace.length-1` - that means you will never generate the last key. – Bergi May 04 '13 at 09:37
  • We are using Math.round and not Math.floor so it should be ok – Xotic750 May 04 '13 at 09:40
  • Uh, didn't see that. Still, the generated keys are not evenly distributed then. – Bergi May 04 '13 at 09:40
  • No, random is not a perfectly secure and evenly distributed method for password generation. Better would be to use ´crypto.getRandomValues´ but support across browsers is very weak at present. The solution by @Juhana has the same problem by adding 1 to the random value. – Xotic750 May 04 '13 at 09:43
  • No, the problem is not `Math.random()`, that one can assumed to be quite evenly distributed. The problem is using `Math.round` instead of `Math.floor`. – Bergi May 04 '13 at 09:53
  • I didn't explain myself very well wrt even distribution, anyway, I don't think it will make much difference in this example. But yes, there is room for improvement, and the only Math.random that I know of as being suitable for the job is that implemented by Opera, until the Crypto object becomes common among browsers. If the reader is worried, it would be very easy to change from Math.round to Math.floor. If I have time later today then I may append an example with that modification, and perhaps even one using crypto.getRandomValue But +1 for alerting the reader – Xotic750 May 04 '13 at 12:16
  • @Bergi: Can you elaborate on why `Math.round` is bad? – user555 May 04 '13 at 14:07
  • @Xotic750: I like how clean the code looks. But it still doesn't force for example punctuation. There is still a chance that the password generated will not contain punctuation. – user555 May 04 '13 at 14:18
  • @user555: The first and last character from the keyspace would be chosen only with half of the probability of the others. Juhana's solution is much better (and also does force punctuation) – Bergi May 04 '13 at 16:24
  • @user555,no, as it is it doesn't force any particular characters, it just creates a keyspace of your choice and lets random pick something from it, so it is possible to get only numbers or only lower case alpha etc etc, it wouldn't be to hard to add such a requirement though, you could for example keep a count of each type in the while loop and discard when a particular value is reached and just let it continue looping, but you would need to draft some kind of specification for how the password would be structured. – Xotic750 May 04 '13 at 17:33
  • Updated with an example that allows you to enforce the number of characters included from any particular set. – Xotic750 May 05 '13 at 05:27
0

I think you need to pick you characters from different arrays depending on the requirements for the array. This is my solution:

var alphas = "abcdefghjknpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
var specials = "!#$%&/()=`";
var numbers = "1234567890";

var requiredSpecials = 1;
var requiredNumbers = 2;

function pickRandom(str, count){
    var remaining = str;
    var result = "";
    while (remaining.length > 0 && count > 0){
        // pick random char from remaining
        var char = remaining.charAt(Math.floor(Math.random() *remaining.length));

        // remove char from remaining. (Just replace with empty string)
        remaining = remaining.replace(char, "");

        // add char to result
        result += char;

        // decrement count
        count -= 1;
    }

    return result;
}

function shuffleString(str){
    var arr = str.split(''); // Convert to array
    arr.sort(function(){ // Sort by random
        return 0.5-Math.random()
    })
    return arr.join(''); // Join back to string
}

function generate(length){
    var specialCount = requiredSpecials;
    var numberCount = requiredNumbers;
    var alphaCount = length - specialCount - numberCount;

    var tmp = pickRandom(alphas, alphaCount) + pickRandom(specials, specialCount) + pickRandom(numbers, numberCount);

    // Shuffle and return
    return shuffleString(tmp);
}

alert(generate(9))

You can also play around with it at jsFiddle

jorgenfb
  • 2,215
  • 1
  • 21
  • 31