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

WebForm 密码生成器工具

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2019年10月18日

CPOL

8分钟阅读

viewsIcon

17227

downloadIcon

154

在之前的文章中,我们考虑了一个WebForm密码生成器;本文将介绍其实现结果。

1. 背景 目录

WebForm Generate Password

我最近发布了一个生成密码的 WinForm 应用程序( WinForm 生成密码工具 [^])。正如我在该文章中所提到的,我曾考虑将其作为 WebForm 应用程序来编写。本文将介绍其实现结果。

2. 简介 目录

由于一些读者可能对该工具的设计和实现有异议,我希望引用 John Walker 的话 [^]。

2.1. 为什么使用 JavaScript 加密? 目录

乍一看,JavaScript 似乎是一种奇怪的加密实现选择。这些程序相当大且复杂,下载和运行它们比 Java Applet 或访问 Web 服务器上的 CGI 程序所需的时间更长。我选择 JavaScript 是出于两个原因:*安全性*和*透明性*。

安全性。 加密的唯一目的是保护隐私。这意味着该过程不能涉及任何安全性可疑的链接。如果消息由 Web 服务器加密,它们将必须通过 Internet,任何中间站点都可能截获它们。即使某些机制(如安全 HTTP)绝对可以防止数据被截获,您仍然无法确定执行加密的站点没有将其副本保存在文件中,并方便地标记上您的 Internet 地址。

为了获得任何程度的安全性,所有处理都必须在*您的*计算机上完成,而不涉及与 Internet 上其他站点的任何传输或交互。带有 JavaScript 的 Web 浏览器可以实现这一点,因为嵌入在这些页面中的程序完全在您自己的计算机上运行,并且不通过 Internet 传输任何内容。输出仅显示在文本框中,允许您将其复制并粘贴到另一个应用程序。从那时起,安全性取决于您。

透明性 任何与安全相关的工具的有效性都取决于其设计和实现。*透明性*意味着,本质上,所有运动部件都是可见的,因此您可以自己判断该工具是否值得信赖。就程序而言,这意味着必须提供完整的源代码,并且您可以验证您运行的程序与提供的源代码相符。

JavaScript 的本质实现了这种透明性。程序嵌入在您与之交互的网页中;要检查它们,您只需使用浏览器的“查看源文件”功能,或者将页面保存到您计算机上的文件中并用文本编辑器阅读;页面引用的任何 JavaScript 组件都可以类似地下载并以源代码形式进行检查。JavaScript 是一种解释型语言,消除了运行与声称的源代码不同的程序的风险:对于解释型语言,您所读即您所运行。

3. 实现 目录

与 WinForm 工具不同,WebForm 工具更容易将用户界面与实际的密码生成分开。因此,密码生成器可以独立于其他 WebForm 组件存在。因此,有两个 JavaScript 文件:一个用于生成密码(generate_password.js),另一个用于支持用户界面ui.js)。

3.1. 密码生成器 目录

通过调用 *GeneratePassword* 模块中的*全局*入口点 *generate_password* 来完成密码生成。

    // ********************************************* generate_password

    // global entry point
    /// <summary>
    /// wrap the creation of character sets and the generation of the 
    /// password, returning the generated password
    /// </summary>
    function generate_password ( options )
      {
      var character_sets = [ ];

      character_sets = create_character_sets ( options );

      return ( create_password ( character_sets,
                                 options.desired_length ) );

      } // generate_password

“options”参数是一个 JSON 结构,其中包含字符集和密码生成所需长度的布尔值。

    var options = 
      {
      desired_length: MINIMUM_PASSWORD_CHARACTERS,
      lower_case:     true,
      numbers:        true,
      symbols:        true,
      upper_case:     true,
      all_characters: true,
      easy_to_read:   false,
      easy_to_say:    false,

      last_no_comma:0
      };

“options”组件的值在用户界面中设置。

密码生成器与 WinForm 实现中的类似,只是将 C# 转换为 JavaScript。遇到的主要问题是缺少与 C# RNGCryptoServiceProvider 的 GetBytes 方法等效的 JavaScript 方法。由于我只想使用“纯粹”的 Javascript,我被迫使用了令人讨厌的 Math.random 函数。

    // ************************************************ random_integer

    // local entry point
    /// <summary>
    /// using the built-in Math.random, generate a uniformly 
    /// distributed integer random variate in the range [ min, max )
    /// </summary>
    function random_integer ( min, max ) 
      {

      return Math.floor ( Math.random ( ) * ( max - min ) ) + min;

      } // random_integer

