Download All Files (as .zip)

ECOO 2012 Regional - Problem #2: Password Strength

The problem:

Passwords are the most basic information protection devices. But many people compromise the security of their data by choosing passwords that are easy for a human or a bot to guess. The table below summarizes one set of criteria for determining the strength of a password, and shows how to apply these criteria to two different example passwords.

    Example 1: "ECOO()123abcd9876"        Example 2: "1234512345"
Score Description Ex1 Ex2
Length Score 4 points for each character in the password. +68 +40
Basic Requirements To qualify, must be at least 8 chars long and also contain 3 of the four basic character types (upper case letters, lower case letters, digits, and symbols*). Score two points for the length plus another two for each of the four types of characters it contains. +10 --
Upper Case Add (length - n) * 2, where n is the number of upper case letters and n > 0. +26 --
Lower Case Add (length - n) * 2, where n is the number of lower case letters and n > 0. +26 --
Digits Add 4 * n, where n is the number of digits, but only if n < length. +28 --
Symbols Add 6 * n, where n is the number of symbol characters. +12 --
Middle Digits and Symbols Add 2 * n, where n is the number of digits and symbols not in the first or last position. +16 +16
Letters Only If the password contains only letters, subtract 1 point for each letter. -- --
Digits Only If the password contains only digits, subtract 1 point for each digit. -- -10
Consecutive Upper Case Subtract 2 * (n - 1) for each set of n consecutive upper case characters. -6 --
Consecutive Lower Case Subtract 2 * (n - 1) for each set of n consecutive lower case characters. -6 --
Consecutive Digits Subtract 2 * (n - 1) for each set of n consecutive digits. -10 -18
Sequential Letters Subtract 3 * (n - 2), where n is the length of the longest case-sensitive sequence of letters longer than 2 (e.g. "abcd" or "CBA" but not "aBcD" or "SJD" or "a"). -6 --
Sequential Digits Subtract 3 * (n - 2), where n is the length of the longest sequence of digits longer than 2 (e.g. "9876" but not "28" or "6"). -6 -9
TOTAL SCORE Add up all the above. Negative scores become 0. Scores over 100 become 100. 0-20 = Very Weak, 21-40 = Weak, 41-60 = Good, 61-80 = Strong, 81-100 = Very Strong 100
VERY
STRONG
19
VERY
WEAK
*A symbol is any character that is not a letter or digit.

DATA21.txt (DATA22.txt for the second try) contains ten passwords, one per line. Write a program that reads each password, computes its total score based on the above criteria, and categorizes the password as Very Weak, Weak, Good, Strong, or Very Strong.

Sample Input
ECOO()123abcd9876
1234512345
ecoo2012
Sample Output
Very Strong (score = 100)
Very Weak (score = 19)
Good (score = 47)

My solution (in Java):

import java.io.File;
import java.util.ArrayList;
import java.util.Scanner;

public class Problem_2_Password_Strength {

    static int score; // to be modified by methods

