WinForm 生成密码工具






4.27/5 (3投票s)
WinForm 生成密码工具
背景
我最近在开发一个 WinForm 应用程序,该应用程序
- 要求用户和管理员使用用户名和密码登录。
- 允许管理员添加用户并为其分配密码。
- 允许用户和管理员在线重置密码。
我在网上搜索了一个密码生成器,但没有找到满意的。于是我决定自己做一个。本文介绍了我开发的产品。
引言

我需要一个窗体,在这个例子中是 WinForm,它允许用户和管理员生成密码。结果的界面如图左所示。
如果读者注意到与 LastPass [^] 的安全密码生成工具相似,那么他们是对的,因为该工具是我在此介绍的生成密码工具设计的基石。
我联系了 LastPass,询问他们是否有一个或计划提供一个密码生成 API。我被告知这个想法在他们的功能列表中。
实现
在设计过程中,我意识到我需要三个组件:一个密码生成器,一个计算密码强度的方法,以及一个窗体间通信的机制。
密码生成器
我在 kitsu.eb 在 生成随机密码 [^] 中的回答中找到一个密码生成器。需要进行修改以满足用户的选择,即
- 指定用于生成密码的字符类别(大写字母、小写字母、数字、特殊字符)。
- 根据“易于发音”或“易于阅读”等标准限制字符类别的内容。
默认的字符类别是
const string DIGITS = "0123456789";
const string LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
const string SPECIAL = @"!@#$%^&*()+=~[:'<>?,.|";
const string UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