请注意,*random_integer* 是 *GeneratePassword* 模块*本地*的入口点。

确定在密码生成中使用哪些字符是在*本地*入口点 *create_character_sets* 中完成的。不幸的是,没有普遍接受的方法来包含字符类常量(例如,*export* 或 *import*)。因此,*DIGITS*、*LOWERCASE*、*PUNCTUATION*、*SPECIAL* 和 *UPPERCASE* 需要从 *ui.js* 中复制。

除了从 C# 转换为 JavaScript 之外,WinForm 实现和 WebForm 实现的 *create_character_sets* 之间的唯一区别是,存储字符类的结构现在是数组(而不是 List)。

    // ***************************************** create_character_sets

    // local entry point
    /// <summary>
    /// create an array of character sets that are dependent on the 
    /// contents of the options JavaScript object (JSON)
    /// </summary>
    /// <warning>
    /// the values of the character class constants DIGITS, LOWERCASE, 
    /// PUNCTUATION, SPECIAL, and UPPERCASE must duplicate those in 
    /// ui.js.
    /// </warning>
    function create_character_sets ( options )
        {
        const DIGITS =      '0123456789';
        const LOWERCASE =   'abcdefghijklmnopqrstuvwxyz';
        const PUNCTUATION = '!#%&()*,-./:,?@[\]_{}"\'';
        const SPECIAL =     '!@#$%^&*()+=~[:\'<>?,.|';
        const UPPERCASE =   'ABCDEFGHIJKLMNOPQRSTUVWXYZ';

        var character_sets = [ ];
        var digits = "";
        var lowercase = "";
        var special = "";
        var uppercase = "";

        if ( options.numbers )              // does user want digits?
          {
          digits = DIGITS;
          }

        if ( options.lower_case )           // does user want lowercase?
          {
          lowercase = LOWERCASE;
          }

        if ( options.symbols )              // does user want symbols?
          {
          special = SPECIAL;
          }

        if ( options.upper_case )           // does user want uppercase?
          {
          uppercase = UPPERCASE;
          }

        if ( options.all_characters )
          {
                                    // all_characters => no changes 
                                    // will made

          }
        else if ( options.easy_to_say )
          {
          digits = "";
          special = "";
          }
        else if ( options.easy_to_read )
          {
                                    // remove the ambiguous 
                                    // characters 01OIoli!|
          if ( ( digits !== null ) && ( digits.length > 0 ) )
            {
            digits = digits.replace ( "0", "" ).
                            replace ( "1", "" );
            }

          if ( ( uppercase !== null ) && ( uppercase.length > 0 ) )
            {
            uppercase = uppercase.replace ( "O", "" ).
                                  replace ( "I", "" );
            }

          if ( ( lowercase !== null ) && ( lowercase.length > 0 ) )
            {
            lowercase = lowercase.replace ( "o", "" ).
                                  replace ( "l", "" ).
                                  replace ( "i", "" );
            }

          if ( ( special !== null ) && ( special.length > 0 ) )
            {
            special = special.replace ( "!", "" ).
                              replace ( "|", "" );
            }
          }

        character_sets.length = 0;
                                    // if a the copy of a character 
                                    // class has a non-zero length, 
                                    // add it to the array
        if ( uppercase.length > 0 )
          {
          character_sets.push ( uppercase );
          }

        if ( lowercase.length > 0 )
          {
          character_sets.push ( lowercase );
          }

        if ( digits.length > 0 )
          {
          character_sets.push ( digits );
          }

        if ( special.length > 0 )
          {
          character_sets.push ( special );
          }

        return ( character_sets );

        } // create_character_sets

现在字符集反映了用户的选择,可以开始创建新密码了。与 WinForm 实现一样,WebForm 实现使用了 kitsu.eb 在 Generating Random Passwords [^] 中的答案中找到的密码生成器算法的修改版本。

