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?
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_<language>_<country>.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)