Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions Sources/CryptoExtras/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ add_library(CryptoExtras
"OPRFs/VOPRF+API.swift"
"OPRFs/VOPRFClient.swift"
"OPRFs/VOPRFServer.swift"
"RSA/BoringSSLPassphraseCallbackManager.swift"
"RSA/RSA+BlindSigning.swift"
"RSA/RSA.swift"
"RSA/RSA_boring.swift"
Expand Down
87 changes: 87 additions & 0 deletions Sources/CryptoExtras/RSA/BoringSSLPassphraseCallbackManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftCrypto open source project
//
// Copyright (c) 2026 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
//
//===----------------------------------------------------------------------===//

/// An internal protocol that exists to let us avoid problems with generic types.
///
/// The issue we have here is that we want to allow users to use whatever collection type suits them best to set
/// the passphrase. For this reason, ``_RSA/Signing/PrivateKey/PassphraseSetter`` is a generic function, generic over the `Collection`
/// protocol. However, that causes us an issue, because we need to stuff that callback into a
/// ``BoringSSLPassphraseCallbackManager`` in order to create an `Unmanaged` and round-trip the pointer through C code.
///
/// That makes ``BoringSSLPassphraseCallbackManager`` a generic object, and now we're in *real* trouble, because
/// `Unmanaged` requires us to specify the *complete* type of the object we want to unwrap. In this case, we
/// don't know it, because it's generic!
///
/// Our way out is to note that while the class itself is generic, the only function we want to call in the
/// ``globalBoringSSLPassphraseCallback`` is not. Thus, rather than try to hold the actual specific ``BoringSSLPassphraseCallbackManager``,
/// we can hold it inside a protocol existential instead, so long as that protocol existential gives us the correct
/// function to call. Hence: ``CallbackManagerProtocol``, a private protocol with a single conforming type.
internal protocol CallbackManagerProtocol: AnyObject {
func invoke(buffer: UnsafeMutableBufferPointer<CChar>) -> CInt
}

/// This class exists primarily to work around the fact that Swift does not let us stuff
/// a closure into an `Unmanaged`. Instead, we use this object to keep hold of it.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
final class BoringSSLPassphraseCallbackManager<Bytes: Collection>: CallbackManagerProtocol
where Bytes.Element == UInt8 {
private let userCallback: _RSA.Signing.PrivateKey.PassphraseCallback<Bytes>

init(userCallback: @escaping _RSA.Signing.PrivateKey.PassphraseCallback<Bytes>) {
// We have to type-erase this.
self.userCallback = userCallback
}

func invoke(buffer: UnsafeMutableBufferPointer<CChar>) -> CInt {
var count: CInt = 0

do {
try self.userCallback { passphraseBytes in
// If we don't have enough space for the passphrase plus NUL, bail out.
guard passphraseBytes.count < buffer.count else { return }
_ = buffer.initialize(from: passphraseBytes.lazy.map { CChar($0) })
count = CInt(passphraseBytes.count)

// We need to add a NUL terminator, in case the user did not.
buffer[Int(passphraseBytes.count)] = 0
}
} catch {
// If we hit an error here, we just need to tolerate it. We'll return zero-length.
count = 0
}

return count
}
}

/// Our global static BoringSSL passphrase callback. This is used as a thunk to dispatch out to
/// the user-provided callback.
func globalBoringSSLPassphraseCallback(
buf: UnsafeMutablePointer<CChar>?,
size: CInt,
rwflag: CInt,
u: UnsafeMutableRawPointer?
) -> CInt {
guard let buffer = buf, let userData = u else {
preconditionFailure(
"Invalid pointers passed to passphrase callback, buf: \(String(describing: buf)) u: \(String(describing: u))"
)
}
let bufferPointer = UnsafeMutableBufferPointer(start: buffer, count: Int(size))
guard let cbManager = Unmanaged<AnyObject>.fromOpaque(userData).takeUnretainedValue() as? CallbackManagerProtocol
else {
preconditionFailure("Failed to pass object that can handle callback")
}
return cbManager.invoke(buffer: bufferPointer)
}
39 changes: 39 additions & 0 deletions Sources/CryptoExtras/RSA/RSA.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,45 @@ extension _RSA.Signing {
}
}

/// A ``_RSA/Signing/PrivateKey/PassphraseCallback`` is a callback that will be invoked by Swift Crypto when it needs to
/// get access to a private key that is stored in encrypted form.
///
/// This callback will be invoked with one argument, a non-escaping closure that must be called with the
/// passphrase. Failing to call the closure will cause decryption to fail.
///
/// The reason this design has been used is to allow you to secure any memory storing the passphrase after
/// use. We guarantee that after the ``_RSA/Signing/PrivateKey/PassphraseSetter`` closure has been invoked the `Collection`
/// you have passed in will no longer be needed by BoringSSL, and so you can safely destroy any memory it
/// may be using if you need to.
public typealias PassphraseCallback<Bytes: Collection> = (PassphraseSetter<Bytes>) throws -> Void where Bytes.Element == UInt8

