Support Apple Remote Desktop auth via account username+password
RoyalVNCKit prioritizes .diffieHellman (ARD) over .vnc during handshake when both are offered. My delegate adapter was passing an empty username to VNCUsernamePasswordCredential, so any Mac with user-account screen sharing enabled rejected the credential before .vnc fallback could happen. Fix: persist a username on SavedConnection and pipe it through to the credential callback. Leave blank to use the VNC-only password path. AddConnection footer now explains the two Mac paths: • User account (ARD) — macOS short name + full account password • VNC-only password — blank username + ≤8 char password Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ public final class SessionController {
|
|||||||
public let displayName: String
|
public let displayName: String
|
||||||
public let host: String
|
public let host: String
|
||||||
public let port: Int
|
public let port: Int
|
||||||
|
public let username: String
|
||||||
|
|
||||||
private let keychainTag: String
|
private let keychainTag: String
|
||||||
private let passwordProvider: any PasswordProviding
|
private let passwordProvider: any PasswordProviding
|
||||||
@@ -47,6 +48,7 @@ public final class SessionController {
|
|||||||
displayName: String,
|
displayName: String,
|
||||||
host: String,
|
host: String,
|
||||||
port: Int,
|
port: Int,
|
||||||
|
username: String = "",
|
||||||
keychainTag: String,
|
keychainTag: String,
|
||||||
viewOnly: Bool = false,
|
viewOnly: Bool = false,
|
||||||
clipboardSyncEnabled: Bool = true,
|
clipboardSyncEnabled: Bool = true,
|
||||||
@@ -59,6 +61,7 @@ public final class SessionController {
|
|||||||
self.displayName = displayName
|
self.displayName = displayName
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
|
self.username = username
|
||||||
self.keychainTag = keychainTag
|
self.keychainTag = keychainTag
|
||||||
self.viewOnly = viewOnly
|
self.viewOnly = viewOnly
|
||||||
self.clipboardSyncEnabled = clipboardSyncEnabled
|
self.clipboardSyncEnabled = clipboardSyncEnabled
|
||||||
@@ -78,6 +81,7 @@ public final class SessionController {
|
|||||||
displayName: saved.displayName,
|
displayName: saved.displayName,
|
||||||
host: saved.host,
|
host: saved.host,
|
||||||
port: saved.port,
|
port: saved.port,
|
||||||
|
username: saved.username,
|
||||||
keychainTag: saved.keychainTag,
|
keychainTag: saved.keychainTag,
|
||||||
viewOnly: saved.viewOnly,
|
viewOnly: saved.viewOnly,
|
||||||
clipboardSyncEnabled: saved.clipboardSyncEnabled,
|
clipboardSyncEnabled: saved.clipboardSyncEnabled,
|
||||||
@@ -302,7 +306,8 @@ public final class SessionController {
|
|||||||
let adapter = DelegateAdapter(
|
let adapter = DelegateAdapter(
|
||||||
controller: self,
|
controller: self,
|
||||||
passwordProvider: passwordProvider,
|
passwordProvider: passwordProvider,
|
||||||
keychainTag: keychainTag
|
keychainTag: keychainTag,
|
||||||
|
username: username
|
||||||
)
|
)
|
||||||
connection.delegate = adapter
|
connection.delegate = adapter
|
||||||
self.connection = connection
|
self.connection = connection
|
||||||
@@ -366,13 +371,16 @@ private final class DelegateAdapter: NSObject, VNCConnectionDelegate, @unchecked
|
|||||||
weak var controller: SessionController?
|
weak var controller: SessionController?
|
||||||
let passwordProvider: any PasswordProviding
|
let passwordProvider: any PasswordProviding
|
||||||
let keychainTag: String
|
let keychainTag: String
|
||||||
|
let username: String
|
||||||
|
|
||||||
init(controller: SessionController,
|
init(controller: SessionController,
|
||||||
passwordProvider: any PasswordProviding,
|
passwordProvider: any PasswordProviding,
|
||||||
keychainTag: String) {
|
keychainTag: String,
|
||||||
|
username: String) {
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.passwordProvider = passwordProvider
|
self.passwordProvider = passwordProvider
|
||||||
self.keychainTag = keychainTag
|
self.keychainTag = keychainTag
|
||||||
|
self.username = username
|
||||||
}
|
}
|
||||||
|
|
||||||
func connection(_ connection: VNCConnection,
|
func connection(_ connection: VNCConnection,
|
||||||
@@ -398,7 +406,7 @@ private final class DelegateAdapter: NSObject, VNCConnectionDelegate, @unchecked
|
|||||||
case .vnc:
|
case .vnc:
|
||||||
credential = VNCPasswordCredential(password: pwd)
|
credential = VNCPasswordCredential(password: pwd)
|
||||||
case .appleRemoteDesktop, .ultraVNCMSLogonII:
|
case .appleRemoteDesktop, .ultraVNCMSLogonII:
|
||||||
credential = VNCUsernamePasswordCredential(username: "", password: pwd)
|
credential = VNCUsernamePasswordCredential(username: username, password: pwd)
|
||||||
@unknown default:
|
@unknown default:
|
||||||
credential = VNCPasswordCredential(password: pwd)
|
credential = VNCPasswordCredential(password: pwd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public final class SavedConnection {
|
|||||||
public var displayName: String
|
public var displayName: String
|
||||||
public var host: String
|
public var host: String
|
||||||
public var port: Int
|
public var port: Int
|
||||||
|
public var username: String
|
||||||
public var colorTagRaw: String
|
public var colorTagRaw: String
|
||||||
public var lastConnectedAt: Date?
|
public var lastConnectedAt: Date?
|
||||||
public var preferredEncodings: [String]
|
public var preferredEncodings: [String]
|
||||||
@@ -23,6 +24,7 @@ public final class SavedConnection {
|
|||||||
displayName: String,
|
displayName: String,
|
||||||
host: String,
|
host: String,
|
||||||
port: Int = 5900,
|
port: Int = 5900,
|
||||||
|
username: String = "",
|
||||||
colorTag: ColorTag = .blue,
|
colorTag: ColorTag = .blue,
|
||||||
preferredEncodings: [String] = ["7", "16", "5", "6"],
|
preferredEncodings: [String] = ["7", "16", "5", "6"],
|
||||||
keychainTag: String = UUID().uuidString,
|
keychainTag: String = UUID().uuidString,
|
||||||
@@ -37,6 +39,7 @@ public final class SavedConnection {
|
|||||||
self.displayName = displayName
|
self.displayName = displayName
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
|
self.username = username
|
||||||
self.colorTagRaw = colorTag.rawValue
|
self.colorTagRaw = colorTag.rawValue
|
||||||
self.lastConnectedAt = nil
|
self.lastConnectedAt = nil
|
||||||
self.preferredEncodings = preferredEncodings
|
self.preferredEncodings = preferredEncodings
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public struct AddConnectionView: View {
|
|||||||
@State private var displayName = ""
|
@State private var displayName = ""
|
||||||
@State private var host = ""
|
@State private var host = ""
|
||||||
@State private var port = "5900"
|
@State private var port = "5900"
|
||||||
|
@State private var username = ""
|
||||||
@State private var password = ""
|
@State private var password = ""
|
||||||
@State private var revealPassword = false
|
@State private var revealPassword = false
|
||||||
@State private var colorTag: ColorTag = .blue
|
@State private var colorTag: ColorTag = .blue
|
||||||
@@ -45,6 +46,17 @@ public struct AddConnectionView: View {
|
|||||||
!host.trimmingCharacters(in: .whitespaces).isEmpty
|
!host.trimmingCharacters(in: .whitespaces).isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var authFooterText: String {
|
||||||
|
if isEditing {
|
||||||
|
return "Leave password blank to keep the current one. Stored in iOS Keychain (this device only)."
|
||||||
|
}
|
||||||
|
return """
|
||||||
|
Two Mac paths:
|
||||||
|
• User account (ARD) — enter your macOS short name + full account password. No length limit. Enabled in System Settings → Sharing → Screen Sharing → Allow access for…
|
||||||
|
• VNC-only password — leave the username blank; macOS truncates to 8 characters. Enabled via Screen Sharing → ⓘ → "VNC viewers may control screen with password".
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
@@ -75,12 +87,17 @@ public struct AddConnectionView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
|
TextField("Username (optional)", text: $username)
|
||||||
|
#if os(iOS)
|
||||||
|
.textInputAutocapitalization(.never)
|
||||||
|
#endif
|
||||||
|
.autocorrectionDisabled()
|
||||||
HStack {
|
HStack {
|
||||||
Group {
|
Group {
|
||||||
if revealPassword {
|
if revealPassword {
|
||||||
TextField("VNC password", text: $password)
|
TextField("Password", text: $password)
|
||||||
} else {
|
} else {
|
||||||
SecureField(isEditing ? "Replace password" : "VNC password",
|
SecureField(isEditing ? "Replace password" : "Password",
|
||||||
text: $password)
|
text: $password)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,9 +113,7 @@ public struct AddConnectionView: View {
|
|||||||
} header: {
|
} header: {
|
||||||
Text("Authentication")
|
Text("Authentication")
|
||||||
} footer: {
|
} footer: {
|
||||||
Text(isEditing
|
Text(authFooterText)
|
||||||
? "Leave blank to keep the current password. Stored in iOS Keychain (this device only)."
|
|
||||||
: "macOS Screen Sharing's VNC password is limited to 8 characters. Stored in iOS Keychain (this device only).")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
@@ -199,6 +214,7 @@ public struct AddConnectionView: View {
|
|||||||
displayName = existing.displayName
|
displayName = existing.displayName
|
||||||
host = existing.host
|
host = existing.host
|
||||||
port = String(existing.port)
|
port = String(existing.port)
|
||||||
|
username = existing.username
|
||||||
colorTag = existing.colorTag
|
colorTag = existing.colorTag
|
||||||
quality = existing.quality
|
quality = existing.quality
|
||||||
inputMode = existing.inputMode
|
inputMode = existing.inputMode
|
||||||
@@ -214,10 +230,12 @@ public struct AddConnectionView: View {
|
|||||||
|
|
||||||
private func save() {
|
private func save() {
|
||||||
let portInt = Int(port) ?? 5900
|
let portInt = Int(port) ?? 5900
|
||||||
|
let trimmedUsername = username.trimmingCharacters(in: .whitespaces)
|
||||||
if let existing = editing {
|
if let existing = editing {
|
||||||
existing.displayName = displayName
|
existing.displayName = displayName
|
||||||
existing.host = host
|
existing.host = host
|
||||||
existing.port = portInt
|
existing.port = portInt
|
||||||
|
existing.username = trimmedUsername
|
||||||
existing.colorTag = colorTag
|
existing.colorTag = colorTag
|
||||||
existing.quality = quality
|
existing.quality = quality
|
||||||
existing.inputMode = inputMode
|
existing.inputMode = inputMode
|
||||||
@@ -232,6 +250,7 @@ public struct AddConnectionView: View {
|
|||||||
displayName: displayName,
|
displayName: displayName,
|
||||||
host: host,
|
host: host,
|
||||||
port: portInt,
|
port: portInt,
|
||||||
|
username: trimmedUsername,
|
||||||
colorTag: colorTag,
|
colorTag: colorTag,
|
||||||
quality: quality,
|
quality: quality,
|
||||||
inputMode: inputMode,
|
inputMode: inputMode,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ settings:
|
|||||||
SWIFT_STRICT_CONCURRENCY: complete
|
SWIFT_STRICT_CONCURRENCY: complete
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING: YES
|
ENABLE_USER_SCRIPT_SANDBOXING: YES
|
||||||
CODE_SIGN_STYLE: Automatic
|
CODE_SIGN_STYLE: Automatic
|
||||||
|
DEVELOPMENT_TEAM: V3PF3M6B6U
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
VNCCore:
|
VNCCore:
|
||||||
|
|||||||
Reference in New Issue
Block a user