From db509cdbe4dd15c1963f75837d74a5f420c67ce1 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Mon, 7 Aug 2017 14:20:00 +0200 Subject: [PATCH] HMAC and PBKDF2 --- Sources/CryptoKitten/HMAC.swift | 39 +++++++ Sources/CryptoKitten/Hash.swift | 1 + Sources/CryptoKitten/MD5.swift | 3 +- Sources/CryptoKitten/PBKDF2_HMAC.swift | 125 ++++++++++++++++++++++ Sources/CryptoKitten/SHA1.swift | 1 + Tests/CryptoKittenTests/MD5Tests.swift | 47 ++++---- Tests/CryptoKittenTests/PBKDF2Tests.swift | 105 +++++++++--------- Tests/CryptoKittenTests/SHA1Tests.swift | 58 +++++----- 8 files changed, 269 insertions(+), 110 deletions(-) create mode 100644 Sources/CryptoKitten/HMAC.swift create mode 100644 Sources/CryptoKitten/PBKDF2_HMAC.swift diff --git a/Sources/CryptoKitten/HMAC.swift b/Sources/CryptoKitten/HMAC.swift new file mode 100644 index 0000000..39d3827 --- /dev/null +++ b/Sources/CryptoKitten/HMAC.swift @@ -0,0 +1,39 @@ +public class HMAC { + /// Authenticates a message using the provided `Hash` algorithm + /// + /// - parameter message: The message to authenticate + /// - parameter key: The key to authenticate with + /// + /// - returns: The authenticated message + public static func authenticate(_ message: [UInt8], withKey key: [UInt8]) -> [UInt8] { + var key = key + + // If it's too long, hash it first + if key.count > Variant.chunkSize { + key = Variant.hash(key) + } + + // Add padding + if key.count < Variant.chunkSize { + key = key + [UInt8](repeating: 0, count: Variant.chunkSize - key.count) + } + + // XOR the information + var outerPadding = [UInt8](repeating: 0x5c, count: Variant.chunkSize) + var innerPadding = [UInt8](repeating: 0x36, count: Variant.chunkSize) + + for i in 0.. { + /// Derives a key from a given set of parameters + /// + /// - parameter password: The password to hash + /// - parameter salt: The random salt that should be unique to the user's credentials, used for preventing Rainbow Tables + /// - parameter iterations: The amount of iterations to use for strengthening the key, higher is stronger/safer but also slower + /// - parameter keySize: The amount of bytes to output + /// + /// - throws: Invalid input bytes for password or salt + /// - throws: Too large amount of key bytes requested + /// - throws: Too little iterations + /// + /// - returns: The derived key bytes + public static func derive(fromPassword password: [UInt8], saltedWith salt: [UInt8], iterating iterations: Int = 10_000, derivedKeyLength keySize: Int? = nil) throws -> [UInt8] { + + // Used to create a block number to append to the salt before deriving + func integerBytes(blockNum block: UInt32) -> [UInt8] { + var bytes = [UInt8](repeating: 0, count: 4) + bytes[0] = UInt8((block >> 24) & 0xFF) + bytes[1] = UInt8((block >> 16) & 0xFF) + bytes[2] = UInt8((block >> 8) & 0xFF) + bytes[3] = UInt8(block & 0xFF) + return bytes + } + + // Authenticated using HMAC with precalculated keys (saves 50% performance) + func authenticate(innerPadding: [UInt8], outerPadding: [UInt8], message: [UInt8]) throws -> [UInt8] { + let innerPaddingHash: [UInt8] = Variant.hash(innerPadding + message) + let outerPaddingHash: [UInt8] = Variant.hash(outerPadding + innerPaddingHash) + + return outerPaddingHash + } + + let keySize = keySize ?? Variant.chunkSize + + // Check input values to be correct + guard iterations > 0 else { + throw PBKDF2Error.cannotIterateZeroTimes + } + + guard password.count > 0 else { + throw PBKDF2Error.cannotDeriveFromPassword(password) + } + + guard salt.count > 0 else { + throw PBKDF2Error.cannotDeriveFromSalt(salt) + } + + guard keySize <= Int(((pow(2,32) as Double) - 1) * Double(Variant.chunkSize)) else { + throw PBKDF2Error.keySizeTooBig(keySize) + } + + // MARK - Precalculate paddings + var password = password + + // If the key is too long, hash it first + if password.count > Variant.chunkSize { + password = Variant.hash(password) + } + + // Add padding + if password.count < Variant.chunkSize { + password = password + [UInt8](repeating: 0, count: Variant.chunkSize - password.count) + } + + // XOR the information + var outerPadding = [UInt8](repeating: 0x5c, count: Variant.chunkSize) + var innerPadding = [UInt8](repeating: 0x36, count: Variant.chunkSize) + + for i in 0.. Bool { + let newHash = try derive(fromPassword: password, saltedWith: salt, iterating: iterations, derivedKeyLength: against.count) + + return newHash == against + } +} + +fileprivate func xor(_ lhs: inout [UInt8], _ rhs: [UInt8]) { + assert(lhs.count == rhs.count) + + for i in 0...authenticate( -// message: [UInt8](test.message.utf8), -// withKey: [UInt8](test.key.utf8) -// ).hexString.lowercased() -// XCTAssertEqual(result, test.expected.lowercased()) -// } -// } + func testHMAC() throws { + let tests: [(key: String, message: String, expected: String)] = [ + ( + "vapor", + "hello", + "bbd98ab1dbed72cdf3e924ae7eaf7943" + ), + ( + "true", + "2+2=4", + "37bda9a2b521d4623883b3acb7d9c3f7" + ) + ] + + for test in tests { + let result = HMAC.authenticate( + [UInt8](test.message.utf8), + withKey: [UInt8](test.key.utf8) + ).hexString.lowercased() + XCTAssertEqual(result, test.expected.lowercased()) + } + } } diff --git a/Tests/CryptoKittenTests/PBKDF2Tests.swift b/Tests/CryptoKittenTests/PBKDF2Tests.swift index 3aa1865..56c5e0e 100644 --- a/Tests/CryptoKittenTests/PBKDF2Tests.swift +++ b/Tests/CryptoKittenTests/PBKDF2Tests.swift @@ -1,57 +1,50 @@ -//import XCTest -//import CryptoKitten -// -//class PBKDF2Tests: XCTestCase { -// static var allTests = [ -// ("testValidation", testValidation), -// ("testSHA1", testSHA1), -// ("testMD5", testMD5), -// ("testPerformance", testPerformance), -// ] -// -// func testValidation() throws { -// let result = try PBKDF2.deriveKey(fromPassword: [UInt8]("vapor".utf8), saltedWith: [UInt8]("V4P012".utf8), iteratingTimes: 1000, derivedKeyLength: 10) -// -// XCTAssert(try PBKDF2.validate(password: [UInt8]("vapor".utf8), saltedWith: [UInt8]("V4P012".utf8), against: result, iterating: 1000)) -// } -// -// func testSHA1() throws { -// // Source: PHP/produce_tests.php -// let tests: [(key: String, salt: String, expected: String, iterations: Int)] = [ -// ("password", "longsalt", "1712d0a135d5fcd98f00bb25407035c41f01086a", 1000), -// ("password2", "othersalt", "7a0363dd39e51c2cf86218038ad55f6fbbff6291", 1000), -// ("somewhatlongpasswordstringthatIwanttotest", "1", "8cba8dd99a165833c8d7e3530641c0ecddc6e48c", 1000), -// ("p", "somewhatlongsaltstringthatIwanttotest", "31593b82b859877ea36dc474503d073e6d56a33d", 1000), -// ] -// -// for test in tests { -// let result = try PBKDF2.deriveKey(fromPassword: [UInt8](test.key.utf8), saltedWith: [UInt8](test.salt.utf8), iteratingTimes: test.iterations).hexString.lowercased() -// -// XCTAssertEqual(result, test.expected.lowercased()) -// } -// } -// -// func testMD5() throws { -// // Source: PHP/produce_tests.php -// let tests: [(key: String, salt: String, expected: String, iterations: Int)] = [ -// ("password", "longsalt", "95d6567274c3ed283041d5135c798823", 1000), -// ("password2", "othersalt", "78e4d28875d6f3b92a01dbddc07370f1", 1000), -// ("somewhatlongpasswordstringthatIwanttotest", "1", "c91a23ffd2a352f0f49c6ce64146fc0a", 1000), -// ("p", "somewhatlongsaltstringthatIwanttotest", "4d0297fc7c9afd51038a0235926582bc", 1000), -// ] -// -// for test in tests { -// let result = try PBKDF2.deriveKey(fromPassword: [UInt8](test.key.utf8), saltedWith: [UInt8](test.salt.utf8), iteratingTimes: test.iterations).hexString.lowercased() -// -// XCTAssertEqual(result, test.expected.lowercased()) -// } -// } -// -// func testPerformance() { -// // ~0.137 release -// measure { -// _ = try! PBKDF2.deriveKey(fromPassword: [UInt8]("p".utf8), saltedWith: [UInt8]("somewhatlongsaltstringthatIwanttotest".utf8), iteratingTimes: 10_000) -// } -// } -//} +import XCTest +import CryptoKitten + +class PBKDF2Tests: XCTestCase { + static var allTests = [ + ("testSHA1", testSHA1), + ("testMD5", testMD5), + ("testPerformance", testPerformance), + ] + + func testSHA1() throws { + // Source: PHP/produce_tests.php + let tests: [(key: String, salt: String, expected: String, iterations: Int)] = [ + ("password", "longsalt", "1712d0a135d5fcd98f00bb25407035c41f01086a", 1000), + ("password2", "othersalt", "7a0363dd39e51c2cf86218038ad55f6fbbff6291", 1000), + ("somewhatlongpasswordstringthatIwanttotest", "1", "8cba8dd99a165833c8d7e3530641c0ecddc6e48c", 1000), + ("p", "somewhatlongsaltstringthatIwanttotest", "31593b82b859877ea36dc474503d073e6d56a33d", 1000), + ] + + for test in tests { + let result = try PBKDF2_HMAC.derive(fromPassword: [UInt8](test.key.utf8), saltedWith: [UInt8](test.salt.utf8), iterating: test.iterations, derivedKeyLength: SHA1.digestSize).hexString.lowercased() + + XCTAssertEqual(result, test.expected.lowercased()) + } + } + + func testMD5() throws { + // Source: PHP/produce_tests.php + let tests: [(key: String, salt: String, expected: String, iterations: Int)] = [ + ("password", "longsalt", "95d6567274c3ed283041d5135c798823", 1000), + ("password2", "othersalt", "78e4d28875d6f3b92a01dbddc07370f1", 1000), + ("somewhatlongpasswordstringthatIwanttotest", "1", "c91a23ffd2a352f0f49c6ce64146fc0a", 1000), + ("p", "somewhatlongsaltstringthatIwanttotest", "4d0297fc7c9afd51038a0235926582bc", 1000), + ] + + for test in tests { + let result = try PBKDF2_HMAC.derive(fromPassword: [UInt8](test.key.utf8), saltedWith: [UInt8](test.salt.utf8), iterating: test.iterations, derivedKeyLength: MD5.digestSize).hexString.lowercased() + + XCTAssertEqual(result, test.expected.lowercased()) + } + } + + func testPerformance() { + // ~0.137 release + measure { + _ = try! PBKDF2_HMAC.derive(fromPassword: [UInt8]("p".utf8), saltedWith: [UInt8]("somewhatlongsaltstringthatIwanttotest".utf8), iterating: 10_000, derivedKeyLength: SHA1.digestSize) + } + } +} diff --git a/Tests/CryptoKittenTests/SHA1Tests.swift b/Tests/CryptoKittenTests/SHA1Tests.swift index d5f97ab..b3fea63 100644 --- a/Tests/CryptoKittenTests/SHA1Tests.swift +++ b/Tests/CryptoKittenTests/SHA1Tests.swift @@ -5,7 +5,7 @@ class SHA1Tests: XCTestCase { static var allTests = [ ("testBasic", testBasic), ("testPerformance", testPerformance), -// ("testHMAC", testHMAC), + ("testHMAC", testHMAC), ] func testBasic() throws { @@ -59,33 +59,33 @@ class SHA1Tests: XCTestCase { _ = SHA1.hash(data) } -// func testHMAC() throws { -// let tests: [(key: String, message: String, expected: String)] = [ -// ( -// "vapor", -// "hello", -// "bb2a9aabb537902647f3f40bfecb679bf0d7d64b" -// ), -// ( -// "true", -// "2+2=4", -// "35836a9520eb061ad7e267ac37ab3ee1fafa6e4b" -// ) -// ] -// -// for test in tests { -// let result = HMAC.authenticate( -// message: [UInt8](test.message.utf8), -// withKey: [UInt8](test.key.utf8) -// ).hexString.lowercased() -// XCTAssertEqual(result, test.expected.lowercased()) -// } -// -// // Source: https://github.com/krzyzanowskim/CryptoSwift/blob/swift3-snapshots/CryptoSwiftTests/HMACTests.swift -// XCTAssertEqual( -// HMAC.authenticate(message: [], withKey: []), -// [0xfb,0xdb,0x1d,0x1b,0x18,0xaa,0x6c,0x08,0x32,0x4b,0x7d,0x64,0xb7,0x1f,0xb7,0x63,0x70,0x69,0x0e,0x1d] -// ) -// } + func testHMAC() throws { + let tests: [(key: String, message: String, expected: String)] = [ + ( + "vapor", + "hello", + "bb2a9aabb537902647f3f40bfecb679bf0d7d64b" + ), + ( + "true", + "2+2=4", + "35836a9520eb061ad7e267ac37ab3ee1fafa6e4b" + ) + ] + + for test in tests { + let result = HMAC.authenticate( + [UInt8](test.message.utf8), + withKey: [UInt8](test.key.utf8) + ).hexString.lowercased() + XCTAssertEqual(result, test.expected.lowercased()) + } + + // Source: https://github.com/krzyzanowskim/CryptoSwift/blob/swift3-snapshots/CryptoSwiftTests/HMACTests.swift + XCTAssertEqual( + HMAC.authenticate([], withKey: []), + [0xfb,0xdb,0x1d,0x1b,0x18,0xaa,0x6c,0x08,0x32,0x4b,0x7d,0x64,0xb7,0x1f,0xb7,0x63,0x70,0x69,0x0e,0x1d] + ) + } }