/// An ``_RSA/Signing/PrivateKey/PassphraseSetter`` is a closure that you must invoke to provide a passphrase to BoringSSL.
/// It will be provided to you when your ``_RSA/Signing/PrivateKey/PassphraseCallback`` is invoked.
public typealias PassphraseSetter<Bytes: Collection> = (Bytes) -> Void where Bytes.Element == UInt8

/// Construct an RSA private key from an encrypted PEM representation.
///
/// This initializer accepts a callback that will be invoked with a closure that must be called
/// with the passphrase. This design allows you to securely manage passphrase memory after use.
/// After the ``_RSA/Signing/PrivateKey/PassphraseSetter`` closure has been invoked, the passphrase bytes you passed in
/// will no longer be needed, and you can safely destroy any memory it may be using.
///
/// - Parameters:
/// - encryptedPEMRepresentation: The encrypted PEM representation of the private key.
/// - passphraseCallback: A callback that will be invoked with a ``_RSA/Signing/PrivateKey/PassphraseSetter`` closure.
/// You must call the provided closure with the passphrase bytes.
///
/// - Throws: An error if the key could not be initialized or the passphrase is incorrect.
public init<T: Collection>(
encryptedPEMRepresentation: String,
passphraseCallback: @escaping PassphraseCallback<T>
) throws where T.Element == UInt8 {
self.backing = try BackingPrivateKey(
encryptedPEMRepresentation: encryptedPEMRepresentation,
passphraseCallback: passphraseCallback
)
}

/// Construct an RSA private key from a DER representation.
///
/// This constructor supports key sizes of 2048 bits or more. Users should validate that key sizes are appropriate
Expand Down
38 changes: 38 additions & 0 deletions Sources/CryptoExtras/RSA/RSA_boring.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ internal struct BoringSSLRSAPrivateKey: Sendable {
self.backing = try Backing(pemRepresentation: pemRepresentation)
}

init<T: Collection>(
encryptedPEMRepresentation: String,
passphraseCallback: @escaping _RSA.Signing.PrivateKey.PassphraseCallback<T>
) throws where T.Element == UInt8 {
let manager = BoringSSLPassphraseCallbackManager(userCallback: passphraseCallback)
self.backing = try Backing(
encryptedPEMRepresentation: encryptedPEMRepresentation,
callbackManager: manager
)
}

init<Bytes: DataProtocol>(derRepresentation: Bytes) throws {
self.backing = try Backing(derRepresentation: derRepresentation)
}
Expand Down Expand Up @@ -604,6 +615,33 @@ extension BoringSSLRSAPrivateKey {
CCryptoBoringSSL_EVP_PKEY_assign_RSA(self.pointer, rsaPrivateKey)
}

fileprivate init(
encryptedPEMRepresentation: String,
callbackManager: CallbackManagerProtocol
) throws {
var encryptedPEMRepresentation = encryptedPEMRepresentation
self.pointer = CCryptoBoringSSL_EVP_PKEY_new()

let rsaPrivateKey = try encryptedPEMRepresentation.withUTF8 { utf8Ptr in
try BIOHelper.withReadOnlyMemoryBIO(wrapping: utf8Ptr) { bio in
let key = withExtendedLifetime(callbackManager) { callbackManager -> OpaquePointer? in
CCryptoBoringSSL_PEM_read_bio_RSAPrivateKey(
bio,
nil,
{ globalBoringSSLPassphraseCallback(buf: $0, size: $1, rwflag: $2, u: $3) },
Unmanaged.passUnretained(callbackManager as AnyObject).toOpaque()
)
}
guard let key else {
throw CryptoKitError.internalBoringSSLError()
}

return key
}
}
CCryptoBoringSSL_EVP_PKEY_assign_RSA(self.pointer, rsaPrivateKey)
}

