iOS - HMAC 加密算法和一个 MD5 加密的问题

最近阅读

前言

最近我们部门重新定义了一下应用更新的接口,使用到的主要加密方式主要有 HMAC SHA1 加密和 MD5 加密。

HMAC SHA1 加密

HMAC SHA1 加密的方法是在 Stack Overflow 上看到的 CommonHMAC in Swift

大概分两步:

  1. 定义一个 HMAC 加密方式的枚举
  2. 实现一个 String 的拓展方法用来加密

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
enum HMACAlgorithm {
case MD5, SHA1, SHA224, SHA256, SHA384, SHA512

func toCCHmacAlgorithm() -> CCHmacAlgorithm {
var result: Int = 0
switch self {
case .MD5:
result = kCCHmacAlgMD5
case .SHA1:
result = kCCHmacAlgSHA1
case .SHA224:
result = kCCHmacAlgSHA224
case .SHA256:
result = kCCHmacAlgSHA256
case .SHA384:
result = kCCHmacAlgSHA384
case .SHA512:
result = kCCHmacAlgSHA512
}
return CCHmacAlgorithm(result)
}

func digestLength() -> Int {
var result: CInt = 0
switch self {
case .MD5:
result = CC_MD5_DIGEST_LENGTH
case .SHA1:
result = CC_SHA1_DIGEST_LENGTH
case .SHA224:
result = CC_SHA224_DIGEST_LENGTH
case .SHA256:
result = CC_SHA256_DIGEST_LENGTH
case .SHA384:
result = CC_SHA384_DIGEST_LENGTH
case .SHA512:
result = CC_SHA512_DIGEST_LENGTH
}
return Int(result)
}
}

extension String {
func hmac(algorithm: HMACAlgorithm, key: String) -> String {
let cKey = key.cStringUsingEncoding(NSUTF8StringEncoding)
let cData = self.cStringUsingEncoding(NSUTF8StringEncoding)
var result = [CUnsignedChar](count: Int(algorithm.digestLength()), repeatedValue: 0)
CCHmac(algorithm.toCCHmacAlgorithm(), cKey!, strlen(cKey!), cData!, strlen(cData!), &result)
var hmacData:NSData = NSData(bytes: result, length: (Int(algorithm.digestLength())))
var hmacBase64 = hmacData.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.Encoding76CharacterLineLength)
return String(hmacBase64)
}
}

MD5 加密的问题

细心的朋友注意到,上面 HMAC 加密的枚举里有一个 MD5 的字段,如果同时用到 HMAC 加密和 MD5 加密的朋友,需要注意一下 Hmac-MD5 加密和 MD5 加密的区别,推荐可以用在线加密工具比较直观的看到加密结果的区别:在线加解密。另外这个工具也可以用于通信双方用来和自己解密的结果校验是否正确。

回归正题,MD5 加密本身没什么难度,网上搜索一大把。抱着做笔记的目的贴一下 Swift 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
///字符直接转 md5
func conversionToMD5() -> String {
let str = self.cString(using: String.Encoding.utf8)
let strLen = CUnsignedInt(self.lengthOfBytes(using: String.Encoding.utf8))
let digestLen = Int(CC_MD5_DIGEST_LENGTH)
let result = UnsafeMutablePointer<UInt8>.allocate(capacity: 16)
CC_MD5(str!, strLen, result)
let hash = NSMutableString()
for i in 0 ..< digestLen {
hash.appendFormat("%02x", result[i])
}
free(result)
return String(format: hash as String)
}

然后问题就来了:iOS 端的加密和服务器那边的加密结果无法对应,导致鉴权失败。
排查问题,让服务器同事打印在 MD5 加密前的每个步骤的值,发现都是一致的,在 MD5 加密后就不一致了。然后去就看了一下他写的源码,服务器那边的逻辑使用 Java 写的,发现他丢进 MD5 加密时是把上一步的结果调用 getBytes() 语句后丢进去的。即加密的是字符串转换后的字节数组。所以在 iOS 端这边的处理也需要将 String 转换成 Data 数据来加密。

所以步骤分为以下两步:
1.把 String 类型数据转换成 16 进制 Data 数据——注意是 16 进制的 Data 数据,如果直接用原生方法 "<#String#>".(using: .utf8) 转换肯定是不行的。
2.把 Data 数据丢进 MD5 方法中进行加密
而在网上搜了一圈,Swift 版本大部分看到的都是用字符串进行 MD5 加密,最后仍是在 Stack Overflow 上看到了一个正确的结果 - MD5 of Data in Swift 3

最后和 StringData 的方法整合以后。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
///转换成 Data 数据再转换成 md5
func conversionToDataToMD5() -> String{
let data = Data(hex: self)
var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))

data.withUnsafeBytes {
CC_MD5($0.baseAddress, UInt32(data.count), &digest)
}

var hashString = ""

for byte in digest {
hashString += String(format:"%02x", UInt8(byte))
}

return hashString
}

extension Data {
public init(hex: String) {
self.init(bytes: Array<UInt8>(hex: hex))
}
public var bytes: Array<UInt8> {
return Array(self)
}
}

extension Array {
public init(reserveCapacity: Int) {
self = Array<Element>()
self.reserveCapacity(reserveCapacity)
}
}
extension Array where Element == UInt8 {
public init(hex: String) {
self.init(reserveCapacity: hex.unicodeScalars.lazy.underestimatedCount)
var buffer: UInt8?
var skip = hex.hasPrefix("0x") ? 2 : 0
for char in hex.unicodeScalars.lazy {
guard skip == 0 else {
skip -= 1
continue
}
guard char.value >= 48 && char.value <= 102 else {
removeAll()
return
}
let v: UInt8
let c: UInt8 = UInt8(char.value)
switch c {
case let c where c <= 57:
v = c - 48
case let c where c >= 65 && c <= 70:
v = c - 55
case let c where c >= 97:
v = c - 87
default:
removeAll()
return
}
if let b = buffer {
append(b << 4 | v)
buffer = nil
} else {
buffer = v
}
}
if let b = buffer {
append(b)
}
}
}

使用:

1
let md5Str = "两个黄鹂鸣翠柳,一行白鹭上青天。".conversionToDataToMD5()

然后因为业务原因,也用 OC 实现了这一整套的加密流程,最后一步的转换加密也大同小异:

  1. 转换成 16 进制 NSData 数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    +(NSData*)getHexDataWithString:(NSString*)string{
    const char *buf = [string UTF8String];

    NSMutableData *data = [NSMutableData data];
    if (buf){
    uint32_t len = strlen(buf);
    char singleNumberString[3] = {'\0', '\0', '\0'};
    uint32_t singleNumber = 0;
    for(uint32_t i = 0 ; i < len; i+=2){
    if ( ((i+1) < len) && isxdigit(buf[i]) && (isxdigit(buf[i+1])) ){

    singleNumberString[0] = buf[i];

    singleNumberString[1] = buf[i + 1];

    sscanf(singleNumberString, "%x", &singleNumber);

    uint8_t tmp = (uint8_t)(singleNumber & 0x000000FF);

    [data appendBytes:(void *)(&tmp)length:1];
    }else{
    break;
    }
    }
    }
    return data;
    }
  2. NSData MD5 加密
    OCMD5 的加密就概不累述了,OC 比 Swift 更容易搜到,放一个我参考的博客:Objective-C NSString NSData Byte 等转换