65.9K
CodeProject 正在变化。 阅读更多。
Home

WinForm 生成密码工具

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.27/5 (3投票s)

2019年9月24日

CPOL

6分钟阅读

viewsIcon

9220

downloadIcon

453

WinForm 生成密码工具

背景 目录

我最近在开发一个 WinForm 应用程序,该应用程序

  1. 要求用户和管理员使用用户名和密码登录。
  2. 允许管理员添加用户并为其分配密码。
  3. 允许用户和管理员在线重置密码。

我在网上搜索了一个密码生成器,但没有找到满意的。于是我决定自己做一个。本文介绍了我开发的产品。

引言 目录

Generate Password

我需要一个窗体,在这个例子中是 WinForm,它允许用户和管理员生成密码。结果的界面如图左所示。

如果读者注意到与 LastPass [^] 的安全密码生成工具相似,那么他们是对的,因为该工具是我在此介绍的生成密码工具设计的基石。

我联系了 LastPass,询问他们是否有一个或计划提供一个密码生成 API。我被告知这个想法在他们的功能列表中。


实现 目录

在设计过程中,我意识到我需要三个组件:一个密码生成器,一个计算密码强度的方法,以及一个窗体间通信的机制。

密码生成器 目录

我在 kitsu.eb 在 生成随机密码 [^] 中的回答中找到一个密码生成器。需要进行修改以满足用户的选择,即

  1. 指定用于生成密码的字符类别(大写字母、小写字母、数字、特殊字符)。
  2. 根据“易于发音”或“易于阅读”等标准限制字符类别的内容。

默认的字符类别是

        const string  DIGITS = "0123456789";
        const string  LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
        const string  SPECIAL = @"!@#$%^&*()+=~[:'<>?,.|";
        const string  UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
Generate Password

生成新密码的第一步是修改默认字符集的副本,以反映用户的意愿,然后填充一个计算效率高的数据结构来支持实际的密码生成。create_character_sets 方法执行这些功能。

如果需要某个特定的字符集(由于用户选择了“要包含的字符”复选框中的一个或多个),则将默认字符集放入默认字符集的副本中;如果未选择,则将副本设置为空字符串。

然后应用用户的进一步限制。在该工具中,有三个限制。

  1. 所有字符 - 对字符集副本不做任何修改。
  2. 易于发音 - 移除数字和特殊字符集(副本被设置为空字符串)。
  3. 易于阅读 - 从副本中移除歧义字符 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 命名空间目录是

Generate Password Module
  PasswordGeneration
    GenerateGlobals
    GenerateHelp
    GeneratePassword
    generate_help_text
    generate_icon
    generate_image
    help_image
    regenerate_image
    RichTextBoxExtensions
    TransparentLabel

regenerate_password 末尾所示,新生成的密码的值被放入 GenerateGlobals 类中的 generated_password。任何实例化了生成密码窗体的窗体都可以从那里检索它。


测试实现 目录

Test Generate Password

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

Test Generate Password

由于需要窗体之间的通信,我使用了名为 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 原文
© . All rights reserved.