fileprivate convenience init<Bytes: DataProtocol>(derRepresentation: Bytes) throws {
if derRepresentation.regions.count == 1 {
try self.init(contiguousDerRepresentation: derRepresentation.regions.first!)
Expand Down
130 changes: 130 additions & 0 deletions Tests/CryptoExtrasTests/EncryptedPEMTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftCrypto open source project
//
// Copyright (c) 2026 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 CryptoExtras
import XCTest

final class EncryptedPEMTests: XCTestCase {
func testPBES2WithAES128EncryptedKeyInit() {
let pbes2WithAES128EncryptedPrivateKey = """
-----BEGIN ENCRYPTED PRIVATE KEY-----
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
-----END ENCRYPTED PRIVATE KEY-----
"""

XCTAssertNoThrow(
try _RSA.Signing.PrivateKey(
encryptedPEMRepresentation: pbes2WithAES128EncryptedPrivateKey
) { passphraseSetter in
passphraseSetter("foobar".utf8)
}
)
XCTAssertThrowsError(
try _RSA.Signing.PrivateKey(
encryptedPEMRepresentation: pbes2WithAES128EncryptedPrivateKey
) { passphraseSetter in
passphraseSetter("wrong".utf8)
}
)
}

func testPBES2WithTripleDESKeyEncryptedKeyInit() {
let pbes2WithTripleDESEncryptedPrivateKey = """
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFJDBWBgkqhkiG9w0BBQ0wSTAxBgkqhkiG9w0BBQwwJAQQ8HZLW3BDKXdsGjxA
5BM8GgICCAAwDAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQIUo0QnIb9O+wEggTI
GqGG0X9OWxs8opGqJ6ynfJzCUy1TJh9CGJgBBVOMS8zqz7qAkBCKhT+VPCtn7W0g
GTf+OhOkj7YnmN/GSwbih/O33NFXoVQrP+kJOTRYFne2zVQ5KvG48oN3P7T4tHMP
zRqq7+qpz6Y0906z/6RmVZWEPryAb0xYEd2DhdX4wBMyHfTf28u10ivEsfTWa5/5
/n4ENmwAce2MLUbvNGgtXvgbiDn5ITj17Reyal3hTzRoL3J6kLj6xFpBkaAAvvQP
O8FGaVuvi4seeWPVAwwuksRiCwA+wPi3eyREPwG8Q4tS2IKwJqUrbPjrIhxl7HwK
bb2iaQ+es+FZIHXHWvfWiEUyDs2OMcErlUqx8Qaf9K/3o8KFdyqZ7qOKNjK+Z0BC
AHelXjvO62N/sNoK8318LYOkCZ1Wd820JdSTac3AVy9BGQRu7GfhcpjjNbOxsjhz
HSnrZR8PIRNujTyLC8b2fzsTpDNLUE6KYiNzZWfUDOVMmm9xi64kwCMvsKsLd47n
4VdaPHaqqSA3XkXIDyqAZUKo6r2CUkJH6CYKuVLl6GsA6lLFxVCHtdbQu6MopymO
0+XkLTJrZItEB4ZIbtG88/ubnYOPqOn7Jvi7W8TEDBXw9inGO4osj7wSnWNEsTRx
8P/uF9ygpKTANuR84welaJk6c3pxf96esfmxkp7XxGdRx9o0OWbSqB1C4LUjWmKs
LpPF8TvnzFlZyfyW5VyzOs8/4zNO7B0S6X5Ywwytobo0G0/6/eilFIPGZfLTz7gw
2LPMYKgi+OjE27KGUS3fSDlVkcQqrfrADchtEM6bSYHU1B0K8QE2bkRVM97DRVTv
lngqxvr9yeE+ILCGOf/kTfqGqvoampUUUUMi8is80oSlSVApYiZ3uWJWOMsHlH7X
H1sONAARzhbm+BQ7QRFTH41mMmHIzNSuXVYItRxbC4VkbRqMzLCfZtsCCZ7Mupo7
a9FdDMDsLeA28EDTESzWEPREk4i0wvJ1QRLdQFJ9GL+RP/YsV1GEwRqHu9lsZCAL
Oz83V41/NfSuTrykZFKaLA2D4DjVGbyinxxcThUL/3u3k98EjBKdfMj6wMF8hKCx
eYvowNOJUdMG3+i7Bo5rKhKZ5mIeRP3MGvelvSQ8gXm03pM255iDV441Ir4F/mpJ
TaMXySqhZef4Ls6tkxsq7E2mXhsPkJVy/hSmnbqZi3FltMGvkbiM3aqNJQcG67mO
NX66Zlbb8JHi6OG8o7H3u6i3BTeyvQkPO0n+sUWrYo1vqemykDUHAdqLdSIdC4pb
kCvRncCw729CD1B3IVkvSZ6NTqNxGCqmc1g/6bkqCaNOXXOqiT4Fzxxv+FCt2sGf
m2G8BvdBVILRRVSGgmG7ahvIY5O+911duS6vkzoxF39VJjrYXcqzKRh71zfAM3J0
h9GLgrI+lZ7HYCi4eDsSMOdARfL6C7beA6Jaa4snHfGNNrwCECuV0zKrB61n33nN
wE1Nc+gPZ4rbYeYUa8EvdchNB5JdMTyKqOAHrHrM4EberwnZAZMk4Aal/PLAup5L
mrdalZF0qlLUetwUPmAGMuW34igiV084ecKxsuZWXvKtLTHhiTN4NYBgV2rvJ2LE
PRiLIoKv+M+qjywhjPeQbD70byOdIx5J
-----END ENCRYPTED PRIVATE KEY-----
"""

XCTAssertNoThrow(
try _RSA.Signing.PrivateKey(
encryptedPEMRepresentation: pbes2WithTripleDESEncryptedPrivateKey
) { passphraseSetter in
passphraseSetter("foobar".utf8)
}
)
XCTAssertThrowsError(
try _RSA.Signing.PrivateKey(
encryptedPEMRepresentation: pbes2WithTripleDESEncryptedPrivateKey
) { passphraseSetter in
passphraseSetter("wrong".utf8)
}
)
}
}