From 12d30a20d7ca8dcd925cbdd596f9488aabf921e1 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sun, 25 Jan 2026 18:39:28 +0100 Subject: [PATCH 1/5] Add support for encrypted PEM keys via BoringSSL --- Sources/CryptoExtras/RSA/RSA.swift | 11 ++ Sources/CryptoExtras/RSA/RSA_boring.swift | 37 +++++ .../CryptoExtrasTests/EncryptedPEMTests.swift | 126 ++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 Tests/CryptoExtrasTests/EncryptedPEMTests.swift diff --git a/Sources/CryptoExtras/RSA/RSA.swift b/Sources/CryptoExtras/RSA/RSA.swift index 57b673db..74a1c4dc 100644 --- a/Sources/CryptoExtras/RSA/RSA.swift +++ b/Sources/CryptoExtras/RSA/RSA.swift @@ -183,6 +183,17 @@ extension _RSA.Signing { } } + /// Construct an RSA private key from an encrypted PEM representation. + /// + /// - Parameters: + /// - encryptedPEMRepresentation: The encrypted PEM representation of the private key. + /// - encryptionPassword: The password used to decrypt the PEM representation. + /// + /// - Throws: An error if the key could not be initialized. + public init(encryptedPEMRepresentation: String, encryptionPassword: String) throws { + self.backing = try BackingPrivateKey(encryptedPEMRepresentation: encryptedPEMRepresentation, encryptionPassword: encryptionPassword) + } + /// 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 diff --git a/Sources/CryptoExtras/RSA/RSA_boring.swift b/Sources/CryptoExtras/RSA/RSA_boring.swift index 038bd744..96a9bd0f 100644 --- a/Sources/CryptoExtras/RSA/RSA_boring.swift +++ b/Sources/CryptoExtras/RSA/RSA_boring.swift @@ -94,6 +94,13 @@ internal struct BoringSSLRSAPrivateKey: Sendable { self.backing = try Backing(pemRepresentation: pemRepresentation) } + init(encryptedPEMRepresentation: String, encryptionPassword: String) throws { + self.backing = try Backing( + encryptedPEMRepresentation: encryptedPEMRepresentation, + encryptionPassword: encryptionPassword + ) + } + init(derRepresentation: Bytes) throws { self.backing = try Backing(derRepresentation: derRepresentation) } @@ -604,6 +611,36 @@ extension BoringSSLRSAPrivateKey { CCryptoBoringSSL_EVP_PKEY_assign_RSA(self.pointer, rsaPrivateKey) } + fileprivate init(encryptedPEMRepresentation: String, encryptionPassword: String) 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 evpKey = encryptionPassword.withCString { passwordPtr in + CCryptoBoringSSL_PEM_read_bio_PrivateKey( + bio, + nil, + nil, + UnsafeMutableRawPointer(mutating: passwordPtr) + ) + } + + guard let evpKey else { + throw CryptoKitError.internalBoringSSLError() + } + defer { CCryptoBoringSSL_EVP_PKEY_free(evpKey) } + + guard let rsaKey = CCryptoBoringSSL_EVP_PKEY_get1_RSA(evpKey) else { + throw CryptoKitError.internalBoringSSLError() + } + + return rsaKey + } + } + CCryptoBoringSSL_EVP_PKEY_assign_RSA(self.pointer, rsaPrivateKey) + } + fileprivate convenience init(derRepresentation: Bytes) throws { if derRepresentation.regions.count == 1 { try self.init(contiguousDerRepresentation: derRepresentation.regions.first!) diff --git a/Tests/CryptoExtrasTests/EncryptedPEMTests.swift b/Tests/CryptoExtrasTests/EncryptedPEMTests.swift new file mode 100644 index 00000000..6e551d19 --- /dev/null +++ b/Tests/CryptoExtrasTests/EncryptedPEMTests.swift @@ -0,0 +1,126 @@ +//===----------------------------------------------------------------------===// +// +// 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, + encryptionPassword: "foobar" + ) + ) + XCTAssertThrowsError( + try _RSA.Signing.PrivateKey( + encryptedPEMRepresentation: pbes2WithAES128EncryptedPrivateKey, + encryptionPassword: "wrong" + ) + ) + } + + 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, + encryptionPassword: "foobar" + ) + ) + XCTAssertThrowsError( + try _RSA.Signing.PrivateKey( + encryptedPEMRepresentation: pbes2WithTripleDESEncryptedPrivateKey, + encryptionPassword: "wrong" + ) + ) + } +} From 9461fd3fa42233070842dcadc0d7e3bf15d01186 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Wed, 28 Jan 2026 11:28:24 +0100 Subject: [PATCH 2/5] Use `PEM_read_bio_RSAPrivateKey` --- Sources/CryptoExtras/RSA/RSA_boring.swift | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Sources/CryptoExtras/RSA/RSA_boring.swift b/Sources/CryptoExtras/RSA/RSA_boring.swift index 96a9bd0f..95ff4134 100644 --- a/Sources/CryptoExtras/RSA/RSA_boring.swift +++ b/Sources/CryptoExtras/RSA/RSA_boring.swift @@ -617,25 +617,19 @@ extension BoringSSLRSAPrivateKey { let rsaPrivateKey = try encryptedPEMRepresentation.withUTF8 { utf8Ptr in try BIOHelper.withReadOnlyMemoryBIO(wrapping: utf8Ptr) { bio in - let evpKey = encryptionPassword.withCString { passwordPtr in - CCryptoBoringSSL_PEM_read_bio_PrivateKey( + let key = encryptionPassword.withCString { passwordPtr in + CCryptoBoringSSL_PEM_read_bio_RSAPrivateKey( bio, nil, nil, UnsafeMutableRawPointer(mutating: passwordPtr) ) } - - guard let evpKey else { + guard let key else { throw CryptoKitError.internalBoringSSLError() } - defer { CCryptoBoringSSL_EVP_PKEY_free(evpKey) } - guard let rsaKey = CCryptoBoringSSL_EVP_PKEY_get1_RSA(evpKey) else { - throw CryptoKitError.internalBoringSSLError() - } - - return rsaKey + return key } } CCryptoBoringSSL_EVP_PKEY_assign_RSA(self.pointer, rsaPrivateKey) From 9fdb8068755002fb321fb5303f3b5327fdf61f94 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sat, 7 Feb 2026 18:10:23 +0100 Subject: [PATCH 3/5] Use passphrase callback from NIOSSL --- .../BoringSSLPassphraseCallbackManager.swift | 87 +++++++++++++++++++ Sources/CryptoExtras/RSA/RSA.swift | 38 ++++++-- Sources/CryptoExtras/RSA/RSA_boring.swift | 19 ++-- .../CryptoExtrasTests/EncryptedPEMTests.swift | 28 +++--- 4 files changed, 149 insertions(+), 23 deletions(-) create mode 100644 Sources/CryptoExtras/RSA/BoringSSLPassphraseCallbackManager.swift diff --git a/Sources/CryptoExtras/RSA/BoringSSLPassphraseCallbackManager.swift b/Sources/CryptoExtras/RSA/BoringSSLPassphraseCallbackManager.swift new file mode 100644 index 00000000..328c3c81 --- /dev/null +++ b/Sources/CryptoExtras/RSA/BoringSSLPassphraseCallbackManager.swift @@ -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) -> 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: CallbackManagerProtocol +where Bytes.Element == UInt8 { + private let userCallback: _RSA.Signing.PrivateKey.PassphraseCallback + + init(userCallback: @escaping _RSA.Signing.PrivateKey.PassphraseCallback) { + // We have to type-erase this. + self.userCallback = userCallback + } + + func invoke(buffer: UnsafeMutableBufferPointer) -> 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?, + 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.fromOpaque(userData).takeUnretainedValue() as? CallbackManagerProtocol + else { + preconditionFailure("Failed to pass object that can handle callback") + } + return cbManager.invoke(buffer: bufferPointer) +} diff --git a/Sources/CryptoExtras/RSA/RSA.swift b/Sources/CryptoExtras/RSA/RSA.swift index 74a1c4dc..50716725 100644 --- a/Sources/CryptoExtras/RSA/RSA.swift +++ b/Sources/CryptoExtras/RSA/RSA.swift @@ -183,15 +183,43 @@ 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 = (PassphraseSetter) 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) -> 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. - /// - encryptionPassword: The password used to decrypt the PEM representation. - /// - /// - Throws: An error if the key could not be initialized. - public init(encryptedPEMRepresentation: String, encryptionPassword: String) throws { - self.backing = try BackingPrivateKey(encryptedPEMRepresentation: encryptedPEMRepresentation, encryptionPassword: encryptionPassword) + /// - 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( + encryptedPEMRepresentation: String, + passphraseCallback: @escaping PassphraseCallback + ) throws where T.Element == UInt8 { + self.backing = try BackingPrivateKey( + encryptedPEMRepresentation: encryptedPEMRepresentation, + passphraseCallback: passphraseCallback + ) } /// Construct an RSA private key from a DER representation. diff --git a/Sources/CryptoExtras/RSA/RSA_boring.swift b/Sources/CryptoExtras/RSA/RSA_boring.swift index 95ff4134..dd967f2f 100644 --- a/Sources/CryptoExtras/RSA/RSA_boring.swift +++ b/Sources/CryptoExtras/RSA/RSA_boring.swift @@ -94,10 +94,14 @@ internal struct BoringSSLRSAPrivateKey: Sendable { self.backing = try Backing(pemRepresentation: pemRepresentation) } - init(encryptedPEMRepresentation: String, encryptionPassword: String) throws { + init( + encryptedPEMRepresentation: String, + passphraseCallback: @escaping _RSA.Signing.PrivateKey.PassphraseCallback + ) throws where T.Element == UInt8 { + let manager = BoringSSLPassphraseCallbackManager(userCallback: passphraseCallback) self.backing = try Backing( encryptedPEMRepresentation: encryptedPEMRepresentation, - encryptionPassword: encryptionPassword + callbackManager: manager ) } @@ -611,18 +615,21 @@ extension BoringSSLRSAPrivateKey { CCryptoBoringSSL_EVP_PKEY_assign_RSA(self.pointer, rsaPrivateKey) } - fileprivate init(encryptedPEMRepresentation: String, encryptionPassword: String) throws { + 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 = encryptionPassword.withCString { passwordPtr in + let key = withExtendedLifetime(callbackManager) { callbackManager -> OpaquePointer? in CCryptoBoringSSL_PEM_read_bio_RSAPrivateKey( bio, nil, - nil, - UnsafeMutableRawPointer(mutating: passwordPtr) + { globalBoringSSLPassphraseCallback(buf: $0, size: $1, rwflag: $2, u: $3) }, + Unmanaged.passUnretained(callbackManager as AnyObject).toOpaque() ) } guard let key else { diff --git a/Tests/CryptoExtrasTests/EncryptedPEMTests.swift b/Tests/CryptoExtrasTests/EncryptedPEMTests.swift index 6e551d19..2e5c795e 100644 --- a/Tests/CryptoExtrasTests/EncryptedPEMTests.swift +++ b/Tests/CryptoExtrasTests/EncryptedPEMTests.swift @@ -64,15 +64,17 @@ final class EncryptedPEMTests: XCTestCase { XCTAssertNoThrow( try _RSA.Signing.PrivateKey( - encryptedPEMRepresentation: pbes2WithAES128EncryptedPrivateKey, - encryptionPassword: "foobar" - ) + encryptedPEMRepresentation: pbes2WithAES128EncryptedPrivateKey + ) { passphraseSetter in + passphraseSetter("foobar".utf8) + } ) XCTAssertThrowsError( try _RSA.Signing.PrivateKey( - encryptedPEMRepresentation: pbes2WithAES128EncryptedPrivateKey, - encryptionPassword: "wrong" - ) + encryptedPEMRepresentation: pbes2WithAES128EncryptedPrivateKey + ) { passphraseSetter in + passphraseSetter("wrong".utf8) + } ) } @@ -112,15 +114,17 @@ final class EncryptedPEMTests: XCTestCase { XCTAssertNoThrow( try _RSA.Signing.PrivateKey( - encryptedPEMRepresentation: pbes2WithTripleDESEncryptedPrivateKey, - encryptionPassword: "foobar" - ) + encryptedPEMRepresentation: pbes2WithTripleDESEncryptedPrivateKey + ) { passphraseSetter in + passphraseSetter("foobar".utf8) + } ) XCTAssertThrowsError( try _RSA.Signing.PrivateKey( - encryptedPEMRepresentation: pbes2WithTripleDESEncryptedPrivateKey, - encryptionPassword: "wrong" - ) + encryptedPEMRepresentation: pbes2WithTripleDESEncryptedPrivateKey + ) { passphraseSetter in + passphraseSetter("wrong".utf8) + } ) } } From 05a5cafdd6fde5db51e62c74edf8105f453ef127 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sat, 7 Feb 2026 18:20:10 +0100 Subject: [PATCH 4/5] Update CMakeLists --- Sources/CryptoExtras/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/CryptoExtras/CMakeLists.txt b/Sources/CryptoExtras/CMakeLists.txt index e0810703..22bc012a 100644 --- a/Sources/CryptoExtras/CMakeLists.txt +++ b/Sources/CryptoExtras/CMakeLists.txt @@ -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" From 84ff8b8147ac382eed3a9db0c435e6c145c3be8c Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Thu, 12 Feb 2026 18:09:48 +0100 Subject: [PATCH 5/5] Format code --- .../CryptoExtrasTests/EncryptedPEMTests.swift | 152 +++++++++--------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/Tests/CryptoExtrasTests/EncryptedPEMTests.swift b/Tests/CryptoExtrasTests/EncryptedPEMTests.swift index 2e5c795e..73fc088e 100644 --- a/Tests/CryptoExtrasTests/EncryptedPEMTests.swift +++ b/Tests/CryptoExtrasTests/EncryptedPEMTests.swift @@ -18,49 +18,49 @@ import XCTest final class EncryptedPEMTests: XCTestCase { func testPBES2WithAES128EncryptedKeyInit() { let pbes2WithAES128EncryptedPrivateKey = """ - -----BEGIN ENCRYPTED PRIVATE KEY----- - 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 - -----END ENCRYPTED PRIVATE KEY----- - """ + -----BEGIN ENCRYPTED PRIVATE KEY----- + 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 + -----END ENCRYPTED PRIVATE KEY----- + """ XCTAssertNoThrow( try _RSA.Signing.PrivateKey( @@ -77,41 +77,41 @@ final class EncryptedPEMTests: XCTestCase { } ) } - + 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----- - """ - + -----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