生成新密码的第一步是修改默认字符集的副本,以反映用户的意愿,然后填充一个计算效率高的数据结构来支持实际的密码生成。create_character_sets 方法执行这些功能。
如果需要某个特定的字符集(由于用户选择了“要包含的字符”复选框中的一个或多个),则将默认字符集放入默认字符集的副本中;如果未选择,则将副本设置为空字符串。
然后应用用户的进一步限制。在该工具中,有三个限制。
- 所有字符 - 对字符集副本不做任何修改。
- 易于发音 - 移除数字和特殊字符集(副本被设置为空字符串)。
- 易于阅读 - 从副本中移除歧义字符 01OIoli!|。
应用了用户的限制后,将非零长度的副本添加到 character_sets 列表中。
List < string > character_sets = new List < string > ( );
如果副本的长度为零,则该字符类别将不参与密码生成,其副本也不会被添加到 character_sets 列表中。如果副本已被用户通过应用限制进行修改,则将修改后的字符类别添加到 character_sets 列表中。
// ************************************* create_character_sets
/// <summary>
/// creates a data structure that will contain the character
/// classes that will take part in password generation
/// </summary>
/// <param name="all_characters">
/// use any character combination in the password
/// </param>
/// <param name="easy_to_read">
/// avoid ambiguous characters in the password
/// </param>
/// <param name="easy_to_say">
/// avoid numbers and special characters in the password
/// </param>
/// <param name="lower_case">
/// include lowercase characters in the password
/// </param>
/// <param name="numbers">
/// include numeric characters in the password
/// </param>
/// <param name="symbols">
/// include special characters in the password
/// </param>
/// <param name="upper_case">
/// include uppercase characters in the password
/// </param>
/// <returns>
/// a list of strings containing character classes that are to
/// be used to generate a password
/// </returns>
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 ) // does user want digits?
{
digits = DIGITS;
}
if ( lower_case ) // does user want lowercase?
{
lowercase = LOWERCASE;
}
if ( symbols ) // does user want symbols?
{
special = SPECIAL;
}
if ( upper_case ) // does user want uppercase?
{
uppercase = UPPERCASE;
}
// all_characters need not be
// tested because if it was
// true, no changes would be
// made
if ( easy_to_say )
{
digits = String.Empty;
special = String.Empty;
}
else if ( easy_to_read )
{
// remove the ambiguous
// characters 01OIoli!|
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 );
}
}
// generate a computationally
// efficient data structure
list.Clear ( );
// if a character class' copy
// has a non-zero length, add
// it to the List
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 );
} // create_character_sets
在修订了字符类别之后,就可以开始实际的密码生成了。请注意,除了用户指定的要使用的字符外,密码生成还取决于用户指定的所需长度。generate_password 是实际生成密码的方法。
需要对源算法进行修改,使其能够响应用户的选择。原始算法创建的密码由大写字母、小写字母、数字和特殊字符组成。虽然完全可以接受,但这种方法无法满足用户的一系列选择。
主要的变化是整合 character_sets 数据结构。该结构作为参数传递给 generate_password。回想一下,character_sets 结构只包含应该参与密码生成的字符类别。
// ***************************************** generate_password
/// <summary>
/// generate a cryptographically strong password of the
// desired length using the specified character sets
/// </summary>
/// <param name="character_sets">
/// List < string > containing one or more characters sets
/// that are to be used to generate the password
/// </param>
/// <param name="desired_length">
/// desired length of the password
/// </param>
/// <returns>
/// string containing a cryptographically strong password
/// </returns>
string generate_password ( List < string > character_sets,
int desired_length )
{
byte [ ] bytes;
string characters;
int index = -1;
StringBuilder sb = new StringBuilder ( );
// get a cryptographically
// strong sequence of random
// bytes
bytes = new byte [ desired_length ];
new RNGCryptoServiceProvider ( ).GetBytes ( bytes );
foreach ( byte b in bytes )
{
// randomly select a character
// class for each byte
index = random.Next ( character_sets.Count );
characters = character_sets [ index ];
// use mod to project byte b
// into the correct range
sb.Append ( characters [ b % characters.Length ] );
}
return ( sb.ToString ( ) );
} // generate_password
在 generate_password 中,变量 random 之前被声明为全局变量
using System;
:
:
private readonly static Random random = new Random ( );
并且 RNGCryptoServiceProvider 通过以下方式实例化
using System.Security.Cryptography;
不必要的变量 characters 包含一个中间结果,以便于阅读。我认为替代的代码是难以理解的。
计算密码强度
我搜索了互联网,寻找一个可以确定密码强度的方法。我发现最智能的方法是 密码强度控件 [^]。需要进行重大修改。作者包含了一个 DataTable,它显然用于调试和统计支持。DataTable 和所有对它的引用都被删除了。
由于算法必须检查生成的密码中的每个字符,因此对循环内语句的效率给予了关注。结果,左侧的代码被右侧的代码替换了。
if (Char.IsDigit(ch)) if ( Char.IsDigit ( current_character ) )
{ {
iDigit++; digit_count++;
current_type = Types.DIGIT;
if (ConsecutiveMode == 3) }
iConsecutiveDigit++;
ConsecutiveMode = 3;
}
此更改已应用于所有字符集(大写、小写和特殊字符)。
为了区分字符类型,声明了一个定义类型的枚举和一个包含连续类型计数的数组。然后初始化了该数组。
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;
}
这使得以下语句
if ( current_type == prior_type )
{
types [ ( int ) current_type ]++;
}
prior_type = current_type;
有效地消除了大量代码。最终进行了各种其他测试,得到了密码强度方法。
最后,去除了密码中间包含数字或符号的测试。
// ***************************************** password_strength
/// <summary>
/// Determines how strong a password is based on different
/// criteria; 0 is very weak and 100 is very strong
///
/// Concept from
///
/// https://codeproject.org.cn/script/Articles/
/// ViewDownloads.aspx?aid=59186
///
/// which has been significantly modified
/// </summary>
/// <param name="password">
/// string containing the password whose strength is to be
/// determined
/// </param>
/// <returns>
/// integer containing the strength of the password; in the
/// range [ 0 - 100 ]
/// </returns>
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;
}
// scan password
foreach ( char current_character in password.
ToCharArray ( ) )
{
// count digits
if ( Char.IsDigit ( current_character ) )
{
digit_count++;
current_type = Types.DIGIT;
}
// count uppercase characters
else if ( Char.IsUpper ( current_character ) )
{
uppercase_count++;
current_type = Types.UPPERCASE;
}
// count lowercase characters
else if ( Char.IsLower ( current_character ) )
{
lowercase_count++;
current_type = Types.LOWERCASE;
}
// count symbols
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;
// count repeated letters
if ( Char.IsLetter ( current_character ) )
{
if ( repeated.Contains ( Char.ToLower (
current_character ) ) )
{
repeated_count++;
}
else
{
repeated.Add ( Char.ToLower (
current_character ),
0 );
}
}
}
// check for sequential alpha
// string patterns (forward
// and reverse)
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++;
}
}
// ADDITIONS TO STRENGTH
running_score = ( ( 4 * password_length ) +
( 2 * ( password_length -
uppercase_count ) ) +
( 2 * ( password_length -
lowercase_count ) ) +
( 4 * digit_count ) +
( 6 * symbol_count ) );
// requirments
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 );
}
// DEDUCTIONS FROM STRENGTH
// if only letters
if ( ( digit_count == 0 ) && ( symbol_count == 0 ) )
{
running_score -= password_length;
}
// if only digits
if ( digit_count == password_length )
{
running_score -= password_length;
}
// if repeated letters
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;
// confine result to range
if ( running_score > 100 )
{
running_score = 100;
}
else if ( running_score < 0 )
{
running_score = 0;
}
return ( running_score );
} // password_strength
总结
GeneratePassword.cs 的部分内容专门用于为该工具提供用户界面。我将各种方法的调用隔离到了 regenerate_password 中。该方法是对用于填充用户界面的其他方法调用的封装。
// *************************************** regenerate_password
/// <summary>
/// regenerate_password is a wrapper around the three major
/// password generation calculate and display methods:
///
/// create_character_sets
/// generate_password
/// password_strength
///
/// in addition it places the generated password into the
/// globally known class named GenerateGlobals
/// </summary>
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;
} // regenerate_password
窗体间通信
窗体可以通过多种方式将值传递给另一个窗体。
- 事件处理程序
- 属性
- Reflection(反射)
- 共享类
我决定通过一个名为 GenerateGlobals 的全局已知类来实现通信。在生成密码工具及其调用窗体中使用的 using 别名指令 [^] 可能是
using GG = PasswordGeneration.GenerateGlobals;
PasswordGeneration 命名空间目录是