对该范例的主要更改是整合了 *character_sets* 数据结构。该结构作为 *character_sets* 参数传递给 *create_password*。请记住,*character_sets* 结构仅包含应参与密码生成的字符类。*create_password* 是一个*本地*入口点。

    // *********************************************** create_password

    // local entry point
    /// <summary>
    /// from the desired characters and the desired length, create a 
    /// password
    /// </summary>
    function create_password ( character_sets,
                               desired_length )
      {
      var bytes = [ ];
      var characters = "";
      var i = 0;
      var index = -1;
      var password = "";
                                  // get a sequence of random bytes;
                                  // unfortunately there is no 
                                  // equivalent to GetBytes of the 
                                  // RNGCryptoServiceProvider available
                                  // in JavaScript; so we resort to 
                                  // multiple invocations of 
                                  // Math.random
      for ( i = 0; ( i < desired_length ); i++ )
        {
        bytes [ i ] = random_integer ( 0, 128 );
        }

      for ( i = 0; ( i < bytes.length ); i++ )
          {
          var b = bytes [ i ];
                                  // randomly select a character 
                                  // class for each byte
          index = random_integer ( 0, character_sets.length );
          characters = character_sets [ index ];
                                  // use mod to project byte b 
                                  // into the correct range
          password += characters [ b % characters.length ];
          }     

      return ( password );

      } // create_password

引入中间变量 *characters* 是为了提高可读性。我相信替代代码是难以理解的。

3.2. 用户界面 目录

Test Harness

该项目的大量代码是用户界面,用户通过用户界面表达其意图。所有屏幕操作都通过 *ui.js* 中的方法进行。

读者应该注意到,测试工具(前图左侧)和生成密码(前图右侧)都存在于同一个 html 文件 (index.html) 中。相关的 HTML 是

	<body>

    <div class="container"
         style="width:99%;
                background-color:#B0E0E6;">

        <div class="subcontainer"
             id="test_harness_div"
             style="width:100%; 
                    display:block;
                    background-color:#B0E0E6;
                    padding-bottom:25px;">
          <h3>Test Generate Password</h3>
          <button class="button blue_button"
                  style="display:block; 
                         margin: auto;
                         margin-bottom:1em;"
                  onclick="UI.toggle_display('test_harness_div','generate_password_div')">
            Generate Password
          </button>
          :
          :
        </div>

        <div class="subcontainer" 
             id="generate_password_div" 
             style="width:100%; 
                    background-color:#B0E0E6;
                    display:none;">

          <div style="width:99%;
                      margin-left:0.5em;">
          :
          :

相关的 CSS 是

body 
  {
  padding-left:10px;
	margin:1em;
	width:350px;
	}

.container
  {
  position: relative;
  }

.subcontainer 
  {
  position: absolute;
  }

而 *toggle_display* 是

    // ************************************************ toggle_display

    // global entry point
    /// <summary>
    /// if a <div> is displayed, hide it; if a <div> is not displayed, 
    /// show it
    /// </summary>
    function toggle_display ( div_1_id, div_2_id )
      {
      var div_1 = document.getElementById ( div_1_id );
      var div_2 = document.getElementById ( div_2_id );

      div_1.style.display = ( div_1.style.display == "none" ? 
                                                    "block" : 
                                                    "none"); 
      div_2.style.display = ( div_2.style.display == "none" ? 
                                                    "block" : 
                                                    "none"); 
      } // toggle_display

*toggle_display* 是测试工具中“生成密码”按钮以及生成密码中“取消”和“接受”按钮的 *onclick* 事件的事件处理程序。

3.2.1. 收集用户偏好 目录

为了生成用户的密码,需要收集用户的偏好。这通过生成密码的用户界面来完成。

除了生成的密码之外,用户界面的一个非常重要的产物是 *options* JSON 结构。该结构记录了用于实际生成密码的用户偏好。

    var options = 
      {
      desired_length: MINIMUM_PASSWORD_CHARACTERS,
      lower_case:     true,
      numbers:        true,
      symbols:        true,
      upper_case:     true,
      all_characters: true,
      easy_to_read:   false,
      easy_to_say:    false,

      last_no_comma:0
      };

初始设置要求一个包含大写、小写、数字和特殊字符的六字符(*MINIMUM_PASSWORD_CHARACTERS*)密码。

3.2.1.1. 期望长度 目录

密码的期望长度(以字符为单位)通过类型为 *number* 的 *input* 元素指定。*set_desired_length* 处理程序用于 *onclick* 事件,它是

    // ******************************************** set_desired_length

    // global entry point
    /// <summary>
    /// given a value of desired password length, place it into the 
    /// options object and regenerate the password
    /// </summary>
    function set_desired_length ( up_down_value )
      {

      options.desired_length = up_down_value;
      regenerate_password ( );

      }

当 *options* 中记录了新的密码长度时,会重新生成密码。

3.2.1.2. 期望字符类别 目录

