关于消息摘要及对应的Swift实现

开发过程中我们经常会遇到对数据进行一些类似MD5SHA-256等的处理,那处理的作用是什么以及为什么要这么做,就是今天要讨论的内容。如果你对这些算法的概念已经很熟悉,那就不用继续往下看了,如果你感觉生疏,或者并不能说清楚他们的作用,那就跟着我一起回顾一下吧。

从MD5说起

我门从最常见的MD5说起。下面是维基百科对MD5的定义:

MD5消息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。

MD5的概念中出现了两个名词:信息摘要算法 + 密码散列函数。 我们再分别来看这两个概念的含义。

信息摘要算法

消息摘要算法消息摘要是把任意长度的输入揉和而产生长度固定的伪随机输入的算法。

它具有以下特点:

  • 无论输入的消息有多长,计算出来的消息摘要的长度总是固定的。例如应用MD5算法摘要的消息有128个比特位,一般认为,摘要的最终输出越长,该摘要算法就越安全。变长输入,定长输出。

  • 一般地,只要输入的消息不同,对其进行摘要以后产生的摘要消息也必不相同;但相同的输入必会产生相同的输出。

  • 消息摘要函数是单向函数,即只能进行正向的信息摘要,而无法从摘要中恢复出任何的消息,甚至根本就找不到任何与原信息相关的信息。

  • 好的摘要算法,没有人能从中找到“碰撞”,即无法找到两条不同的消息,使它们的摘要相同。

从理论上来说,不管使用什么样的摘要算法,必然存在2个不同的消息,对应同样的摘要。因为输入是一个无穷集合,而输出是一个有限集合。但是实际上,很难或者说根本不可能人为的造出具有同样摘要的2个不同消息。

密码散列函数:

密码散列函数(Cryptographic hash function),又译为加密散列函数,是散列函数的一种。它被认为是一种单向函数,也就是说极其难以由散列函数输出的结果,回推输入的数据是什么。

概念梳理

这里比较重要的就是理解信息摘要,它概念就是把一段数据压缩成固定长度的数据,但是我们不能通过压缩后的数据反推原始数据。

有一种应用场景比较能说明问题: 我们在登录的时候,对用户密码进行MD5计算之后再传到服务器,之后数据库显示的用户信息就是MD5之后的16字节字符串。管理员即使能访问用户数据库但他并不能由MD5处理之后的数据得知原始密码。 而客户端这边却可以在输入密码之后,进行MD5计算然后与服务器密码进行比较。

SHA家族

安全散列算法(英语:Secure Hash Algorithm,缩写为SHA)是一个密码散列函数家族,是FIPS所认证的安全散列算法。能计算出一个数字消息所对应到的,长度固定的字符串(又称消息摘要)的算法。

SHA的出现就是解决MD5不安全的问题,它是一种比MD5更安全的密码散列函数。

SHA家族的算法,由美国国家安全局(NSA)所设计,并由美国国家标准与技术研究院(NIST)发布,是美国的政府标准,其分别是:

名称简介应用范围发布时间
SHA-0SHA-1前身-1993
SHA-1曾被视为MD5的后继者,但SHA-1的安全性在2000年以后已经不被大多数的加密场景所接受。广泛1995
SHA-2包括SHA-224/SHA-256/SHA-384/SHA-512,目前并未被破解广泛2001
SHA-3比SHA-2更安全的散列函数,并无替代SHA-2的计划较小2015

散列函数的安全性

上面说到了MD5是不安全的,不安全意味着可以找到MD5的“碰撞”,就是两个文件可以产生相同的“指纹”。这意味着,当你在网络上使用电子签名签署一份合同后,还可能找到另外一份具有相同签名但内容迥异的合同,这样两份合同的真伪性便无从辨别。

2004年8月17日,在CRYPTO 2004的Rump会议上,王小云,冯登国、来学嘉,和于红波宣布了攻击MD5、SHA-0 和其他杂凑函数的初步结果。他们攻击SHA-0的计算复杂度是2的40次方,这意味着他们的攻击成果比Joux还有其他人所做的更好。

