From b07ec88ff2195f90b49502e957ceb46a1c0a3bc5 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Mon, 25 Mar 2019 15:51:43 +0100 Subject: [PATCH] Add passphrase parameter to ConfigurationParser Use it to decrypt encrypted PEMs. --- .../Sources/Core/ConfigurationParser.swift | 40 +++++++++++++------ TunnelKit/Sources/Core/CryptoContainer.swift | 8 ++++ TunnelKitTests/ConfigurationParserTests.swift | 6 +++ 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/TunnelKit/Sources/Core/ConfigurationParser.swift b/TunnelKit/Sources/Core/ConfigurationParser.swift index 60e2c46..01640bb 100644 --- a/TunnelKit/Sources/Core/ConfigurationParser.swift +++ b/TunnelKit/Sources/Core/ConfigurationParser.swift @@ -114,25 +114,27 @@ public class ConfigurationParser { Parses an .ovpn file from an URL. - Parameter url: The URL of the configuration file. + - Parameter passphrase: The optional passphrase for encrypted data. - Parameter returnsStripped: When `true`, stores the stripped file into `ParsingResult.strippedLines`. Defaults to `false`. - Returns: The `ParsingResult` outcome of the parsing. - Throws: `ParsingError` if the configuration file is wrong or incomplete. */ - public static func parsed(fromURL url: URL, returnsStripped: Bool = false) throws -> ParsingResult { + public static func parsed(fromURL url: URL, passphrase: String? = nil, returnsStripped: Bool = false) throws -> ParsingResult { let lines = try String(contentsOf: url).trimmedLines() - return try parsed(fromLines: lines, originalURL: url, returnsStripped: returnsStripped) + return try parsed(fromLines: lines, passphrase: passphrase, originalURL: url, returnsStripped: returnsStripped) } /** Parses an .ovpn file as an array of lines. - Parameter lines: The array of lines holding the configuration. + - Parameter passphrase: The optional passphrase for encrypted data. - Parameter originalURL: The optional original URL of the configuration file. - Parameter returnsStripped: When `true`, stores the stripped file into `ParsingResult.strippedLines`. Defaults to `false`. - Returns: The `ParsingResult` outcome of the parsing. - Throws: `ParsingError` if the configuration file is wrong or incomplete. */ - public static func parsed(fromLines lines: [String], originalURL: URL? = nil, returnsStripped: Bool = false) throws -> ParsingResult { + public static func parsed(fromLines lines: [String], passphrase: String? = nil, originalURL: URL? = nil, returnsStripped: Bool = false) throws -> ParsingResult { var strippedLines: [String]? = returnsStripped ? [] : nil var warning: ParsingError? = nil @@ -209,10 +211,20 @@ public class ConfigurationParser { clientCertificate = CryptoContainer(pem: currentBlock.joined(separator: "\n")) case "key": + let isEncrypted = normalizeEncryptedPEMBlock(block: ¤tBlock) let container = CryptoContainer(pem: currentBlock.joined(separator: "\n")) - clientKey = container - if container.isEncrypted { - unsupportedError = ParsingError.unsupportedConfiguration(option: "encrypted client certificate key") + if isEncrypted { + guard let passphrase = passphrase else { + unsupportedError = ParsingError.unsupportedConfiguration(option: "encrypted client certificate key (missing passphrase)") + break + } + do { + clientKey = try container.decrypted(with: passphrase) + } catch let e { + unsupportedError = ParsingError.unsupportedConfiguration(option: e.localizedDescription) + } + } else { + clientKey = container } case "tls-auth": @@ -451,6 +463,16 @@ public class ConfigurationParser { warning: warning ) } + + private static func normalizeEncryptedPEMBlock(block: inout [String]) -> Bool { + + // XXX: restore blank line after encryption header (easier than tweaking trimmedLines) + if block.count >= 3 && block[1].contains("ENCRYPTED") { + block.insert("", at: 3) + return true + } + return false + } } private extension SocketType { @@ -472,9 +494,3 @@ extension String { } } } - -extension CryptoContainer { - var isEncrypted: Bool { - return pem.contains("ENCRYPTED") - } -} diff --git a/TunnelKit/Sources/Core/CryptoContainer.swift b/TunnelKit/Sources/Core/CryptoContainer.swift index c56ea6c..d556d34 100644 --- a/TunnelKit/Sources/Core/CryptoContainer.swift +++ b/TunnelKit/Sources/Core/CryptoContainer.swift @@ -36,6 +36,7 @@ // import Foundation +import __TunnelKitNative /// Represents a cryptographic container in PEM format. public struct CryptoContainer: Equatable { @@ -73,3 +74,10 @@ extension CryptoContainer: Codable { try container.encode(pem) } } + +extension CryptoContainer { + func decrypted(with passphrase: String) throws -> CryptoContainer { + let decryptedPEM = try TLSBox.decryptedPrivateKey(fromPEM: pem, passphrase: passphrase) + return CryptoContainer(pem: decryptedPEM) + } +} diff --git a/TunnelKitTests/ConfigurationParserTests.swift b/TunnelKitTests/ConfigurationParserTests.swift index 61bb49d..fec82bb 100644 --- a/TunnelKitTests/ConfigurationParserTests.swift +++ b/TunnelKitTests/ConfigurationParserTests.swift @@ -80,6 +80,12 @@ class ConfigurationParserTests: XCTestCase { XCTAssertThrowsError(try ConfigurationParser.parsed(fromLines: lines)) } + func testEncryptedCertificateKey() throws { + let url = Bundle(for: ConfigurationParserTests.self).url(forResource: "tunnelbear", withExtension: "enc.ovpn")! + XCTAssertThrowsError(try ConfigurationParser.parsed(fromURL: url)) + XCTAssertNoThrow(try ConfigurationParser.parsed(fromURL: url, passphrase: "foobar")) + } + private func url(withName name: String) -> URL { return Bundle(for: ConfigurationParserTests.self).url(forResource: name, withExtension: "ovpn")! }