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:
Trey T
2026-04-16 22:25:27 -05:00
parent 497b8a42be
commit 689e30d59a
4 changed files with 39 additions and 8 deletions

View File

@@ -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)
} }

View File

@@ -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

View File

@@ -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,

View File

@@ -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: