From 7cb61135cb6401c1f15098250065b86d34cbadd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Kope=C4=87?= Date: Fri, 18 Oct 2024 19:16:05 +0200 Subject: [PATCH] New API Proposal: Password based KDF (#98) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: I've added a proposed implementation of PBKDF2, as per proposal in issue #59 to allow easy creation of SymmetricKey from relatively low entropy data sets, such as user provided passwords. Importance: This API change is attempting to simplify the creation of SymmetricKeys from user entered passwords. It uses an ageing algorithm – PBKDF2, but it is still widely supported and popular. A newer algorithm – scrypt is added as a better, more secure alternative. Modifications: I've proposed a new API: KDF namespace with Scrypt struct and an Insecure namespace with PBKDF2 struct with associated hash function and a single static method which takes a passphrase and salt, the size of the resulting key in bytes and optionally the number of rounds to use in the PBKDF2 algorithm. In the case of scrypt algorithm there is no associated hash function due to the nature of the algorithm, the single static method takes passphrase and salt, the size of the resulting key in bytes and optionally: the number of rounds, block size and parallelism factor. I implemented this functions using both CommonCrypto and BoringSSL where available. The function names and parameters are heavily influenced by current HKDF implementation. Additionally, I've added test cases to test the proposed API. I've decided to use test vectors described in RFC6070 for PBKDF2 and test vectors described in RFC7914 for scrypt. Result: This will add a new API to _CryptoKitExtras. It follows the naming conventions from CryptoKit's HKDF implementation. --- Sources/_CryptoExtras/CMakeLists.txt | 6 ++ .../_CryptoExtras/Key Derivation/KDF.swift | 29 ++++++ .../PBKDF2/BoringSSL/PBKDF2_boring.swift | 80 ++++++++++++++++ .../BoringSSL/PBKDF2_commoncrypto.swift | 85 +++++++++++++++++ .../Key Derivation/PBKDF2/PBKDF2.swift | 77 ++++++++++++++++ .../Scrypt/BoringSSL/Scrypt_boring.swift | 62 +++++++++++++ .../Key Derivation/Scrypt/Scrypt.swift | 41 +++++++++ Tests/_CryptoExtrasTests/PBKDF2Tests.swift | 91 +++++++++++++++++++ Tests/_CryptoExtrasTests/ScryptTests.swift | 87 ++++++++++++++++++ .../_CryptoExtrasTests/Utils/RFCVector.swift | 3 +- .../Utils/XCTestUtils.swift | 34 +++++++ .../rfc-6070-PBKDF2-SHA1.txt | 77 ++++++++++++++++ .../_CryptoExtrasVectors/rfc-7914-scrypt.txt | 48 ++++++++++ 13 files changed, 719 insertions(+), 1 deletion(-) create mode 100644 Sources/_CryptoExtras/Key Derivation/KDF.swift create mode 100644 Sources/_CryptoExtras/Key Derivation/PBKDF2/BoringSSL/PBKDF2_boring.swift create mode 100644 Sources/_CryptoExtras/Key Derivation/PBKDF2/BoringSSL/PBKDF2_commoncrypto.swift create mode 100644 Sources/_CryptoExtras/Key Derivation/PBKDF2/PBKDF2.swift create mode 100644 Sources/_CryptoExtras/Key Derivation/Scrypt/BoringSSL/Scrypt_boring.swift create mode 100644 Sources/_CryptoExtras/Key Derivation/Scrypt/Scrypt.swift create mode 100644 Tests/_CryptoExtrasTests/PBKDF2Tests.swift create mode 100644 Tests/_CryptoExtrasTests/ScryptTests.swift create mode 100644 Tests/_CryptoExtrasTests/Utils/XCTestUtils.swift create mode 100644 Tests/_CryptoExtrasVectors/rfc-6070-PBKDF2-SHA1.txt create mode 100644 Tests/_CryptoExtrasVectors/rfc-7914-scrypt.txt diff --git a/Sources/_CryptoExtras/CMakeLists.txt b/Sources/_CryptoExtras/CMakeLists.txt index 35a46f80..b2d05631 100644 --- a/Sources/_CryptoExtras/CMakeLists.txt +++ b/Sources/_CryptoExtras/CMakeLists.txt @@ -15,6 +15,12 @@ add_library(_CryptoExtras "ChaCha20CTR/BoringSSL/ChaCha20CTR_boring.swift" "ChaCha20CTR/ChaCha20CTR.swift" + "Key Derivation/KDF.swift" + "Key Derivation/PBKDF2/BoringSSL/PBKDF2_boring.swift" + "Key Derivation/PBKDF2/BoringSSL/PBKDF2_commoncrypto.swift" + "Key Derivation/PBKDF2/PBKDF2.swift" + "Key Derivation/Scrypt/BoringSSL/Scrypt_boring.swift" + "Key Derivation/Scrypt/Scrypt.swift" "RSA/RSA+BlindSigning.swift" "RSA/RSA.swift" "RSA/RSA_boring.swift" diff --git a/Sources/_CryptoExtras/Key Derivation/KDF.swift b/Sources/_CryptoExtras/Key Derivation/KDF.swift new file mode 100644 index 00000000..f07413e7 --- /dev/null +++ b/Sources/_CryptoExtras/Key Derivation/KDF.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import Crypto +#if canImport(Darwin) || swift(>=5.9.1) +import Foundation +#else +@preconcurrency import Foundation +#endif + +/// A container for Key Detivation Function algorithms. +public enum KDF: Sendable { + /// A container for older, cryptographically insecure algorithms. + /// + /// - Important: These algorithms aren’t considered cryptographically secure, + /// but the framework provides them for backward compatibility with older + /// services that require them. For new services, avoid these algorithms. + public enum Insecure: Sendable {} +} diff --git a/Sources/_CryptoExtras/Key Derivation/PBKDF2/BoringSSL/PBKDF2_boring.swift b/Sources/_CryptoExtras/Key Derivation/PBKDF2/BoringSSL/PBKDF2_boring.swift new file mode 100644 index 00000000..5d3e0212 --- /dev/null +++ b/Sources/_CryptoExtras/Key Derivation/PBKDF2/BoringSSL/PBKDF2_boring.swift @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2021-2024 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import Crypto +#if canImport(Darwin) || swift(>=5.9.1) +import Foundation +#else +@preconcurrency import Foundation +#endif + +#if !canImport(CommonCrypto) +@_implementationOnly import CCryptoBoringSSL +@_implementationOnly import CCryptoBoringSSLShims + +internal struct BoringSSLPBKDF2 { + /// Derives a secure key using the provided hash function, passphrase and salt. + /// + /// - Parameters: + /// - password: The passphrase, which should be used as a basis for the key. This can be any type that conforms to `DataProtocol`, like `Data` or an array of `UInt8` instances. + /// - salt: The salt to use for key derivation. + /// - outputByteCount: The length in bytes of resulting symmetric key. + /// - rounds: The number of rounds which should be used to perform key derivation. + /// - Returns: The derived symmetric key. + static func deriveKey(from password: Passphrase, salt: Salt, using hashFunction: KDF.Insecure.PBKDF2.HashFunction, outputByteCount: Int, rounds: Int) throws -> SymmetricKey { + // This should be SecureBytes, but we can't use that here. + var derivedKeyData = Data(count: outputByteCount) + + let rc = derivedKeyData.withUnsafeMutableBytes { derivedKeyBytes -> Int32 in + let saltBytes: ContiguousBytes = salt.regions.count == 1 ? salt.regions.first! : Array(salt) + return saltBytes.withUnsafeBytes { saltBytes -> Int32 in + let passwordBytes: ContiguousBytes = password.regions.count == 1 ? password.regions.first! : Array(password) + return passwordBytes.withUnsafeBytes { passwordBytes -> Int32 in + return CCryptoBoringSSL_PKCS5_PBKDF2_HMAC(passwordBytes.baseAddress!, passwordBytes.count, + saltBytes.baseAddress!, saltBytes.count, + UInt32(rounds), hashFunction.digest, + derivedKeyBytes.count, derivedKeyBytes.baseAddress!) + } + } + } + + guard rc == 1 else { + throw CryptoKitError.internalBoringSSLError() + } + + return SymmetricKey(data: derivedKeyData) + } +} + +extension KDF.Insecure.PBKDF2.HashFunction { + var digest: OpaquePointer { + switch self { + case .insecureMD5: + return CCryptoBoringSSL_EVP_md5() + case .insecureSHA1: + return CCryptoBoringSSL_EVP_sha1() + case .insecureSHA224: + return CCryptoBoringSSL_EVP_sha224() + case .sha256: + return CCryptoBoringSSL_EVP_sha256() + case .sha384: + return CCryptoBoringSSL_EVP_sha384() + case .sha512: + return CCryptoBoringSSL_EVP_sha512() + default: + preconditionFailure("Unsupported hash function: \(self.rawValue)") + } + } +} + +#endif diff --git a/Sources/_CryptoExtras/Key Derivation/PBKDF2/BoringSSL/PBKDF2_commoncrypto.swift b/Sources/_CryptoExtras/Key Derivation/PBKDF2/BoringSSL/PBKDF2_commoncrypto.swift new file mode 100644 index 00000000..36fc8d25 --- /dev/null +++ b/Sources/_CryptoExtras/Key Derivation/PBKDF2/BoringSSL/PBKDF2_commoncrypto.swift @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2021-2024 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import Crypto +#if canImport(Darwin) || swift(>=5.9.1) +import Foundation +#else +@preconcurrency import Foundation +#endif + +#if canImport(CommonCrypto) +@_implementationOnly import CommonCrypto + +internal struct CommonCryptoPBKDF2 { + /// Derives a secure key using the provided hash function, passphrase and salt. + /// + /// - Parameters: + /// - password: The passphrase, which should be used as a basis for the key. This can be any type that conforms to `DataProtocol`, like `Data` or an array of `UInt8` instances. + /// - salt: The salt to use for key derivation. + /// - outputByteCount: The length in bytes of resulting symmetric key. + /// - rounds: The number of rounds which should be used to perform key derivation. + /// - Returns: The derived symmetric key. + static func deriveKey(from password: Passphrase, salt: Salt, using hashFunction: KDF.Insecure.PBKDF2.HashFunction, outputByteCount: Int, rounds: Int) throws -> SymmetricKey { + // This should be SecureBytes, but we can't use that here. + var derivedKeyData = Data(count: outputByteCount) + + let derivationStatus = derivedKeyData.withUnsafeMutableBytes { derivedKeyBytes -> Int32 in + let saltBytes: ContiguousBytes = salt.regions.count == 1 ? salt.regions.first! : Array(salt) + return saltBytes.withUnsafeBytes { saltBytes -> Int32 in + let passwordBytes: ContiguousBytes = password.regions.count == 1 ? password.regions.first! : Array(password) + return passwordBytes.withUnsafeBytes { passwordBytes -> Int32 in + return CCKeyDerivationPBKDF( + CCPBKDFAlgorithm(kCCPBKDF2), + passwordBytes.baseAddress!, + passwordBytes.count, + saltBytes.baseAddress!, + saltBytes.count, + hashFunction.ccHash, + UInt32(rounds), + derivedKeyBytes.baseAddress!, + derivedKeyBytes.count) + } + } + } + + if derivationStatus != kCCSuccess { + throw CryptoKitError.underlyingCoreCryptoError(error: derivationStatus) + } + + return SymmetricKey(data: derivedKeyData) + } +} + +extension KDF.Insecure.PBKDF2.HashFunction { + var ccHash: CCPBKDFAlgorithm { + switch self { + case .insecureMD5: + return CCPBKDFAlgorithm(kCCHmacAlgMD5) + case .insecureSHA1: + return CCPBKDFAlgorithm(kCCPRFHmacAlgSHA1) + case .insecureSHA224: + return CCPBKDFAlgorithm(kCCPRFHmacAlgSHA224) + case .sha256: + return CCPBKDFAlgorithm(kCCPRFHmacAlgSHA256) + case .sha384: + return CCPBKDFAlgorithm(kCCPRFHmacAlgSHA384) + case .sha512: + return CCPBKDFAlgorithm(kCCPRFHmacAlgSHA512) + default: + preconditionFailure("Unsupported hash function: \(self.rawValue)") + } + } +} + +#endif diff --git a/Sources/_CryptoExtras/Key Derivation/PBKDF2/PBKDF2.swift b/Sources/_CryptoExtras/Key Derivation/PBKDF2/PBKDF2.swift new file mode 100644 index 00000000..3a855653 --- /dev/null +++ b/Sources/_CryptoExtras/Key Derivation/PBKDF2/PBKDF2.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2021-2024 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import Crypto +#if canImport(Darwin) || swift(>=5.9.1) +import Foundation +#else +@preconcurrency import Foundation +#endif + +#if canImport(CommonCrypto) +fileprivate typealias BackingPBKDF2 = CommonCryptoPBKDF2 +#else +fileprivate typealias BackingPBKDF2 = BoringSSLPBKDF2 +#endif + +extension KDF.Insecure { + /// An implementation of PBKDF2 key derivation function. + public struct PBKDF2: Sendable { + /// Derives a symmetric key using the PBKDF2 algorithm. + /// + /// - Parameters: + /// - password: The passphrase, which should be used as a basis for the key. This can be any type that conforms to `DataProtocol`, like `Data` or an array of `UInt8` instances. + /// - salt: The salt to use for key derivation. + /// - hashFunction: The hash function to use for key derivation. + /// - outputByteCount: The length in bytes of resulting symmetric key. + /// - rounds: The number of rounds which should be used to perform key derivation. The minimum allowed number of rounds is 210,000. + /// - Throws: An error if the number of rounds is less than 210,000 + /// - Note: The correct choice of rounds depends on a number of factors such as the hash function used, the speed of the target machine, and the intended use of the derived key. A good rule of thumb is to use rounds in the hundered of thousands or millions. For more information see OWASP's [Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html). + /// - Returns: The derived symmetric key. + public static func deriveKey(from password: Passphrase, salt: Salt, using hashFunction: HashFunction, outputByteCount: Int, rounds: Int) throws -> SymmetricKey { + guard rounds >= 210_000 else { + throw CryptoKitError.incorrectParameterSize + } + return try PBKDF2.deriveKey(from: password, salt: salt, using: hashFunction, outputByteCount: outputByteCount, unsafeUncheckedRounds: rounds) + } + + /// Derives a symmetric key using the PBKDF2 algorithm. + /// + /// - Parameters: + /// - password: The passphrase, which should be used as a basis for the key. This can be any type that conforms to `DataProtocol`, like `Data` or an array of `UInt8` instances. + /// - salt: The salt to use for key derivation. + /// - hashFunction: The hash function to use for key derivation. + /// - outputByteCount: The length in bytes of resulting symmetric key. + /// - unsafeUncheckedRounds: The number of rounds which should be used to perform key derivation. + /// - Warning: This method allows the use of parameters which may result in insecure keys. It is important to ensure that the used parameters do not compromise the security of the application. + /// - Returns: The derived symmetric key. + public static func deriveKey(from password: Passphrase, salt: Salt, using hashFunction: HashFunction, outputByteCount: Int, unsafeUncheckedRounds: Int) throws -> SymmetricKey { + return try BackingPBKDF2.deriveKey(from: password, salt: salt, using: hashFunction, outputByteCount: outputByteCount, rounds: unsafeUncheckedRounds) + } + + public struct HashFunction: Equatable, Hashable, Sendable { + let rawValue: String + + public static let insecureMD5 = HashFunction(rawValue: "insecure_md5") + public static let insecureSHA1 = HashFunction(rawValue: "insecure_sha1") + public static let insecureSHA224 = HashFunction(rawValue: "insecure_sha224") + public static let sha256 = HashFunction(rawValue: "sha256") + public static let sha384 = HashFunction(rawValue: "sha384") + public static let sha512 = HashFunction(rawValue: "sha512") + + init(rawValue: String) { + self.rawValue = rawValue + } + } + } +} diff --git a/Sources/_CryptoExtras/Key Derivation/Scrypt/BoringSSL/Scrypt_boring.swift b/Sources/_CryptoExtras/Key Derivation/Scrypt/BoringSSL/Scrypt_boring.swift new file mode 100644 index 00000000..1076de1a --- /dev/null +++ b/Sources/_CryptoExtras/Key Derivation/Scrypt/BoringSSL/Scrypt_boring.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Crypto +#if canImport(Darwin) || swift(>=5.9.1) +import Foundation +#else +@preconcurrency import Foundation +#endif +@_implementationOnly import CCryptoBoringSSL +@_implementationOnly import CCryptoBoringSSLShims + +internal struct BoringSSLScrypt { + /// Derives a secure key using the provided passphrase and salt. + /// + /// - Parameters: + /// - password: The passphrase, which should be used as a basis for the key. This can be any type that conforms to `DataProtocol`, like `Data` or an array of `UInt8` instances. + /// - salt: The salt to use for key derivation. + /// - outputByteCount: The length in bytes of resulting symmetric key. + /// - rounds: The number of rounds which should be used to perform key derivation. Must be a power of 2. + /// - blockSize: The block size to be used by the algorithm. + /// - parallelism: The parallelism factor indicating how many threads should be run in parallel. + /// - Returns: The derived symmetric key. + static func deriveKey(from password: Passphrase, salt: Salt, outputByteCount: Int, rounds: Int, blockSize: Int, parallelism: Int, maxMemory: Int? = nil) throws -> SymmetricKey { + // This should be SecureBytes, but we can't use that here. + var derivedKeyData = Data(count: outputByteCount) + + // This computes the maximum amount of memory that will be used by the scrypt algorithm with an additional memory page to spare. This value will be used by the BoringSSL as the memory limit for the algorithm. An additional memory page is added to the computed value (using POSIX specification) to ensure that the memory limit is not too tight. + let maxMemory = maxMemory ?? (128 * rounds * blockSize * parallelism + Int(sysconf(Int32(_SC_PAGESIZE)))) + + let result = derivedKeyData.withUnsafeMutableBytes { derivedKeyBytes -> Int32 in + let saltBytes: ContiguousBytes = salt.regions.count == 1 ? salt.regions.first! : Array(salt) + return saltBytes.withUnsafeBytes { saltBytes -> Int32 in + let passwordBytes: ContiguousBytes = password.regions.count == 1 ? password.regions.first! : Array(password) + return passwordBytes.withUnsafeBytes { passwordBytes -> Int32 in + return CCryptoBoringSSL_EVP_PBE_scrypt(passwordBytes.baseAddress!, passwordBytes.count, + saltBytes.baseAddress!, saltBytes.count, + UInt64(rounds), UInt64(blockSize), + UInt64(parallelism), maxMemory, + derivedKeyBytes.baseAddress!, derivedKeyBytes.count) + } + } + } + + guard result == 1 else { + throw CryptoKitError.internalBoringSSLError() + } + + return SymmetricKey(data: derivedKeyData) + } +} diff --git a/Sources/_CryptoExtras/Key Derivation/Scrypt/Scrypt.swift b/Sources/_CryptoExtras/Key Derivation/Scrypt/Scrypt.swift new file mode 100644 index 00000000..cae5f773 --- /dev/null +++ b/Sources/_CryptoExtras/Key Derivation/Scrypt/Scrypt.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import Crypto +#if canImport(Darwin) || swift(>=5.9.1) +import Foundation +#else +@preconcurrency import Foundation +#endif + +fileprivate typealias BackingScrypt = BoringSSLScrypt + +extension KDF { + /// An implementation of scrypt key derivation function. + public enum Scrypt: Sendable { + /// Derives a symmetric key using the scrypt algorithm. + /// + /// - Parameters: + /// - password: The passphrase, which should be used as a basis for the key. This can be any type that conforms to `DataProtocol`, like `Data` or an array of `UInt8` instances. + /// - salt: The salt to use for key derivation. + /// - outputByteCount: The length in bytes of resulting symmetric key. + /// - rounds: The number of rounds which should be used to perform key derivation. Must be a power of 2 less than `2^(128 * blockSize / 8)`. + /// - blockSize: The block size to use for key derivation. + /// - parallelism: The parallelism factor to use for key derivation. Must be a positive integer less than or equal to `((2^32 - 1) * 32) / (128 * blockSize)`. + /// - maxMemory: The maximum amount of memory allowed to use for key derivation. If not provided, the default value is computed for the provided parameters. + /// - Returns: The derived symmetric key. + public static func deriveKey(from password: Passphrase, salt: Salt, outputByteCount: Int, rounds: Int, blockSize: Int, parallelism: Int, maxMemory: Int? = nil) throws -> SymmetricKey { + return try BackingScrypt.deriveKey(from: password, salt: salt, outputByteCount: outputByteCount, rounds: rounds, blockSize: blockSize, parallelism: parallelism) + } + } +} diff --git a/Tests/_CryptoExtrasTests/PBKDF2Tests.swift b/Tests/_CryptoExtrasTests/PBKDF2Tests.swift new file mode 100644 index 00000000..7fae61a0 --- /dev/null +++ b/Tests/_CryptoExtrasTests/PBKDF2Tests.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2021-2024 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +import Crypto +@testable import _CryptoExtras + +// Test Vectors are coming from https://tools.ietf.org/html/rfc6070 +class PBKDF2Tests: XCTestCase { + struct RFCTestVector: Codable { + var hash: String + var inputSecret: [UInt8] + var salt: [UInt8] + var rounds: Int + var outputLength: Int + var derivedKey: [UInt8] + + enum CodingKeys: String, CodingKey { + case hash = "Hash" + case inputSecret = "P" + case salt = "S" + case rounds = "c" + case outputLength = "dkLen" + case derivedKey = "DK" + } + } + + func oneshotTesting(_ vector: RFCTestVector, hash: KDF.Insecure.PBKDF2.HashFunction) throws { + let (contiguousInput, discontiguousInput) = vector.inputSecret.asDataProtocols() + let (contiguousSalt, discontiguousSalt) = vector.salt.asDataProtocols() + + let DK1 = try KDF.Insecure.PBKDF2.deriveKey(from: contiguousInput, salt: contiguousSalt, using: hash, + outputByteCount: vector.outputLength, + unsafeUncheckedRounds: vector.rounds) + + let DK2 = try KDF.Insecure.PBKDF2.deriveKey(from: discontiguousInput, salt: contiguousSalt, using: hash, + outputByteCount: vector.outputLength, + unsafeUncheckedRounds: vector.rounds) + + let DK3 = try KDF.Insecure.PBKDF2.deriveKey(from: contiguousInput, salt: discontiguousSalt, using: hash, + outputByteCount: vector.outputLength, + unsafeUncheckedRounds: vector.rounds) + + let DK4 = try KDF.Insecure.PBKDF2.deriveKey(from: discontiguousInput, salt: discontiguousSalt, using: hash, + outputByteCount: vector.outputLength, + unsafeUncheckedRounds: vector.rounds) + + let expectedDK = SymmetricKey(data: vector.derivedKey) + XCTAssertEqual(DK1, expectedDK) + XCTAssertEqual(DK2, expectedDK) + XCTAssertEqual(DK3, expectedDK) + XCTAssertEqual(DK4, expectedDK) + } + + func testRFCVector(_ vector: RFCTestVector, hash: KDF.Insecure.PBKDF2.HashFunction) throws { + try oneshotTesting(vector, hash: hash) + } + + func testRfcTestVectorsSHA1() throws { + var decoder = try orFail { try RFCVectorDecoder(bundleType: self, fileName: "rfc-6070-PBKDF2-SHA1") } + let vectors = try orFail { try decoder.decode([RFCTestVector].self) } + + for vector in vectors { + precondition(vector.hash == "SHA-1") + try orFail { try self.testRFCVector(vector, hash: .insecureSHA1) } + } + } + + func testRoundsParameterCheck() { + let (contiguousInput, contiguousSalt) = (Data("password".utf8), Data("salt".utf8)) + + XCTAssertThrowsError(try KDF.Insecure.PBKDF2.deriveKey(from: contiguousInput, salt: contiguousSalt, using: .insecureSHA1, + outputByteCount: 20, rounds: 209_999)) + + XCTAssertNoThrow(try KDF.Insecure.PBKDF2.deriveKey(from: contiguousInput, salt: contiguousSalt, using: .insecureSHA1, + outputByteCount: 20, unsafeUncheckedRounds: 209_999)) + + XCTAssertNoThrow(try KDF.Insecure.PBKDF2.deriveKey(from: contiguousInput, salt: contiguousSalt, using: .insecureSHA1, + outputByteCount: 20, rounds: 210_000)) + } +} diff --git a/Tests/_CryptoExtrasTests/ScryptTests.swift b/Tests/_CryptoExtrasTests/ScryptTests.swift new file mode 100644 index 00000000..28f82881 --- /dev/null +++ b/Tests/_CryptoExtrasTests/ScryptTests.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +import Crypto +@testable import _CryptoExtras + +// Test Vectors are coming from https://tools.ietf.org/html/rfc7914 +class ScryptTests: XCTestCase { + struct RFCTestVector: Codable { + var inputSecret: [UInt8] + var salt: [UInt8] + var rounds: Int + var blockSize: Int + var parallelism: Int + var outputLength: Int + var derivedKey: [UInt8] + + enum CodingKeys: String, CodingKey { + case inputSecret = "P" + case salt = "S" + case rounds = "N" + case blockSize = "r" + case parallelism = "p" + case outputLength = "dkLen" + case derivedKey = "DK" + } + } + + func oneshotTesting(_ vector: RFCTestVector) throws { + let (contiguousInput, discontiguousInput) = vector.inputSecret.asDataProtocols() + let (contiguousSalt, discontiguousSalt) = vector.salt.asDataProtocols() + + let DK1 = try KDF.Scrypt.deriveKey(from: contiguousInput, salt: contiguousSalt, + outputByteCount: vector.outputLength, + rounds: vector.rounds, + blockSize: vector.blockSize, + parallelism: vector.parallelism) + + let DK2 = try KDF.Scrypt.deriveKey(from: discontiguousInput, salt: contiguousSalt, + outputByteCount: vector.outputLength, + rounds: vector.rounds, + blockSize: vector.blockSize, + parallelism: vector.parallelism) + + let DK3 = try KDF.Scrypt.deriveKey(from: contiguousInput, salt: discontiguousSalt, + outputByteCount: vector.outputLength, + rounds: vector.rounds, + blockSize: vector.blockSize, + parallelism: vector.parallelism) + + let DK4 = try KDF.Scrypt.deriveKey(from: discontiguousInput, salt: discontiguousSalt, + outputByteCount: vector.outputLength, + rounds: vector.rounds, + blockSize: vector.blockSize, + parallelism: vector.parallelism) + + let expectedDK = SymmetricKey(data: vector.derivedKey) + XCTAssertEqual(DK1, expectedDK) + XCTAssertEqual(DK2, expectedDK) + XCTAssertEqual(DK3, expectedDK) + XCTAssertEqual(DK4, expectedDK) + } + + func testRFCVector(_ vector: RFCTestVector) throws { + try oneshotTesting(vector) + } + + func testRfcTestVectors() throws { + var decoder = try orFail { try RFCVectorDecoder(bundleType: self, fileName: "rfc-7914-scrypt") } + let vectors = try orFail { try decoder.decode([RFCTestVector].self) } + + for vector in vectors { + try orFail { try self.testRFCVector(vector) } + } + } +} diff --git a/Tests/_CryptoExtrasTests/Utils/RFCVector.swift b/Tests/_CryptoExtrasTests/Utils/RFCVector.swift index 52c52e9e..2684a51c 100644 --- a/Tests/_CryptoExtrasTests/Utils/RFCVector.swift +++ b/Tests/_CryptoExtrasTests/Utils/RFCVector.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftCrypto open source project // -// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Copyright (c) 2019-2024 Apple Inc. and the SwiftCrypto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -68,6 +68,7 @@ extension RFCVectorDecoder: Decoder { return [] } + var userInfo: [CodingUserInfoKey: Any] { return [:] } func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key: CodingKey { diff --git a/Tests/_CryptoExtrasTests/Utils/XCTestUtils.swift b/Tests/_CryptoExtrasTests/Utils/XCTestUtils.swift new file mode 100644 index 00000000..15cb3073 --- /dev/null +++ b/Tests/_CryptoExtrasTests/Utils/XCTestUtils.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest + +// Xcode 11.4 catches errors thrown during tests and reports them on the +// correct line. But Linux and older Xcodes do not, so we need to use this +// wrapper as long as those platforms are supported. +func orFail(file: StaticString = #file, line: UInt = #line, _ closure: () throws -> T) throws -> T { + func wrapper(_ closure: () throws -> U, file: StaticString, line: UInt) throws -> U { + do { + return try closure() + } catch { + XCTFail("Function threw error: \(error)", file: file, line: line) + throw error + } + } + + if #available(macOS 10.15.4, macCatalyst 13.4, iOS 13.4, tvOS 13.4, watchOS 6.0, *) { + return try closure() + } else { + return try wrapper(closure, file: file, line: line) + } +} diff --git a/Tests/_CryptoExtrasVectors/rfc-6070-PBKDF2-SHA1.txt b/Tests/_CryptoExtrasVectors/rfc-6070-PBKDF2-SHA1.txt new file mode 100644 index 00000000..52eff000 --- /dev/null +++ b/Tests/_CryptoExtrasVectors/rfc-6070-PBKDF2-SHA1.txt @@ -0,0 +1,77 @@ +# A.1. Test Case 1 +# Basic test case with SHA-1 + +COUNT = 1 + +Hash = SHA-1 +P = 70617373776f7264 +S = 73616c74 +c = 1 +dkLen = 20 + +DK = 0c60c80f961f0e71f3a9b524af6012062fe037a6 + +# A.2. Test Case 2 +# Test with SHA-1 and larger rounds + +COUNT = 2 + +Hash = SHA-1 +P = 70617373776f7264 +S = 73616c74 +c = 2 +dkLen = 20 + +DK = ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957 + +# A.3. Test Case 3 +# Test with SHA-1 and even larger rounds + +COUNT = 3 + +Hash = SHA-1 +P = 70617373776f7264 +S = 73616c74 +c = 4096 +dkLen = 20 + +DK = 4b007901b765489abead49d926f721d065a429c1 + +# A.5. Test Case 5 +# Test with SHA-1 and huge rounds + +COUNT = 5 + +Hash = SHA-1 +P = 70617373776f7264 +S = 73616c74 +c = 16777216 +dkLen = 20 + +DK = eefe3d61cd4da4e4e9945b3d6ba2158c2634e984 + +# A.6. Test Case 6 +# Test with SHA-1 and longer inputs/outputs + +COUNT = 6 + +Hash = SHA-1 +P = 70617373776f726450415353574f524470617373776f7264 +S = 73616c7453414c5473616c7453414c5473616c7453414c5473616c7453414c5473616c74 +c = 4096 +dkLen = 25 + +DK = 3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038 + +# A.7. Test Case 7 +# Test with SHA-1 and NUL characters + +COUNT = 7 + +Hash = SHA-1 +P = 7061737300776f7264 +S = 7361006c74 +c = 4096 +dkLen = 16 + +DK = 56fa6aa75548099dcc37d7f03425e0c3 diff --git a/Tests/_CryptoExtrasVectors/rfc-7914-scrypt.txt b/Tests/_CryptoExtrasVectors/rfc-7914-scrypt.txt new file mode 100644 index 00000000..6d770bae --- /dev/null +++ b/Tests/_CryptoExtrasVectors/rfc-7914-scrypt.txt @@ -0,0 +1,48 @@ +# A.1. Test Case 1 + +COUNT = 1 + +P = +S = +N = 16 +r = 1 +p = 1 +dkLen = 64 +DK = 77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906 + +# A.2. Test Case 2 + +COUNT = 2 + +P = 70617373776f7264 +S = 4e61436c +N = 1024 +r = 8 +p = 16 +dkLen = 64 +DK = fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640 + +# A.3. Test Case 3 + +COUNT = 3 + +P = 706c656173656c65746d65696e +S = 536f6469756d43686c6f72696465 +N = 16384 +r = 8 +p = 1 +dkLen = 64 +DK = 7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887 + +# A.4. Test Case 4 + +COUNT = 4 + +P = 706c656173656c65746d65696e +S = 536f6469756d43686c6f72696465 +N = 1048576 +r = 8 +p = 1 +dkLen = 64 + +DK = 2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa478e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4