Skip to content

Instantly share code, notes, and snippets.

@proxpero
Last active February 16, 2017 16:48
Show Gist options
  • Save proxpero/c5aedb64f19f0d4c9ae63b8eee1a1b3e to your computer and use it in GitHub Desktop.
Save proxpero/c5aedb64f19f0d4c9ae63b8eee1a1b3e to your computer and use it in GitHub Desktop.
Small, zero-dependency extensions on `String` and Collections of `UInt8` that return the md5 hash of self.
// As described in: http://proxpero.com/2017/02/14/an-implementation-of-md5/
extension Collection where Iterator.Element == UInt8 {
/// Returns the md5 hash of self.
public var md5: String {
return self.md5Digest.lazy.reduce("") {
var s = String($1, radix: 16)
if s.characters.count == 1 {
s = "0" + s
}
return $0 + s
}
}
// For reference:
// https://tools.ietf.org/html/rfc1321
// https://en.wikipedia.org/wiki/MD5#Pseudocode
private var md5Digest: [UInt8] {
typealias Word = UInt32
typealias Byte = UInt8
// s specifies the per-round shift amounts
let s: [Word] = [
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21
]
// K[i] = abs(sin(i + 1)) * 232 (radians)
let k: [Word] = [
0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
]
// Initialize variables
var a: Word = 0x67452301
var b: Word = 0xefcdab89
var c: Word = 0x98badcfe
var d: Word = 0x10325476
var message = Array(self)
// The length of the message before the padding bits were added.
let length = UInt64(message.count * 8)
/*
from RFC 1321:
Padding is performed as follows: a single "1" bit is appended to the
message, and then "0" bits are appended so that the length in bits of
the padded message becomes congruent to 448, modulo 512. In all, at
least one bit and at most 512 bits are appended.
*/
// Pre-processing: adding a single 1 bit
message.append(0x80)
// append "0" bit until message length in bits ≡ 448 (mod 512)
repeat {
message.append(0x0)
} while (message.count * 8) % 512 != 448
// A 64-bit representation of b (the length of the message before the
// padding bits were added) is appended to the result of the previous
// step.
for i in (0...7).map({ UInt64($0 * 8) }) {
message.append(Byte((length >> i) & 0xFF))
}
// Divide message into successive 512-bit (64-byte) chunks
let chunks = (0..<(message.count / 64))
.map { $0 * 64 }
.map { Array(message[$0..<$0+64]) }
// Process each chunk:
for chunk in chunks {
// Break chunk into sixteen 32-bit words m[i], 0 ≤ i ≤ 15
func part(_ index: Int, _ offset: Int, _ shift: Word) -> Word {
return Word(chunk[4 * index + offset]) << shift
}
var m = (0...15).map { i in
part(i, 0, 0) | part(i, 1, 8) | part(i, 2, 16) | part(i, 3, 24)
}
// Initialize hash value for this chunk:
var A = a
var B = b
var C = c
var D = d
//Main loop:
for index in k.indices {
var f: Word = 0
var g = 0
switch index {
case 0...15:
f = (B & C) | ((~B) & D)
g = index
case 16...31:
f = (B & D) | (C & (~D))
g = ((5*index + 1) % 16)
case 32...47:
f = B ^ C ^ D
g = ((3*index + 5) % 16)
case 48...63:
f = C ^ (B | (~D))
g = ((7*index) % 16)
default:
break
}
func rotateLeft(_ x: Word, by amount: Word) -> Word{
return ((x << amount) & 0xFFFFFFFF) | (x >> (32 - amount))
}
let dTemp = D
D = C
C = B
B = B &+ rotateLeft(A &+ f &+ k[index] &+ m[g], by: s[index])
A = dTemp
}
// Add this chunk's hash to result so far:
a = a &+ A
b = b &+ B
c = c &+ C
d = d &+ D
}
// a append b append c append d
let result = [a, b, c, d].flatMap { word in
[Word]([00, 08, 16, 24]).map { Byte((word >> $0) & 0xFF) }
}
return result
}
}
extension String {
/// Returns the md5 hash of self.
public var md5: String {
return self.utf8.md5
}
}
// RFC 1321 test suite
let tests = [
("", "d41d8cd98f00b204e9800998ecf8427e"),
("a", "0cc175b9c0f1b6a831c399e269772661"),
("abc", "900150983cd24fb0d6963f7d28e17f72"),
("message digest", "f96b697d7cb7938d525a2f31aaf161d0"),
("abcdefghijklmnopqrstuvwxyz", "c3fcd3d76192e4007dfb496cca67e13b"),
("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", "d174ab98d277d9f5a5611c2c9f419d9f"),
("12345678901234567890123456789012345678901234567890123456789012345678901234567890", "57edf4a22be3c955ac49da2e2107b67a")
]
for (text, hash) in tests {
assert(text.md5 == hash)
print("test passed.")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment