diff --git a/Configuration/UTMQemuConfiguration+Arguments.swift b/Configuration/UTMQemuConfiguration+Arguments.swift index eb4142284..a7924c6e0 100644 --- a/Configuration/UTMQemuConfiguration+Arguments.swift +++ b/Configuration/UTMQemuConfiguration+Arguments.swift @@ -146,7 +146,11 @@ import Virtualization // for getting network interfaces "unix=on" "addr=\(spiceSocketURL.lastPathComponent)" } - "disable-ticketing=on" + if let _ = qemu.spiceServerPassword { + "password-secret=secspice0" + } else { + "disable-ticketing=on" + } if !isRemoteSpice { "image-compression=off" "playback-compression=off" @@ -176,6 +180,11 @@ import Virtualization // for getting network interfaces f("-vga") f("none") } + if let password = qemu.spiceServerPassword { + // assume anyone who can read this is in our trust domain + f("-object") + f("secret,id=secspice0,data=\(password)") + } } private func filterDisplayIfRemote(_ display: any QEMUDisplayDevice) -> any QEMUDisplayDevice { diff --git a/Configuration/UTMQemuConfigurationQEMU.swift b/Configuration/UTMQemuConfigurationQEMU.swift index 7af246ff0..3752ac624 100644 --- a/Configuration/UTMQemuConfigurationQEMU.swift +++ b/Configuration/UTMQemuConfigurationQEMU.swift @@ -77,6 +77,9 @@ struct UTMQemuConfigurationQEMU: Codable { /// Set to TLS public key for SPICE server in SubjectPublicKey. Not saved. var spiceServerPublicKey: Data? + + /// Set to a password shared with the client. Not saved. + var spiceServerPassword: String? enum CodingKeys: String, CodingKey { case hasDebugLog = "DebugLog" diff --git a/Platform/macOS/UTMDataExtension.swift b/Platform/macOS/UTMDataExtension.swift index 254c774fd..3c6ad4b68 100644 --- a/Platform/macOS/UTMDataExtension.swift +++ b/Platform/macOS/UTMDataExtension.swift @@ -82,7 +82,7 @@ extension UTMData { /// - options: Start options /// - server: Remote server /// - Returns: Port number to SPICE server - func startRemote(vm: VMData, options: UTMVirtualMachineStartOptions, forClient client: UTMRemoteServer.Remote) async throws -> (port: UInt16, publicKey: Data) { + func startRemote(vm: VMData, options: UTMVirtualMachineStartOptions, forClient client: UTMRemoteServer.Remote) async throws -> (port: UInt16, publicKey: Data, password: String) { guard let wrapped = vm.wrapped as? UTMQemuVirtualMachine, type(of: wrapped).capabilities.supportsRemoteSession else { throw UTMDataError.unsupportedBackend } @@ -94,7 +94,7 @@ extension UTMData { } try await wrapped.start(options: options.union(.remoteSession)) vmWindows[vm] = session - return (wrapped.config.qemu.spiceServerPort!, wrapped.config.qemu.spiceServerPublicKey!) + return (wrapped.config.qemu.spiceServerPort!, wrapped.config.qemu.spiceServerPublicKey!, wrapped.config.qemu.spiceServerPassword!) } func stop(vm: VMData) { diff --git a/Remote/UTMRemoteClient.swift b/Remote/UTMRemoteClient.swift index 10b03b97f..37413c9c8 100644 --- a/Remote/UTMRemoteClient.swift +++ b/Remote/UTMRemoteClient.swift @@ -268,9 +268,9 @@ extension UTMRemoteClient { return fileUrl } - func startVirtualMachine(id: UUID, options: UTMVirtualMachineStartOptions) async throws -> (port: UInt16, publicKey: Data) { + func startVirtualMachine(id: UUID, options: UTMVirtualMachineStartOptions) async throws -> (port: UInt16, publicKey: Data, password: String) { let reply = try await _startVirtualMachine(parameters: .init(id: id, options: options)) - return (reply.spiceServerPort, reply.spiceServerPublicKey) + return (reply.spiceServerPort, reply.spiceServerPublicKey, reply.spiceServerPassword) } func stopVirtualMachine(id: UUID, method: UTMVirtualMachineStopMethod) async throws { diff --git a/Remote/UTMRemoteMessage.swift b/Remote/UTMRemoteMessage.swift index 0fb5236c1..f211eab19 100644 --- a/Remote/UTMRemoteMessage.swift +++ b/Remote/UTMRemoteMessage.swift @@ -130,6 +130,7 @@ extension UTMRemoteMessageServer { struct Reply: Serializable, Codable { let spiceServerPort: UInt16 let spiceServerPublicKey: Data + let spiceServerPassword: String } } diff --git a/Remote/UTMRemoteServer.swift b/Remote/UTMRemoteServer.swift index 782be413b..502da5fea 100644 --- a/Remote/UTMRemoteServer.swift +++ b/Remote/UTMRemoteServer.swift @@ -631,8 +631,8 @@ extension UTMRemoteServer { private func _startVirtualMachine(parameters: M.StartVirtualMachine.Request) async throws -> M.StartVirtualMachine.Reply { let vm = try await findVM(withId: parameters.id) - let (port, publicKey) = try await data.startRemote(vm: vm, options: parameters.options, forClient: client) - return .init(spiceServerPort: port, spiceServerPublicKey: publicKey) + let (port, publicKey, password) = try await data.startRemote(vm: vm, options: parameters.options, forClient: client) + return .init(spiceServerPort: port, spiceServerPublicKey: publicKey, spiceServerPassword: password) } private func _stopVirtualMachine(parameters: M.StopVirtualMachine.Request) async throws -> M.StopVirtualMachine.Reply { diff --git a/Remote/UTMRemoteSpiceVirtualMachine.swift b/Remote/UTMRemoteSpiceVirtualMachine.swift index 016b87aa5..2a99ed4e4 100644 --- a/Remote/UTMRemoteSpiceVirtualMachine.swift +++ b/Remote/UTMRemoteSpiceVirtualMachine.swift @@ -170,7 +170,11 @@ extension UTMRemoteSpiceVirtualMachine { options.insert(.hasDebugLog) } #endif - let ioService = UTMSpiceIO(host: server.host, tlsPort: Int(spiceServer.port), serverPublicKey: spiceServer.publicKey, options: options) + let ioService = UTMSpiceIO(host: server.host, + tlsPort: Int(spiceServer.port), + serverPublicKey: spiceServer.publicKey, + password: spiceServer.password, + options: options) ioService.logHandler = { (line: String) -> Void in guard !line.contains("spice_make_scancode") else { return // do not log key presses for privacy reasons diff --git a/Services/UTMExtensions.swift b/Services/UTMExtensions.swift index 1e15aa03c..bb4eabbfd 100644 --- a/Services/UTMExtensions.swift +++ b/Services/UTMExtensions.swift @@ -384,6 +384,11 @@ extension String { } return Int(numeric) } + + static func random(length: Int) -> String { + let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + return String((0.. *mutableDisplays; @@ -74,11 +75,12 @@ - (instancetype)initWithSocketUrl:(NSURL *)socketUrl options:(UTMSpiceIOOptions) return self; } -- (instancetype)initWithHost:(NSString *)host tlsPort:(NSInteger)tlsPort serverPublicKey:(NSData *)serverPublicKey options:(UTMSpiceIOOptions)options { +- (instancetype)initWithHost:(NSString *)host tlsPort:(NSInteger)tlsPort serverPublicKey:(NSData *)serverPublicKey password:(NSString *)password options:(UTMSpiceIOOptions)options { if (self = [super init]) { self.host = host; self.tlsPort = tlsPort; self.serverPublicKey = serverPublicKey; + self.password = password; self.options = options; self.mutableDisplays = [NSMutableArray array]; self.mutableSerials = [NSMutableArray array]; @@ -94,6 +96,7 @@ - (void)initializeSpiceIfNeeded { self.spiceConnection = [[CSConnection alloc] initWithUnixSocketFile:relativeSocketFile]; } else { self.spiceConnection = [[CSConnection alloc] initWithHost:self.host tlsPort:[@(self.tlsPort) stringValue] serverPublicKey:self.serverPublicKey]; + self.spiceConnection.password = self.password; } self.spiceConnection.delegate = self; self.spiceConnection.audioEnabled = (self.options & UTMSpiceIOOptionsHasAudio) == UTMSpiceIOOptionsHasAudio; diff --git a/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7ee61175e..3dccf7ff1 100644 --- a/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,7 +15,7 @@ "location" : "https://github.com/utmapp/CocoaSpice.git", "state" : { "branch" : "visionos", - "revision" : "9591cdf41282a7e6edbe7b705adbb957592ba347" + "revision" : "9d286ba10b8ed953bf21c04ddd64237372163132" } }, {