Background
I was recently working on a WinForm application that
- Requires users and administrators to sign in with a username and
password.
- Allows an administrator to add users and assign them passwords.
- Allows users and administrators to reset their passwords online.
I researched the internet for a password generator and found nothing
to my liking. So I decided to roll my own. This article describes the
resulting product.
Table of Contents
The symbol
returns the reader to the top of the Table of Contents.
Introduction
I needed a form, in this case a WinForm, that would allow users and
administrators to generate passwords. The results took on the
appearance of the form to the left.
If readers note a resemblance to the
LastPass
[^] Generate Secure Password tool, they would
be right, since that tool formed the basis for the design of the
Generate Password tool, presented here.
I approached LastPass to determine if they had, or were planning to
have, a password generating API. I was advised that the idea was in
their features list.
Implementation
During design, I realized that I needed three components: a password
generator, a means to compute the strength of the password, and a
means of inter-form communication.
Password Generator
I found a password generator in kitsu.eb's answer in
Generating Random Passwords
[^]. Modifications were required to honor the
user's choices to
- Specify the character classes (uppercase, lowercase, numeric, special)
to be used to generate the password.
- Limit the contents of the character classes based on criteria such as
"easy to say" or "easy to read".
The default character classes are:
const string DIGITS = "0123456789";
const string LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
const string SPECIAL = @"!@#$%^&*()+=~[:'<>?,.|";
const string UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
The first step in generating a new password is to modify copies of the
default character sets so as to reflect user desires and then to
populate a computationally efficient data structure to support the
actual password generation. The create_character_sets method
performs these functions.
If a particular character set is desired (as a result of the user
choosing one or more of the "Characters to Include" checkboxes), the
default character set is placed into a copy of the default character
set; if not chosen, the copy is set to the empty string.
The user's further restrictions are then applied. In the tool, there
are three restrictions.
- All characters - no modifications are made to the copies of the
character sets.
- Easy to say - the digits and special character sets are removed (the
copies are set to the empty string).
- Easy to read - the ambiguous characters 01OIoli!| are removed from the
copies.
When the user's restrictions have been applied, non zero-length copies
are added to the character_sets List.
List < string > character_sets = new List < string > ( );
If a copy has a length of zero that character class will not
participate in the password generation and its copy will not be added
to the character_sets List. If the copy has been modified
by the user applying restrictions, then that reduced character class
will be added to the character_sets List.
List < string > create_character_sets ( bool all_characters,
bool easy_to_read,
bool easy_to_say,
bool lower_case,
bool numbers,
bool symbols,
bool upper_case )
{
string digits = String.Empty;;
List < string > list = new List < string > ( );
string lowercase = String.Empty;
string special = String.Empty;
string uppercase = String.Empty;
if ( numbers )
{
digits = DIGITS;
}
if ( lower_case )
{
lowercase = LOWERCASE;
}
if ( symbols )
{
special = SPECIAL;
}
if ( upper_case )
{
uppercase = UPPERCASE;
}
if ( easy_to_say )
{
digits = String.Empty;
special = String.Empty;
}
else if ( easy_to_read )
{
if ( !String.IsNullOrEmpty ( digits ) )
{
digits = digits.Replace ( "0", String.Empty ).
Replace ( "1", String.Empty );
}
if ( !String.IsNullOrEmpty ( uppercase ) )
{
uppercase = uppercase.
Replace ( "O", String.Empty ).
Replace ( "I", String.Empty );
}
if ( !String.IsNullOrEmpty ( lowercase ) )
{
lowercase = lowercase.
Replace ( "o", String.Empty ).
Replace ( "l", String.Empty ).
Replace ( "i", String.Empty );
}
if ( !String.IsNullOrEmpty ( special ) )
{
special = special.Replace ( "!", String.Empty ).
Replace ( "|", String.Empty );
}
}
list.Clear ( );
if ( uppercase.Length > 0 )
{
list.Add ( uppercase );
}
if ( lowercase.Length > 0 )
{
list.Add ( lowercase );
}
if ( digits.Length > 0 )
{
list.Add ( digits );
}
if ( special.Length > 0 )
{
list.Add ( special );
}
return ( list );
}
With the character classes revised, the actual password generation can
commence. Note that, in addition to the user-specified characters to
be used, password generation depends upon the user-specified desired
length. generate_password is the method that actually generates
the password.
Modifications to the source algorithm were needed to make it
responsive to user choices. The original algorithm created passwords
that were made up of upper- and lower-case letters, digits, and
special characters. Although quite acceptable, this approach could not
honor a set of user choices.
The major change was to incorporate the character_sets data
structure. The structure was passed to generate_password as a
parameter. Recall that the character_sets strucure contains
only those character classes that should participate in the generation
of a password.
string generate_password ( List < string > character_sets,
int desired_length )
{
byte [ ] bytes;
string characters;
int index = -1;
StringBuilder sb = new StringBuilder ( );
bytes = new byte [ desired_length ];
new RNGCryptoServiceProvider ( ).GetBytes ( bytes );
foreach ( byte b in bytes )
{
index = random.Next ( character_sets.Count );
characters = character_sets [ index ];
sb.Append ( characters [ b % characters.Length ] );
}
return ( sb.ToString ( ) );
}
In generate_password, the variable random was previously
declared globally as:
using System;
:
:
private readonly static Random random = new Random ( );
and RNGCryptoServiceProvider was instantiated by
using System.Security.Cryptography;
The unnecessary variable characters contains an intermediate
result for readability. I believe the alternative code is
unintelligible.
Computing the password strength
I searched the internet for a method that determined the strength of a
password. The one that I found to be most intelligent was
Password Strength Control
[^]. Major modifications were required. The
author had included a DataTable that was apparently used for debugging
and statistical support. The DataTable and all references to it were
removed.
Since the algorithm must examine each character in the generated
password, some attention was given to the efficiency of statements
within the loop. As a result, the code to the left was replaced by the
code to the right.
if (Char.IsDigit(ch)) if ( Char.IsDigit ( current_character ) )
{ {
iDigit++; digit_count++;
current_type = Types.DIGIT;
if (ConsecutiveMode == 3) }
iConsecutiveDigit++;
ConsecutiveMode = 3;
}
This change was made for all character sets (uppercase, lowercase, and
special).
To differentiate between character types, an enum defining the types
and an array to contain the counts of consecutive types were declared.
Then the array was initialized.
enum Types
{
NOT_SPECIFIED,
SYMBOL,
DIGIT,
UPPERCASE,
LOWERCASE,
NUMBER_TYPES
}
:
:
int [ ] consecutives;
:
:
consecutives = new int [ ( int ) Types.NUMBER_TYPES ];
for ( int i = ( int ) Types.NOT_SPECIFIED;
( i < ( int ) Types.NUMBER_TYPES );
i++ )
{
consecutives [ i ] = 0;
}
This allowed the following statements:
if ( current_type == prior_type )
{
types [ ( int ) current_type ]++;
}
prior_type = current_type;
effectively eliminating a significant amount of code. Various other
tests were performed finally yielding a password strength method.
Lastly, the test for digits or symbols in the middle of the password
was eliminated.
int password_strength ( string password )
{
int [ ] consecutives;
Types current_type = Types.NOT_SPECIFIED;
int digit_count = 0;
int lowercase_count = 0;
int password_length = password.Length;
Types prior_type = Types.NOT_SPECIFIED;
Hashtable repeated = new Hashtable();
int repeated_count = 0;
int requirments = 0;
int running_score = 0;
int sequential_alphabetic_count = 0;
int sequential_number_count = 0;
int symbol_count = 0;
int uppercase_count = 0;
consecutives = new int [ ( int ) Types.NUMBER_TYPES ];
for ( int i = ( int ) Types.NOT_SPECIFIED;
( i < ( int ) Types.NUMBER_TYPES );
i++ )
{
consecutives [ i ] = 0;
}
foreach ( char current_character in password.
ToCharArray ( ) )
{
if ( Char.IsDigit ( current_character ) )
{
digit_count++;
current_type = Types.DIGIT;
}
else if ( Char.IsUpper ( current_character ) )
{
uppercase_count++;
current_type = Types.UPPERCASE;
}
else if ( Char.IsLower ( current_character ) )
{
lowercase_count++;
current_type = Types.LOWERCASE;
}
else if ( Char.IsSymbol ( current_character ) ||
Char.IsPunctuation ( current_character ) )
{
symbol_count++;
current_type = Types.SYMBOL;
}
if ( current_type == prior_type )
{
consecutives [ ( int ) current_type ]++;
}
prior_type = current_type;
if ( Char.IsLetter ( current_character ) )
{
if ( repeated.Contains ( Char.ToLower (
current_character ) ) )
{
repeated_count++;
}
else
{
repeated.Add ( Char.ToLower (
current_character ),
0 );
}
}
}
for ( int i = 0; ( i < 23 ); i++ )
{
string forward = LOWERCASE.Substring ( i, 3 );
string reverse = reverse_string ( forward );
if ( ( password.ToLower ( ).
IndexOf ( forward ) != -1 ) ||
( password.ToLower ( ).
IndexOf ( reverse ) != -1 ) )
{
sequential_alphabetic_count++;
}
}
for ( int i = 0; ( i < 8 ); i++)
{
string forward = DIGITS.Substring ( i, 3 );
string reverse = reverse_string ( forward );
if ( ( password.ToLower ( ).
IndexOf ( forward ) != -1 ) ||
( password.ToLower ( ).
IndexOf ( reverse ) != -1 ) )
{
sequential_number_count++;
}
}
running_score = ( ( 4 * password_length ) +
( 2 * ( password_length -
uppercase_count ) ) +
( 2 * ( password_length -
lowercase_count ) ) +
( 4 * digit_count ) +
( 6 * symbol_count ) );
requirments = 0;
if ( password_length >=
MINIMUM_PASSWORD_CHARACTERS )
{
requirments++;
}
if ( uppercase_count > 0 )
{
requirments++;
}
if ( lowercase_count > 0 )
{
requirments++;
}
if ( digit_count > 0 )
{
requirments++;
}
if ( symbol_count > 0 )
{
requirments++;
}
if ( requirments > 3 )
{
running_score += ( 2 * requirments );
}
if ( ( digit_count == 0 ) && ( symbol_count == 0 ) )
{
running_score -= password_length;
}
if ( digit_count == password_length )
{
running_score -= password_length;
}
if ( repeated_count > 1 )
{
running_score -= ( repeated_count *
( repeated_count - 1 ) );
}
for ( int i = 0; ( i < ( int ) Types.NUMBER_TYPES ); i++ )
{
running_score -= ( 2 * consecutives [ i ] );
}
running_score -= sequential_alphabetic_count;
running_score -= sequential_number_count;
if ( running_score > 100 )
{
running_score = 100;
}
else if ( running_score < 0 )
{
running_score = 0;
}
return ( running_score );
}
Wrapping it up
Parts of the GeneratePassword.cs are specifically designed to provide
a user interface for the tool. I have isolated the invocations of
various methods to the regenerate_password. This method is a
wrapper around the invocations of the other methods used to
populate the user interface.
void regenerate_password ( )
{
List < string > character_sets = new List < string > ( );
string generated_password = String.Empty;
int strength = 0;
character_sets = create_character_sets ( all_characters,
easy_to_read,
easy_to_say,
lower_case,
numbers,
symbols,
upper_case );
generated_password = generate_password ( character_sets,
desired_length );
strength = password_strength ( generated_password );
password_strength_PB.Value = strength;
strength_LAB.Text = strength_in_words ( strength );
GG.generated_password = generated_password;
generated_password_TB.Clear ( );
generated_password_TB.Text = generated_password;
}
Inter-form Communication
There are a number of ways by which a form can communicate values to
another form:
- Event Handlers
- Properties
- Reflection
- Shared classes
I decided a to implement communication through a globally known class
named GenerateGlobals. The
using alias directive
[^] used in both the
Generate Password tool and its invoking form might be:
using GG = PasswordGeneration.GenerateGlobals;
The PasswordGeneration namespace directory is:
PasswordGeneration
GenerateGlobals
GenerateHelp
GeneratePassword
generate_help_text
generate_icon
generate_image
help_image
regenerate_image
RichTextBoxExtensions
TransparentLabel
As shown towards the end of
regenerate_password, the
value of the newly generated password is placed into
generated_password in the GenerateGlobals class. Any
form that instantiated the Generated Password form, can retrieve it
from there.
Testing the Implementation
To test the Generate Password tool, a harness was developed named
TestPasswordGeneration. Its purposes were to trigger the Generate
Password tool and display the password that was acceptable to the
user.
Because communication between forms was required, I used the global
class named GenerateGlobals.
The following code causes the form to appear and then optionally
display the newly generated password. It is the event handler for the
Generate button Click event.
generate_password_form = new PasswordGeneration.GeneratePassword ( );
if ( ( ( PasswordGeneration.GeneratePassword )
generate_password_form ).initialize_form ( ) )
{
generate_password_form.ShowDialog ( );
if ( !String.IsNullOrEmpty ( GG.generated_password ) )
{
generated_password = GG.generated_password;
generated_password_TB.Clear ( );
generated_password_TB.Text = generated_password;
generated_password_TB.Visible = true;
}
}
References
Conclusion
I have presented a WinForm that allows developers to provide
administrators and users with the ability to generate passwords. I am
considering developing the same functionality for WebForms.
Development Environment
The Generate Password tool was developed in the following environment:
Microsoft Windows 7 Professional SP 1 |
Microsoft Visual Studio 2008 Professional SP1 |
Microsoft Visual C# 2008 |
Microsoft .Net Framework Version 3.5 SP1 |
Microsoft Office PowerPoint 2003 |
History
09/20/2019
| Original article
|