Skip to content

Commit

Permalink
HMAC and PBKDF2
Browse files Browse the repository at this point in the history
  • Loading branch information
Joannis committed Aug 7, 2017
1 parent 0942aed commit db509cd
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 110 deletions.
39 changes: 39 additions & 0 deletions Sources/CryptoKitten/HMAC.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
public class HMAC<Variant: Hash> {
/// 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..<key.count {
outerPadding[i] = key[i] ^ outerPadding[i]
}

for i in 0..<key.count {
innerPadding[i] = key[i] ^ innerPadding[i]
}

// Hash the information
let innerPaddingHash: [UInt8] = Variant.hash(innerPadding + message)
let outerPaddingHash: [UInt8] = Variant.hash(outerPadding + innerPaddingHash)

return outerPaddingHash
}
}
1 change: 1 addition & 0 deletions Sources/CryptoKitten/Hash.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Foundation
public protocol Hash : class {
/// The amount of processed bytes per chunk
static var chunkSize: Int { get }
static var digestSize: Int { get }
static var littleEndian: Bool { get }

/// The current length of hashes bytes in bits
Expand Down
3 changes: 2 additions & 1 deletion Sources/CryptoKitten/MD5.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ fileprivate let k: [UInt32] = [ 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
fileprivate let chunkSize = 64

public final class MD5 : Hash {
public static var littleEndian = true
public static let littleEndian = true
public static let chunkSize = 64
public static let digestSize = 16

// The initial hash
var a0: UInt32 = 0x67452301
Expand Down
125 changes: 125 additions & 0 deletions Sources/CryptoKitten/PBKDF2_HMAC.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import Foundation

public enum PBKDF2Error: Error {
case cannotIterateZeroTimes
case cannotDeriveFromPassword([UInt8])
case cannotDeriveFromSalt([UInt8])
case keySizeTooBig(Int)
}

public final class PBKDF2_HMAC<Variant: Hash> {
/// 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..<password.count {
outerPadding[i] = password[i] ^ outerPadding[i]
}

for i in 0..<password.count {
innerPadding[i] = password[i] ^ innerPadding[i]
}

// This is where all the processing happens
let blocks = UInt32((keySize + Variant.digestSize - 1) / Variant.digestSize)
var response = [UInt8]()

// Loop over all blocks
for block in 1...blocks {
let s = salt + integerBytes(blockNum: block)

// Iterate the first time
var ui = try authenticate(innerPadding: innerPadding, outerPadding: outerPadding, message: s)
var u1 = ui

// Continue iterating for this block
for _ in 0..<iterations - 1 {
u1 = try authenticate(innerPadding: innerPadding, outerPadding: outerPadding, message: u1)
xor(&ui, u1)
}

// Append the response to be returned
response.append(contentsOf: ui)
}

return Array(response[0..<keySize])
}

public static func validate(_ password: [UInt8], saltedWith salt: [UInt8], against: [UInt8], iterating iterations: Int) throws -> 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..<lhs.count {
lhs[i] = lhs[i] ^ rhs[i]
}
}
1 change: 1 addition & 0 deletions Sources/CryptoKitten/SHA1.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
public final class SHA1 : Hash {
public static let digestSize = 20
public static let chunkSize = 64
public static let littleEndian = false

Expand Down
47 changes: 23 additions & 24 deletions Tests/CryptoKittenTests/MD5Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class MD5Tests: XCTestCase {
static var allTests = [
("testBasic", testBasic),
("testPerformance", testPerformance),
// ("testHMAC", testHMAC),
("testHMAC", testHMAC),
]

func testBasic() throws {
Expand Down Expand Up @@ -35,27 +35,26 @@ class MD5Tests: XCTestCase {
}
}

//
// 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<MD5>.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<MD5>.authenticate(
[UInt8](test.message.utf8),
withKey: [UInt8](test.key.utf8)
).hexString.lowercased()
XCTAssertEqual(result, test.expected.lowercased())
}
}
}
105 changes: 49 additions & 56 deletions Tests/CryptoKittenTests/PBKDF2Tests.swift
Original file line number Diff line number Diff line change
@@ -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<SHA1>.deriveKey(fromPassword: [UInt8]("vapor".utf8), saltedWith: [UInt8]("V4P012".utf8), iteratingTimes: 1000, derivedKeyLength: 10)
//
// XCTAssert(try PBKDF2<SHA1>.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<SHA1>.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<MD5>.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<SHA1>.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<SHA1>.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<MD5>.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<SHA1>.derive(fromPassword: [UInt8]("p".utf8), saltedWith: [UInt8]("somewhatlongsaltstringthatIwanttotest".utf8), iterating: 10_000, derivedKeyLength: SHA1.digestSize)
}
}
}

Loading

0 comments on commit db509cd

Please sign in to comment.