PasswordGeneration GenerateGlobals GenerateHelp GeneratePassword generate_help_text generate_icon generate_image help_image regenerate_image RichTextBoxExtensions TransparentLabel
如 regenerate_password 末尾所示,新生成的密码的值被放入 GenerateGlobals 类中的 generated_password。任何实例化了生成密码窗体的窗体都可以从那里检索它。
测试实现

为了测试生成密码工具,开发了一个名为 TestPasswordGeneration 的测试平台。它的目的是触发生成密码工具并显示用户可接受的密码。

由于需要窗体之间的通信,我使用了名为 GenerateGlobals 的全局类。
以下代码将显示窗体,然后选择性地显示新生成的密码。它是“生成”按钮 Click 事件的事件处理程序。
generate_password_form = new PasswordGeneration.GeneratePassword ( );
if ( ( ( PasswordGeneration.GeneratePassword )
generate_password_form ).initialize_form ( ) )
{
// use modal (ShowDialog) so
// that the generated password
// can be captured
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;
}
}
参考文献
结论
我介绍了一个 WinForm,它允许开发人员为管理员和用户提供生成密码的能力。我正在考虑为 WebForms 开发相同的功能。
开发环境
生成密码工具是在以下环境中开发的
Microsoft Windows 7 专业版 SP 1 |
Microsoft Visual Studio 2008 专业版 SP1 |
Microsoft Visual C# 2008 |
Microsoft .Net Framework Version 3.5 SP1 |
Microsoft Office PowerPoint 2003 |
历史
09/20/2019 | 原文 |