WebForm 密码生成器工具





5.00/5 (6投票s)
在之前的文章中,我们考虑了一个WebForm密码生成器;本文将介绍其实现结果。
目录
1. 背景
我最近发布了一个生成密码的 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. 用户界面
该项目的大量代码是用户界面,用户通过用户界面表达其意图。所有屏幕操作都通过 *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* 来实现的。如果取消密码生成,显示与“测试密码生成”的初始显示相同。但是,如果接受密码,则会显示一个修改后的表单。
除了显示接受的密码外,用户还可以将返回的密码复制到剪贴板。提供此功能是为了以便将密码进一步复制到另一个文本框中。
4. 参考
- WinForm 生成密码工具 [^]
- John Walker [^]
- 生成随机密码 [^]
5. 结论
我提供了一个 WebForm,使用户能够生成密码。
6. 浏览器兼容性
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 | 原文 |