4
\$\begingroup\$

GUI-based pseudo-random password generator. Modification of this app, improved according to suggestions from Code Review users. Additional feature: calculation of time to crack password on average desktop PC, which is determined as time to go through all possible combinations of characters.

Ideas for further improvement?

enter image description here

Application.java - entry point

package com.singularityfx.passgen;

import java.awt.EventQueue;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.logging.Logger;

import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;

import com.singularityfx.utils.ExceptionReporter;

/**
 * Application entry point.
 * Responsible for configuration of locale and look & feel, and for loading
 * the GUI.
 *
 */
public class Application {
    private static final String DEFAULT_LANGUAGE = "en";
    private static final String DEFAULT_COUNTRY = "US";
    private static Logger log = Logger.getLogger(Application.class.getName());
    /**
     * GUI messages for locale specified in <code>resources/Locale.properties</code>
     */
    static ResourceBundle messages;
    static Locale locale;
    /**
     * Loads GUI messages for locale specified in 
     * <code>resources/Locale.properties</code> from 
     * <code>resources/MessagesBundle_&lt;language&gt;_&lt;country&gt;.properties</code>
     */
    private static void loadMessages() {
        Properties localeProperties = new Properties();
        InputStream in = Application.class.getResourceAsStream("/Locale.properties");
        try {
            localeProperties.load(in);
            locale = new Locale(
                    localeProperties.getProperty("language", DEFAULT_LANGUAGE),
                    localeProperties.getProperty("country", DEFAULT_COUNTRY));
        } catch (IOException e) {
            log.severe(e.toString());
            locale = new Locale(DEFAULT_LANGUAGE, DEFAULT_COUNTRY);
            new ExceptionReporter(e, false);
        }
        messages = ResourceBundle.getBundle("MessagesBundle", locale);
    }
    /**
     * Creates Event Dispatch Thread and loads GUI
     */
    private static void loadGUI() {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                new GUI().createAndShowGUI();
            }
        });
    }
    /**
     * Checks if Nimbus look & feel is available. If yes - sets Nimbus as  
     * current look & feel. Otherwise default look & feel is used.
     */
    private static void setLookAndFeel() {
        try {
            for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(info.getName())) {
                    UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (Exception e) {
            StringBuilder msg = new StringBuilder();
            msg.append(e.toString()).append(". ")
                .append("Default look & feel will be used");
            log.info(msg.toString());
        }
    }
    /**
     * Entry point.
     */
    public static void main(String[] args) {
        loadMessages();
        setLookAndFeel();
        loadGUI();
    }
}

GUI.java

package com.singularityfx.passgen;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.MessageFormat;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFormattedTextField;
import javax.swing.JOptionPane;
import javax.swing.JFormattedTextField.AbstractFormatter;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.JTextArea;
import javax.swing.JTextField;

/**
 * Represents Graphical User Interface. Creates and configures GUI components.
 * Includes ActionListener to launch password generation process.
 */
@SuppressWarnings("serial")
public class GUI extends JPanel implements ActionListener {
    private static final int FRAME_MIN_WIDTH = 320;
    private static final int FRAME_MIN_HEIGHT = 0;
    private static final int BORDER_WIDTH = 5;
    private static final int DEFAULT_NUMBER_OF_CHARACTERS = 12;
    private static final int DAYS_IN_MONTH = 30;
    private static final BigDecimal SECOND = BigDecimal.valueOf(1);
    private static final BigDecimal MINUTE = BigDecimal.valueOf(60);
    private static final BigDecimal HOUR = BigDecimal.valueOf(3600);
    private static final BigDecimal DAY = BigDecimal.valueOf(3600 * 24);
    private static final BigDecimal MONTH = BigDecimal.valueOf(3600 * 24 * DAYS_IN_MONTH);
    private static final BigDecimal YEAR = BigDecimal.valueOf(3600 * 24 * 365);
    private PassGen passwordGenerator;
    private JLabel lblNumberOfChars = 
            new JLabel(Application.messages.getString("lblNumberOfChars"));
    private JFormattedTextField txtNumberOfChars = new JFormattedTextField();
    private JCheckBox useSpecial = new JCheckBox(
            Application.messages.getString("useSpecial"));
    private JLabel lblPW =
            new JLabel(Application.messages.getString("lblPW"));
    private JTextArea txtPassword = new JTextArea();
    private JButton btnGenerate = 
            new JButton(Application.messages.getString("btnGenerate"));
    private JTextArea txtInfo = new JTextArea(3, 0);
    /**
     * Verifies if the number of characters field contains a valid positive
     * integer. Otherwise displays a message and forbids to remove focus from
     * the field. The check is done upon an attempt to remove focus from the
     * field.
     */
    private InputVerifier inputVerifier = new InputVerifier() {
        @Override
        public boolean verify(JComponent input) {
            JFormattedTextField ftf = (JFormattedTextField)input;
            AbstractFormatter formatter = ftf.getFormatter();
            if (formatter != null) {
                String text = ftf.getText();
                try {
                    int number = Integer.parseInt(text);
                    return (number > 0);
                } catch (NumberFormatException e) {
                    return false;
                }
            }
            return true;
        }
        @Override
        public boolean shouldYieldFocus(JComponent input) {
            boolean shouldYieldFocus = verify(input);
            if (!shouldYieldFocus) {
                JOptionPane.showMessageDialog(
                        GUI.this, 
                        Application.messages.getString("invalidNumber"));
            }
            return shouldYieldFocus;
        }
    };

