3、密码存储方案:明文存储的风险、哈希存储的实现、加盐哈希的工程实践
密码存储这事儿,说简单也简单,说复杂……嗯,坑真的不少。我见过太多项目,上线第一天就被脱裤,原因就是密码存得太“实在”。今天咱们就把这块掰开揉碎,从最危险的明文存储,一路聊到工业级的加盐哈希方案。
3.1 明文存储:你这是在给黑客送大礼
明文存储,就是把用户密码原封不动写进数据库。比如用户设了 123456,数据库里就存个 123456。你想想看,这跟把家门钥匙挂在门外有什么区别?
- 一锅端:数据库被拖,所有用户密码直接暴露。黑客连破解都不用。
- 撞库风险:很多人一个密码走天下。你这边泄露了,人家支付宝、可能也跟着遭殃。
- 法律责任:GDPR、《个人信息保护法》都明文要求密码必须加密存储。明文存储被查到,罚款能让你公司直接关门。
我在早期一个嵌入式项目里就见过这种事。那是个智能门锁的后台,密码直接明文存SQLite。我当时就问他:“你这锁是防贼还是防自己?”后来花了两个通宵帮他们重构。说白了,2025年的今天,明文存储已经不是一个技术选择,而是一个态度问题——你压根没把用户安全当回事。
3.2 哈希存储:单向函数的基本功
哈希存储,核心思路就是:存密码的“指纹”,不存密码本身。用户登录时,把输入的密码再做一次哈希,跟数据库里的指纹比对。指纹匹配,就放行。
哈希函数有几个关键特性:
- 单向性:从哈希值几乎不可能反推出原始密码。
- 抗碰撞性:不同输入很难产生相同输出。
- 雪崩效应:输入改一丁点,输出天翻地覆。
但这里有个坑——不是所有哈希函数都适合存密码。MD5、SHA-1、甚至普通的SHA-256,都不行。为什么?因为它们设计出来是为了校验数据完整性,跑得飞快。而密码哈希恰恰需要慢,慢到暴力破解的成本高到黑客放弃。
- bcrypt:嵌入式设备上比较推荐,可调工作因子(cost factor)。
- PBKDF2:标准成熟,但抗GPU攻击稍弱。
- scrypt:内存硬,对ASIC/GPU不友好。
- Argon2:2015年密码哈希竞赛冠军,目前最推荐。
我个人习惯在受限设备上用 bcrypt。它实现简单,库到处都有,而且工作因子调高一点,就能在安全性和性能之间找到平衡。我曾经在一个Cortex-M4的板子上调bcrypt的cost,从4调到10,哈希时间从50ms涨到2秒——嗯,2秒对用户来说还能忍,但对暴力破解来说,成本直接翻了2^6=64倍。
3.2.1 代码示例:C语言下的SHA-256哈希(仅演示,不推荐用于密码)
#include <stdio.h>
#include <string.h>
#include "mbedtls/sha256.h"
void hash_password(const char *password, unsigned char *output) {
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts(&ctx, 0); // 0 = SHA-256
mbedtls_sha256_update(&ctx, (unsigned char*)password, strlen(password));
mbedtls_sha256_finish(&ctx, output);
mbedtls_sha256_free(&ctx);
}
int main() {
unsigned char hash[32];
hash_password("my_secret_pass", hash);
// 输出hex格式
for(int i = 0; i < 32; i++) printf("%02x", hash[i]);
return 0;
}
3.3 加盐哈希:给每个密码穿件“防弹衣”
光哈希就够了吗?不够。你想想看,如果两个用户都设了 123456,他们的哈希值是不是一模一样?这就是彩虹表攻击的突破口——黑客提前算好常见密码的哈希值,一比对就知道你密码是啥。
解决方案就是加盐(Salt)。盐是一个随机字符串,每个用户都不一样。存密码时,把盐和密码拼在一起再哈希。这样,就算两个用户密码相同,哈希值也完全不同。
3.3.1 加盐哈希的标准流程
- 注册时:
- 生成一个随机盐(建议16字节以上)。
- 把盐和密码拼接,做哈希。
- 数据库里存:
盐 + 哈希值。
- 登录时:
- 从数据库取出该用户的盐。
- 把用户输入的密码和盐拼接,做哈希。
- 比对结果和数据库里的哈希值。
- 每个用户独立:全局一个盐等于没加。
- 足够随机:用加密安全的随机数生成器(如
/dev/urandom或硬件TRNG)。 - 长度至少16字节:128位,彩虹表基本不可能预计算。
- 盐不需要保密:它和哈希值一起存,黑客拿到也没事——只要盐够随机,他就没法预计算。
3.3.2 代码示例:bcrypt加盐哈希(推荐方案)
#include <stdio.h>
#include <bcrypt.h>
int main() {
const char *password = "user_password_123";
char hash[BCRYPT_HASHSIZE];
// 生成哈希(内部自动生成盐)
if (bcrypt_newhash(password, 10, hash, BCRYPT_HASHSIZE) != 0) {
fprintf(stderr, "bcrypt hash failed\n");
return -1;
}
printf("Hash: %s\n", hash);
// 输出示例: $2b$10$...(包含了算法标识、cost、盐和哈希)
// 验证密码
const char *input = "user_password_123";
if (bcrypt_checkpass(input, hash) == 0) {
printf("密码正确\n");
} else {
printf("密码错误\n");
}
return 0;
}
3.4 加盐哈希的工程实践:嵌入式设备上的取舍
受限设备上做密码存储,有几个现实问题:
- 算力有限:bcrypt的cost不能设太高,否则用户等得想砸设备。
- 存储空间:哈希值和盐要占ROM/RAM,得精打细算。
- 随机数源:有些MCU没有硬件TRNG,得靠软件熵池。
我建议的工程实践:
- 优先用bcrypt或Argon2i:库体积小,实现成熟。mbedTLS和WolfSSL都支持。
- cost值从8开始调:在目标硬件上实测,保证哈希时间在500ms~2s之间。
- 盐用硬件TRNG生成:没有的话,用ADC噪声+系统时钟抖动凑熵。
- 哈希结果存Flash时加CRC校验:防止Flash位翻转导致认证失败。
- 盐重用:有个项目偷懒,所有用户用同一个盐。结果彩虹表一打一个准。后来全部重做。
- cost设太高:在低端MCU上设了12,登录一次等8秒。用户投诉说“这锁反应比我还慢”。
- 随机数不够随机:用
rand()生成盐,结果每次重启后盐都一样。黑客直接预计算好了。
3.5 知识体系总览
下面这张图,把本章的核心逻辑串起来了。从明文存储到加盐哈希,每一步都在解决上一个方案的问题。
3.6 方案对比总结
| 方案 | 安全性 | 实现复杂度 | 性能 | 推荐场景 |
|---|---|---|---|---|
| 明文存储 | ❌ 极低 | ⭐ 极简 | ⚡ 最快 | ❌ 永远不要用 |
| 简单哈希(SHA-256) | ⚠️ 低(抗彩虹表弱) | ⭐⭐ 简单 | ⚡ 快 | ❌ 不推荐用于密码 |
| 加盐哈希(bcrypt) | ✅ 高 | ⭐⭐⭐ 中等 | 🐢 慢(可调) | ✅ 嵌入式首选 |
| 加盐哈希(Argon2) | ✅✅ 最高 | ⭐⭐⭐⭐ 较高 | 🐢 慢(内存硬) | ✅ 资源充足的设备 |
嗯,总结一下:明文存储是自杀,简单哈希是自欺欺人,加盐哈希才是正道。在受限设备上,我建议你从bcrypt入手,cost设10左右,配合硬件TRNG生成的盐。这套方案我用了好几年,没出过问题。记住,密码存储不是功能,是底线。底线破了,产品再炫也没用。