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;
}
💡 小提示:mbedTLS是嵌入式设备上很常用的加密库。上面的代码只是演示哈希的基本流程。实际项目中,千万别直接用SHA-256存密码——跑得太快,黑客一秒能试几亿次。

3.3 加盐哈希:给每个密码穿件“防弹衣”

光哈希就够了吗?不够。你想想看,如果两个用户都设了 123456,他们的哈希值是不是一模一样?这就是彩虹表攻击的突破口——黑客提前算好常见密码的哈希值,一比对就知道你密码是啥。

解决方案就是加盐(Salt)。盐是一个随机字符串,每个用户都不一样。存密码时,把盐和密码拼在一起再哈希。这样,就算两个用户密码相同,哈希值也完全不同。

3.3.1 加盐哈希的标准流程

  1. 注册时:
    • 生成一个随机盐(建议16字节以上)。
    • 把盐和密码拼接,做哈希。
    • 数据库里存:盐 + 哈希值。
  2. 登录时:
    • 从数据库取出该用户的盐。
    • 把用户输入的密码和盐拼接,做哈希。
    • 比对结果和数据库里的哈希值。
🔑 盐的工程要点:
  • 每个用户独立:全局一个盐等于没加。
  • 足够随机:用加密安全的随机数生成器(如 /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;
}
💡 注意:bcrypt的哈希结果自带盐和cost信息,你不需要单独存盐。验证时直接拿存好的哈希串和用户输入比对就行。这省了不少事——我早期自己拼盐和哈希,结果格式搞错,排查了半天。

3.4 加盐哈希的工程实践:嵌入式设备上的取舍

受限设备上做密码存储,有几个现实问题:

  • 算力有限:bcrypt的cost不能设太高,否则用户等得想砸设备。
  • 存储空间:哈希值和盐要占ROM/RAM,得精打细算。
  • 随机数源:有些MCU没有硬件TRNG,得靠软件熵池。

我建议的工程实践:

  1. 优先用bcrypt或Argon2i:库体积小,实现成熟。mbedTLS和WolfSSL都支持。
  2. cost值从8开始调:在目标硬件上实测,保证哈希时间在500ms~2s之间。
  3. 盐用硬件TRNG生成:没有的话,用ADC噪声+系统时钟抖动凑熵。
  4. 哈希结果存Flash时加CRC校验:防止Flash位翻转导致认证失败。
⚠️ 我曾经踩过的坑:
  • 盐重用:有个项目偷懒,所有用户用同一个盐。结果彩虹表一打一个准。后来全部重做。
  • cost设太高:在低端MCU上设了12,登录一次等8秒。用户投诉说“这锁反应比我还慢”。
  • 随机数不够随机:用 rand() 生成盐,结果每次重启后盐都一样。黑客直接预计算好了。

3.5 知识体系总览

下面这张图,把本章的核心逻辑串起来了。从明文存储到加盐哈希,每一步都在解决上一个方案的问题。

密码存储方案演进 明文存储 哈希存储 加盐哈希 风险 数据库被拖 = 密码全泄露 撞库、法律责任 风险 彩虹表攻击 相同密码相同哈希 注意 盐要随机、每个用户独立 cost值需实测调优 推荐算法:bcrypt / Argon2i / scrypt / PBKDF2 嵌入式工程要点 • 优先用bcrypt,库体积小,mbedTLS/WolfSSL都支持 • cost值从8开始调,目标硬件实测,保证500ms~2s • 盐用硬件TRNG生成,没有则用ADC噪声+时钟抖动 • 哈希结果存Flash时加CRC校验,防止位翻转

3.6 方案对比总结

方案 安全性 实现复杂度 性能 推荐场景
明文存储 ❌ 极低 ⭐ 极简 ⚡ 最快 ❌ 永远不要用
简单哈希(SHA-256) ⚠️ 低(抗彩虹表弱) ⭐⭐ 简单 ⚡ 快 ❌ 不推荐用于密码
加盐哈希(bcrypt) ✅ 高 ⭐⭐⭐ 中等 🐢 慢(可调) ✅ 嵌入式首选
加盐哈希(Argon2) ✅✅ 最高 ⭐⭐⭐⭐ 较高 🐢 慢(内存硬) ✅ 资源充足的设备

嗯,总结一下:明文存储是自杀,简单哈希是自欺欺人,加盐哈希才是正道。在受限设备上,我建议你从bcrypt入手,cost设10左右,配合硬件TRNG生成的盐。这套方案我用了好几年,没出过问题。记住,密码存储不是功能,是底线。底线破了,产品再炫也没用。