From 1c4a800cb3233b20c662f2a6b4c752692cf976b4 Mon Sep 17 00:00:00 2001 From: Kevin Wooten Date: Fri, 26 Jan 2024 20:43:45 -0700 Subject: [PATCH] Add support for importing/exporting PKCS8 key pairs --- Sources/ShieldSecurity/PEM.swift | 58 ++++++++++++++ Sources/ShieldSecurity/SecCertificate.swift | 25 +++--- Sources/ShieldSecurity/SecKeyPair.swift | 84 ++++++++++++++++++++- Tests/SecKeyPairTests.swift | 18 +++++ 4 files changed, 170 insertions(+), 15 deletions(-) create mode 100644 Sources/ShieldSecurity/PEM.swift diff --git a/Sources/ShieldSecurity/PEM.swift b/Sources/ShieldSecurity/PEM.swift new file mode 100644 index 000000000..8cc94032d --- /dev/null +++ b/Sources/ShieldSecurity/PEM.swift @@ -0,0 +1,58 @@ +// +// PEM.swift +// Shield +// +// Copyright © 2021 Outfox, inc. +// +// +// Distributed under the MIT License, See LICENSE for details. +// + +import Foundation +import Regex + +public enum PEM { + + public struct Kind: RawRepresentable { + + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + public static let certificate = Self(rawValue: "CERTIFICATE") + public static let pkcs8PrivateKey = Self(rawValue: "PRIVATE KEY") + + } + + private static let pemRegex = + Regex(#"-----BEGIN ([\w\s]+)-----\s*([a-zA-Z0-9\s/+]+=*)\s*-----END \1-----"#) + private static let pemWhitespaceRegex = Regex(#"[\n\t\s]+"#) + + public static func read(pem: String) -> [(Kind, Data)] { + + pemRegex.allMatches(in: pem) + .compactMap { match in + + guard + let kindCapture = match.captures.first, + let kind = kindCapture, + let dataCapture = match.captures.last, + let base64Data = dataCapture?.replacingAll(matching: pemWhitespaceRegex, with: ""), + let data = Data(base64Encoded: base64Data) + else { + return nil + } + + return (.init(rawValue: kind), data) + } + + } + + public static func write(kind: Kind, data: Data) -> String { + let pem = data.base64EncodedString().chunks(ofCount: 64).joined(separator: "\n") + return "-----BEGIN \(kind.rawValue)-----\n\(pem)\n-----END \(kind.rawValue)-----" + } + +} diff --git a/Sources/ShieldSecurity/SecCertificate.swift b/Sources/ShieldSecurity/SecCertificate.swift index 303cd9e68..ff8044ca1 100644 --- a/Sources/ShieldSecurity/SecCertificate.swift +++ b/Sources/ShieldSecurity/SecCertificate.swift @@ -227,8 +227,7 @@ public extension SecCertificate { } var pemEncoded: String { - let pem = derEncoded.base64EncodedString().chunks(ofCount: 64).joined(separator: "\n") - return "-----BEGIN CERTIFICATE-----\n\(pem)\n-----END CERTIFICATE-----" + PEM.write(kind: .certificate, data: derEncoded) } var derEncoded: Data { @@ -362,21 +361,19 @@ public extension SecCertificate { return try load(pem: certsPEM) } - private static let pemRegex = - Regex(#"-----BEGIN CERTIFICATE-----\s*([a-zA-Z0-9\s/+]+=*)\s*-----END CERTIFICATE-----"#) - private static let pemWhitespaceRegex = Regex(#"[\n\t\s]+"#) + static func load(pem: String, strict: Bool = false) throws -> [SecCertificate] { - static func load(pem: String) throws -> [SecCertificate] { + return try PEM.read(pem: pem) + .compactMap { (kind, data) in - return try pemRegex.allMatches(in: pem) - .map { match in + guard kind == .certificate else { + if strict { + throw SecCertificateError.loadFailed + } + return nil + } - guard - let capture = match.captures.first, - let base64Data = capture?.replacingAll(matching: pemWhitespaceRegex, with: ""), - let data = Data(base64Encoded: base64Data), - let cert = SecCertificateCreateWithData(nil, data as CFData) - else { + guard let cert = SecCertificateCreateWithData(nil, data as CFData) else { throw SecCertificateError.loadFailed } diff --git a/Sources/ShieldSecurity/SecKeyPair.swift b/Sources/ShieldSecurity/SecKeyPair.swift index 8d3c53f25..34485d65a 100644 --- a/Sources/ShieldSecurity/SecKeyPair.swift +++ b/Sources/ShieldSecurity/SecKeyPair.swift @@ -333,7 +333,7 @@ public struct SecKeyPair { case bits256 = 32 } - /// Encodes the key pair's private key in PKCS#8 format and then encrypts it using PBKDF and packages + /// Encodes the key pair's private key in PKCS#8 format and then encrypts it using PBKDF and packages it /// into PKCS#8 encrypted format. /// /// With the exported key and original password, ``import(data:password:)`` @@ -407,6 +407,34 @@ public struct SecKeyPair { return encryptedPrivateKeyInfoData } + /// Encodes the key pair's private key in PKCS#8 format and then encrypts it using PBKDF and packages it + /// into PKCS#8 encrypted format, and finally encodes in in PEM format. + /// + /// With the exported key and original password, ``import(data:password:)`` + /// can be used to recover the original `SecKey`. + /// + /// - Parameters: + /// - password: Password use for key encryption. + /// - derivedKeySize: PBKDF target key size. + /// - psuedoRandomAlgorithm: Which psuedo random algorithm should be used with PBKDF. + /// - keyDerivationTiming: Time PBKDF function should take to generate encryption key. + /// - Returns: Encrypted PKCS#8 encoded private key. + /// + public func exportPEM( + password: String, + derivedKeySize: ExportKeySize = exportDerivedKeySizeDefault, + psuedoRandomAlgorithm: PBKDF.PsuedoRandomAlgorithm = exportPsuedoRandomAlgorithmDefault, + keyDerivationTiming: TimeInterval = exportKeyDerivationTimingDefault + ) throws -> String { + + let der = try export(password: password, + derivedKeySize: derivedKeySize, + psuedoRandomAlgorithm: psuedoRandomAlgorithm, + keyDerivationTiming: keyDerivationTiming) + + return PEM.write(kind: .pkcs8PrivateKey, data: der) + } + /// Encodes the key pair's private key in PKCS#8 format. /// /// With the exported key and original password, ``import(data:password:)`` @@ -419,6 +447,19 @@ public struct SecKeyPair { return try privateKey.encodePKCS8() } + /// Encodes the key pair's private key in PKCS#8 format and encodes it in PEM. + /// + /// The exported key, ``import(data:)`` can be used to recover the original `SecKey`. + /// + /// - Returns: Encoded encrypted key and PBKDF paraemters. + /// + public func exportPEM() throws -> String { + + let data = try privateKey.encodePKCS8() + + return PEM.write(kind: .pkcs8PrivateKey, data: data) + } + /// Decrypts an encrypted PKCS#8 encrypted private key and builds a complete key pair. /// /// This is the reverse operation of ``export(password:derivedKeyLength:keyDerivationTiming:)``. @@ -435,6 +476,29 @@ public struct SecKeyPair { return try self.import(data: data, password: password) } + /// Decrypts a PEM guarded, encrypted, PKCS#8 encrypted private key and builds a complete key pair. + /// + /// This is the reverse operation of ``export(password:derivedKeyLength:keyDerivationTiming:)``. + /// + /// - Note: Only supports PKCS#8's PBES2 sceheme using PBKDF2 for key derivation. + /// + /// - Parameters: + /// - pem: PEM guarded exported private key. + /// - password: Password used during key export. + /// - Returns: ``SecKeyPair`` for the decrypted & decoded private key. + /// + public static func `import`(pem: String, password: String) throws -> SecKeyPair { + + guard + let (kind, data) = PEM.read(pem: pem).first, + kind == .pkcs8PrivateKey + else { + throw SecKeyPair.Error.invalidEncodedPrivateKey + } + + return try `import`(data: data, password: password) + } + /// Decrypts an encrypted PKCS#8 encrypted private key and builds a complete key pair. /// /// This is the reverse operation of ``export(password:derivedKeyLength:keyDerivationTiming:)``. @@ -502,6 +566,24 @@ public struct SecKeyPair { return try self.import(data: data) } + /// Decodes a PEM guarded PKCS#8 encoded private key and builds a complete key pair. + /// + /// - Parameters: + /// - pem: PEM guarded exported private key. + /// - Returns: ``SecKeyPair`` for the decrypted & decoded private key. + /// + public static func `import`(pem: String) throws -> SecKeyPair { + + guard + let (kind, data) = PEM.read(pem: pem).first, + kind == .pkcs8PrivateKey + else { + throw SecKeyPair.Error.invalidEncodedPrivateKey + } + + return try `import`(data: data) + } + /// Decodes a PKCS#8 encoded private key and builds a complete key pair. /// /// - Parameters: diff --git a/Tests/SecKeyPairTests.swift b/Tests/SecKeyPairTests.swift index 02298bb1e..a92ca6577 100644 --- a/Tests/SecKeyPairTests.swift +++ b/Tests/SecKeyPairTests.swift @@ -212,6 +212,24 @@ class SecKeyPairTests: XCTestCase { XCTAssertThrowsError(try SecKeyPair.import(data: exportedKeyData, password: "456")) } + func testImportExportPEM() throws { + keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256, flags: []) + + let exportedKeyData = try keyPair.exportPEM() + + XCTAssertNoThrow(try SecKeyPair.import(pem: exportedKeyData)) + } + + func testImportExportEncryptedPEM() throws { + keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256, flags: []) + + let exportedKeyData = try keyPair.exportPEM(password: "123") + + _ = try SecKeyPair.import(pem: exportedKeyData, password: "123") + + XCTAssertThrowsError(try SecKeyPair.import(pem: exportedKeyData, password: "456")) + } + func testImportExportEC192() throws { keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 192, flags: [])