remote: add fingerprint verification for client and server
This commit is contained in:
parent
427a2012a4
commit
eda9e94de9
|
@ -173,9 +173,14 @@ private struct ServerConnectView: View {
|
|||
}
|
||||
if !server.fingerprint.isEmpty {
|
||||
Section {
|
||||
Text(server.fingerprint)
|
||||
let fingerprint = (server.fingerprint ^ remoteClient.fingerprint).hexString()
|
||||
if #available(iOS 16.4, *) {
|
||||
Text(fingerprint).monospaced()
|
||||
} else {
|
||||
Text(fingerprint)
|
||||
}
|
||||
} header: {
|
||||
Text("Server Fingerprint")
|
||||
Text("Fingerprint")
|
||||
}
|
||||
}
|
||||
if isPasswordRequired {
|
||||
|
|
|
@ -85,8 +85,9 @@ fileprivate struct ServerOverview: View {
|
|||
}.width(16)
|
||||
TableColumn("Name", value: \.name)
|
||||
.width(ideal: 200)
|
||||
TableColumn("Fingerprint", value: \.fingerprint)
|
||||
.width(ideal: 300)
|
||||
TableColumn("Fingerprint") { client in
|
||||
Text((client.fingerprint ^ remoteServer.serverFingerprint).hexString())
|
||||
}.width(ideal: 300)
|
||||
TableColumn("Last Seen", value: \.lastSeen) { client in
|
||||
Text(DateFormatter.localizedString(from: client.lastSeen, dateStyle: .short, timeStyle: .short))
|
||||
}.width(ideal: 150)
|
||||
|
|
|
@ -29,6 +29,10 @@ actor UTMRemoteClient {
|
|||
|
||||
private(set) var server: Remote!
|
||||
|
||||
nonisolated var fingerprint: [UInt8] {
|
||||
keyManager.fingerprint ?? []
|
||||
}
|
||||
|
||||
@MainActor
|
||||
init(data: UTMRemoteData) {
|
||||
self.state = State()
|
||||
|
@ -96,7 +100,7 @@ actor UTMRemoteClient {
|
|||
guard let host = connection.connection.currentPath?.remoteEndpoint?.hostname else {
|
||||
throw ConnectionError.cannotDetermineHost
|
||||
}
|
||||
guard let fingerprint = connection.peerCertificateChain.first?.fingerprint().hexString() else {
|
||||
guard let fingerprint = connection.peerCertificateChain.first?.fingerprint() else {
|
||||
throw ConnectionError.cannotFindFingerprint
|
||||
}
|
||||
if server.fingerprint.isEmpty {
|
||||
|
@ -126,7 +130,7 @@ actor UTMRemoteClient {
|
|||
extension UTMRemoteClient {
|
||||
@MainActor
|
||||
class State: ObservableObject {
|
||||
typealias ServerFingerprint = String
|
||||
typealias ServerFingerprint = [UInt8]
|
||||
|
||||
struct DiscoveredServer: Identifiable {
|
||||
let hostname: String
|
||||
|
@ -154,7 +158,7 @@ extension UTMRemoteClient {
|
|||
case fingerprint, hostname, port, model, name, lastSeen, password
|
||||
}
|
||||
|
||||
var id: String {
|
||||
var id: ServerFingerprint {
|
||||
fingerprint
|
||||
}
|
||||
|
||||
|
@ -166,7 +170,7 @@ extension UTMRemoteClient {
|
|||
self.hostname = ""
|
||||
self.name = ""
|
||||
self.lastSeen = Date()
|
||||
self.fingerprint = ""
|
||||
self.fingerprint = []
|
||||
}
|
||||
|
||||
init(from discovered: DiscoveredServer) {
|
||||
|
@ -175,7 +179,7 @@ extension UTMRemoteClient {
|
|||
self.name = discovered.name
|
||||
self.lastSeen = Date()
|
||||
self.endpoint = discovered.endpoint
|
||||
self.fingerprint = ""
|
||||
self.fingerprint = []
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -465,8 +469,8 @@ extension UTMRemoteClient {
|
|||
return NSLocalizedString("Password is incorrect.", comment: "UTMRemoteClient")
|
||||
case .fingerprintUntrusted(_):
|
||||
return NSLocalizedString("This host is not yet trusted. You should verify that the fingerprints match what is displayed on the host and then select Trust to continue.", comment: "UTMRemoteClient")
|
||||
case .fingerprintMismatch(let fingerprint):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("The fingerprint '\(fingerprint)' does not match the saved value for this host. This means that the UTM Server was reset, a different host is using the same name, or an attacker is pretending to be the host. For your protection, you need to delete this saved host to continue.", comment: "UTMRemoteClient"), fingerprint)
|
||||
case .fingerprintMismatch(_):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("The host fingerprint does not match the saved value. This means that UTM Server was reset, a different host is using the same name, or an attacker is pretending to be the host. For your protection, you need to delete this saved host to continue.", comment: "UTMRemoteClient"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -148,6 +148,32 @@ extension Array where Element == UInt8 {
|
|||
func hexString() -> String {
|
||||
self.map({ String(format: "%02X", $0) }).joined(separator: ":")
|
||||
}
|
||||
|
||||
init?(hexString: String) {
|
||||
let cleanString = hexString.replacingOccurrences(of: ":", with: "")
|
||||
guard cleanString.count % 2 == 0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var byteArray = [UInt8]()
|
||||
var index = cleanString.startIndex
|
||||
|
||||
while index < cleanString.endIndex {
|
||||
let nextIndex = cleanString.index(index, offsetBy: 2)
|
||||
if let byte = UInt8(cleanString[index..<nextIndex], radix: 16) {
|
||||
byteArray.append(byte)
|
||||
} else {
|
||||
return nil // Invalid hex character
|
||||
}
|
||||
index = nextIndex
|
||||
}
|
||||
self = byteArray
|
||||
}
|
||||
|
||||
static func ^(lhs: Self, rhs: Self) -> Self {
|
||||
let length = Swift.min(lhs.count, rhs.count)
|
||||
return (0..<length).map({ lhs[$0] ^ rhs[$0] })
|
||||
}
|
||||
}
|
||||
|
||||
enum UTMRemoteKeyManagerError: Error {
|
||||
|
|
|
@ -117,6 +117,7 @@ actor UTMRemoteServer {
|
|||
return
|
||||
}
|
||||
try await keyManager.load()
|
||||
await state.setServerFingerprint(keyManager.fingerprint!)
|
||||
registerNotifications()
|
||||
listener = Task {
|
||||
await withErrorNotification {
|
||||
|
@ -145,7 +146,7 @@ actor UTMRemoteServer {
|
|||
|
||||
private func newRemoteConnection(_ connection: Connection) async {
|
||||
let remoteAddress = connection.connection.endpoint.hostname ?? "\(connection.connection.endpoint)"
|
||||
guard let fingerprint = connection.peerCertificateChain.first?.fingerprint().hexString() else {
|
||||
guard let fingerprint = connection.peerCertificateChain.first?.fingerprint() else {
|
||||
connection.close()
|
||||
return
|
||||
}
|
||||
|
@ -190,7 +191,7 @@ actor UTMRemoteServer {
|
|||
}
|
||||
|
||||
private func establishConnection(_ connection: Connection) async {
|
||||
guard let fingerprint = connection.peerCertificateChain.first?.fingerprint().hexString() else {
|
||||
guard let fingerprint = connection.peerCertificateChain.first?.fingerprint() else {
|
||||
connection.close()
|
||||
return
|
||||
}
|
||||
|
@ -217,6 +218,7 @@ actor UTMRemoteServer {
|
|||
private func resetServer() async {
|
||||
await withErrorNotification {
|
||||
try await keyManager.reset()
|
||||
await state.setServerFingerprint(keyManager.fingerprint!)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -279,7 +281,7 @@ extension UTMRemoteServer {
|
|||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
Task {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
guard let fingerprint = userInfo["FINGERPRINT"] as? String else {
|
||||
guard let hexString = userInfo["FINGERPRINT"] as? String, let fingerprint = State.ClientFingerprint(hexString: hexString) else {
|
||||
return
|
||||
}
|
||||
switch response.actionIdentifier {
|
||||
|
@ -328,31 +330,33 @@ extension UTMRemoteServer {
|
|||
center.delegate = nil
|
||||
}
|
||||
|
||||
private func notifyNewConnection(remoteAddress: String, fingerprint: String, isUnknown: Bool = false) async {
|
||||
private func notifyNewConnection(remoteAddress: String, fingerprint: State.ClientFingerprint, isUnknown: Bool = false) async {
|
||||
let settings = await center.notificationSettings()
|
||||
let combinedFingerprint = (fingerprint ^ keyManager.fingerprint!).hexString()
|
||||
guard settings.authorizationStatus == .authorized else {
|
||||
logger.info("Notifications disabled, ignoring connection request from '\(remoteAddress)' with fingerprint '\(fingerprint)'")
|
||||
logger.info("Notifications disabled, ignoring connection request from '\(remoteAddress)' with fingerprint '\(combinedFingerprint)'")
|
||||
return
|
||||
}
|
||||
let content = UNMutableNotificationContent()
|
||||
if isUnknown {
|
||||
content.title = NSString.localizedUserNotificationString(forKey: "Unknown Remote Client", arguments: nil)
|
||||
content.body = NSString.localizedUserNotificationString(forKey: "A client with fingerprint '%@' is attempting to connect.", arguments: [fingerprint])
|
||||
content.body = NSString.localizedUserNotificationString(forKey: "A client with fingerprint '%@' is attempting to connect.", arguments: [combinedFingerprint])
|
||||
content.categoryIdentifier = "UNKNOWN_REMOTE_CLIENT"
|
||||
} else {
|
||||
content.title = NSString.localizedUserNotificationString(forKey: "Remote Client Connected", arguments: nil)
|
||||
content.body = NSString.localizedUserNotificationString(forKey: "Established connection from %@.", arguments: [remoteAddress])
|
||||
content.categoryIdentifier = "TRUSTED_REMOTE_CLIENT"
|
||||
}
|
||||
content.userInfo = ["FINGERPRINT": fingerprint]
|
||||
let request = UNNotificationRequest(identifier: fingerprint,
|
||||
let clientFingerprint = fingerprint.hexString()
|
||||
content.userInfo = ["FINGERPRINT": clientFingerprint]
|
||||
let request = UNNotificationRequest(identifier: clientFingerprint,
|
||||
content: content,
|
||||
trigger: nil)
|
||||
do {
|
||||
try await center.add(request)
|
||||
if !isUnknown {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(15)) {
|
||||
self.center.removeDeliveredNotifications(withIdentifiers: [fingerprint])
|
||||
self.center.removeDeliveredNotifications(withIdentifiers: [clientFingerprint])
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
@ -383,13 +387,14 @@ extension UTMRemoteServer {
|
|||
extension UTMRemoteServer {
|
||||
@MainActor
|
||||
class State: ObservableObject {
|
||||
typealias ClientFingerprint = String
|
||||
typealias ClientFingerprint = [UInt8]
|
||||
typealias ServerFingerprint = [UInt8]
|
||||
struct Client: Codable, Identifiable, Hashable {
|
||||
let fingerprint: ClientFingerprint
|
||||
var name: String
|
||||
var lastSeen: Date
|
||||
|
||||
var id: String {
|
||||
var id: ClientFingerprint {
|
||||
fingerprint
|
||||
}
|
||||
|
||||
|
@ -440,6 +445,12 @@ extension UTMRemoteServer {
|
|||
|
||||
@Published private(set) var isServerActive = false
|
||||
|
||||
@Published private(set) var serverFingerprint: ServerFingerprint = [] {
|
||||
didSet {
|
||||
UserDefaults.standard.setValue(serverFingerprint.hexString(), forKey: "ServerFingerprint")
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
var _approvedClients = Set<Client>()
|
||||
if let array = UserDefaults.standard.array(forKey: "TrustedClients") {
|
||||
|
@ -456,6 +467,9 @@ extension UTMRemoteServer {
|
|||
}
|
||||
self.blockedClients = _blockedClients
|
||||
self.allClients = Array(_approvedClients) + Array(_blockedClients)
|
||||
if let value = UserDefaults.standard.string(forKey: "ServerFingerprint"), let serverFingerprint = ServerFingerprint(hexString: value) {
|
||||
self.serverFingerprint = serverFingerprint
|
||||
}
|
||||
}
|
||||
|
||||
func isConnected(_ fingerprint: ClientFingerprint) -> Bool {
|
||||
|
@ -522,6 +536,10 @@ extension UTMRemoteServer {
|
|||
approvedClients.remove(client)
|
||||
blockedClients.insert(client)
|
||||
}
|
||||
|
||||
fileprivate func setServerFingerprint(_ fingerprint: ServerFingerprint) {
|
||||
serverFingerprint = fingerprint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue