更好地确定和管理密码强度
这里有一个更好的方法来确定和管理密码强度。
也许是我谷歌搜索的运气不太好,但我找不到一个好的、强大的 C# 实现来处理强密码(事实上,除了逻辑上的复制粘贴信息熵的实现之外,我真的没找到多少)。它们都基于一个相对标准的评估,即所有提交的密码都是随机的——嗯哼!
首先,我建议阅读这篇文章 [http://en.wikipedia.org/wiki/Password_strength]。这是一篇很好的文章,涵盖了密码的相对强度,并为确定随机密码和人类生成的密码的强度提供了指导。
密码的主要问题在于人类需要记住它们,或者将它们写下来。从历史上看,在技术上有个有趣的转折,你只需要担心你的同事访问/滥用你的密码,因为当时有隐形的物理安全措施——你只能在你身在办公室时登录。因此,那时你最大的威胁是你的同事。不幸的是,随着互联网和 VPN 技术的出现,物理位置的二次防御有效地被消除了。所以现在你的威胁范围从与你一起工作的人扩大到了全世界!再加上这些人是有经济动机的,并且可以直接针对你——现在外面要可怕得多!
因此,在深入研究实现之前,我们需要回顾一些众所周知需要避免的事情,以帮助提高密码强度。
- 避免序列——基于键盘或字母(abcd、qwert、1234、!@#$% 等)
- 避免使用字典单词,尤其是常见的!请注意,常见的拼写错误也会被用于基于字典的攻击——所以除非你的拼写错误非常不寻常,否则你很可能会在字典中找到它!
- 避免使用 leet/1337 密码替换单词(例如 P@ssw0rd、M1cr0$0ft、0\/\/n3d)。同样,这些现在都在字典里了,所以虽然暴力破解可能更难——但对于字典攻击来说它们非常容易。当然,使用 1337 风格并没有什么坏处,但它确实无助于防御针对性攻击。
- 避免使用团队名称、社会安全号码、驾照号码等。
为尽量减少泄露风险而应避免的事项
- 为不同的在线账户使用不同的密码。
- 避免使用可以在网上轻易找到的关于你的信息作为密码重置方案。出生日期、出生地、学校名称等。
- 如果任何账户需要最严格的密码控制,那就是你的电子邮件账户。几乎所有的在线系统都与电子邮件账户相关联。如果需要重置密码,通常会发送到你的电子邮件地址。如果电子邮件账户被泄露,那么这真的是打开了潘多拉的盒子。
好了,让我们从最弱的“安全”方法开始——信息熵。
- 此强度计算仅适用于“随机”密码。没有人(至少据我所知)能真正自己生成随机密码。我知道的最佳方法是启动记事本,让你的两岁孩子在键盘上乱打。然后取这段文字,随机更改字符的大小写并插入特殊字符。不幸的是,这仍然很弱,因为我们只有两只手,键盘又自然地分为我们双手会去的地方。这种生成方式的分布并不像人们想象的那么随机——我也不推荐!但至少你有一个起点,然后你还得把它写下来!
- [0-9] – 每个字符有 10 种可能的符号 – 3.32 位的二进制对数熵
- [a-z] – 每个字符有 26 种可能的符号 – 4.7 位的二进制对数熵
- [A-Z] – 每个字符有 26 种可能的符号 – 4.7 位的二进制对数熵
- [A-Z, 0-9] – 每个字符有 36 种可能的符号 – 5.17 位的二进制对数熵
- [A-Z,a-z] – 每个字符有 52 种可能的符号 – 5.7 位的二进制对数熵
- [A-Z, a-z, 0-9] – 每个字符有 62 种可能的符号 – 5.95 位的二进制对数熵
- [A-Z, a-z, 0-9, 特殊字符] – 每个字符有 94 种可能的符号 – 6.55 位的二进制对数熵
所以我们可以看到,生成一个使用完全随机信息的强密码本身就很困难,但这种方法却是 Web 应用程序最常用来确定密码强度的。这并不足够强大,因为人类天生就不随机。使用这个理论,以下非随机密码生成的結果都暗示着密码很强大:
- 12345678901234567890 – 20*3.32 => 66.4 位的熵
- !!!!!!!!!!!!!!!!!!!! – 10 * 6.55 => 65.5 位的熵
- !@#$%^&*() – 10 & 6.55 => 65.5 位的熵
- qwertyuiop[]qwertyuiop[] = 24 * 6.55 => 157.2 位的熵。
我们中更敏锐的人会发现最后两个密码是通过在美式键盘上用手指划过一行生成的。输入 24 个字符的密码花了不到 3 秒钟。所以如果有人在工作场所或图书馆看到有人输入这样的密码——很容易复制。显然,你可以看到,对于人类用户来说,他们会选择最容易记住和输入的密码——这永远不会是随机的!
所以为了帮助我们的用户避免成为受害者,我们必须努力剥夺他们“轻松”的途径。我们必须假设密码不会是数学上的随机——所以我们需要从不同的立场开始。我们必须确保消除“黑帽”攻击者试图利用的人类弱点。
回到文章开头,我们将创建一个interface
来定义一个“密码策略”,它为我们提供一种方法来帮助强制执行更强的密码——或者至少允许系统为处理密码设置一个通用的语言。
/// <summary>
/// Interface for defining a password policy
/// </summary>
/// <remarks>
/// This security policy determines whether passwords
/// meet pre-determined complexity requirements.
///
/// If this policy is enabled, passwords must meet the
/// following minimum requirements:
///
/// Not contain the user's account name or parts of the
/// user's full name that exceed four consecutive
/// characters.
/// Be at least <see cref="MinimumPasswordLength"/>
/// characters in length
/// Contain characters from three of the following
/// four categories:
/// English uppercase characters (A through Z)
/// English lowercase characters (a through z)
/// Base 10 digits (0 through 9)
/// Non-alphabetic characters (for example, !, $, #, %)
///
/// Complexity requirements are enforced when passwords
/// are changed or created.
/// </remarks>
public interface IPasswordPolicy : IPolicy
{
/// <summary>
/// Indicates the minimum password strength index for
/// this policy (see PasswordStrengthIndex)
/// </summary>
/// <remarks>
/// This value is based of a calculation of
/// information entropy after sequences
/// and dictionary words have been
/// removed.
/// </remarks>
/// <value>
/// The minimum index of the password strength.
/// </value>
PasswordStrengthIndex MinimumPasswordStrengthIndex
{
get;
set;
}
/// <summary>
/// Gets or sets the minimum length of the password.
/// </summary>
/// <value>The minimum length of the password.</value>
int MinimumPasswordLength
{
get;
set;
}
/// <summary>
/// Gets or sets the maximum length of the password.
/// </summary>
/// <value>The maximum length of the password.</value>
int MaximumPasswordLength
{
get;
set;
}
/// <summary>
/// If policy requires mixed case
/// </summary>
/// <value>true if policy needs mixed case</value>
bool RequireMixedCase
{
get;
set;
}
/// <summary>
/// If policy needs digits
/// </summary>
/// <value>true if policy needs digits.</value>
bool RequireDigits
{
get;
set;
}
/// <summary>
/// If policy needs special characters
/// </summary>
/// <value>
/// true if require special characters are needed
/// </value>
bool RequireSpecialCharacters
{
get;
set;
}
/// <summary>
/// Indicates if the username needs to be additionally
/// supplied to verify the password complexity against
/// </summary>
/// <value>
/// true require username to check password against
/// </value>
bool RequireUsernameToCheckPasswordAgainst
{
get;
set;
}
/// <summary>
/// Gets or sets the maximum count of characters
/// in a sequence
/// </summary>
/// <value>The maximum count of characters
/// in a sequence.</value>
int MaximumCharacterSequenceCount
{
get;
set;
}
/// <summary>
/// The duration of the lockout in minutes.
/// </summary>
/// <remarks>
/// This security setting determines the number of
/// minutes a locked-out account remains locked
/// out before automatically becoming unlocked.
/// The available range is from 0 minutes through
/// 99,999 minutes.
/// If you set the account lockout duration to less
/// than zero, the account will be locked out until an
/// administrator explicitly unlocks it. If an account
/// lockout threshold is defined, the account lockout
/// duration must be greater than or equal to
/// the reset time.
/// </remarks>
/// <value>The duration of the lockout.</value>
int LockoutDuration
{
get;
set;
}
/// <summary>
/// Gets or sets the lockout threshold.
/// </summary>
/// <remarks>
/// This security setting determines the number of
/// failed logon attempts that causes a user
/// account to be locked out. A locked-out
/// account cannot be used until it is reset
/// by an administrator or until the
/// lockout duration for the account has expired. You
/// can set a value between 0 and 999 failed
/// logon attempts. If you set the value to 0,
/// the account will never be locked out.
/// </remarks>
/// <value>The lockout threshold.</value>
int LockoutThreshold
{
get;
set;
}
/// <summary>
/// Reset account lockout after X minutes
/// </summary>
/// <remarks>
/// This security setting determines the number of
/// minutes that must elapse after a failed logon
/// attempt before the failed logon attempt
/// counter is reset to 0 bad logon attempts.
/// The available range is 1 minute to
/// 99,999 minutes.
/// </remarks>
/// <value>The duration of the lockout.</value>
int LockoutResetInMinutes
{
get;
set;
}
}
你可以看到这个密码策略模板扩展了最初的轮廓,不仅为熵位数提供了指导,而且还允许策略涵盖在密码处理不正确的情况下的锁定策略和密码到期方法。如果你查看源代码,你还会看到可用的选项,但为了本文的重点,我们尽量保持主题:)
那么,关于实际的强度测试,说到底这反而相当简单。我们将使用一个接口定义(IPassword
)来处理密码处理器(使测试和模拟更容易),这样我们就可以有多个实现(考虑 MEF!)。现在是实际实现。
- 使用各种查找表检查序列,以确定是否存在任何序列。如果检测到序列长度,并且其长度超过允许的最大长度,则密码将不符合策略。这些表包括:
- 字母 + 数字序列
- QWERTY 美式键盘
- QWERTY 英式键盘
- AZERTY 键盘
- 执行简单的
DecodeEliteEncoding
,然后执行一个简单的硬编码字典匹配,查找非常常见的密码。 - 如果提供(并且如果需要),将密码元素与用户名进行比较。
最终的实现仍然相当简单,并且很容易改进。最明显的是支持自定义字典和添加更多自定义键盘序列。其他扩展将是存储密码并成为真正的密码令牌服务。我们可以将调用 Google 密码评分服务而不是上述实现留给读者来实现 IPassword
。
一切都很好!我希望这(以及源代码)能帮助人们提供一种更好的方法来加强密码。
链接的源代码可能会随时间而变化,因此请经常回来查看。源代码使用了 Microsoft 测试框架,目前实现了 100% 的代码覆盖率!尽管我认为 100% 并不像人们想象的那么重要。
最后,目标是让一切都变得更安全——实际上,最好的方法是结合使用一个强大的、容易记住的密码和每分钟都会变化的硬件令牌。
一如既往,欢迎反馈!
Gareth