2005年二月,王小云和殷益群、于红波再度发表了对SHA-0破密的算法,可在2的39次方的计算复杂度内就找到碰撞。

2009年,中国科学院的谢涛和冯登国仅用了2的20.96次方的碰撞算法复杂度,破解了MD5的碰撞抵抗,该攻击在普通计算机上运行只需要数秒钟。

2017年荷兰密码学研究小组CWI和Google正式宣布攻破了SHA-1。

摘要的Swift实现

Swift有一个比较成熟的专门处理摘要,加密等安全相关的框架CryptoSwift

以下是Swift对摘要算法的原生实现,需要引入CommonCrypto框架,这个已经内置到Xcode里面了,列出了对String和Data的摘要扩展。

import Foundation
import CommonCrypto

enum CryptoAlgorithm {
    case MD5, SHA1, SHA224, SHA256, SHA384, SHA512
    
    var digestLength: Int {
        var result: Int32 = 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 {
    var md5:    String { return digest(string: self, algorithm: .MD5) }
    var sha1:   String { return digest(string: self, algorithm: .SHA1) }
    var sha224: String { return digest(string: self, algorithm: .SHA224) }
    var sha256: String { return digest(string: self, algorithm: .SHA256) }
    var sha384: String { return digest(string: self, algorithm: .SHA384) }
    var sha512: String { return digest(string: self, algorithm: .SHA512) }
    
    func digest(string: String, algorithm: CryptoAlgorithm) -> String {
        var result: [CUnsignedChar]
        let digestLength = Int(algorithm.digestLength)
        if let cdata = string.cString(using: String.Encoding.utf8) {
            result = Array(repeating: 0, count: digestLength)
            switch algorithm {
            case .MD5:      CC_MD5(cdata, CC_LONG(cdata.count-1), &result)
            case .SHA1:     CC_SHA1(cdata, CC_LONG(cdata.count-1), &result)
            case .SHA224:   CC_SHA224(cdata, CC_LONG(cdata.count-1), &result)
            case .SHA256:   CC_SHA256(cdata, CC_LONG(cdata.count-1), &result)
            case .SHA384:   CC_SHA384(cdata, CC_LONG(cdata.count-1), &result)
            case .SHA512:   CC_SHA512(cdata, CC_LONG(cdata.count-1), &result)
            }
        } else {
            fatalError("Nil returned when processing input strings as UTF8")
        }
        return (0..<digestLength).reduce("") { $0 + String(format: "%02hhx", result[$1])}
    }
}

extension Data {
    var md5:    String { return digest(data: self, algorithm: .MD5) }
    var sha1:   String { return digest(data: self, algorithm: .SHA1) }
    var sha224: String { return digest(data: self, algorithm: .SHA224) }
    var sha256: String { return digest(data: self, algorithm: .SHA256) }
    var sha384: String { return digest(data: self, algorithm: .SHA384) }
    var sha512: String { return digest(data: self, algorithm: .SHA512) }
    
    func digest(data: Data, algorithm: CryptoAlgorithm) -> String {
        var result: [CUnsignedChar]
        let digestLength = Int(algorithm.digestLength)
        let pdata = (data as NSData).bytes
        result = Array(repeating: 0, count: digestLength)
        switch algorithm {
        case .MD5:      CC_MD5(pdata, CC_LONG(data.count), &result)
        case .SHA1:     CC_SHA1(pdata, CC_LONG(data.count), &result)
        case .SHA224:   CC_SHA224(pdata, CC_LONG(data.count), &result)
        case .SHA256:   CC_SHA256(pdata, CC_LONG(data.count), &result)
        case .SHA384:   CC_SHA384(pdata, CC_LONG(data.count), &result)
        case .SHA512:   CC_SHA512(pdata, CC_LONG(data.count), &result)
        }
        
        return (0..<digestLength).reduce("") { $0 + String(format: "%02hhx", result[$1])}
    }
}