Skip to content

Commit

Permalink
New API Proposal: Password based KDF (#98)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
admkopec authored Oct 18, 2024
1 parent 4518e5e commit 7cb6113
Show file tree
Hide file tree
Showing 13 changed files with 719 additions and 1 deletion.
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)
}
}

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 {
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

0 comments on commit 7cb6113

Please sign in to comment.