    public static void main(String[] args) {

        try {
            Scanner file = new Scanner(new File("C:\\Users\\Mike\\Desktop\\problem_2_password_strength_DATA20.txt"));
            ArrayList<String> list = new ArrayList<String>();
            
            // read in the file to the list
            while (file.hasNextLine()) {
                list.add(file.nextLine());
            }

            for (String password : list) {
                score = 0; // reset it each password
                score += password.length() * 4;
                
                // call methods to alter the score based on the password grading
                
                basicRequirements(password);
                counts(password);
                
                if (password.matches("[0-9]+")) // regex; only digits
                    score -= password.length(); // -1 for each digit
                if (password.matches("[a-zA-Z]+")) // regex; only letters
                    score -= password.length(); // -1 for each letter
                
                middles(password);
                
                // check consecutive types for each (uppercase letters, lowercase letters and digits)
                consecutiveTypes(password, "[0-9]"); // or consecutiveTypes(password, "\\d");
                consecutiveTypes(password, "[a-z]");
                consecutiveTypes(password, "[A-Z]");
                
                sequentialTypes(password);
                
                // negative score goes to 0, score > 100 goes to 100
                if (score < 0) score = 0;
                if (score > 100) score = 100;
                
                // display output based on intervals
                if (score >= 0 && score <= 20) System.out.println("Very Weak (score = " + score + ")");
                if (score >= 21 && score <= 40) System.out.println("Weak (score = " + score + ")");
                if (score >= 41 && score <= 60) System.out.println("Good (score = " + score + ")");
                if (score >= 61 && score <= 80) System.out.println("Strong (score = " + score + ")");
                if (score >= 81 && score <= 100) System.out.println("Very Strong (score = " + score + ")");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public static void middles(String password) {
        for (int i = 1; i < password.length() - 1; i++) { // for every character in password string
            String c = password.substring(i, i+1); // current character in password
            if (c.matches("[^a-zA-Z]")) { // not a letter, so a digit or symbol
                score += 2;
            }
        }
    }
    
    /* parameter s is the password string
       and regex is the type to check. 
       ex: digits would be "[0-9]" or "\\d" */
    public static void consecutiveTypes(String s, String regex) {

        // ArrayList that holds the length of consecutive set (as element) for each consecutive set (each set its own element)
        ArrayList<Integer> type = new ArrayList<Integer>();
        
        for (int i = 0; i < s.length()-1; i++) { // for every character in password

            int count = 1; // length of consecutive set
            String s1 = s.substring(i,i+1), s2 = s.substring(i+1,i+2); // adjacent characters
            
            // while loop to keep iterating through a consecutive set
            while (s1.matches(regex) && s2.matches(regex)) { // if adjacent are both the same type
                count++; // length of consecutive set increases
                i++; // move over one index in the password because we've counted it in the consecutive set
                
                // can't check any more adjacent characters because we're at the end of the password, so break
                if (i > s.length() - 2) {
                    break;
                }
                // reset the adjacent characters
                s1 = s.substring(i,i+1);
                s2 = s.substring(i+1,i+2);
            }
            type.add(count); // add length of consecutive set to list once the while loop is done (consecutive set is over)
        }
        
        // update the score
        for (int length : type) { // for every number length of consecutive set of every consecutive set
            score -= 2 * (length - 1);
        }
    }

    public static void sequentialTypes(String s) {

        // ArrayLists that hold the length of sequential sets (as element) for each sequential set (each set its own element)
        ArrayList<Integer> letters = new ArrayList<Integer>(), digits = new ArrayList<Integer>();
        
        for (int i = 0; i < s.length()-1; i++) { // for every character in password

            int count = 1; // length of sequential set
            char c1 = s.charAt(i), c2 = s.charAt(i+1); // adjacent characters
            
            // check if sequence should be increasing or decreasing (difference in ASCII values positive or negative)
            int upOrDown;
            if ((int)(c1) - (int)(c2) < 0) upOrDown = -1;
            else upOrDown = 1;
            
            // while loop to keep iterating through a consecutive set
            // both characters are digits; and the difference between the two ASCII values is 1 (they're sequential) and following the sequential trend
            while (String.valueOf(c1).matches("[0-9]") && String.valueOf(c2).matches("[0-9]") && (int)(c1) - (int)(c2) == upOrDown) {
                count++; // length of sequential set increases
                i++; // move over one index in the password because we've counted it in the sequential set
                
                // can't check any more adjacent characters because we're at the end of the password, so break
                if (i > s.length() - 2) {
                    break;
                }
                
                // reset the adjacent characters
                c1 = s.charAt(i);
                c2 = s.charAt(i+1);
            }
            digits.add(count); // add length of sequential set to list once the while loop is done (sequential set is over)
        }
        
        // do the same thing as the above for loop (with digit type), but instead with lowercase letters and then again with uppercase letters
        // but add the sequential lengths to the same list because we are checking sequential LETTERS, so the longest
        // sequential set length needs to include both lowercase and uppercase for the letters
        for (int i = 0; i < s.length()-1; i++) {
            
            int count = 1;
            char c1 = s.charAt(i), c2 = s.charAt(i+1);
            
            int upOrDown;
            if ((int)(c1) - (int)(c2) < 0) upOrDown = -1;
            else upOrDown = 1;
            
            while (String.valueOf(c1).matches("[a-z]") && String.valueOf(c2).matches("[a-z]") && (int)(c1) - (int)(c2) == upOrDown) {
                count++;
                i++;
                
                if (i > s.length() - 2) {
                    break;
                }
                
                c1 = s.charAt(i);
                c2 = s.charAt(i+1);
            }
            letters.add(count);
        }
        for (int i = 0; i < s.length()-1; i++) {
            
            int count = 1;
            char c1 = s.charAt(i), c2 = s.charAt(i+1);
            
            int upOrDown;
            if ((int)(c1) - (int)(c2) < 0) upOrDown = -1;
            else upOrDown = 1;
            
            while (String.valueOf(c1).matches("[A-Z]") && String.valueOf(c2).matches("[A-Z]") && (int)(c1) - (int)(c2) == upOrDown) {
                count++;
                i++;
                
                if (i > s.length() - 2) {
                    break;
                }
                
                c1 = s.charAt(i);
                c2 = s.charAt(i+1);
            }
            letters.add(count);
        }
        
        int max = 2; // initialize to 2 just incase there is no other max found, this way the default is less than what can be updated to the score
        for (int i : digits) { // for every sequential digit set length
            if (i > max) { // if this length is greater than the previous max
                max = i; // this is the new max
            }
        }
        if (max > 2) { // if length is longer than 2 (has to be a sequence of at least 3 in length)
            score -= 3 * (max - 2); // update score
        }
        
        // do the same thing as above except now with every sequential LETTER set length
        max = 2;
        for (int i : letters) {
            if (i > max) {
                max = i;
            }
        }
        if (max > 2) {
            score -= 3 * (max - 2);
        }
    }

    public static void counts(String password) {
        // counts of each
        int uppers = 0, lowers = 0, digits = 0, symbols = 0;
        
        for (int i = 0; i < password.length(); i++) { // for every character in password
            
            String c = password.substring(i,i+1); // current character in password
            
            // count occurrences
            if (c.matches("[a-z]")) { // lowercase
                lowers++;
            } else if (c.matches("[A-Z]")) { // uppercase
                uppers++;
            } else if (c.matches("[0-9]")) { // digits
                digits++;
            } else if (c.matches("[^a-zA-Z0-9]")) { // symbols
                symbols++;
            }
        }
        
        if (uppers > 0) {
            score += (password.length() - uppers) * 2;
        }
        if (lowers > 0) {
            score += (password.length() - lowers) * 2;
        }
        if (digits < password.length()) {
            score += 4 * digits;
        }
        score += 6 * symbols;
    }

    /* 8 length
     * 3 of: uppercase, lowercase, symbol, digit
     * digits: 48-57
     */
    public static void basicRequirements(String password) {
        
        boolean longEnough = false;
        if (password.length() > 7) 
            longEnough = true;

        int types = 0;

        boolean digit = false, uppercase = false, lowercase = false, symbol = false;

        for (int i = 0; i < password.length(); i++) { // for every character in password string
            
            // current character in password string
            String s = password.substring(i,i+1);
            char ch = password.charAt(i);
            
            if (s.matches("[0-9]")) // regex
            //if (Character.isDigit(ch)) // Character class
            //if (ch >= 48 && ch <= 57) // ASCII value
                digit = true;
            else if (s.matches("[A-Z]")) // regex
            //else if (Character.isUpperCase(ch)) // Character class
            //else if (ch >= 65 && ch <= 90) // ASCII value
                uppercase = true; // 
            else if (s.matches("[a-z]")) // regex
            //else if (Character.isLowerCase(ch)) // Character class
            //else if (ch >= 97 && ch <= 122) // ASCII value
                lowercase = true;
            else if (s.matches("[^a-zA-Z0-9]")) // regex
            //else if (!Character.isLetterOrDigit(ch)) // Character class
            //else if (ch < 48 || (ch > 57 && ch < 65) || (ch < 97 && ch > 90) || ch > 122) // ASCII value
            //else
                symbol = true;
        }

        if (symbol) types++;
        if (digit) types++;
        if (uppercase) types++;
        if (lowercase) types++;

        boolean qualify = false;
        if (types >= 3 && longEnough) 
            qualify = true;

        if (qualify) {
            score += 2 + types * 2;
        }
    }
}
DOWNLOAD as .java

My test cases (as .txt files):

Using their sample input file:

ECOO()123abcd9876
1234512345
ecoo2012
ecoo
Ecoo
EcooEcoo123
(TheBest?)45(45)abCD
Ecoo123
39283
foo()
foobar()

And the output to that is:

Very Strong (score = 100)
Very Weak (score = 19)
Good (score = 47)
Very Weak (score = 6)
Very Weak (score = 16)
Very Strong (score = 81)
Very Strong (score = 100)
Good (score = 53)
Very Weak (score = 13)
Weak (score = 34)
Weak (score = 40)
DOWNLOAD as .txt

Using their first judging test input:

12345678
a
(**)
1abcCBA2
abcdcbAedcba
education2012()
Education()
(An)(ApPlE)(a)(DaY)(kEePs)(ThE)(dOcToR)(aWaY)
aBc
abcdefg0987654321

And the output to that is:

Very Weak (score = 4)
Very Weak (score = 3)
Good (score = 44)
Good (score = 57)
Weak (score = 33)
Very Strong (score = 93)
Strong (score = 78)
Very Strong (score = 100)
Very Weak (score = 15)
Strong (score = 80)
DOWNLOAD as .txt

And that is exactly the expected output.

Using their second judging test input:

RABBITRABBIT
!nopasaran!
god
LifeBeginsAt40
IHaveADream(1963)
0
012
aBcDeFgHiJkLmNo
ponmlkj543
AAAAAAAAAAAAAAAAAA

And the output to that is:

Very Weak (score = 14)
Good (score = 41)
Very Weak (score = 5)
Very Strong (score = 92)
Very Strong (score = 100)
Very Weak (score = 3)
Very Weak (score = 4)
Strong (score = 75)
Weak (score = 28)
Very Weak (score = 20)
DOWNLOAD as .txt

And that is exactly the expected output.


           Created: April 21, 2014
     Last updated: June 14, 2014
Completed in full by: Michael Yaworski