    public GUI() {
        setupPanel();
        addNumberOfCharactersField();
        addEmptySpace();
        addUseSpecialCheckbox();
        addEmptySpace();
        addGenerateButton();
        addEmptySpace();
        addHorizontalLine();
        addEmptySpace();
        addPasswordField();
        addEmptySpace();
        addTxtInfo();
        addEmptySpace();
    }

    private void setupPanel() {
        this.setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
        this.setBorder(BorderFactory.createEmptyBorder(
                BORDER_WIDTH, BORDER_WIDTH, BORDER_WIDTH, BORDER_WIDTH));
    }

    private void addNumberOfCharactersField() {
        lblNumberOfChars.setAlignmentX(CENTER_ALIGNMENT);
        this.add(lblNumberOfChars);
        txtNumberOfChars.setAlignmentX(CENTER_ALIGNMENT);
        txtNumberOfChars.setHorizontalAlignment(JTextField.LEFT);
        txtNumberOfChars.setValue(DEFAULT_NUMBER_OF_CHARACTERS);
        txtNumberOfChars.setInputVerifier(inputVerifier);
        txtNumberOfChars.addActionListener(this);
        this.add(txtNumberOfChars);
    }

    private void addUseSpecialCheckbox() {
        useSpecial.setAlignmentX(CENTER_ALIGNMENT);
        this.add(useSpecial);
    }

    private void addGenerateButton() {
        btnGenerate.setAlignmentX(CENTER_ALIGNMENT);
        btnGenerate.addActionListener(this);
        this.add(btnGenerate);
    }

    private void addPasswordField() {
        lblPW.setAlignmentX(CENTER_ALIGNMENT);
        this.add(lblPW);
        txtPassword.setAlignmentX(CENTER_ALIGNMENT);
        this.add(txtPassword);
    }

    private void addTxtInfo() {
        txtInfo.setAlignmentX(CENTER_ALIGNMENT);
        txtInfo.setLineWrap(true);
        txtInfo.setWrapStyleWord(true);
        txtInfo.setForeground(Color.GRAY);
        this.add(txtInfo);
    }

    private void addEmptySpace() {
        this.add(Box.createRigidArea(new Dimension(0, BORDER_WIDTH)));
    }

    private void addHorizontalLine() {
        this.add(new JSeparator());
    }

