Internal
Public Access
Add Apple Screen Sharing authentication for VNC (security type 30)
Apple's macOS Screen Sharing server doesn't speak standard VNC Authentication -- it uses a Diffie-Hellman key exchange followed by AES-128-ECB-encrypted credentials, security type 30. Apple never published this scheme (it's not part of RFC 6143); this implements the well-established reverse-engineered wire format: the server sends a generator, prime, and its own DH public key; the client generates an ephemeral keypair, derives the shared secret, MD5-hashes it into an AES key, and sends back its public key plus a 128-byte encrypted username+password buffer. The DH/AES math lives in new src/vnc_apple_dh_auth.h/.cpp as a pure, socket-free helper (mirroring vncAuthResponse()'s shape for standard VNC Auth), built entirely on modern EVP_PKEY-based OpenSSL 3.0 APIs -- no deprecated low-level DH_* calls, unlike VNC Authentication's necessary use of classic DES. Reuses Profile::username (already a shared field) since Apple's scheme needs an actual macOS account name, unlike password-only VNC Authentication. Security-type preference when multiple are offered is now None > AppleDH > VNCAuth, since DH+AES is strictly stronger than static- challenge DES. Adds a DH round-trip test (generates a real 512-bit group at test time, computes the response, then independently re-derives the shared secret as the server would and decrypts the credentials back out -- proving self-consistency without needing a hand-computed expected value), a fake-server integration test for the full RFB 3.8 handshake sequencing, and a preference-order test. Not yet live-verified against a real macOS Screen Sharing server -- that's next. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+97
-10
@@ -1,5 +1,6 @@
|
||||
#include "vnc_session_backend.h"
|
||||
|
||||
#include "vnc_apple_dh_auth.h"
|
||||
#include "vnc_pixel_codecs.h"
|
||||
|
||||
#include <QPainter>
|
||||
@@ -49,6 +50,12 @@ constexpr qint32 kEncTight = 7;
|
||||
// the framebuffer.
|
||||
constexpr qint32 kEncCursor = -239;
|
||||
|
||||
// Apple's Screen Sharing authentication (Diffie-Hellman + AES). Not part
|
||||
// of RFC 6143 -- see vnc_apple_dh_auth.h.
|
||||
constexpr quint8 kSecurityTypeNone = 1;
|
||||
constexpr quint8 kSecurityTypeVncAuth = 2;
|
||||
constexpr quint8 kSecurityTypeAppleDh = 30;
|
||||
|
||||
constexpr std::array<qint32, 6> kAnnouncedEncodings = { kEncRaw, kEncCopyRect, kEncHextile,
|
||||
kEncZRLE, kEncTight, kEncCursor };
|
||||
|
||||
@@ -184,7 +191,8 @@ VncSessionBackend::VncSessionBackend(const Profile& profile, QObject* parent)
|
||||
m_tightInflateInitialized{ false, false, false, false },
|
||||
m_tightCompressionMode(0),
|
||||
m_tightFilterId(0),
|
||||
m_tightLengthByteIndex(0)
|
||||
m_tightLengthByteIndex(0),
|
||||
m_appleAuthPrimeLength(0)
|
||||
{
|
||||
std::memset(m_zrleInflateStream, 0, sizeof(z_stream_s));
|
||||
for (z_stream_s* stream : m_tightInflateStreams) {
|
||||
@@ -517,6 +525,8 @@ void VncSessionBackend::resetProtocolState()
|
||||
m_tightInflateInitialized[i] = false;
|
||||
}
|
||||
}
|
||||
m_appleAuthGenerator.clear();
|
||||
m_appleAuthPrimeLength = 0;
|
||||
}
|
||||
|
||||
bool VncSessionBackend::haveBytes(int count) const
|
||||
@@ -781,11 +791,18 @@ void VncSessionBackend::processReceiveBuffer()
|
||||
m_offeredSecurityTypes = m_recvBuffer.left(m_securityTypeCount);
|
||||
m_recvBuffer.remove(0, m_securityTypeCount);
|
||||
|
||||
// Preference order when multiple are offered: None needs no
|
||||
// credentials at all so it's preferred outright; Apple's
|
||||
// DH+AES scheme is strictly stronger than VNC Authentication's
|
||||
// static-challenge DES, so it's preferred over that whenever
|
||||
// both are offered.
|
||||
quint8 chosen = 0;
|
||||
if (m_offeredSecurityTypes.contains(char(1))) {
|
||||
chosen = 1;
|
||||
} else if (m_offeredSecurityTypes.contains(char(2))) {
|
||||
chosen = 2;
|
||||
if (m_offeredSecurityTypes.contains(static_cast<char>(kSecurityTypeNone))) {
|
||||
chosen = kSecurityTypeNone;
|
||||
} else if (m_offeredSecurityTypes.contains(static_cast<char>(kSecurityTypeAppleDh))) {
|
||||
chosen = kSecurityTypeAppleDh;
|
||||
} else if (m_offeredSecurityTypes.contains(static_cast<char>(kSecurityTypeVncAuth))) {
|
||||
chosen = kSecurityTypeVncAuth;
|
||||
}
|
||||
if (chosen == 0) {
|
||||
QStringList offered;
|
||||
@@ -803,8 +820,10 @@ void VncSessionBackend::processReceiveBuffer()
|
||||
m_chosenSecurityType = chosen;
|
||||
m_socket->write(QByteArray(1, static_cast<char>(chosen)));
|
||||
|
||||
if (chosen == 2) {
|
||||
if (chosen == kSecurityTypeVncAuth) {
|
||||
m_rfbState = RfbState::WaitingVncAuthChallenge;
|
||||
} else if (chosen == kSecurityTypeAppleDh) {
|
||||
m_rfbState = RfbState::WaitingAppleAuthGeneratorLength;
|
||||
} else if (m_negotiatedMinorVersion >= 8) {
|
||||
m_rfbState = RfbState::WaitingSecurityResult;
|
||||
} else {
|
||||
@@ -823,13 +842,16 @@ void VncSessionBackend::processReceiveBuffer()
|
||||
|
||||
if (type == 0) {
|
||||
m_rfbState = RfbState::WaitingSecurityFailureReasonLength;
|
||||
} else if (type == 1) {
|
||||
m_chosenSecurityType = 1;
|
||||
} else if (type == kSecurityTypeNone) {
|
||||
m_chosenSecurityType = kSecurityTypeNone;
|
||||
sendClientInit();
|
||||
m_rfbState = RfbState::WaitingServerInitHeader;
|
||||
} else if (type == 2) {
|
||||
m_chosenSecurityType = 2;
|
||||
} else if (type == kSecurityTypeVncAuth) {
|
||||
m_chosenSecurityType = kSecurityTypeVncAuth;
|
||||
m_rfbState = RfbState::WaitingVncAuthChallenge;
|
||||
} else if (type == kSecurityTypeAppleDh) {
|
||||
m_chosenSecurityType = kSecurityTypeAppleDh;
|
||||
m_rfbState = RfbState::WaitingAppleAuthGeneratorLength;
|
||||
} else {
|
||||
failConnection(
|
||||
QStringLiteral(
|
||||
@@ -888,6 +910,71 @@ void VncSessionBackend::processReceiveBuffer()
|
||||
break;
|
||||
}
|
||||
|
||||
case RfbState::WaitingAppleAuthGeneratorLength: {
|
||||
if (!haveBytes(2)) {
|
||||
return;
|
||||
}
|
||||
m_pendingLength = readU16BE(m_recvBuffer, 0);
|
||||
m_recvBuffer.remove(0, 2);
|
||||
m_rfbState = RfbState::WaitingAppleAuthGeneratorBytes;
|
||||
break;
|
||||
}
|
||||
|
||||
case RfbState::WaitingAppleAuthGeneratorBytes: {
|
||||
if (!haveBytes(static_cast<int>(m_pendingLength))) {
|
||||
return;
|
||||
}
|
||||
m_appleAuthGenerator = m_recvBuffer.left(static_cast<int>(m_pendingLength));
|
||||
m_recvBuffer.remove(0, static_cast<int>(m_pendingLength));
|
||||
m_rfbState = RfbState::WaitingAppleAuthPrimeLength;
|
||||
break;
|
||||
}
|
||||
|
||||
case RfbState::WaitingAppleAuthPrimeLength: {
|
||||
if (!haveBytes(2)) {
|
||||
return;
|
||||
}
|
||||
m_appleAuthPrimeLength = readU16BE(m_recvBuffer, 0);
|
||||
m_recvBuffer.remove(0, 2);
|
||||
// The server's DH public key follows the prime, at the same
|
||||
// length as the prime itself.
|
||||
m_pendingLength = m_appleAuthPrimeLength * 2;
|
||||
m_rfbState = RfbState::WaitingAppleAuthPrimeAndServerKey;
|
||||
break;
|
||||
}
|
||||
|
||||
case RfbState::WaitingAppleAuthPrimeAndServerKey: {
|
||||
if (!haveBytes(static_cast<int>(m_pendingLength))) {
|
||||
return;
|
||||
}
|
||||
const int primeLength = static_cast<int>(m_appleAuthPrimeLength);
|
||||
const QByteArray prime = m_recvBuffer.left(primeLength);
|
||||
const QByteArray serverPublicKey = m_recvBuffer.mid(primeLength, primeLength);
|
||||
m_recvBuffer.remove(0, static_cast<int>(m_pendingLength));
|
||||
|
||||
const VncAppleDhAuth::Response response = VncAppleDhAuth::computeResponse(
|
||||
m_appleAuthGenerator, prime, serverPublicKey, profile().username,
|
||||
m_activeOptions.password);
|
||||
if (response.clientPublicKey.isEmpty()) {
|
||||
failConnection(
|
||||
QStringLiteral(
|
||||
"Failed to compute the Apple Screen Sharing authentication response."),
|
||||
QStringLiteral("VncAppleDhAuth::computeResponse() failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
m_socket->write(response.clientPublicKey);
|
||||
m_socket->write(response.encryptedCredentials);
|
||||
|
||||
if (m_negotiatedMinorVersion >= 8) {
|
||||
m_rfbState = RfbState::WaitingSecurityResult;
|
||||
} else {
|
||||
sendClientInit();
|
||||
m_rfbState = RfbState::WaitingServerInitHeader;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case RfbState::WaitingSecurityResult: {
|
||||
if (!haveBytes(4)) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user