3
0
Fork 0
trifid_mobile/ios/NebulaNetworkExtension/Site.swift

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

509 lines
15 KiB
Swift
Raw Permalink Normal View History

2020-07-27 20:43:58 +00:00
import NetworkExtension
import MobileNebula
2021-04-27 15:29:28 +00:00
import SwiftyJSON
2020-07-27 20:43:58 +00:00
extension String: Error {}
2021-04-27 15:29:28 +00:00
enum IPCResponseType: String, Codable {
case error = "error"
case success = "success"
}
2020-07-27 20:43:58 +00:00
2021-04-27 15:29:28 +00:00
class IPCResponse: Codable {
var type: IPCResponseType
//TODO: change message to data?
var message: JSON?
2021-04-27 15:29:28 +00:00
init(type: IPCResponseType, message: JSON?) {
2020-07-27 20:43:58 +00:00
self.type = type
self.message = message
}
}
2021-04-27 15:29:28 +00:00
class IPCRequest: Codable {
var command: String
var arguments: JSON?
2021-04-27 15:29:28 +00:00
init(command: String, arguments: JSON?) {
self.command = command
2020-07-27 20:43:58 +00:00
self.arguments = arguments
}
2021-04-27 15:29:28 +00:00
init(command: String) {
self.command = command
2020-07-27 20:43:58 +00:00
}
}
struct CertificateInfo: Codable {
var cert: Certificate
var rawCert: String
var validity: CertificateValidity
2020-07-27 20:43:58 +00:00
enum CodingKeys: String, CodingKey {
case cert = "Cert"
case rawCert = "RawCert"
case validity = "Validity"
}
}
struct Certificate: Codable {
var fingerprint: String
var signature: String
var details: CertificateDetails
2022-11-22 16:20:35 +00:00
/// An empty initializer to make error reporting easier
2020-07-27 20:43:58 +00:00
init() {
fingerprint = ""
signature = ""
details = CertificateDetails()
}
}
struct CertificateDetails: Codable {
var name: String
var notBefore: String
var notAfter: String
var publicKey: String
var groups: [String]
var ips: [String]
var subnets: [String]
var isCa: Bool
var issuer: String
2022-11-22 16:20:35 +00:00
/// An empty initializer to make error reporting easier
2020-07-27 20:43:58 +00:00
init() {
name = ""
notBefore = ""
notAfter = ""
publicKey = ""
groups = []
ips = ["ERROR"]
subnets = []
isCa = false
issuer = ""
}
}
struct CertificateValidity: Codable {
var valid: Bool
var reason: String
2020-07-27 20:43:58 +00:00
enum CodingKeys: String, CodingKey {
case valid = "Valid"
case reason = "Reason"
}
}
let statusMap: Dictionary<NEVPNStatus, Bool> = [
NEVPNStatus.invalid: false,
NEVPNStatus.disconnected: false,
NEVPNStatus.connecting: false,
2020-07-27 20:43:58 +00:00
NEVPNStatus.connected: true,
NEVPNStatus.reasserting: true,
NEVPNStatus.disconnecting: true,
]
let statusString: Dictionary<NEVPNStatus, String> = [
NEVPNStatus.invalid: "Invalid configuration",
NEVPNStatus.disconnected: "Disconnected",
NEVPNStatus.connecting: "Connecting...",
NEVPNStatus.connected: "Connected",
NEVPNStatus.reasserting: "Reasserting...",
NEVPNStatus.disconnecting: "Disconnecting...",
]
// Represents a site that was pulled out of the system configuration
2021-04-27 15:29:28 +00:00
class Site: Codable {
2020-07-27 20:43:58 +00:00
// Stored in manager
var name: String
var id: String
2020-07-27 20:43:58 +00:00
// Stored in proto
var staticHostmap: Dictionary<String, StaticHosts>
var unsafeRoutes: [UnsafeRoute]
2023-12-05 15:24:19 +00:00
var dnsResolvers: [String]
2020-07-27 20:43:58 +00:00
var cert: CertificateInfo?
var ca: [CertificateInfo]
var lhDuration: Int
var port: Int
var mtu: Int
var cipher: String
var sortKey: Int
var logVerbosity: String
2021-04-27 15:29:28 +00:00
var connected: Bool? //TODO: active is a better name
2020-07-27 20:43:58 +00:00
var status: String?
var logFile: String?
var managed: Bool
// The following fields are present if managed = true
var lastManagedUpdate: String?
var rawConfig: String?
/// If true then this site needs to be migrated to the filesystem. Should be handled by the initiator of the site
var needsToMigrateToFS: Bool = false
2020-07-27 20:43:58 +00:00
// A list of error encountered when trying to rehydrate a site from config
var errors: [String]
2021-04-27 15:29:28 +00:00
var manager: NETunnelProviderManager?
var incomingSite: IncomingSite?
/// Creates a new site from a vpn manager instance. Mainly used by the UI. A manager is required to be able to edit the system profile
2021-04-27 15:29:28 +00:00
convenience init(manager: NETunnelProviderManager) throws {
2020-07-27 20:43:58 +00:00
//TODO: Throw an error and have Sites delete the site, notify the user instead of using !
let proto = manager.protocolConfiguration as! NETunnelProviderProtocol
try self.init(proto: proto)
self.manager = manager
self.connected = statusMap[manager.connection.status]
self.status = statusString[manager.connection.status]
}
2021-04-27 15:29:28 +00:00
convenience init(proto: NETunnelProviderProtocol) throws {
2020-07-27 20:43:58 +00:00
let dict = proto.providerConfiguration
if dict?["config"] != nil {
let config = dict?["config"] as? Data ?? Data()
let decoder = JSONDecoder()
let incoming = try decoder.decode(IncomingSite.self, from: config)
self.init(incoming: incoming)
self.needsToMigrateToFS = true
return
}
let id = dict?["id"] as? String ?? nil
if id == nil {
throw("Non-conforming site \(String(describing: dict))")
}
try self.init(path: SiteList.getSiteConfigFile(id: id!, createDir: false))
}
/// Creates a new site from a path on the filesystem. Mainly ussed by the VPN process or when in simulator where we lack a NEVPNManager
convenience init(path: URL) throws {
let config = try Data(contentsOf: path)
2020-07-27 20:43:58 +00:00
let decoder = JSONDecoder()
let incoming = try decoder.decode(IncomingSite.self, from: config)
2020-07-27 20:43:58 +00:00
self.init(incoming: incoming)
}
2020-07-27 20:43:58 +00:00
init(incoming: IncomingSite) {
var err: NSError?
incomingSite = incoming
2020-07-27 20:43:58 +00:00
errors = []
name = incoming.name
id = incoming.id
staticHostmap = incoming.staticHostmap
unsafeRoutes = incoming.unsafeRoutes ?? []
2023-12-05 15:24:19 +00:00
dnsResolvers = incoming.dnsResolvers ?? []
lhDuration = incoming.lhDuration
port = incoming.port
cipher = incoming.cipher
sortKey = incoming.sortKey ?? 0
logVerbosity = incoming.logVerbosity ?? "info"
mtu = incoming.mtu ?? 1300
managed = incoming.managed ?? false
lastManagedUpdate = incoming.lastManagedUpdate
rawConfig = incoming.rawConfig
2020-07-27 20:43:58 +00:00
do {
let rawCert = incoming.cert
let rawDetails = MobileNebulaParseCerts(rawCert, &err)
if (err != nil) {
throw err!
}
2020-07-27 20:43:58 +00:00
var certs: [CertificateInfo]
2020-07-27 20:43:58 +00:00
certs = try JSONDecoder().decode([CertificateInfo].self, from: rawDetails.data(using: .utf8)!)
if (certs.count == 0) {
throw "No certificate found"
}
cert = certs[0]
if (!cert!.validity.valid) {
errors.append("Certificate is invalid: \(cert!.validity.reason)")
}
2020-07-27 20:43:58 +00:00
} catch {
errors.append("Error while loading certificate: \(error.localizedDescription)")
}
2020-07-27 20:43:58 +00:00
do {
let rawCa = incoming.ca
let rawCaDetails = MobileNebulaParseCerts(rawCa, &err)
if (err != nil) {
throw err!
}
ca = try JSONDecoder().decode([CertificateInfo].self, from: rawCaDetails.data(using: .utf8)!)
2020-07-27 20:43:58 +00:00
var hasErrors = false
ca.forEach { cert in
if (!cert.validity.valid) {
hasErrors = true
}
}
if (hasErrors && !managed) {
2020-07-27 20:43:58 +00:00
errors.append("There are issues with 1 or more ca certificates")
}
2020-07-27 20:43:58 +00:00
} catch {
ca = []
errors.append("Error while loading certificate authorities: \(error.localizedDescription)")
}
do {
logFile = try SiteList.getSiteLogFile(id: self.id, createDir: true).path
} catch {
logFile = nil
errors.append("Unable to create the site directory: \(error.localizedDescription)")
}
if (managed && (try? getDNCredentials())?.invalid != false) {
errors.append("Unable to fetch managed updates - please re-enroll the device")
}
if (errors.isEmpty) {
do {
let encoder = JSONEncoder()
let rawConfig = try encoder.encode(incoming)
let key = try getKey()
let strConfig = String(data: rawConfig, encoding: .utf8)
var err: NSError?
MobileNebulaTestConfig(strConfig, key, &err)
if (err != nil) {
throw err!
}
} catch {
errors.append("Config test error: \(error.localizedDescription)")
}
}
2020-07-27 20:43:58 +00:00
}
2020-07-27 20:43:58 +00:00
// Gets the private key from the keystore, we don't always need it in memory
func getKey() throws -> String {
guard let keyData = KeyChain.load(key: "\(id).key") else {
throw "failed to get key from keychain"
2020-07-27 20:43:58 +00:00
}
//TODO: make sure this is valid on return!
return String(decoding: keyData, as: UTF8.self)
}
func getDNCredentials() throws -> DNCredentials {
if (!managed) {
throw "unmanaged site has no dn credentials"
}
let rawDNCredentials = KeyChain.load(key: "\(id).dnCredentials")
if rawDNCredentials == nil {
throw "failed to find dn credentials in keychain"
}
let decoder = JSONDecoder()
return try decoder.decode(DNCredentials.self, from: rawDNCredentials!)
}
func invalidateDNCredentials() throws {
let creds = try getDNCredentials()
creds.invalid = true
if (!(try creds.save(siteID: self.id))) {
throw "failed to store dn credentials in keychain"
}
}
func validateDNCredentials() throws {
let creds = try getDNCredentials()
creds.invalid = false
if (!(try creds.save(siteID: self.id))) {
throw "failed to store dn credentials in keychain"
}
}
func getConfig() throws -> Data {
return try self.incomingSite!.getConfig()
}
2020-07-27 20:43:58 +00:00
// Limits what we export to the UI
private enum CodingKeys: String, CodingKey {
case name
case id
case staticHostmap
case cert
case ca
case lhDuration
case port
case cipher
case sortKey
case connected
case status
case logFile
case unsafeRoutes
2023-12-05 15:24:19 +00:00
case dnsResolvers
2020-07-27 20:43:58 +00:00
case logVerbosity
case errors
case mtu
case managed
case lastManagedUpdate
case rawConfig
2020-07-27 20:43:58 +00:00
}
}
class StaticHosts: Codable {
var lighthouse: Bool
var destinations: [String]
}
class UnsafeRoute: Codable {
var route: String
var via: String
var mtu: Int?
}
class DNCredentials: Codable {
var hostID: String
var privateKey: String
var counter: Int
var trustedKeys: String
var invalid: Bool {
get { return _invalid ?? false }
set { _invalid = newValue }
}
private var _invalid: Bool?
func save(siteID: String) throws -> Bool {
let encoder = JSONEncoder()
let rawDNCredentials = try encoder.encode(self)
return KeyChain.save(key: "\(siteID).dnCredentials", data: rawDNCredentials, managed: true)
}
enum CodingKeys: String, CodingKey {
case hostID
case privateKey
case counter
case trustedKeys
case _invalid = "invalid"
}
}
2020-07-27 20:43:58 +00:00
// This class represents a site coming in from flutter, meant only to be saved and re-loaded as a proper Site
struct IncomingSite: Codable {
var name: String
var id: String
var staticHostmap: Dictionary<String, StaticHosts>
var unsafeRoutes: [UnsafeRoute]?
2023-12-05 15:24:19 +00:00
vat dnsResolvers: [String]?
2020-07-27 20:43:58 +00:00
var cert: String
var ca: String
var lhDuration: Int
var port: Int
var mtu: Int?
var cipher: String
var sortKey: Int?
var logVerbosity: String?
var key: String?
var managed: Bool?
// The following fields are present if managed = true
var dnCredentials: DNCredentials?
var lastManagedUpdate: String?
var rawConfig: String?
func getConfig() throws -> Data {
2020-07-27 20:43:58 +00:00
let encoder = JSONEncoder()
var config = self
config.key = nil
config.dnCredentials = nil
return try encoder.encode(config)
}
2020-07-27 20:43:58 +00:00
func save(manager: NETunnelProviderManager?, saveToManager: Bool = true, callback: @escaping (Error?) -> ()) {
let configPath: URL
do {
configPath = try SiteList.getSiteConfigFile(id: self.id, createDir: true)
} catch {
callback(error)
return
}
print("Saving to \(configPath)")
2020-07-27 20:43:58 +00:00
do {
if (self.key != nil) {
let data = self.key!.data(using: .utf8)
if (!KeyChain.save(key: "\(self.id).key", data: data!, managed: self.managed ?? false)) {
return callback("failed to store key material in keychain")
}
}
do {
if ((try self.dnCredentials?.save(siteID: self.id)) == false) {
return callback("failed to store dn credentials in keychain")
}
} catch {
return callback(error)
}
try self.getConfig().write(to: configPath)
2020-07-27 20:43:58 +00:00
} catch {
return callback(error)
}
#if targetEnvironment(simulator)
// We are on a simulator and there is no NEVPNManager for us to interact with
2020-07-27 20:43:58 +00:00
callback(nil)
#else
if saveToManager {
self.saveToManager(manager: manager, callback: callback)
} else {
callback(nil)
}
#endif
}
private func saveToManager(manager: NETunnelProviderManager?, callback: @escaping (Error?) -> ()) {
2020-07-27 20:43:58 +00:00
if (manager != nil) {
// We need to refresh our settings to properly update config
manager?.loadFromPreferences { error in
if (error != nil) {
return callback(error)
}
return self.finishSaveToManager(manager: manager!, callback: callback)
2020-07-27 20:43:58 +00:00
}
return
}
return finishSaveToManager(manager: NETunnelProviderManager(), callback: callback)
2020-07-27 20:43:58 +00:00
}
private func finishSaveToManager(manager: NETunnelProviderManager, callback: @escaping (Error?) -> ()) {
2020-07-27 20:43:58 +00:00
// Stuff our details in the protocol
let proto = manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol()
proto.providerConfiguration = ["id": self.id]
proto.serverAddress = "Nebula"
2020-07-27 20:43:58 +00:00
// Finish up the manager, this is what stores everything at the system level
manager.protocolConfiguration = proto
//TODO: cert name? manager.protocolConfiguration?.username
//TODO: This is what is shown on the vpn page. We should add more identifying details in
manager.localizedDescription = self.name
2020-07-27 20:43:58 +00:00
manager.isEnabled = true
manager.saveToPreferences{ error in
return callback(error)
}
}
}