    public void createAndShowGUI() {
        JFrame frame = new JFrame(Application.messages.getString("windowTitle"));
        frame.setMinimumSize(new Dimension(FRAME_MIN_WIDTH, FRAME_MIN_HEIGHT));
        frame.add(this);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    /**
     * Listens to ActionEvent which is a signal to start password generation 
     * and displaying. Calls {@link #displayPw()} to do the actual job.
     * @see #displayPw()
     */
    @Override
    public void actionPerformed(ActionEvent evt) {
        if ((evt.getSource() == btnGenerate)
           || (evt.getSource() == txtNumberOfChars)) {
            displayPw();
        }
    }

    /**
     * Generates the password using {@link PassGen} and displays it in the GUI.
     * Displays approximate time to crack password on average desktop PC.
     */
    private void displayPw() {
        txtPassword.setText("");
        passwordGenerator = new PassGen(useSpecial.isSelected());
        int numberOfChars = 0;
        numberOfChars = (int) txtNumberOfChars.getValue();
        char[] pw = passwordGenerator.generate(numberOfChars);
        /* The following loop is used in order to avoid creation of additional
         * String, as String with password information cannot be manually 
         * removed from memory thus increasing security risk.
         */
        for (int i = 0; i < pw.length; i++) {
            txtPassword.append(Character.toString(pw[i]));
            pw[i] = 0; // Fill array with zeroes to remove password information
                       // from memory
        }
        txtInfo.setText(formTimeToCrackMessage());
    }

    private String formTimeToCrackMessage() {
        BigDecimal timeToCrack = passwordGenerator
                .calculateTimeToCrack((int)txtNumberOfChars.getValue());
        String numberOfUnits = null;
        String messageKey = "timeToCrack";

        if (timeToCrack.compareTo(SECOND) < 0) {
            numberOfUnits = String.format(
                    Application.locale,
                    "%1$.8f",
                    timeToCrack.doubleValue()
                    );
            messageKey += "Seconds";
        } else if (timeToCrack.compareTo(MINUTE) < 0) {
            messageKey += "Seconds";
        } else if (timeToCrack.compareTo(HOUR) < 0) {
            timeToCrack = timeToCrack.divide(MINUTE, 0, RoundingMode.HALF_DOWN);
            messageKey += "Minutes";
        } else if (timeToCrack.compareTo(DAY) < 0) {
            timeToCrack = timeToCrack.divide(HOUR, 0, RoundingMode.HALF_DOWN);
            messageKey += "Hours";
        } else if (timeToCrack.compareTo(MONTH) < 0) {
            timeToCrack = timeToCrack.divide(DAY, 0, RoundingMode.HALF_DOWN);
            messageKey += "Days";
        } else if (timeToCrack.compareTo(YEAR) < 0) {
            timeToCrack = timeToCrack.divide(MONTH, 0, RoundingMode.HALF_DOWN);
            messageKey += "Months";
        } else {
            timeToCrack = timeToCrack.divide(YEAR, 0, RoundingMode.HALF_DOWN);
            messageKey += "Years";
        }

        if (numberOfUnits == null) {
            numberOfUnits = timeToCrack.toBigInteger().toString();
        }

        return MessageFormat.format(
                Application.messages.getString(messageKey),
                numberOfUnits
                );
    }
}

PassGen.java - class responsible for generating the password

package com.singularityfx.passgen;

import java.math.BigDecimal;
import java.security.SecureRandom;
import java.util.Random;

/**
 * Pseudo-random password generator.
 * Generated password can be of any desired length and may consist of
 * alphanumerical characters only or alphanumerical characters plus special
 * symbols.
 */
public class PassGen {
    private static final String LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
    private static final String UPPERCASE = LOWERCASE.toUpperCase();
    private static final String DIGITS = "0123456789";
    private static final String SPECIAL = " `~!@#$%^&*()_-+={}[]|:;'<>,.?/\"\\";
    /**
     * Number of calculations per second used to determine time to crack the
     * password
     */
    private static final BigDecimal CALCULATIONS_PER_SECOND = 
            BigDecimal.valueOf(4_000_000_000L);
    /**
     * Characters that can be used in generated passwords 
     */
    private char[] characters;
    private Random random = new SecureRandom();
    /**
     * Constructor. Creates set of characters that can be used in generated 
     * passwords.
     * @param  useSpecial  if true, generated password will consist of
     * alphanumeric and special characters, otherwise alphanumeric only
     * @see #characters
     */
    public PassGen(boolean useSpecial) {
        StringBuilder sb = new StringBuilder();
        sb.append(LOWERCASE).append(UPPERCASE).append(DIGITS);
        if (useSpecial) {
            sb.append(SPECIAL);
        }
        characters = sb.toString().toCharArray();
    }

    /**
     * Generates the password of specified length from the characters contained
     * in {@link #characters}.
     * @param  length  length of password
     * @return         generated password of specified length
     */
    public char[] generate(int length) {
        char[] pw = new char[length];
        for (int i = 0; i < length; i++) {
            pw[i] = characters[random.nextInt(characters.length)];
        }
        return pw;
    }

