MD5 / SHA / HMAC / AES / RSA
分类:摘要哈希、消息认证码、对称加密、非对称加密 开发环境:Objective‑C / CommonCrypto
一、MD5(哈希摘要算法)
MD5 属于单向哈希摘要算法,输入任意长度数据,输出固定128bit,通常格式化为32位十六进制字符串(0‑9,a‑z)。
- ✅ 优点:运算速度快;单向不可逆,无法从摘要还原原始数据
- ❌ 缺点:存在公开哈希碰撞漏洞;网上彩虹表网站(如 cmd5.com)可对简单原文反向查询;不建议新项目用于安全场景
// 错误写法:只适合普通UTF‑8文本,不能处理二进制(图片/视频)、不能处理内嵌\0的字符串
const char *utfstring = self.UTF8String;
uint8_t buffer[CC_MD5_DIGEST_LENGTH];
CC_MD5(utfstring, strlen(utfstring), buffer);
问题根源:
strlen依靠\0字节判断结尾;二进制图片、文件内部大量存在0x00,会直接截断计算。
// 字符串安全获取字节长度(避免内嵌\0截断)
NSUInteger byteLen = [self lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
// 二进制文件/图片统一使用 NSData,使用 .length 获取真实字节数,不受0x00影响
NSData *fileData = ...;
CC_MD5(fileData.bytes, (CC_LONG)fileData.length, buffer);
MD5 加盐(简单拼接加盐,不推荐用于密码,仅历史方案)
逻辑:md5(原始数据 + 固定盐值),增加彩虹表破解难度。
缺点:盐是全局固定字符串;盐一旦泄露,全部摘要失效;不能抵御碰撞攻击;密码场景优先使用 bcrypt / Argon2,而不是简单MD5加盐。
示例:密码=123456,盐=abbi12390‑0‑zs_0‑a9d*87123jaj89sd
md5("123456abbi12390‑0‑zs_0‑a9d*87123jaj89sd")
二、SHA 系列哈希摘要算法
SHA 是一类安全哈希函数,同样是单向摘要,不可解密还原原文。
| 算法 | 状态 | 输出 | 说明 |
|---|---|---|---|
| SHA‑1 | 废弃 | 160bit(40位16进制) | 已出现公开碰撞,仅兼容老系统 |
| SHA‑256 | ✅推荐 | 256bit(64位16进制) | 当前业务首选 |
| SHA‑512 | ✅推荐 | 512bit(128位16进制) | 更高安全等级 |
注意:SHA 和 MD5 一样:普通哈希只能校验数据完整性(有没有被改),不能校验是谁发送的数据,任何人都可以重新计算摘要伪造。
三、HMAC 基于哈希的消息认证码(RFC2104)
带密钥的哈希摘要,不是简单的
hash(key+data),内部有 ipad/opad 双哈希构造。 能力:✅完整性校验 + ✅身份鉴权;只有持有相同密钥的一方,才能算出合法tag。 属于对称体系:加密校验双方共用密钥。
#import <CommonCrypto/CommonHMAC.h>
// data:原始二进制(图片/请求报文,支持内部大量0x00)
// key:密钥 NSData,不要用字符串直接转,建议密码学安全随机生成
uint8_t hmacResult[CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256,
key.bytes, key.length,
data.bytes, data.length,
hmacResult);
HMAC vs 简单加盐
- 简单加盐:大多全局固定盐;盐泄露全部数据失效。
- HMAC:可为每个用户/每台设备独立分配独立密钥;某一个密钥泄露只影响单个用户,不影响全局;员工离职可以直接吊销对应key。
HMAC各版本选型
- HMAC‑MD5:❌新项目禁止。虽暂无完全实用破解,但底层MD5存在碰撞漏洞,安全审计报风险。
- HMAC‑SHA1:❌废弃,仅老系统兼容。
- HMAC‑SHA256:✅业务主流;API签名、Webhook、JWT‑HS256、文件鉴权。
- HMAC‑SHA512:✅高安全场景。
典型业务场景
- API接口签名:请求参数+时间戳做HMAC‑SHA256,防篡改、防重放。
- Webhook回调来源校验。
- JWT HS256(对称签名)。
- 图片、视频二进制文件:校验文件没被篡改,同时确认发送方身份,支持含大量
0x00二进制。
HMAC业务增强方案
-
HMAC + 设备锁 注册时为设备生成独立HMAC密钥;更换新设备登录,本地无对应密钥;需要旧设备确认授权,服务端再下发新密钥给新设备;密钥不和账号密码存储在一起。
-
HMAC + 时间戳(防重放攻击) 黑客捕获到合法HMAC tag,直接重复发送请求(重放攻击)。 方案:原始数据拼接分钟级/秒级时间戳再计算HMAC;服务端校验时允许±1分钟时间窗口;超出窗口直接拒绝。
示例:
HMAC‑SHA256( key, data + 当前Unix分钟时间戳 )
重要坑:HMAC tag对比不能直接memcmp/==普通字节比较,要使用恒定时间比较函数,防止计时侧信道攻击。
四、AES 对称加密算法
对称加密:加密和解密使用同一个密钥;可以真正还原原始明文(和MD5/SHA单向摘要完全不同)。 MD5/HMAC只做摘要校验;AES用来真正加密保护数据内容。
-
常见规格:AES‑128、AES‑192、AES‑256(密钥越长安全性越高)
-
工作模式:
- ECB:❌不安全,不要使用,相同明文块输出密文相同
- CBC:需要IV随机向量;
- GCM:✅推荐;同时完成加密 + 完整性校验(AEAD模式),自带防篡改,业务首选。
// CommonCrypto CCCrypt 完成AES加密解密
// 关键点:密钥必须密码学随机生成;IV向量每次加密必须随机,不可写死
注意点
- 密钥不能硬编码写死代码;
- GCM模式会输出密文+tag;解密必须校验tag,否则密文可以被篡改;
- 对称加密最大风险:密钥保管,如果密钥泄露,全部密文可被解密。
使用场景
本地数据库加密、传输报文加密、文件内容加密。
五、RSA 非对称加密算法
非对称:一对密钥
- 私钥(private key) :自己秘密保管,绝不外泄;用来签名、解密。
- 公钥(public key) :可以公开分发;用来验签、加密。
核心区别对比对称AES: AES:同一个密钥加密解密; RSA:公钥加密,只有私钥可以解密;私钥签名,所有人用公钥验签。
RSA两个主要用途
-
加密:公钥加密数据,私钥解密。 ⚠️ RSA不适合加密大体积数据!只能加密少量字节(2048位RSA最多加密~245字节)。 业务标准做法:原始大数据用AES加密;再用RSA加密AES密钥(混合加密)。
-
数字签名:私钥对数据摘要签名;公钥校验签名。
签名≠加密:签名不加密原文;证明:这份数据是私钥持有者发出,没有被篡改。
RSA选型
- RSA‑1024:❌不安全,淘汰
- RSA‑2048:✅现有业务最低底线
- RSA‑3072:✅长期保密数据推荐
填充模式: 加密必须 OAEP;签名必须 PSS;禁止老旧PKCS1‑v1.5(有漏洞)。
RSA风险
- 私钥一旦泄露,全部签名、加密全部失效;
- 量子计算机Shor算法未来可以直接破解RSA;超长保密数据需要考虑后量子密码;
- 性能差,比AES慢很多,不适合大文件直接加密。
典型场景
HTTPS证书、接口非对称签名、加密传递AES会话密钥。
概念总览对比表
| 算法 | 类型 | 是否可逆(解密) | 核心能力 | 密钥 |
|---|---|---|---|---|
| MD5 | 哈希摘要 | ❌不可逆 | 完整性校验,不鉴权 | 无 |
| SHA256 | 哈希摘要 | ❌不可逆 | 完整性校验,不鉴权 | 无 |
| HMAC‑SHA256 | 消息认证码 | ❌不可逆 | 完整性 + 身份鉴权 | 对称密钥 |
| AES‑GCM | 对称加密 | ✅可解密 | 加密数据、完整性校验 | 同一个密钥加密解密 |
| RSA | 非对称加密签名 | ✅可解密 | 加密小数据 / 数字签名 | 公钥、私钥密钥对 |
重要开发避坑清单
- 哈希摘要不能当加密使用:MD5/SHA不能还原原文,需要保密数据要用AES/RSA。
- 二进制数据(图片、视频)绝对禁止
strlen获取长度,一律使用NSData.length。 - 不要自己实现密码学,全部调用系统库 CommonCrypto / OpenSSL,不要手写HMAC、AES逻辑。
- 密码存储:不要MD5加盐;优先bcrypt / Argon2。
- HMAC 不等于简单拼接key+hash,必须使用标准CCHmac接口。
- AES禁止ECB模式;IV向量每次加密随机生成,不能硬编码。
- RSA不要加密大文件,采用AES+RSA混合加密。
- 安全审计:MD5、HMAC‑MD5、SHA‑1 均标记风险项,新项目规避。
CryptoUtil.h
#import <Foundation/Foundation.h>
@interface CryptoUtil : NSObject
#pragma mark - 哈希摘要 MD5 / SHA256
/// MD5摘要 返回32位小写16进制
+ (NSString *)md5WithData:(NSData *)data;
/// SHA256摘要 返回64位小写16进制
+ (NSString *)sha256WithData:(NSData *)data;
/// 字符串便捷方法,内部转UTF8‑NSData
+ (NSString *)md5WithString:(NSString *)str;
+ (NSString *)sha256WithString:(NSString *)str;
#pragma mark - HMAC‑SHA256 消息认证码
/// HMAC‑SHA256,返回十六进制字符串
+ (NSString *)hmacSHA256WithData:(NSData *)data keyData:(NSData *)key;
/// 恒定时间比较HMAC结果,防御计时攻击
+ (BOOL)constantTimeCompare:(NSData *)data1 with:(NSData *)data2;
#pragma mark - AES‑GCM 对称加密(推荐AEAD)
/**
AES‑256‑GCM 加密
@param plainData 明文
@param key 32字节密钥(AES256)
@param iv 12字节随机IV(GCM推荐12字节)
@param aad 附加认证数据,可以传nil
@param tagOut 输出认证tag(16字节)
@return 密文data,失败返回nil
*/
+ (NSData *)aes256GcmEncrypt:(NSData *)plainData
key:(NSData *)key
iv:(NSData *)iv
aad:(NSData *)aad
tagOut:(NSData **)tagOut;
/**
AES‑256‑GCM 解密
@param cipherData 密文
@param key 32字节密钥
@param iv 加密时的iv
@param aad 附加认证数据,和加密一致
@param tag 加密输出的tag
@return 明文,tag校验失败返回nil
*/
+ (NSData *)aes256GcmDecrypt:(NSData *)cipherData
key:(NSData *)key
iv:(NSData *)iv
aad:(NSData *)aad
tag:(NSData *)tag;
/// 生成密码学安全随机字节
+ (NSData *)secureRandomBytes:(size_t)len;
#pragma mark - RSA SHA256 签名验签(PKCS1‑PSS)
/**
RSA私钥PEM签名 SHA256‑PSS
@param rawData 待签名原始数据
@param privateKeyPem pem格式私钥字符串
@return base64签名结果,失败nil
*/
+ (NSString *)rsaSignSHA256PSS:(NSData *)rawData privatePem:(NSString *)privateKeyPem;
/**
RSA公钥验签 SHA256‑PSS
@param rawData 原始数据
@param signBase64 base64签名
@param publicKeyPem pem公钥
@return YES验签通过
*/
+ (BOOL)rsaVerifySHA256PSS:(NSData *)rawData
signBase64:(NSString *)signBase64
publicPem:(NSString *)publicKeyPem;
@end
CryptoUtil.m
#import "CryptoUtil.h"
#import <CommonCrypto/CommonCrypto.h>
#import <Security/Security.h>
@implementation CryptoUtil
#pragma mark - MD5 / SHA256
+ (NSString *)md5WithData:(NSData *)data {
uint8_t digest[CC_MD5_DIGEST_LENGTH];
CC_MD5(data.bytes, (CC_LONG)data.length, digest);
return [self hexStringFromBytes:digest length:CC_MD5_DIGEST_LENGTH];
}
+ (NSString *)sha256WithData:(NSData *)data {
uint8_t digest[CC_SHA256_DIGEST_LENGTH];
CC_SHA256(data.bytes, (CC_LONG)data.length, digest);
return [self hexStringFromBytes:digest length:CC_SHA256_DIGEST_LENGTH];
}
+ (NSString *)md5WithString:(NSString *)str {
NSData *d = [str dataUsingEncoding:NSUTF8StringEncoding];
return [self md5WithData:d];
}
+ (NSString *)sha256WithString:(NSString *)str {
NSData *d = [str dataUsingEncoding:NSUTF8StringEncoding];
return [self sha256WithData:d];
}
+ (NSString *)hexStringFromBytes:(const uint8_t *)bytes length:(size_t)len {
NSMutableString *hex = [NSMutableString stringWithCapacity:len * 2];
for (NSUInteger i = 0; i < len; i++) {
[hex appendFormat:@"%02x", bytes[i]];
}
return hex;
}
#pragma mark - HMAC‑SHA256
+ (NSString *)hmacSHA256WithData:(NSData *)data keyData:(NSData *)key {
uint8_t hmacBuf[CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256,
key.bytes, key.length,
data.bytes, data.length,
hmacBuf);
return [self hexStringFromBytes:hmacBuf length:CC_SHA256_DIGEST_LENGTH];
}
+ (BOOL)constantTimeCompare:(NSData *)data1 with:(NSData *)data2 {
if (data1.length != data2.length) return NO;
uint8_t diff = 0;
const uint8_t *b1 = data1.bytes;
const uint8_t *b2 = data2.bytes;
for (NSUInteger i = 0; i < data1.length; i++) {
diff |= b1[i] ^ b2[i];
}
return (diff == 0);
}
#pragma mark - AES‑GCM
+ (NSData *)secureRandomBytes:(size_t)len {
NSMutableData *data = [NSMutableData dataWithLength:len];
int ret = SecRandomCopyBytes(kSecRandomDefault, len, data.mutableBytes);
if (ret != errSecSuccess) return nil;
return data;
}
+ (NSData *)aes256GcmEncrypt:(NSData *)plainData
key:(NSData *)key
iv:(NSData *)iv
aad:(NSData *)aad
tagOut:(NSData **)tagOut {
if (key.length != kCCKeySizeAES256 || iv.length == 0 || !tagOut) return nil;
size_t cipherLen = plainData.length;
NSMutableData *cipherBuf = [NSMutableData dataWithLength:cipherLen];
uint8_t tagBuf[CC_GCM_TAG_LENGTH];
CCCryptorStatus status = CCCryptorGCMOneshot(kCCEncrypt,
kCCAlgorithmAES,
key.bytes, key.length,
iv.bytes, iv.length,
aad.bytes, aad.length,
plainData.bytes, plainData.length,
cipherBuf.mutableBytes,
tagBuf, CC_GCM_TAG_LENGTH);
if (status != kCCSuccess) return nil;
*tagOut = [NSData dataWithBytes:tagBuf length:CC_GCM_TAG_LENGTH];
return cipherBuf;
}
+ (NSData *)aes256GcmDecrypt:(NSData *)cipherData
key:(NSData *)key
iv:(NSData *)iv
aad:(NSData *)aad
tag:(NSData *)tag {
if (key.length != kCCKeySizeAES256 || tag.length != CC_GCM_TAG_LENGTH) return nil;
NSMutableData *plainBuf = [NSMutableData dataWithLength:cipherData.length];
CCCryptorStatus status = CCCryptorGCMOneshot(kCCDecrypt,
kCCAlgorithmAES,
key.bytes, key.length,
iv.bytes, iv.length,
aad.bytes, aad.length,
cipherData.bytes, cipherData.length,
plainBuf.mutableBytes,
(uint8_t *)tag.bytes, tag.length);
if (status != kCCSuccess) return nil;
return plainBuf;
}
#pragma mark - RSA 工具(Security.framework)
static SecKeyRef secKeyFromPem(NSString *pemStr, BOOL isPrivate) {
NSData *data = [[pemStr stringByReplacingOccurrencesOfString:@"\n" withString:@""] dataUsingEncoding:NSUTF8StringEncoding];
if (!data) return NULL;
CFErrorRef err = NULL;
NSDictionary *opts = @{
(__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA,
(__bridge id)kSecAttrKeyClass: isPrivate ? (__bridge id)kSecAttrKeyClassPrivate : (__bridge id)kSecAttrKeyClassPublic
};
SecKeyRef key = SecKeyCreateWithData((__bridge CFDataRef)data, (__bridge CFDictionaryRef)opts, &err);
return key;
}
+ (NSString *)rsaSignSHA256PSS:(NSData *)rawData privatePem:(NSString *)privateKeyPem {
SecKeyRef privKey = secKeyFromPem(privateKeyPem, YES);
if (!privKey) return nil;
CFErrorRef err = NULL;
NSDictionary *signOpts = @{
(__bridge id)kSecDigestType: (__bridge id)kSecDigestSHA2,
(__bridge id)kSecPSSSaltLength: @(20)
};
NSData *sigData = (__bridge_transfer NSData *)SecKeyCreateSignature(privKey,
kSecSignatureAlgorithmRSASHA256PSS,
(__bridge CFDataRef)rawData,
(__bridge CFDictionaryRef)signOpts,
&err);
CFRelease(privKey);
if (!sigData) return nil;
return [sigData base64EncodedStringWithOptions:0];
}
+ (BOOL)rsaVerifySHA256PSS:(NSData *)rawData
signBase64:(NSString *)signBase64
publicPem:(NSString *)publicKeyPem {
SecKeyRef pubKey = secKeyFromPem(publicKeyPem, NO);
if (!pubKey) return NO;
NSData *sigData = [[NSData alloc] initWithBase64EncodedString:signBase64 options:0];
if (!sigData) { CFRelease(pubKey); return NO; }
CFErrorRef err = NULL;
NSDictionary *opts = @{
(__bridge id)kSecDigestType: (__bridge id)kSecDigestSHA2,
(__bridge id)kSecPSSSaltLength: @(20)
};
BOOL ok = SecKeyVerifySignature(pubKey,
kSecSignatureAlgorithmRSASHA256PSS,
(__bridge CFDataRef)rawData,
(__bridge CFDataRef)sigData,
(__bridge CFDictionaryRef)opts,
&err);
CFRelease(pubKey);
return ok;
}
@end