passepartout-apple/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Strategies/DefaultUpgradeStrategy.swift

390 lines
13 KiB
Swift
Raw Normal View History

2022-04-12 13:09:14 +00:00
//
2023-05-24 16:19:47 +00:00
// DefaultUpgradeStrategy.swift
2022-04-12 13:09:14 +00:00
// Passepartout
//
// Created by Davide De Rosa on 3/20/22.
2023-03-17 15:56:19 +00:00
// Copyright (c) 2023 Davide De Rosa. All rights reserved.
2022-04-12 13:09:14 +00:00
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import Foundation
import GenericJSON
2022-06-23 21:31:01 +00:00
import PassepartoutCore
2023-05-24 16:19:47 +00:00
import PassepartoutProviders
import PassepartoutVPN
import TunnelKitCore
import TunnelKitManager
import TunnelKitOpenVPNCore
2022-04-12 13:09:14 +00:00
private typealias Map = [String: Any]
2023-05-24 16:19:47 +00:00
public final class DefaultUpgradeStrategy: UpgradeStrategy {
public init() {
}
}
// MARK: Migrate old store
2023-05-24 16:19:47 +00:00
extension DefaultUpgradeStrategy {
private enum LegacyStoreKey: String, KeyStoreLocation, CaseIterable {
case activeProfileId
2023-03-17 20:55:47 +00:00
case launchesOnLogin
2023-03-17 20:55:47 +00:00
case isStatusMenuEnabled
2023-03-17 20:55:47 +00:00
case isShowingFavorites
case confirmsQuit
2023-03-17 20:55:47 +00:00
case logFormat
2023-03-17 20:55:47 +00:00
case tunnelLogFormat
2023-03-17 20:55:47 +00:00
case masksPrivateData
2023-03-17 20:55:47 +00:00
case didHandleSubreddit
2023-03-17 20:55:47 +00:00
case persistenceAuthor
case didMigrateToV2
2023-03-17 20:55:47 +00:00
case other1 = "MasksPrivateData"
2023-03-17 20:55:47 +00:00
case other2 = "DidHandleSubreddit"
2023-03-17 20:55:47 +00:00
case other3 = "Convenience.Reviewer.LastVersion"
2023-03-17 20:55:47 +00:00
case other4 = "didMigrateKeychainContext"
2023-03-17 20:55:47 +00:00
var key: String {
rawValue
}
}
2023-03-17 20:55:47 +00:00
2023-05-24 16:19:47 +00:00
public func doMigrateStore(_ store: KeyValueStore, didMigrate: inout Bool) {
if !didMigrate {
guard let legacyDidMigrateToV2: Bool = store.value(forLocation: LegacyStoreKey.didMigrateToV2) else {
return
}
2023-05-24 16:19:47 +00:00
didMigrate = legacyDidMigrateToV2
}
2023-03-17 20:55:47 +00:00
LegacyStoreKey.allCases.forEach {
store.removeValue(forLocation: $0)
}
}
}
2022-04-12 13:09:14 +00:00
// MARK: Migrate to version 2
2023-05-24 16:19:47 +00:00
extension DefaultUpgradeStrategy {
2022-04-12 13:09:14 +00:00
fileprivate enum MigrationError: Error {
case json
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
case missingId
case missingOpenVPNConfiguration
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
case missingHostname
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
case missingEndpointProtocols
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
case missingProviderName
}
private var appGroup: String {
2022-09-04 18:09:31 +00:00
"group.com.algoritmico.Passepartout"
2022-04-12 13:09:14 +00:00
}
2023-05-24 16:19:47 +00:00
public func migratedProfilesToV2() -> [Profile] {
2022-04-12 13:09:14 +00:00
var migrated: [Profile] = []
pp_log.info("Migrating data to v2")
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
let fm = FileManager.default
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
guard let documents = fm.containerURL(forSecurityApplicationGroupIdentifier: appGroup)?
.appendingPathComponent("Documents") else {
pp_log.info("No data to migrate")
return []
}
let cs = documents.appendingPathComponent("ConnectionService.json")
let hostsFolder = documents.appendingPathComponent("Hosts")
let providersFolder = documents.appendingPathComponent("Providers")
do {
let csJSON = try cs.asJSON()
// pp_log.error(csJSON)
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
do {
var authUserPassUUIDs: Set<String> = []
for host in try fm.contentsOfDirectory(at: hostsFolder, includingPropertiesForKeys: nil) {
guard host.isFileURL && host.pathExtension == "ovpn" else {
continue
}
do {
let uuid = host.deletingPathExtension().lastPathComponent
let content = try String(contentsOf: host)
if content.contains("auth-user-pass") {
authUserPassUUIDs.insert(uuid)
}
} catch {
pp_log.warning("Unable to read host profile .ovpn: \(host)")
}
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
// print(">>> authUserPassUUIDs: \(authUserPassUUIDs)")
for host in try fm.contentsOfDirectory(at: hostsFolder, includingPropertiesForKeys: nil) {
guard host.isFileURL && host.pathExtension == "json" else {
continue
}
do {
let json = try host.asJSON()
// pp_log.error(json)
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
let result = try migratedV1Profile(csJSON, hostMap: json, authUserPass: authUserPassUUIDs)
// pp_log.info(result.profile)
// print(">>> Account: \(result.profile.username) -> \(result.password)")
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
migrated.append(result)
} catch {
pp_log.warning("Unable to migrate host profile: \(host)")
continue
}
}
} catch {
pp_log.warning(error)
}
do {
for provider in try fm.contentsOfDirectory(at: providersFolder, includingPropertiesForKeys: nil) {
guard provider.isFileURL && provider.pathExtension == "json" else {
continue
}
do {
let json = try provider.asJSON()
// pp_log.error(json)
let result = try migratedV1Profile(csJSON, providerMap: json)
2022-04-12 13:09:14 +00:00
// pp_log.info(result.profile)
// print(">>> Account: \(result.profile.username) -> \(result.password)")
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
migrated.append(result)
} catch {
pp_log.warning("Unable to migrate provider profile: \(provider)")
continue
}
}
} catch {
pp_log.warning(error)
}
} catch {
pp_log.warning(error)
}
return migrated
}
// SHARED
//
// username/password ("username")
// trusted networks ("trustedNetworks")
// network settings ("networkChoices", "manualNetworkSettings")
//
// HOST
//
// provider configuration ("parameters") -- not crucial
// custom endpoint ("parameters"?) -- not crucial
// ovpn configuration ("parameters" -> "sessionConfiguration")
//
private func migratedV1Profile(_ cs: Map, hostMap: Map, authUserPass: Set<String>) throws -> Profile {
guard let oldUUIDString = hostMap["id"] as? String else {
throw MigrationError.missingId
}
let name = (cs["hostTitles"] as? Map)?[oldUUIDString] as? String ?? oldUUIDString
let header = Profile.Header(name: name) // new UUID
// configuration
guard let params = hostMap["parameters"] as? Map else {
throw MigrationError.missingOpenVPNConfiguration
}
guard var ovpn = params["sessionConfiguration"] as? Map else {
throw MigrationError.missingOpenVPNConfiguration
}
guard let hostname = ovpn["hostname"] as? String else {
throw MigrationError.missingHostname
}
guard let rawEps = ovpn["endpointProtocols"] as? [String] else {
throw MigrationError.missingEndpointProtocols
}
let eps = rawEps.compactMap(EndpointProtocol.init(rawValue:))
var remotes: [String] = []
eps.forEach {
remotes.append("\(hostname):\($0)")
}
ovpn["remotes"] = remotes
ovpn["authUserPass"] = authUserPass.contains(oldUUIDString)
2022-04-12 13:09:14 +00:00
let cfg = try JSON(ovpn).decode(OpenVPN.Configuration.self)
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
// keychain
let username = hostMap["username"] as? String ?? ""
let password = migratedV1Password(forProfileId: oldUUIDString, profileType: "host", username: username)
var profile = Profile(header, configuration: cfg)
var account = Profile.Account()
account.username = username
account.password = password
profile.account = account
// shared
profile.onDemand = migratedV1TrustedNetworks(hostMap)
profile.networkSettings = migratedV1NetworkSettings(hostMap)
return profile
}
// HOST
//
// poolId -- not crucial
// presetId
// favoriteGroupIds
//
private func migratedV1Profile(_ cs: Map, providerMap: Map) throws -> Profile {
2022-04-12 13:09:14 +00:00
guard let name = providerMap["name"] as? String else {
throw MigrationError.missingProviderName
}
let header = Profile.Header(name: name, providerName: name)
var provider = Profile.Provider(name)
// keychain
var account = Profile.Account()
account.username = providerMap["username"] as? String ?? ""
account.password = migratedV1Password(forProfileId: name, profileType: "provider", username: account.username)
// provider configuration
var settings = Profile.Provider.Settings()
if let apiId = providerMap["poolId"] as? String {
settings.serverId = ProviderServer.id(withName: name, vpnProtocol: .openVPN, apiId: apiId)
}
settings.presetId = providerMap["presetId"] as? String
settings.favoriteLocationIds = Set((providerMap["favoriteGroupIds"] as? [String])?.compactMap {
"\(name):\($0.replacingOccurrences(of: "/", with: ":"))"
} ?? [])
settings.account = account
provider.vpnSettings[.openVPN] = settings
var profile = Profile(header, provider: provider)
// shared
profile.onDemand = migratedV1TrustedNetworks(providerMap)
profile.networkSettings = migratedV1NetworkSettings(providerMap)
return profile
}
private func migratedV1Password(forProfileId profileId: String, profileType: String, username: String) -> String {
let keychain = Keychain(group: appGroup)
let passwordContext = "\(Bundle.main.bundleIdentifier!).\(profileType).\(profileId)"
do {
return try keychain.password(for: username, context: passwordContext)
} catch {
return ""
}
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
private func migratedV1TrustedNetworks(_ map: Map) -> Profile.OnDemand {
var onDemand = Profile.OnDemand()
onDemand.isEnabled = true
if let trusted = map["trustedNetworks"] as? Map {
onDemand.withMobileNetwork = trusted["includesMobile"] as? Bool ?? false
onDemand.withEthernetNetwork = trusted["includesEthernet"] as? Bool ?? false
onDemand.withSSIDs = trusted["includedWiFis"] as? [String: Bool] ?? [:]
if let rawPolicy = trusted["policy"] as? String, let policy = Profile.OnDemand.Policy(rawValue: rawPolicy) {
onDemand.policy = policy
}
}
return onDemand
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
private func migratedV1NetworkSettings(_ map: Map) -> Profile.NetworkSettings {
var settings = Profile.NetworkSettings()
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
if let choices = map["networkChoices"] as? Map {
settings.gateway.choice = migratedV1Choice(choices, key: "gateway")
settings.dns.choice = migratedV1Choice(choices, key: "dns")
settings.proxy.choice = migratedV1Choice(choices, key: "proxy")
settings.mtu.choice = migratedV1Choice(choices, key: "mtu")
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
if let manual = map["manualNetworkSettings"] as? Map {
// gateway
settings.gateway.isDefaultIPv4 = (manual["gatewayPolicies"] as? [String])?.contains("IPv4") ?? false
settings.gateway.isDefaultIPv6 = (manual["gatewayPolicies"] as? [String])?.contains("IPv6") ?? false
// dns
(manual["dnsProtocol"] as? String).map {
settings.dns.configurationType = .init(rawValue: $0) ?? .plain
2022-04-12 13:09:14 +00:00
}
settings.dns.dnsServers = manual["dnsServers"] as? [String] ?? []
settings.dns.dnsSearchDomains = manual["dnsSearchDomains"] as? [String] ?? []
(manual["dnsHTTPSURL"] as? String).map {
settings.dns.dnsHTTPSURL = URL(string: $0)
}
settings.dns.dnsTLSServerName = manual["dnsTLSServerName"] as? String
// proxy
settings.proxy.proxyAddress = manual["proxyAddress"] as? String
settings.proxy.proxyPort = manual["proxyPort"] as? UInt16
(manual["proxyAutoConfigurationURL"] as? String).map {
settings.proxy.proxyAutoConfigurationURL = URL(string: $0)
}
settings.proxy.proxyBypassDomains = manual["proxyBypassDomains"] as? [String] ?? []
// mtu
settings.mtu.mtuBytes = manual["mtuBytes"] as? Int ?? 0
}
return settings
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
private func migratedV1Choice(_ map: Map, key: String) -> Network.Choice {
2022-09-04 18:09:31 +00:00
(map[key] as? String) == "manual" ? .manual : .automatic
2022-04-12 13:09:14 +00:00
}
}
private extension URL {
func asJSON() throws -> Map {
let data = try Data(contentsOf: self)
guard let json = try JSONSerialization.jsonObject(with: data) as? Map else {
2023-05-24 16:19:47 +00:00
throw DefaultUpgradeStrategy.MigrationError.json
2022-04-12 13:09:14 +00:00
}
return json
}
}