加密

2024-12-05T14:21:02+08:00 | 4分钟阅读 | 更新于 2024-12-05T14:21:02+08:00

@

学习目标

类比(先建立直觉):给密码"加密"存储,就像把钥匙锁进一个"越撬越费劲"的保险箱。坏人偷到保险箱(数据库泄露),也得花极大代价才能逐个试出钥匙——这正是"慢哈希"的设计哲学:故意让每次验证都又慢又费内存,拖垮暴力破解。

学完本章你应该能够:

  1. 说清为什么"密码不能用普通哈希(如 MD5/SHA)或对称加密存储",而要用 Argon2id / bcrypt 这类慢哈希 + 加盐算法。
  2. golang.org/x/crypto/argon2 实现 HashVerify,理解 salt、参数(memory / iterations / parallelism)与编码格式的含义。
  3. 在验证时用 crypto/subtle.ConstantTimeCompare 做恒定时间比较,避免计时侧信道。
  4. 讲清 bcrypt 仍是可接受的备选场景(兼容存量数据、内存极度受限),以及它和 Argon2id 的取舍。

前置知识

  • Go 基础:crypto/randencoding/base64strings
  • 哈希与盐(salt)的基本概念
  • golang.org/x/crypto 包的使用

本章你会动手做的事

  1. 跑一遍 Hash("my_password"),观察每次输出的 salt 都不同、但都能 Verify 通过。
  2. 故意把 iterations / memory 调大,测一下 Hash 耗时变化,理解"慢"的意义。
  3. 用 bcrypt 生成一个哈希并 CompareHashAndPassword 校验,对比两者 API 风格。

这张图在讲什么:注册 / 改密时做"慢哈希 + 随机盐"得到自描述编码串存库;登录验证时从库中取出的编码串解析出参数与盐,对输入重算后用恒定时间比较。

flowchart TD
    P[用户密码] -->|随机 salt| H[Argon2id 慢哈希
memory/iter/parallel] H --> E[编码字符串
$argon2id$v=19$m=...$salt$hash] E --> DB[(存储到数据库)] L[登录时输入] --> V[Verify: 解析参数+重算] DB --> V V -->|恒定时间比较| R{匹配?}

推荐实现:Argon2id

使用官方维护的 golang.org/x/crypto/argon2 包:

package password

import (
    "crypto/rand"
    "crypto/subtle"
    "encoding/base64"
    "errors"
    "fmt"
    "strings"

    "golang.org/x/crypto/argon2"
)

// OWASP 2023 推荐参数(根据服务器性能调整)
const (
    memory      = 64 * 1024 // 64 MB
    iterations  = 3
    parallelism = 2
    saltLength  = 16
    keyLength   = 32
)

func Hash(password string) (string, error) {
    salt := make([]byte, saltLength)
    if _, err := rand.Read(salt); err != nil {
        return "", fmt.Errorf("generate salt: %w", err)
    }

    hash := argon2.IDKey([]byte(password), salt, iterations, memory, parallelism, keyLength)

    // 格式: $argon2id$v=19$m=65536,t=3,p=2$<salt>$<hash>
    encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
        argon2.Version, memory, iterations, parallelism,
        base64.RawStdEncoding.EncodeToString(salt),
        base64.RawStdEncoding.EncodeToString(hash),
    )
    return encoded, nil
}

func Verify(password, encodedHash string) (bool, error) {
    // 解析编码字符串提取参数和 salt/hash
    parts := strings.Split(encodedHash, "$")
    if len(parts) != 6 {
        return false, errors.New("invalid hash format")
    }

    var version int
    var memory uint32
    var iterations uint32
    var parallelism uint8
    fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &iterations, &parallelism)
    fmt.Sscanf(parts[2], "v=%d", &version)

    salt, _ := base64.RawStdEncoding.DecodeString(parts[4])
    expectedHash, _ := base64.RawStdEncoding.DecodeString(parts[5])

    computed := argon2.IDKey([]byte(password), salt, iterations, memory, parallelism, uint32(len(expectedHash)))

    return subtle.ConstantTimeCompare(computed, expectedHash) == 1, nil
}

⚠️ 新手必踩的坑:自己实现"快哈希"存密码。MD5 / SHA-256 计算极快,一旦数据库泄露,攻击者用彩虹表或 GPU 集群几秒就能反查出大量明文密码。密码存储必须用 Argon2id / bcrypt 这类"慢哈希 + 每用户随机盐"的算法,并且盐要随机生成、随密文一起存储,而不是写死在代码里。

bcrypt

如果你的项目需要兼容已有 bcrypt 数据、或部署环境内存极度受限(< 64MB),bcrypt 仍然是可接受的选择:

import "golang.org/x/crypto/bcrypt"

hash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) // cost=10
ok := bcrypt.CompareHashAndPassword(hash, []byte(password)) == nil

⚠️ 新手必踩的坑:bcrypt 对密码长度敏感。它只处理前 72 字节,超长部分会被静默截断;同时 cost 越大越安全但也越慢,生产环境一般 cost=10~12,别为了"更安全"无脑拉到很大导致登录接口被拖垮。


自测题与动手练习

自测题(合上书能答出来,才算懂)

  1. 为什么不能用 MD5 / SHA-256 直接存密码?加盐(salt)解决了什么问题?
  2. Argon2id 的 memory / iterations / parallelism 三个参数分别在"拖慢破解"上起什么作用?
  3. 为什么 Verify 要用 subtle.ConstantTimeCompare 而不是普通的 ==?普通比较可能泄露什么?
  4. 编码字符串 $argon2id$v=19$m=65536,t=3,p=2$<salt>$<hash> 里每一段分别代表什么?为什么连参数一起存?
  5. 什么场景下 bcrypt 仍是可接受的选择?它相比 Argon2id 的主要短板是什么?

动手练习(建议真做一遍)

  1. 把 OWASP 推荐参数里的 memory 调到 256MB、iterations 调到 4,测一次 Hash 耗时,直观感受"慢哈希"的成本。
  2. 写一个小脚本,批量生成 1000 个相同密码的哈希,确认 salt 随机、密文各不相同(防彩虹表)。
  3. 用 bcrypt 生成一个哈希,再用一个错误密码 CompareHashAndPassword,确认返回错误而非 panic。

本章小结

  • 别用普通哈希:MD5 / SHA 太快且无盐,一旦库泄露极易被彩虹表 / 暴力破解还原。
  • 用慢哈希 + 盐:Argon2id 是 OWASP 首推,靠内存 + 迭代 + 并行三维度拉高破解成本;salt 随机生成、随密文一起存储。
  • 编码即自描述$argon2id$...$salt$hash 把算法版本与参数都打包进字符串,Verify 时解析后即可复算,免维护额外配置。
  • 验证要恒定时间subtle.ConstantTimeCompare 避免计时侧信道,不让攻击者从耗时差异猜出前缀正确性。
  • bcrypt 仍可用:在需要兼容存量数据或内存 < 64MB 的受限环境,bcrypt(cost=10)是合理备选,但抗 GPU/ASIC 能力弱于 Argon2id。

密码存储是安全底座的第一环;下一章我们把这种"防住坏人"的思路延伸到传输层(mTLS)与配置层(Vault)。

About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!