-
Notifications
You must be signed in to change notification settings - Fork 198
New API Proposal: PBKDF2 #98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
01bebe2
ab4881f
b9b7fb2
51fda71
910fc84
f056b79
78f15ed
4e23a81
2c51e73
9af5c40
ee887f1
7e545be
5e95576
add4813
c96811a
746fd49
506278d
4fea9d5
f8b571b
f5785f4
867eace
589a5a6
ee2b43d
7bd24df
00205b2
3932d8a
635ea61
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Passphrase: DataProtocol, Salt: DataProtocol>(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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Passphrase: DataProtocol, Salt: DataProtocol>(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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Passphrase: DataProtocol, Salt: DataProtocol>(from password: Passphrase, salt: Salt, using hashFunction: HashFunction, outputByteCount: Int, rounds: Int) throws -> SymmetricKey { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This API generally looks good, but I'm wondering if we should add a minimum round count to avoid folks naively using the number If we have use-cases that absolutely need lower round counts I'd love to know, but those might be best served the way we serve RSA key sizes: with a second API that is a bit harder to justify typing.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do actually have such a use case. User+password authentication in Oracle typically encrypts the password with 4096 iterations. Hashes it alongside other data using SHA512, does a bit of CBC de- and encryption with it and some more data. And finally encrypts the end result using another 3 rounds of PBKDF2. (https://github.com/lovetodream/oracle-nio/blob/7fee78535f68c9f3a451beeed15483bc6ac2877b/Sources/OracleNIO/Messages/Coding/OracleFrontendMessageEncoder.swift#L926-L974) It might not be the same for every version of Oracle because this is controlled by the db server, but I confirmed it is the default behaviour with Oracle 23ai (newest release).
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ace, that's helpful. So I think that this then suggests we want two APIs, one like the one here and one closer that maybe uses the label My desire is to enable the use-case you have, while strongly discouraging users who pick up PBKDF2 without much thought from putting in extraordinarily low values.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good to me 👍
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added the If we now have the unsafe API method, is the 1000 rounds requirement enough though, as it was proposed way back in original RFC with intent to increase number of rounds over time?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably not. OWASP suggests a minimum round count of 210k for SHA512-based PBKDF2, and so maybe we should consider that the lower bound. |
||
| 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<Passphrase: DataProtocol, Salt: DataProtocol>(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 | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Passphrase: DataProtocol, Salt: DataProtocol>(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) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Passphrase: DataProtocol, Salt: DataProtocol>(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) | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.