Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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)
}
)
}
}
Loading