Skip to content

Commit

Permalink
Merge pull request #82 from outfoxx/feature/pkcs8pem
Browse files Browse the repository at this point in the history
Add support for importing/exporting PKCS8 key pairs
  • Loading branch information
kdubb committed Jan 27, 2024
2 parents 54a0696 + 1c4a800 commit 7d19bf6
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 15 deletions.
58 changes: 58 additions & 0 deletions Sources/ShieldSecurity/PEM.swift
Original file line number Diff line number Diff line change
@@ -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)-----"
}

}
25 changes: 11 additions & 14 deletions Sources/ShieldSecurity/SecCertificate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down
84 changes: 83 additions & 1 deletion Sources/ShieldSecurity/SecKeyPair.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:)``
Expand Down Expand Up @@ -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:)``
Expand All @@ -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:)``.
Expand All @@ -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:)``.
Expand Down Expand Up @@ -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:
Expand Down
18 changes: 18 additions & 0 deletions Tests/SecKeyPairTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [])

Expand Down

0 comments on commit 7d19bf6

Please sign in to comment.