    /**
     * Calculates approximate time to crack (brute force) the password using
     * an average desktop PC, which is determined as time to go through all 
     * possible combinations of characters. 
     * @param length   length of password
     * @return         time to crack password in seconds
     */
    public BigDecimal calculateTimeToCrack(int length) {
        BigDecimal alphabetSize = BigDecimal.valueOf(characters.length);
        BigDecimal possibleCombinations = alphabetSize.pow(length);
        return possibleCombinations
                .divide(CALCULATIONS_PER_SECOND);
    }
}

ExceptionReporter.java - utility class for displaying the critical error messages to the user

package com.singularityfx.utils;

import javax.swing.JOptionPane;

/**
 * Utility class used to display exception and additional optional custom 
 * message in a dialog window.
 *
 */
public class ExceptionReporter {
    private Exception ex;
    private boolean closeApp;
    private String customMessage;
    /**
     * Platform-specific line separator
     */
    private String ls = System.getProperty("line.separator");
    /**
     * Constructor used when custom error message is not required
     * @param ex       Exception to be reported
     * @param closeApp Flag indicating if application shall be closed after exception is reported
     */
    public ExceptionReporter(Exception ex, boolean closeApp) {
        this(ex, closeApp, null);
    }
    /**
     * Constructor used when custom error message is required
     * @param ex             Exception to be reported
     * @param closeApp       Flag indicating if application shall be closed after exception is reported
     * @param customMessage  Custom message to be displayed along with exception information
     */
    public ExceptionReporter(Exception ex, boolean closeApp, String customMessage) {
        this.ex = ex;
        this.closeApp = closeApp;
        this.customMessage = customMessage;
        report();
    }
    /**
     * Reports exception, then closes the application if necessary 
     * Exception is reported (as Exception.toString()) with additional custom 
     * error message (if supplied during construction of ExceptionReporter 
     * object) to the user via a dialog window.
     */
    private void report() {
        StringBuilder message = new StringBuilder();
        if (customMessage != null) { 
            message.append(customMessage)
                .append(ls)
                .append(ls);
        }
        if (ex != null) {
            message.append(ex.toString());
        }
        JOptionPane.showMessageDialog(null, message.toString(),
                "", JOptionPane.ERROR_MESSAGE);
        if (closeApp) {
            System.exit(1);
        }
    }
}

MessagesBundleTest.java - tests internationalization resource bundles for completeness

package com.singularityfx.passgen;

import java.util.Arrays;
import java.util.Collection;
import java.util.Locale;
import java.util.ResourceBundle;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

/**
 * Checks if messages bundles for all supported locales contains all 
 * required messages.
 * @author SingularityFX
 *
 */
@RunWith(Parameterized.class)
public class MessagesBundleTest {
    /**
     * List of required messages
     */
    private String[] messageKeys = {
        "windowTitle",
        "lblNumberOfChars",
        "lblPW",
        "btnGenerate",
        "invalidNumber",
        "useSpecial",
        "timeToCrackSeconds",
        "timeToCrackMinutes",
        "timeToCrackHours",
        "timeToCrackDays",
        "timeToCrackMonths",
        "timeToCrackYears",
    };
    private ResourceBundle messages;

    public MessagesBundleTest(String language, String country) {
        Locale locale = new Locale(language, country);
        this.messages = ResourceBundle.getBundle("MessagesBundle", locale); 
    }

    /**
     * @return representation of supported locales
     */
    @SuppressWarnings("rawtypes")
    @Parameterized.Parameters
    public static Collection locales() {
        return Arrays.asList(new Object[][] {
                {"en", "US"},
                {"ru", "RU"}        
        });
    }

    /**
     * Will fail with MissingResourceException if any of the message keys
     * are missing in the messages bundle.
     */
    @Test
    public void testMessagesAvailable() {
        for (String messageKey : messageKeys) {
            messages.getString(messageKey);
        }
    }

}

PassGenTest.java - tests PassGen.calculateTimeToCrack() method

package com.singularityfx.passgen;

import static org.junit.Assert.*;

import java.math.BigDecimal;
import org.junit.Test;

public class PassGenTest {
    private static final int NUMBER_OF_CHARACTERS = 12;
    private static final BigDecimal TIME_TO_CRACK_ALPHANUM =
            new BigDecimal("806566690599.474955264");
    private static final BigDecimal TIME_TO_CRACK_SPECIAL =
            new BigDecimal("135090021915659.24072265625");
    private static PassGen passwordGenerator;

    @Test
    public void testCalculateTimeToCrackAlphanumerical() {
        passwordGenerator = new PassGen(false);
        assertEquals(
                TIME_TO_CRACK_ALPHANUM, 
                passwordGenerator.calculateTimeToCrack(NUMBER_OF_CHARACTERS)
                );
    }

