diff --git a/Sources/_CryptoExtras/CMakeLists.txt b/Sources/_CryptoExtras/CMakeLists.txt index 35a46f807..b2d05631a 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 000000000..f07413e77 --- /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 000000000..5d3e02120 --- /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 000000000..36fc8d258 --- /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 000000000..3a8556537 --- /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 000000000..1076de1a2 --- /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 000000000..cae5f773e --- /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 000000000..7fae61a00 --- /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 000000000..28f828812 --- /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 52c52e9e4..2684a51c0 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 000000000..15cb30734 --- /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 000000000..52eff000b --- /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 000000000..6d770bae4 --- /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