iOS 加密解密的使用

2 阅读11分钟

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 简单加盐

  1. 简单加盐:大多全局固定盐;盐泄露全部数据失效。
  2. HMAC:可为每个用户/每台设备独立分配独立密钥;某一个密钥泄露只影响单个用户,不影响全局;员工离职可以直接吊销对应key。

HMAC各版本选型

  • HMAC‑MD5:❌新项目禁止。虽暂无完全实用破解,但底层MD5存在碰撞漏洞,安全审计报风险。
  • HMAC‑SHA1:❌废弃,仅老系统兼容。
  • HMAC‑SHA256:✅业务主流;API签名、Webhook、JWT‑HS256、文件鉴权。
  • HMAC‑SHA512:✅高安全场景。

典型业务场景

  1. API接口签名:请求参数+时间戳做HMAC‑SHA256,防篡改、防重放。
  2. Webhook回调来源校验。
  3. JWT HS256(对称签名)。
  4. 图片、视频二进制文件:校验文件没被篡改,同时确认发送方身份,支持含大量0x00二进制。

HMAC业务增强方案

  1. HMAC + 设备锁 注册时为设备生成独立HMAC密钥;更换新设备登录,本地无对应密钥;需要旧设备确认授权,服务端再下发新密钥给新设备;密钥不和账号密码存储在一起。

  2. 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向量每次加密必须随机,不可写死

注意点

  1. 密钥不能硬编码写死代码;
  2. GCM模式会输出密文+tag;解密必须校验tag,否则密文可以被篡改;
  3. 对称加密最大风险:密钥保管,如果密钥泄露,全部密文可被解密。

使用场景

本地数据库加密、传输报文加密、文件内容加密。


五、RSA 非对称加密算法

非对称:一对密钥

  • 私钥(private key) :自己秘密保管,绝不外泄;用来签名、解密。
  • 公钥(public key) :可以公开分发;用来验签、加密。

核心区别对比对称AES: AES:同一个密钥加密解密; RSA:公钥加密,只有私钥可以解密;私钥签名,所有人用公钥验签。

RSA两个主要用途

  1. 加密:公钥加密数据,私钥解密。 ⚠️ RSA不适合加密大体积数据!只能加密少量字节(2048位RSA最多加密~245字节)。 业务标准做法:原始大数据用AES加密;再用RSA加密AES密钥(混合加密)。

  2. 数字签名:私钥对数据摘要签名;公钥校验签名。

    签名≠加密:签名不加密原文;证明:这份数据是私钥持有者发出,没有被篡改。

RSA选型

  • RSA‑1024:❌不安全,淘汰
  • RSA‑2048:✅现有业务最低底线
  • RSA‑3072:✅长期保密数据推荐

填充模式: 加密必须 OAEP;签名必须 PSS;禁止老旧PKCS1‑v1.5(有漏洞)。

RSA风险

  1. 私钥一旦泄露,全部签名、加密全部失效;
  2. 量子计算机Shor算法未来可以直接破解RSA;超长保密数据需要考虑后量子密码;
  3. 性能差,比AES慢很多,不适合大文件直接加密。

典型场景

HTTPS证书、接口非对称签名、加密传递AES会话密钥。


概念总览对比表

算法类型是否可逆(解密)核心能力密钥
MD5哈希摘要❌不可逆完整性校验,不鉴权
SHA256哈希摘要❌不可逆完整性校验,不鉴权
HMAC‑SHA256消息认证码❌不可逆完整性 + 身份鉴权对称密钥
AES‑GCM对称加密✅可解密加密数据、完整性校验同一个密钥加密解密
RSA非对称加密签名✅可解密加密小数据 / 数字签名公钥、私钥密钥对

重要开发避坑清单

  1. 哈希摘要不能当加密使用:MD5/SHA不能还原原文,需要保密数据要用AES/RSA。
  2. 二进制数据(图片、视频)绝对禁止strlen获取长度,一律使用NSData.length。
  3. 不要自己实现密码学,全部调用系统库 CommonCrypto / OpenSSL,不要手写HMAC、AES逻辑。
  4. 密码存储:不要MD5加盐;优先bcrypt / Argon2。
  5. HMAC 不等于简单拼接key+hash,必须使用标准CCHmac接口。
  6. AES禁止ECB模式;IV向量每次加密随机生成,不能硬编码。
  7. RSA不要加密大文件,采用AES+RSA混合加密。
  8. 安全审计: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