字符类别通过类型为 *check* 的 *input* 元素指定。选择复选框是为了允许多个字符类别被要求。*set_checkbox_checked* 处理程序用于 *onclick* 事件,它是

    // ****************************************** set_checkbox_checked

    // global entry point
    /// <summary>
    /// given a checkbox id and an error message id, set the 
    /// appropriate Boolean value in the options object and 
    /// regenerate the password
    /// </summary>
    function set_checkbox_checked ( checkbox_id,
                                    message_id ) 
      {
      var in_error = false;
      var checkbox = document.getElementById ( checkbox_id );
      var ischecked = checkbox.checked;
      var message = document.getElementById ( message_id );

      message.style.visibility = "hidden";

      switch ( checkbox.id )
        {
        case "lowercase":
          options.lower_case = ischecked;
          break;
        case "numbers":
          options.numbers = ischecked;
          break;
        case "symbols":
          options.symbols = ischecked;
          break;
        case "uppercase":
          options.upper_case = ischecked;
          break;
        default:
          in_error = true;
          message.innerHTML = "Unrecognized CheckBox " + checkbox.id;
          break;
        }

      if ( !in_error )
        {
        if ( !( options.lower_case || 
                options.numbers || 
                options.symbols || 
                options.upper_case ) )
          {
          in_error = true;
          message.innerHTML =
            "At least one checkbox must be checked";
          message.style.visibility = "visible";
          }
        }

      if ( !in_error )
        {
        regenerate_password ( );
        }

      } // set_checkbox_checked

至少必须选中一个复选框;否则将显示错误消息。当复选框被选中或取消选中时,会重新生成密码。

3.2.1.3. 期望字符限制 目录

对字符类的限制通过类型为 *radio* 的 *input* 元素来施加。选择单选按钮是为了每次只允许施加一个限制。*set_radio_checked* 处理程序用于 *onclick* 事件,它是

    // ********************************************* set_radio_checked

    // global entry point
    /// <summary>
    /// given a radiobutton name and an error message id, set the 
    /// appropriate Boolean value in the options object and 
    /// regenerate the password; all radio buttons must be checked 
    /// to insure that an earlier prior checked that is now unchecked 
    /// will be detected
    /// </summary>
    function set_radio_checked ( radiobutton_name,
                                 message_id  ) 
      {
      var in_error = false;
      var message = document.getElementById ( message_id );
      var radio_buttons = 
              document.getElementsByName ( radiobutton_name );

      message.style.visibility = "hidden";

      for ( var i = 0; ( i < radio_buttons.length ); i++ )
        {
        var ischecked = radio_buttons [ i ].checked;

        switch ( radio_buttons [ i ].id )
          {
          case "allcharacters":
            options.all_characters = ischecked;
            break;
          case "easytoread":
            options.easy_to_read = ischecked;
            break;
          case "easytosay":
            options.easy_to_say = ischecked;
            break;
          default:
            in_error = true;
            message.innerHTML = "Unrecognized RadioButton " + 
                                radio_buttons [ i ].id;
            message.style.visibility = "visible";
            break;
          }
        }

      if ( !in_error )
        {
        regenerate_password ( );
        }

      } // set_radio_checked

为了在 *options* JSON 结构中保持单选按钮状态的有效表示,必须记录所有单选按钮的状态。因为与复选框不同,只能记录一个单选按钮为选中状态。当单选按钮被选中或取消选中时,会重新生成密码。

3.2.2. 退出密码生成 目录

无论何时取消密码生成或接受密码,用户都会返回到“测试密码生成”显示。同样,这是通过调用 *toggle_display* 来实现的。如果取消密码生成,显示与“测试密码生成”的初始显示相同。但是,如果接受密码,则会显示一个修改后的表单。

Accepted Generate Password

除了显示接受的密码外,用户还可以将返回的密码复制到剪贴板。提供此功能是为了以便将密码进一步复制到另一个文本框中。

 

4. 参考 目录

5. 结论 目录

我提供了一个 WebForm,使用户能够生成密码。

6. 浏览器兼容性 目录

Browser Compatibility

IE 和 Safari 都无法在 <div> 之间切换,导致用户界面无法使用。Edge 会隐藏 <input type="number"...> 控件的上下箭头部分,直到鼠标悬停在其上。

7. 开发环境 目录

WebForm 生成密码工具是在以下环境中开发的

Microsoft Windows 7 专业版 SP 1
Microsoft Visual Studio 2008 专业版 SP1
JavaScript Lint 0.3.0 (JavaScript-C 1.5 2004-09-24)
Firefox 68.0.2

8. 历史 目录

10/20/2019 原文
© . All rights reserved.