Skip to content
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

New API Proposal: PBKDF2 #98

Merged
merged 27 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
01bebe2
Added a proposed PBKDF2 implementation
admkopec Sep 12, 2021
ab4881f
Tidied up a bit both implementations of PBKDF2
admkopec Sep 13, 2021
b9b7fb2
Rolled back previous changes and added test vectors
admkopec Sep 13, 2021
51fda71
Added additional tests for contiguous and discontiguous inputs
admkopec Sep 14, 2021
910fc84
Moved PBKDF2 implementation to `_CryptoExtras` under `KDF.Insecure` n…
admkopec Mar 13, 2024
f056b79
Added scrypt
admkopec Mar 14, 2024
78f15ed
Added a comment with explanation
admkopec Mar 14, 2024
4e23a81
Aligned folder structure
admkopec Mar 14, 2024
2c51e73
Aligned folder structure
admkopec Mar 14, 2024
9af5c40
Implemented most of the review recommendations
admkopec May 2, 2024
ee887f1
Dropped default parameter for PBKDF2 rounds
admkopec May 2, 2024
7e545be
Converted enum into struct (extensible enum)
admkopec Jun 12, 2024
5e95576
Added Equtable and Hashable conformance to HashFunction struct
admkopec Jun 12, 2024
add4813
Marked insecure HashFunctions as `insecure_` and fixed an issue which…
admkopec Jun 17, 2024
c96811a
Dropped the underscore from `insecure` prefix in HashFunctions struct
admkopec Jun 18, 2024
746fd49
Merge branch 'main' into stagging
admkopec Jun 23, 2024
506278d
Merge branch 'main' into stagging
admkopec Jun 30, 2024
4fea9d5
Merge branch 'main' into stagging
admkopec Jul 4, 2024
f8b571b
Merge branch 'main' into stagging
admkopec Aug 10, 2024
f5785f4
Added Sendable conformance and explicit Int32 conversion for sysconf …
admkopec Oct 12, 2024
867eace
Fixed wrong references to hash functions
admkopec Oct 12, 2024
589a5a6
Merge branch 'main' into stagging
admkopec Oct 12, 2024
ee2b43d
Changed Scrypt's type to enum
admkopec Oct 12, 2024
7bd24df
Added the proposed `unsafeUncheckedRounds` to PBKDF2 API method and a…
admkopec Oct 13, 2024
00205b2
Changed the minimum value of rounds parameter in PBKDF2 to 210,000 (a…
admkopec Oct 16, 2024
3932d8a
Merge branch 'main' into stagging
Lukasa Oct 18, 2024
635ea61
CONTRIBUTORS.md -> CONTRIBUTORS.txt
Lukasa Oct 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Sources/_CryptoExtras/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
29 changes: 29 additions & 0 deletions Sources/_CryptoExtras/Key Derivation/KDF.swift
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)
admkopec marked this conversation as resolved.
Show resolved Hide resolved
}
}

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
77 changes: 77 additions & 0 deletions Sources/_CryptoExtras/Key Derivation/PBKDF2/PBKDF2.swift
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 1. We should certainly add documentation suggesting a high round count (in the hundreds of thousands), but I can't see any world where we'd want to accept less than 1k.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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).

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 unsafeUncheckedRounds (or similar).

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the unsafeUncheckedRounds API method and a guard check in the regular API method to throw an incorrect parameter size error whenever the rounds is less than 1000. Please verify if this is acceptable.

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?

Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
}
}
41 changes: 41 additions & 0 deletions Sources/_CryptoExtras/Key Derivation/Scrypt/Scrypt.swift
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)
}
}
}
Loading