1

I'm working on a basic password generator for This Reddit Programming Exercise. I have very simple password generation working in Kotlin, but I'm struggling to come up with a good way to guarantee a specific minimum number of each character type (i.e. at least 2 each of Upper, Lower, Digit, and special character).

I could easily just do two of each to start the password, but I want them to be randomly distributed, not all front-loaded. I also thought of inserting the characters randomly into a character array, rather than appending to a string, but I was wondering if maybe there is a better way.

Here is what I have currently:

private fun generatePassword() {

    var password = ""

    val passwordLength = (8..25).random()

    for (i in 0..passwordLength) {
        val charType = (1..5).random()
        when (charType) {
            1 -> password += lettersLower[(0..lettersLower.size).random()]
            2 -> password += lettersUpper[(0..lettersUpper.size).random()]
            3 -> password += numbers[(0..numbers.size).random()]
            4 -> password += symbols[(0..symbols.size).random()]
        }
    }
    println(password)
}

This works and generates a random password of a random length of 8-24 characters, and contains a random combination of uppercase letters, lowercase letters, digits, and special characters, but it doesn't guarantee the presence of any specific number of each type, like most websites require nowadays.

I thought of doing something like this:

private fun generatePassword() {

    var password: ArrayList<Char> = arrayListOf()

    var newChar:Char

    var numLow = 0
    var numUpp = 0
    var numDig = 0
    var numSpe = 0

    while (numLow < 2){
        newChar = lettersLower[(0..lettersLower.size).random()]
        password.add((0..password.size).random(), newChar)
        numLow++
    }
    while (numUpp < 2){
        newChar = lettersUpper[(0..lettersUpper.size).random()]
        password.add((0..password.size).random(), newChar)
        numUpp++
    }
    while (numDig < 2){
        newChar = numbers[(0..numbers.size).random()]
        password.add((0..password.size).random(), newChar)
        numDig++
    }
    while (numSpe < 2){
        newChar = symbols[(0..symbols.size).random()]
        password.add((0..password.size).random(), newChar)
        numSpe++
    }

    val passwordLength = (8..25).random() - password.size

    for (i in 0..passwordLength) {

        val charType = (1..5).random()
        when (charType) {
            1 -> {
                newChar = lettersLower[(0..lettersLower.size).random()]
                password.add(newChar)
            }
            2 -> {
                newChar = lettersUpper[(0..lettersUpper.size).random()]
                password.add(newChar)
            }
            3 -> {
                newChar = numbers[(0..numbers.size).random()]
                password.add(newChar)
            }
            4 -> {
                newChar = symbols[(0..symbols.size).random()]
                password.add(newChar)
            }
        }
    }
    for (i in password.indices){
        print(password[i])
    }
    print("\n")
}

But that doesn't seem particularly concise. The other option I thought of is to check if the password contains two of each type of character and generate a new one if it doesn't, but that seems awfully inefficient. Is there a better way to ensure that if I run my RNG 8+ times it will return at least, but not exactly, two ones, two twos, two threes, and two fours?

Here's my random function, if that helps:

// Random function via SO user @s1m0nw1 (https://stackoverflow.com/a/49507413)
private fun ClosedRange<Int>.random() = (Math.random() * (endInclusive - start) + start).toInt()

I also found this SO post in Java, but it's also not very concise and I'm wondering if there is a more general purpose, more concise solution to ensure a minimum number of occurrences for each possible outcome of an RNG over a number of trials greater than the sum of those minimum occurrences.

Sorry for the rambling post. Thanks in advance for your insight.

  • A randomly generated password of a certain length is in some sense weaker if it fulfills constraints on character frequency. An attacker has fewer passwords to try than without the constraints. – Henry Jun 03 '18 at 16:23
  • @Henry - true, but most websites have character type requirements like 2 uppercase, 2 lowercase, 2 numbers, etc. I don't actually plan to use these passwords that I'm generating and am mostly just trying to learn some basic Kotlin stuff. – Andrew Thomas Jun 04 '18 at 00:43

1 Answers1

0

I think your second version is on the right track. It can be simplified to:

import java.util.Random

fun generatePassword() : String {
    // this defines the classes of characters
    // and their corresponding minimum occurrences
    val charClasses = arrayOf(
        ('a'..'z').asIterable().toList() to 2,
        ('A'..'Z').asIterable().toList() to 2,
        ('0'..'9').asIterable().toList() to 2,
        "!@#$%^&*()_+".asIterable().toList() to 2
    )

    val maxLen = 24;
    val minLen = 8;

    var password = StringBuilder()

    val random = Random()

    charClasses.forEach {(chars, minOccur) ->
        (0..minOccur).forEach {
            password.insert(
                if (password.isEmpty()) 0 else random.nextInt(password.length),
                chars[random.nextInt(chars.size)])
        }
    }

    ((password.length + 1)..(random.nextInt(maxLen - minLen + 1) + minLen)).forEach {
        val selected = charClasses[random.nextInt(charClasses.size)]
        password.append(selected.first[random.nextInt(selected.first.size)])
    }
    return password.toString()
}
jingx
  • 3,698
  • 3
  • 24
  • 40