1. 项目概述:这套算法体系到底在解决什么问题
接手过几个带用户体系和数据交换的项目之后,我最大的感受是:密码学算法不是数学课上的抽象概念,而是每个开发者迟早要面对的工程选型。APP登录、Web后台、小程序、服务间接口通信,绕不开“身份认证”“数据传输加密”“完整性校验”“密码存储”这四件事。处理这些事用的,就是对称加密、非对称加密、哈希函数、消息认证码这几类最常见的基础算法。这篇文章不堆数学公式,直接讲清楚每个算法能干什么、不能干什么、怎么选、怎么落地,适合刚接触信息安全的后端和客户端开发,也适合所有被“到底该选哪个加密算法”困扰的人。
1.1 加密算法不是黑魔法,是工程选型问题
很多人一提RSA、AES就发怵,其实我们不需要把椭圆曲线方程式背下来,关键是搞清楚每个算法服务于什么安全目标。我做个粗浅类比:对称加密像一把钥匙开一把锁,加解密速度快,但钥匙怎么安全送到对方手里是个问题;非对称加密像带公共投信口的邮箱,公钥是公开的投信口,私钥是只有你有的取信钥匙,解决了钥匙分发难题,但运算速度慢;哈希函数像给数据取指纹,任何人都能对同样内容算出同样的指纹,但无法从指纹反推内容,所以非常适合做完整性校验和密码存储。
选算法的本质是选安全目标和性能权衡。要机密性,选对称加密;要解决密钥分发,选非对称加密;要校验数据有没有被改过,选哈希;要确认消息确实来自某个持有密钥的对方,选MAC。实际项目从来不是单用一个算法,而是把它们组合成一个方案。比如HTTPS握手过程,先通过非对称或DH类算法协商出临时对称密钥,再用AES加密真正的业务数据,同时用SHA系列做哈希校验,整个流程就是一个标准的算法组合样板。
1.2 四类算法的整体框架
先给一个全局视角,后面每一类单独展开。四类算法的核心区别在于“是否使用同一个密钥”和“是否可逆”。
| 算法类别 | 代表算法 | 核心用途 | 主要特点 |
|---|---|---|---|
| 对称加密 | AES、SM4 | 数据机密性 | 速度快,密钥管理难 |
| 非对称加密 | RSA、ECC | 密钥分发、数字签名 | 速度慢,安全性高 |
| 哈希函数 | SHA-256、SM3 | 完整性校验 | 单向不可逆 |
| 消息认证码 | HMAC | 身份认证、防篡改 | 哈希函数 + 共享密钥 |
这个表格建议直接存下来,做技术方案评审时特别好用。很多人一开始搞混“加密”和“哈希”,以为哈希也是一种加密,其实完全两码事:加密可以解密还原,哈希的天然设计就是不可还原。理解了这张表,后面的细节就只是往框架里填肉。
2. 对称加密:速度快,但模式和密钥管理是两道坎
对称加密是所有加密算法里效率最高的,适合加密大数据量。它的核心难点不在算法本身,而在“模式怎么选”和“密钥怎么管”。下面从AES展开,再说说合规场景常见的SM4。
2.1 AES 的基本规格和适用场景
AES是当前最主流的对称加密标准,分组长度固定128位,密钥长度支持128、192、256位。密钥越长越抗暴力破解,但性能开销也越大。普通业务场景用AES-128足够,安全性要求高的系统直接上AES-256也常见。我自己的习惯是:没有特殊合规要求就用AES-128-GCM,既有足够安全强度,性能也不会拖后腿。
AES的分组加密特性决定了它需要“工作模式”来处理超过16字节的数据。这就像你用一块固定大小的模具去压饼干,一次压一块,多出来的要换个方式继续压。模式选错,再强的算法也会被攻破,所以模式选择比算法本身更需要上心。
2.2 ECB、CBC、GCM 三种模式对比
ECB模式是最原始的做法:每个明文块独立加密,相同的明文块永远产生相同的密文块。听起来没问题,但实际后果很严重——我见过加密后的图片数据在ECB模式下还能隐约看出原始图案轮廓,因为像素颜色相同的区域密文也相同。这个模式没有任何语义安全性,新项目绝对不要用。
CBC模式做了改进:每个明文块先与上一个密文块做异或再加密,这样相同的明文块在不同位置会产生不同密文。但它有两个要求:第一个块需要IV(初始化向量),IV必须随机且每次加密都不能重复;另外需要PKCS7之类的填充方案,因为明文长度不一定刚好是分组的整数倍。CBC只保证机密性,不保证完整性,如果直接用CBC加密网络数据,攻击者可能通过翻转密文位来篡改明文,这是经典的比特翻转攻击。
GCM模式是目前最推荐的。它是认证加密模式,一次加密操作同时输出密文和认证标签(Auth Tag),解密时会校验标签,数据被篡改会直接报错。安全性、性能、易用性都优于CBC,很多现代协议和库的默认推荐就是它。我在自己项目里一律AES-GCM,省心也安全。
注意:GCM模式对nonce(随机数)要求极高,同一个密钥下nonce绝对不能重复。重复一次,攻击者就能恢复出加密密钥。所以实际项目中,nonce要么用安全的随机数生成器,要么用计数器严格递增,绝不能拍脑袋写个时间戳或固定值。
2.3 国密 SM4 在合规场景中的应用
国内政务、金融行业有合规要求时,经常会遇到SM4。SM4分组长度固定128位,密钥长度128位,设计思路和AES类似。从工程实现上看,SM4的加解密API、模式选择、填充方式都和AES保持一致,只是算法替换。所以如果你已经把AES的代码写好了,切SM4的成本并不高。
不过提醒一句:SM4本身只是算法,合规的关键是整个密码体系都走国密,包括SM2、SM3、SM4组合使用,以及算法实现是否经过检测认证。个人开发者大部分场景用AES就够了,涉及政务类项目再按对方具体要求来。别一听“国密”就觉得高大上,它本质就是一组标准和规范,实现起来没那么玄。
3. 非对称加密:慢,但解决了密钥分发难题
对称加密最大的痛点是把密钥安全地发给对方。非对称加密用一对钥匙解决这个问题:公钥公开,私钥私有。别人用你的公钥加密数据,只有你的私钥能解开;你用私钥签名,别人用公钥验证。
3.1 RSA 的参数选择与局限性
RSA的安全性基于大整数分解的困难程度。密钥长度低于1024位已经确认不安全,2048位是目前最低标准,对安全性敏感的系统建议上4096位。我见过不少老项目还在用1024位RSA,只能说赶紧迁移,否则一旦密钥被分解,所有加密数据都等于裸奔。
RSA有两个经常被忽略的工程限制。第一,加密长度受密钥长度限制,2048位密钥配合OAEP填充,一次最多只能加密约190字节明文,所以不能用它直接加密大文件或大报文;第二,解密运算非常慢,大量数据逐一用RSA加密会当场卡死。正确用法是“混合加密”:随机生成一个AES密钥,用RSA加密这个短密钥,真实数据用AES处理。这样既解决密钥分发,又保留了对称加密的速度。
填充方案同样有讲究。RSA加密应用OAEP填充,不要用PKCS1v15;签名应用PSS填充,不要用裸的私钥运算。这些细节在安全审查里都会被揪出来,标准库默认参数往往就是安全推荐值,不要轻易改动。
3.2 ECC 与 Ed25519 在现代签名中的地位
ECC(椭圆曲线加密)在相同安全强度下,密钥长度远小于RSA。256位ECC约等于3072位RSA的安全强度,而且运算效率更高。过去几年ECC在证书、签名、密钥交换中被广泛采用,主流库对它的支持也非常成熟。
签名领域我最推荐的是Ed25519(属于EdDSA算法族)。它的优势很实际:密钥和签名都很短,验证速度快,实现上天然抵抗一系列侧信道攻击,而且生成签名过程中几乎不会踩到老式ECDSA那些“随机数重复导致私钥泄露”的坑。我自己做API签名、软件更新包签名,首选就是Ed25519,代码少、出错概率低。
我遇到不少同学问“RSA是不是要被淘汰了”,其实不是淘汰,而是场景分化:老系统兼容、加密少量数据、证书体系里RSA仍然大量存在;新系统做签名、做密钥交换时,ECC和Ed25519的优势更明显。两者不是对立关系,而是看具体场景选型。
3.3 混合加密是实际项目的主流形态
混合加密不是某种算法,而是一种组合策略:随机生成对称密钥,用非对称算法加密这个对称密钥,再用对称算法加密业务数据。你打开任何一个现代加密通信协议,内部基本都是这个套路。HTTPS的TLS握手就是典型:客户端和服务端通过椭圆曲线或RSA协商出会话密钥,后续大量数据全部走AES。
为什么头疼?因为真实业务数据可能几十兆、几百兆,非对称加密搞不定,而对称密钥本身只有16到32字节,用非对称加密毫无压力。所以“非对称管密钥分发,对称管数据加密”这句话,值得每个开发者牢牢记住。做项目时如果有人问“怎么用RSA加密用户上传的文件”,答案不是硬上RSA,而是先把这个混合思路讲明白。
4. 哈希与MAC:保护完整性,守住密码底线
哈希算法是四类算法里最容易被低估的。很多系统出安全问题,不是在加解密环节,而是在密码存储和完整性校验上用错了哈希。这一部分必须认真对待。
4.1 哈希函数的特性与常见算法状态
哈希函数有三个关键特性:确定性(同一输入永远同一输出)、单向性(从哈希值无法反推原文)、雪崩效应(输入改一个字节,输出面目全非)。这三个特性让它天然适合做“是否被篡改”的校验:比对两个哈希值是否一致,就能知道数据有没有被动过。
常见的哈希算法现状:
| 算法 | 输出长度 | 安全状态 | 使用建议 |
|---|---|---|---|
| MD5 | 128位 | 已破解 | 禁用 |
| SHA-1 | 160位 | 已存在碰撞攻击 | 禁用 |
| SHA-256 | 256位 | 安全 | 推荐 |
| SHA-512 | 512位 | 安全 | 推荐 |
| SM3 | 256位 | 安全 | 合规场景推荐 |
MD5和SHA-1在新项目里一律不要用。有人会说“MD5做文件校验、做缓存键应该没事吧”,做非安全用途可以,但你自己要明确这个边界:只要带一点防篡改、抗碰撞需求,就必须上SHA-256以上。我见过某项目用MD5算文件的完整性校验值,攻击者完全可以构造出相同MD5值的恶意文件,校验形同虚设。
4.2 密码存储的正确姿势
这是全网程序员的经典重灾区。很多系统把用户密码直接做一次MD5或SHA-256就存库,一旦数据库泄露,攻击者用彩虹表或字典可以批量还原出密码。正确做法是“加盐 + 慢哈希”,让每次密码哈希都带上随机盐值,并且让计算过程故意变慢,提高暴力破解成本。
目前推荐顺序是Argon2优先,其次bcrypt或scrypt。如果标准库条件受限,Python的hashlib也提供PBKDF2,虽然不是最优但比裸哈希强太多。核心原则有三个:每个用户的盐必须随机且不同;盐和哈希值一起存库;验证时重新计算再比较,永远不要自己写字符串比较来判断哈希相等,要用库提供的恒时比较函数,避免时序攻击。
4.3 HMAC:给消息盖一个有密钥的“印章”
HMAC是“带密钥的哈希”,在SHA-256等哈希函数基础上引入共享密钥。除了能验证数据完整性,还能确认消息确实来自持有密钥的对方。打个比方:普通哈希是贴一张任何人都能伪造的封条,HMAC是贴一张只有你和对方才有印章的封条。
HMAC在项目里最常见的三个场景:Webhook回调验签、服务间API签名、JWT签名。每次回调数据过来,服务端用共享密钥重新计算HMAC再比对,匹配就说明数据没被改且来源可信。这里有个实操细节:比较HMAC结果必须用恒时比较函数(如hmac.compare_digest),用普通的“==”比较会引入时序侧信道,可能被攻击者逐位猜出正确值。
5. 实操:把前面讲的算法落到代码里
理论讲完了,上点能直接跑的东西。我用Python环境演示AES-GCM、RSA混合加密、密码哈希这几个最常用的场景,所有代码都可以直接抄去改。
5.1 环境准备与库选型
Python生态里涉及密码学有三套常用方案:hashlib是标准库,只有哈希和HMAC;cryptography库功能最全,AES、RSA、ECC、签名、KDF都覆盖;pycryptodome也常用但API风格偏老。我推荐cryptography,安装一行命令,接口现代,默认参数也是安全推荐值。
pip install cryptography如果只是算哈希,用内置hashlib就行,不需要额外依赖。后面示例里涉及加解密的一律用cryptography,涉及哈希的一律用hashlib。
5.2 AES-GCM 加解密示例
这是日常最核心的一段代码。用cryptography的AESGCM接口,一次调用同时完成加密和认证。
import os from cryptography.hazmat.primitives.ciphers.aead import AESGCM # 生成128位密钥,生产环境放在环境变量或密钥管理服务,不要写死在代码里 key = AESGCM.generate_key(bit_length=128) aesgcm = AESGCM(key) # nonce必须随机且每次唯一,长度12字节是推荐值 nonce = os.urandom(12) message = b"这是一段机密业务数据" aad = b"channel:order" # 可选关联数据,会参与认证但不加密 ciphertext = aesgcm.encrypt(nonce, message, aad) print("密文:", ciphertext.hex()) # 解密时如果数据被篡改,会直接抛异常 plaintext = aesgcm.decrypt(nonce, ciphertext, aad) print("解密:", plaintext.decode("utf-8"))这段代码有几个点需要解释清楚。nonce是12字节的随机数,每次加密都必须换新的;AAD关联数据不会加密,但会参与认证标签计算,适合放消息头、路由信息这类需要被看到又不许被改的内容。加密结果里最好把nonce和密文拼在一起存储或传输,否则解密时找不到nonce。
5.3 RSA 加密与签名示例
RSA一般用来加密AES密钥或小数据。下面演示加密一段短消息,以及生成签名。
from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, PrivateFormat # 生成2048位RSA密钥对 private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) public_key = private_key.public_key() # RSA加密:OAEP填充 aes_key = os.urandom(16) # 要传输的对称密钥 ciphertext = public_key.encrypt( aes_key, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) ) plaintext = private_key.decrypt(ciphertext, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None )) print("还原AES密钥:", plaintext.hex() == aes_key.hex()) # 签名:PSS填充 signature = private_key.sign( b"待签名的数据", padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256() ) public_key.verify( signature, b"待签名的数据", padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256() ) print("验签通过")公钥指数65537是标准推荐值,没有特殊理由不要改。注意RSA加密最多只能加密密钥长度除以8再减去填充长度,2048位+OAEP大约190字节,所以务必只用于短数据。签名和加密用的填充不能混,加密用OAEP,签名用PSS。
5.4 密码哈希与校验示例
密码存储用慢哈希。标准库能直接跑的是PBKDF2,生产环境建议进一步换Argon2或bcrypt库。
import hashlib import secrets password = "用户输入的明文密码".encode("utf-8") # 每个用户一个随机盐,16字节起步 salt = secrets.token_bytes(16) # PBKDF2-HMAC-SHA256迭代10万次,次数根据硬件性能调整 digest = hashlib.pbkdf2_hmac("sha256", password, salt, 100_000) stored = salt + digest # 盐和哈希一起存库 # 验证时重新计算再恒时比较 salt = stored[:16] re_digest = hashlib.pbkdf2_hmac("sha256", password, salt, 100_000) print("密码校验通过" if secrets.compare_digest(re_digest, stored[16:]) else "密码错误")PBKDF2的迭代次数不是随便设的。基准目标是让一次验证大约耗时100毫秒左右,这样用户感知不明显,但攻击者离线爆破海量口令的成本暴涨。secrets模块是Python安全随机数的正确打开方式,random模块的伪随机不能用于生成盐值和密钥。
6. 常见问题与排查技巧实录
代码跑通只是第一步,实战里真正耗时间的往往是那些“看着没问题但就是不对”的毛病。这部分整理我的排查记录。
6.1 高频问题速查表
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 解密报错或乱码 | 字符串和bytes混用、编码不一致 | 统一编码,传输用base64 |
| 用ECB模式加密图片后能看出轮廓 | 模式本身不安全 | 换成GCM或CBC |
| RSA加密大文件直接报错 | 明文超出填充上限 | 改用混合加密,RSA只加密AES密钥 |
| GCM解密偶尔报错 | nonce重复使用或AAD不一致 | nonce每次随机,AAD参数保持一致 |
| 密码库被脱库后大量账号被破解 | 裸哈希存储、无盐、弱口令 | 慢哈希+随机盐+强制复杂度 |
| 两段代码算出来的哈希不一致 | 明文编码方式不同 | 加密前统一encode("utf-8") |
第二行问题是我在真实项目里遇到过的,当时同事拿AES-ECB加密一批PDF文档,结果密文文件还能看出原文档的版面结构,就是因为ECB模式下相同排版区域产生相同密文块。这事情不发生在文字数据上不明显,一旦换成图像、结构化数据就原形毕露。
6.2 我踩过的坑和排查思路
第一个坑是IV和nonce的复用。早期我图省事,在一个服务实例里把nonce设为固定值,结果线上同时有上千个客户端通信,全部用同一个nonce,这等于把AES-GCM的防线直接拆掉。后来排查才发现,nonce复用后同一个密钥加密不同明文,攻击者可以做位异或操作还原明文关系。正确做法是每加密一次生成一次随机nonce,或者维护一个持久化计数器严格递增。
第二个坑是密钥硬编码。曾经某个项目把AES密钥写在代码常量里,代码库权限稍微放开就全泄露了,而且到了要轮换密钥的时候,才发现所有历史数据都是用旧密钥加密的,迁移工程量巨大。现在我的习惯是:代码里只留密钥引用,真实值放环境变量或专用的密钥管理服务,并且建立定期轮换机制。密钥轮换时要考虑老数据解密,所以设计数据格式时必须带上密钥版本号。
第三个坑与算法版本有关。有段时间项目同时混用了pycryptodome和cryptography两个库,两边的AES默认参数不同,导致加解密互相不认。排查很久才发现是库之间填充逻辑不一致。所以我现在的原则是:一个项目统一一个密码学库,不要在业务代码里混用多家实现。
7. 选型建议与一点个人心得
最后说点我在实际操作中的体会。密码学算法的选择,往往不是“哪个更强”的问题,而是“哪个更合适”。你做一个内部管理系统,AES-128-GCM搭配RSA-2048做密钥分发已经足够;你做一个面向大量用户的支付或社交应用,就要认真考虑Ed25519签名、Argon2密码哈希、完整的密钥轮换机制。这个选型表可以帮你快速起步:
- 数据传输加密:AES-256-GCM,密钥协商用ECDH或TLS
- 接口签名与验签:Ed25519,老系统兼容时用RSA-PSS
- 密码存储:Argon2或bcrypt,盐随机且独立
- 消息防篡改:HMAC-SHA256,比较用恒时函数
- 合规要求(政务/金融):SM4 + SM2 + SM3组合
再分享一个小技巧:把算法选型整理成团队文档里的“密码学决策表”,每个场景写清楚使用什么算法、什么参数、最小密钥长度、禁止选项。新人接手后直接照着表做,比翻遍代码猜方案高效得多。我自己的项目里一直维护着这样一张表,每次评审代码就拿出来对着检查,踩坑概率直线下降。
别自己发明算法,也别觉得自己写的加解密比标准库更强。密码学最大的坑从来不是算法太难,而是以为自己会了。老老实实用成熟标准、成熟库、成熟参数,把精力放到密钥管理和方案组合上,你的系统就已经超过绝大多数同类项目了。