    @Test
    public void testCalculateTimeToCrackSpecial() {
        passwordGenerator = new PassGen(true);
        assertEquals(
                TIME_TO_CRACK_SPECIAL,
                passwordGenerator.calculateTimeToCrack(NUMBER_OF_CHARACTERS)
                );
    }
}

MessagesBundle_en_US.properties - localized messages

windowTitle = Password Generator
lblNumberOfChars = Enter number of characters
lblPW = Your generated password:
btnGenerate = Generate password
invalidNumber = Please enter a positive integer
useSpecial = Use special symbols?
timeToCrackSeconds = Time to crack password on average desktop PC is {0} second(s)
timeToCrackMinutes = Time to crack password on average desktop PC is {0} minute(s)
timeToCrackHours = Time to crack password on average desktop PC is {0} hour(s)
timeToCrackDays = Time to crack password on average desktop PC is {0} day(s)
timeToCrackMonths = Time to crack password on average desktop PC is {0} month(s)
timeToCrackYears = Time to crack password on average desktop PC is {0} year(s)
\$\endgroup\$
  • \$\begingroup\$ One thing I would suggest is to reduce the size of the text field to put the length, make it a spinner (don't know if java has that) with a minimum limit of 5, and a maximum or around 1000(?). Then I would place the checkbox at its side, if there is space. The textarea where you are writting the duration, I would make the part where is says the time needed to crack to have colors. For example, less than 10 years old be red, 10 to 200 years would be yellow and above that would be green. Another thing would be to generate the password from a dictionary. That would generate pass-frases instead. \$\endgroup\$ – Ismael Miguel Nov 5 '14 at 20:47
  • \$\begingroup\$ You might want to consider switching over to JavaFX, the Java 8 GUI system. Swing will not be getting any more updates. \$\endgroup\$ – xv435 Nov 5 '14 at 20:58
  • \$\begingroup\$ @xv435 - note that JavaFX is much older than Java8, It is bundled with Java7 and was available as a separate component before that. \$\endgroup\$ – rolfl Nov 6 '14 at 13:19
1
\$\begingroup\$

I had to do something similar recently. Only thing I would recommend is to possibly remove characters/letters/symbols that commonly trip up the average user, or maybe even people who might have visibility problems. I chose to exclude !, i, 1, I, l, 0, O, o, S, 5, s, 2, Z, z. But really, you could be less aggressive about what letters you exclude.

\$\endgroup\$
  • 1
    \$\begingroup\$ Is it really useful to restrict characters ? If I didn't see clearly that it was a 5 and use S, does it really matter ? I will still have a good password (I suppose). \$\endgroup\$ – Marc-Andre Nov 5 '14 at 19:28
  • \$\begingroup\$ Functionally, when typed incorrectly in a password field, it would just reject the failed attempt. The user ends up taking a closer look at the letters and hopefully they get it on a subsequent try. You may not notice it, but a lot of existing services that accept serial numbers already avoid using similar letter/numbers for this very reason. I chose to extend that over to passwords. Yeah the user can always try it again, but for such a small change, why not make it easier for the user? I admit this is more preference than anything. \$\endgroup\$ – Nick Albrecht Nov 5 '14 at 22:10
1
\$\begingroup\$

I think you should add some blank lines to improve the readability of your code.

Take these two methods:

public GUI() {
    setupPanel();
    addNumberOfCharactersField();
    addEmptySpace();
    addUseSpecialCheckbox();
    addEmptySpace();
    addGenerateButton();
    addEmptySpace();
    addHorizontalLine();
    addEmptySpace();
    addPasswordField();
    addEmptySpace();
    addTxtInfo();
    addEmptySpace();
}

private void addNumberOfCharactersField() {
    lblNumberOfChars.setAlignmentX(CENTER_ALIGNMENT);
    this.add(lblNumberOfChars);
    txtNumberOfChars.setAlignmentX(CENTER_ALIGNMENT);
    txtNumberOfChars.setHorizontalAlignment(JTextField.LEFT);
    txtNumberOfChars.setValue(DEFAULT_NUMBER_OF_CHARACTERS);
    txtNumberOfChars.setInputVerifier(inputVerifier);
    txtNumberOfChars.addActionListener(this);
    this.add(txtNumberOfChars);
}

By adding some blank lines, you can make it far more readable.

public GUI() {
    setupPanel();

    addNumberOfCharactersField();
    addEmptySpace();

    addUseSpecialCheckbox();
    addEmptySpace();

    addGenerateButton();
    addEmptySpace();

    addHorizontalLine();
    addEmptySpace();

    addPasswordField();
    addEmptySpace();

    addTxtInfo();
    addEmptySpace();
}

private void addNumberOfCharactersField() {
    lblNumberOfChars.setAlignmentX(CENTER_ALIGNMENT);
    this.add(lblNumberOfChars);

    txtNumberOfChars.setAlignmentX(CENTER_ALIGNMENT);
    txtNumberOfChars.setHorizontalAlignment(JTextField.LEFT);
    txtNumberOfChars.setValue(DEFAULT_NUMBER_OF_CHARACTERS);
    txtNumberOfChars.setInputVerifier(inputVerifier);
    txtNumberOfChars.addActionListener(this);
    this.add(txtNumberOfChars);
}

Like this, you separate your method bodies into groups of code that are easier to understand.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.