第二十六 口令(password)
口令概述
计算机、互联网中为了保护系统安全、认证用户身份,经常使用账号/密码(准确来说应该为“口令“)对用户进行访问控制。口令(password)是信息安全领域最古老、最广泛使用的身份认证机制,从1961年 MIT 的 CTSS 时间共享系统首次引入口令机制至今,口令仍然是绝大多数系统用户认证的第一道防线。
口令在计算机互联网中扮演非常重要的作用。据统计,超过80%的数据泄露事件与弱口令或口令管理不当有关。理解口令安全原理、掌握口令保护技术,是每个开发者和安全从业者的必修课。
口令安全威胁
口令面临着多种攻击方式的威胁,了解这些威胁是保护口令安全的第一步。
暴力破解
暴力破解(Brute Force Attack)是最直接的攻击方式,攻击者尝试所有可能的字符组合来猜测口令。对于一个长度为 $n$、字符集大小为 $k$ 的口令,其可能的组合数为 $k^n$。
| 口令长度 | 仅小写字母 ($k=26$) | 大小写+数字 ($k=62$) | 大小写+数字+符号 ($k=95$) |
|---|---|---|---|
| 6 位 | $3.08 \times 10^8$ | $5.68 \times 10^{10}$ | $7.35 \times 10^{11}$ |
| 8 位 | $2.09 \times 10^{11}$ | $2.18 \times 10^{14}$ | $6.63 \times 10^{15}$ |
| 10 位 | $1.41 \times 10^{14}$ | $8.39 \times 10^{17}$ | $5.99 \times 10^{19}$ |
| 12 位 | $9.54 \times 10^{16}$ | $3.23 \times 10^{21}$ | $5.41 \times 10^{23}$ |
现代 GPU 可以每秒尝试数十亿次简单哈希运算,因此短口令在暴力破解面前几乎不堪一击。
字典攻击
字典攻击(Dictionary Attack)比暴力破解更高效。攻击者使用预先收集的常用密码、常见单词、人名、地名等组成的“字典“,逐一尝试。由于大多数人倾向于使用容易记忆的密码,字典攻击的成功率远高于暴力破解。
根据统计,最常用的弱密码包括:
| 排名 | 密码 | 出现频率 |
|---|---|---|
| 1 | 123456 | 约 1% |
| 2 | password | 约 0.8% |
| 3 | 12345678 | 约 0.6% |
| 4 | qwerty | 约 0.4% |
| 5 | abc123 | 约 0.3% |
| 6 | 888888 | 约 0.2% |
| 7 | 111111 | 约 0.2% |
| 8 | 123123 | 约 0.15% |
这些密码在字典攻击面前毫无抵抗力。
彩虹表攻击
彩虹表攻击(Rainbow Table Attack)是一种利用预计算来加速口令破解的技术。攻击者预先计算大量口令的哈希值,并存储在查找表中(彩虹表)。当获取到系统的哈希值后,只需在表中查找即可反推出原始口令。
彩虹表的核心思想是用空间换时间。设口令空间为 $N$,直接暴力破解需要 $O(N)$ 次哈希运算,而彩虹表预计算后查找只需 $O(N^{2/3})$ 的空间和 $O(N^{2/3})$ 的时间。
防御彩虹表攻击的关键手段是使用盐值(salt),即在每个口令哈希前加入随机数据,使得预计算表失效。
钓鱼攻击
钓鱼攻击(Phishing)不直接攻击口令本身,而是通过伪装成合法网站或服务,诱骗用户主动输入口令。钓鱼攻击的技术手段包括:
- 邮件钓鱼:发送伪装成银行、社交媒体等官方通知的邮件
- 网站克隆:搭建与目标网站高度相似的假网站
- 中间人攻击:在用户与服务器之间截获通信
- 短信钓鱼(Smishing):通过短信诱导用户访问钓鱼网站
键盘记录与社会工程学
键盘记录(Keylogging)通过在用户设备上安装恶意软件,记录用户的键盘输入来窃取口令。社会工程学(Social Engineering)则通过心理操纵,让用户主动透露口令信息,例如冒充IT支持人员要求用户提供密码。
如何构造强口令
口令如此重要,如何保护好自己的口令呢?首先口令本身应该足够复杂,足够抵御攻击。
密码长度、复杂度与熵
口令的安全强度可以用信息熵(Information Entropy)来量化。信息熵的计算公式为:
$$H = L \times \log_2(k)$$
其中 $L$ 为口令长度,$k$ 为字符集大小。
| 口令示例 | 长度 | 字符集 | 熵(bit) | 安全等级 |
|---|---|---|---|---|
| 123456 | 6 | 数字(10) | 19.9 | 极弱 |
| password | 8 | 小写(26) | 37.6 | 弱 |
| P@ssw0rd | 8 | 混合(95) | 52.6 | 中等 |
| ARG2on&@! | 9 | 混合(95) | 59.2 | 较强 |
| ARG2on&@!略 | 10 | 混合+中文 | 80+ | 强 |
一般来说,熵值达到 60 bit 以上才被认为具有较好的安全性,80 bit 以上则相当安全。
密码短语(Passphrase)
密码短语(Passphrase)是由多个随机单词组成的口令,例如 “correct-horse-battery-staple”。密码短语的核心优势在于:
- 长度更长:即使每个单词只有4-5个字母,4个单词组成的短语也有16-20个字符
- 更容易记忆:相比随机字符串,有意义的单词组合更容易记住
- 熵值更高:假设使用包含7776个单词的词典(类似 diceware),4个单词的熵为 $4 \times \log_2(7776) \approx 51.7$ bit;5个单词则达到约64.6 bit
密码强度评估
前面我们提到密码有强弱之分,那么该如何量化评估一个密码的强度呢?可以参考这篇博客计算密码强度《如何使用Rust编程语言来产生密码或是计算密码强度?密码又该如何安全保存呢?》。
在 Rust 中,我们可以使用 passwords 库来评估密码强度:
#![allow(unused)]
fn main() {
use passwords::{analyzer, scorer};
fn check_password_strength() {
let pwd = "ARG2on&@!略";
let analyzed = analyzer::analyze(pwd);
println!("Password: {}", pwd);
println!("Length: {} characters", analyzed.length());
println!("Score: {}", scorer::score(&analyzed));
// 分析密码组成
let weak_pwd = "123456";
let weak_analyzed = analyzer::analyze(weak_pwd);
println!("\nWeak Password: {}", weak_pwd);
println!("Score: {}", scorer::score(&weak_analyzed));
let strong_pwd = "Tr0ub4dor&3xYz!@#";
let strong_analyzed = analyzer::analyze(strong_pwd);
println!("\nStrong Password: {}", strong_pwd);
println!("Score: {}", scorer::score(&strong_analyzed));
}
}
passwords 库的 scorer::score() 函数会综合考虑密码长度、字符多样性、常见模式等因素,返回一个0-100的分数。分数越高,密码强度越大。
口令存储安全
绝不存储明文密码
在系统设计中,有一条铁律:永远不要存储用户的明文密码。历史上无数数据泄露事件证明,存储明文密码是极其危险的行为。即使数据库被攻破,攻击者也不应该能直接获取用户的原始密码。
哈希 + 盐值
正确的口令存储方式是使用密码哈希函数对口令进行单向变换,并附加盐值(salt)来防御彩虹表攻击。
盐值(Salt) 是一个随机生成的字符串,每个用户的盐值都不同。存储时将盐值与口令拼接后再进行哈希:
$$\text{stored} = \text{salt} \parallel \text{hash}(\text{salt} \parallel \text{password})$$
盐值的作用:
- 使相同口令产生不同的哈希值
- 使预计算的彩虹表失效
- 增加攻击者需要独立破解每个口令的成本
Argon2:现代密码哈希算法
Argon2 是密码哈希竞赛(Password Hashing Competition, PHC)的获胜者,被认为是目前最安全的密码哈希算法。Argon2 有三个变体:
- Argon2d:纯数据依赖型,抗 GPU 攻击最强,但易受侧信道攻击
- Argon2i:纯独立型,抗侧信道攻击,但对 GPU 攻击的抵抗力稍弱
- Argon2id:混合型,先进行一遍独立型再进行数据依赖型,兼顾两者优势
Argon2 的核心思想是内存硬(memory-hard):它需要大量内存来完成计算,这使得 GPU、ASIC 等并行计算硬件的优势被大大削弱。
#![allow(unused)]
fn main() {
fn argon2() -> Result<(), argon2::password_hash::errors::Error> {
use argon2::{
password_hash::{
rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString,
},
Argon2,
};
use passwords::{analyzer, scorer};
let pwd = "ARG2on&@!略";
let password = pwd.as_bytes(); // Bad password; don't actually use!
let salt = SaltString::generate(&mut OsRng);
// Argon2 with default params (Argon2id v19)
let argon2 = Argon2::default();
// Hash password to PHC string ($argon2id$v=19$...)
let password_hash = argon2.hash_password(password, &salt)?.to_string();
// Verify password against PHC string.
//
// NOTE: hash params from `parsed_hash` are used instead of what is configured in the
// `Argon2` instance.
let parsed_hash = PasswordHash::new(&password_hash)?;
assert!(Argon2::default()
.verify_password(password, &parsed_hash)
.is_ok());
println!("{}", &password_hash);
Ok(())
}
}
Argon2 的 PHC 字符串格式为:
$argon2id$v=19$m=19456,t=2,p=1$salt$hash
其中 m 表示内存成本(KB),t 表示时间迭代次数,p 表示并行度。
PBKDF2:基于密钥派生的方案
PBKDF2(Password-Based Key Derivation Function 2)是一种通过多次迭代哈希运算来增加破解成本的算法。PBKDF2 的核心思想是:
$$DK = \text{PBKDF2}(PRF, \text{password}, \text{salt}, c, dkLen)$$
其中 $c$ 为迭代次数,$PRF$ 为伪随机函数(通常使用 HMAC-SHA256 或 HMAC-SHA512)。迭代次数越多,攻击者每次尝试的成本越高。
#![allow(unused)]
fn main() {
fn pbkdf2() {
use ring::digest;
const CREDENTIAL_LEN: usize = digest::SHA512_OUTPUT_LEN;
let n_iter = NonZeroU32::new(100_000).unwrap();
let rng = rand::SystemRandom::new();
let mut salt = [0u8; CREDENTIAL_LEN];
rng.fill(&mut salt);
let password = "Guess Me If You Can!";
let mut pbkdf2_hash = [0u8; CREDENTIAL_LEN];
pbkdf2::derive(
pbkdf2::PBKDF2_HMAC_SHA512,
n_iter,
&salt,
password.as_bytes(),
&mut pbkdf2_hash,
);
println!("Salt: {}", HEXUPPER.encode(&salt));
println!("PBKDF2 hash: {}", HEXUPPER.encode(&pbkdf2_hash));
let should_succeed = pbkdf2::verify(
pbkdf2::PBKDF2_HMAC_SHA512,
n_iter,
&salt,
password.as_bytes(),
&pbkdf2_hash,
);
let wrong_password = "Definitely not the correct password";
let should_fail = pbkdf2::verify(
pbkdf2::PBKDF2_HMAC_SHA512,
n_iter,
&salt,
wrong_password.as_bytes(),
&pbkdf2_hash,
);
assert!(should_succeed.is_ok());
assert!(!should_fail.is_ok());
}
}
PBKDF2 的迭代次数建议至少 100,000 次(2026年标准),且应随硬件性能提升而定期增加。
bcrypt 与 scrypt
bcrypt 是一种专门为密码哈希设计的算法,内置盐值并使用 Blowfish 密码的变体进行多次加密。bcrypt 的特点是:
- 内置盐值,无需单独管理
- 自适应成本因子(cost factor),可随硬件升级调整
- 故意设计为计算缓慢,增加暴力破解成本
scrypt 在 bcrypt 的基础上增加了内存硬特性,需要大量内存才能完成计算,有效抵御 GPU/ASIC 攻击。scrypt 的参数包括:
- $N$(CPU/内存成本因子)
- $r$(块大小)
- $p$(并行因子)
密码哈希算法对比
| 特性 | Argon2 | bcrypt | scrypt | PBKDF2 |
|---|---|---|---|---|
| 抗暴力破解 | 优秀 | 良好 | 优秀 | 一般 |
| 抗GPU/ASIC | 优秀(内存硬) | 一般 | 优秀(内存硬) | 差 |
| 抗侧信道攻击 | 良好(Argon2i) | 一般 | 一般 | 良好 |
| 可调参数 | 内存/时间/并行度 | 成本因子 | CPU/内存/并行度 | 迭代次数 |
| 标准化 | PHC winner | 广泛使用 | RFC 7914 | RFC 2898 |
| 推荐程度 | 首选 | 推荐 | 推荐 | 可用 |
| Rust 库 | argon2 | rust-bcrypt | scrypt | pbkdf2 |
推荐策略:新项目首选 Argon2id;已有项目使用 bcrypt 也是可接受的;PBKDF2 虽然广泛支持,但在抗 GPU 攻击方面不如前三者。
如何管理你的口令
生活中各种形形色色的密码(口令)数不胜数,如何管理这些密码也是一件十分重要的事情。
密码管理器
使用诸如 LastPass、1Password、Bitwarden 等专业的密码管理器能够同时兼顾密码的安全和便捷。密码管理器的核心功能包括:
- 自动生成强密码:为每个账号生成唯一的随机强密码
- 安全存储:使用强加密算法(如 AES-256)存储所有密码
- 自动填充:在浏览器和移动应用中自动填写密码
- 跨设备同步:在手机、电脑、平板之间安全同步密码库
- 安全审计:检测重复密码、弱密码、已泄露密码
单点登录(SSO)
单点登录(Single Sign-On, SSO)允许用户使用一组凭证登录多个相关系统。常见的 SSO 协议包括:
- OAuth 2.0:授权框架,广泛用于第三方登录(如微信、GitHub 登录)
- SAML:企业级 SSO 标准,常用于企业内部系统
- OpenID Connect:基于 OAuth 2.0 的身份认证层
SSO 的优势是减少用户需要记忆的密码数量,但风险在于一旦 SSO 凭证泄露,所有关联系统都将受到影响。
OTP(一次性口令)
OTP(One Time Password,一次性口令)是一种每次使用后即失效的动态密码机制,可以有效防止口令重放攻击。
- 一文搞懂 OTP 双因素认证
- RFC4226 HOTP: An HMAC-Based One-Time Password Algorithm
- RFC6238 TOTP: Time-Based One-Time Password Algorithm
HOTP:基于计数器的 OTP
HOTP(HMAC-Based One-Time Password)基于一个递增的计数器和共享密钥来生成一次性口令。其计算公式为:
$$HOTP(K, C) = \text{Truncate}(\text{HMAC-SHA1}(K, C))$$
其中 $K$ 为共享密钥,$C$ 为计数器值。Truncate 函数将 HMAC 的输出截取为指定位数(通常6位)的数字。
HOTP 的问题是客户端和服务端的计数器必须保持同步,如果客户端生成了 OTP 但未使用(计数器递增),服务端的计数器就会落后,需要通过“滑动窗口“机制来容错。
TOTP:基于时间的 OTP
TOTP(Time-Based One-Time Password)是 HOTP 的改进版本,使用时间戳代替计数器:
$$TOTP(K, T) = HOTP(K, T)$$
其中 $T = \lfloor \frac{\text{current_time} - T_0}{X} \rfloor$,$T_0$ 为起始时间(通常为 Unix 纪元),$X$ 为时间步长(通常为30秒)。
TOTP 的优势在于不需要计数器同步,只需保证客户端和服务端的时钟大致同步即可。这就是 Google Authenticator、Microsoft Authenticator 等认证器应用的工作原理。
Rust 实现 TOTP
以下是一个使用 Rust 实现 TOTP 的示例:
#![allow(unused)]
fn main() {
use hmac::{Hmac, Mac};
use sha1::Sha1;
type HmacSha1 = Hmac<Sha1>;
/// 生成 TOTP 一次性口令
///
/// # 参数
/// - `secret`: 共享密钥(Base32编码)
/// - `time_step`: 时间步长(秒),通常为30
/// - `digits`: 输出位数,通常为6
///
/// # 返回
/// 指定位数的数字字符串
fn generate_totp(secret: &[u8], time_step: u64, digits: usize) -> String {
let time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
// 计算时间计数器
let counter = time / time_step;
// 将计数器转换为8字节大端序
let counter_bytes: [u8; 8] = counter.to_be_bytes();
// 计算 HMAC-SHA1
let mut mac = HmacSha1::new_from_slice(secret).expect("HMAC can take key of any size");
mac.update(&counter_bytes);
let result = mac.finalize().into_bytes();
// 动态截断
let offset = (result[19] & 0x0f) as usize;
let binary: u32 = ((result[offset] & 0x7f) as u32) << 24
| (result[offset + 1] as u32) << 16
| (result[offset + 2] as u32) << 8
| (result[offset + 3] as u32);
// 取指定位数
let otp = binary % 10u32.pow(digits as u32);
format!("{:0width$}", otp, width = digits)
}
#[test]
fn test_totp() {
// 示例密钥(实际应用中应使用安全的随机密钥)
let secret = b"12345678901234567890"; // 20字节密钥
let code = generate_totp(secret, 30, 6);
println!("Current TOTP code: {}", code);
assert_eq!(code.len(), 6);
}
}
在实际生产环境中,推荐使用成熟的 crate 如 totp-rs:
[dependencies]
totp-rs = "^5.0"
#![allow(unused)]
fn main() {
use totp_rs::{Algorithm, TOTP, Secret};
fn totp_with_lib() {
// 创建 TOTP 实例
let totp = TOTP::new(
Algorithm::SHA1, // 哈希算法
6, // 位数
1, // skew(允许的时间偏差步数)
30, // 时间步长(秒)
Secret::Encoded("12345678901234567890".to_string())
.to_bytes()
.unwrap(),
).unwrap();
// 生成当前 TOTP 码
let code = totp.generate_current().unwrap();
println!("TOTP code: {}", code);
// 验证 TOTP 码
let valid = totp.check_current(&code).unwrap();
println!("Valid: {}", valid);
}
}
多因素认证(MFA)简介
多因素认证(Multi-Factor Authentication, MFA)通过组合多种认证因素来提高安全性。认证因素分为三类:
- 知识因素(Something you know):口令、PIN码、安全问题答案
- 持有因素(Something you have):手机、安全令牌、智能卡
- 固有因素(Something you are):指纹、面部识别、虹膜扫描
MFA 的安全增益可以用概率来理解。假设口令被破解的概率为 $p_1$,OTP 被截获的概率为 $p_2$,则 MFA 被攻破的概率约为 $p_1 \times p_2$,远低于单一因素。
常见的 MFA 方案:
- 口令 + 短信验证码
- 口令 + TOTP(Google Authenticator)
- 口令 + 硬件安全密钥(YubiKey、FIDO2)
- 口令 + 生物识别(指纹、面部)
趣味阅读:历史上的暗语和化名
口令和暗语不仅是计算机时代的产物,在人类历史上,暗语和化名一直扮演着重要角色。
暗语
鸡肋
河山统一
天王盖地虎,宝塔镇河妖天王
宫廷玉液酒,一百八一杯
识时务者为俊杰
风浪越大鱼越贵
龙门飞甲,便知真假
在中国近现代史上,暗语更是被广泛使用:
- 邱小姐:中国第一颗原子弹的代号
- 黄河:中国第一次核试验的代号
- 长江:中国氢弹试验的代号
这些暗语在特定的历史时期和特定的群体中发挥了保密通信的作用。
化名
李得胜
伍豪
军统六哥——郑耀先
军统店小二——许忠义
在革命战争年代,化名是保护身份安全的重要手段。毛泽东曾使用“李得胜“作为化名,周恩来使用“伍豪“(这也是“豪密“命名的由来)。在谍战领域,化名更是特工的基本操作。
拓展思考
常用的账号密码基本都是大小写英文字母、数字、特殊字符的组合,有没有可能使用简体/繁体中文或者其他字符呢?就好比图形验证码通常都使用数字、大小写英文字母,后来演变成12306选择同类图片、手动补齐图片拼图,依次点击指定的文字符号等方式?
这个问题实际上涉及字符编码和系统兼容性的深层问题。现代系统(如 UTF-8 编码的数据库和应用)理论上完全支持中文密码,但在实践中仍面临以下挑战:
- 输入法依赖:中文输入需要输入法,在某些设备或场景下可能不便
- 键盘布局:在非中文键盘上输入中文密码极为困难
- 兼容性:部分老旧系统可能不支持非 ASCII 字符的密码
- 长度计算:中文字符在 UTF-8 中占3个字节,可能影响密码长度限制的判断
不过,在 Rust 中处理 Unicode 密码是完全可行的,正如本章代码示例中的 ARG2on&@!略 就包含了中文字符。
参考链接
- PBKDF2
- How to encrypt user passwords 如何加密用户口令
- 全平台最佳密码管理工具大全 全平台最佳密码管理工具大全:支持 Windows、Linux、Mac、Android、iOS 以及企业应用
- RFC4226 HOTP: An HMAC-Based One-Time Password Algorithm
- RFC6238 TOTP: Time-Based One-Time Password Algorithm
- 一文搞懂 OTP 双因素认证
总结
| 主题 | 关键要点 | 推荐实践 |
|---|---|---|
| 口令威胁 | 暴力破解、字典攻击、彩虹表、钓鱼 | 多层防御,不依赖单一措施 |
| 口令构造 | 长度、复杂度、熵 | 12位以上,混合字符集,或使用密码短语 |
| 口令存储 | 哈希+盐值,绝不存明文 | Argon2id > bcrypt > scrypt > PBKDF2 |
| 口令管理 | 密码管理器、SSO | 使用专业密码管理器,启用MFA |
| 动态口令 | HOTP/TOTP | 30秒步长,6位数字,结合MFA使用 |
| 多因素认证 | 知识+持有+固有 | 至少启用双因素认证 |
练习题
-
安全分析:假设一个攻击者使用 GPU 集群每秒可以尝试 $10^{10}$ 次 SHA-256 哈希运算。对于一个8位纯数字密码(如生日),平均需要多长时间才能破解?如果是12位混合字符密码呢?
-
编程实践:使用 Rust 实现一个密码强度检测器,要求:
- 计算密码的信息熵
- 检测是否包含常见弱密码模式(如123456、password等)
- 检测是否包含用户名或常见单词
- 给出密码强度评分和改进建议
-
算法对比:为什么 Argon2 被认为是比 PBKDF2 更安全的密码哈希算法?请从内存硬度、抗 GPU/ASIC 攻击等角度分析。
-
TOTP 实现:基于本章的 TOTP 示例代码,实现一个完整的命令行 TOTP 认证器:
- 支持添加/删除密钥
- 支持显示当前所有 TOTP 码
- 支持密钥的 Base32 编码/解码
- 数据持久化存储(加密保存)
-
思考题:在密码管理器中,主密码的安全性至关重要。如果主密码被遗忘,用户将无法恢复所有存储的密码。请分析密码管理器的设计如何在安全性和可用性之间取得平衡?是否有更好的替代方案?
-
实战挑战:使用 Rust 实现一个简单的用户认证系统,要求:
- 用户注册:使用 Argon2id 哈希密码并存储
- 用户登录:验证密码并返回 JWT token
- 密码修改:验证旧密码后更新为新密码
- TOTP 二次验证:登录时要求输入 TOTP 码
- 使用 SQLite 存储用户数据