Files
orbithub/src/vnc_session_backend.h
T
ksmithandClaude Sonnet 5 1f026dde70 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>
2026-09-15 21:27:17 -06:00

217 lines
8.4 KiB
C++

#ifndef ORBITHUB_VNC_SESSION_BACKEND_H
#define ORBITHUB_VNC_SESSION_BACKEND_H
#include "session_backend.h"
#include <QAbstractSocket>
#include <QByteArray>
#include <QImage>
#include <QRect>
#include <QRgb>
#include <array>
class QTcpSocket;
struct z_stream_s;
// Implements RFB (RFC 6143) directly against QTcpSocket -- there is no
// permissively licensed VNC client library to vendor the way FreeRDP was
// for RDP (LibVNCClient is GPLv2, gtk-vnc is LGPL but GTK-tied), so this is
// an original implementation. Threading follows SshSessionBackend's model
// (a QObject moved to its own QThread, driven by Qt's own async socket
// signals) rather than RdpSessionBackend's manual worker-thread/blocking
// loop, since QTcpSocket is already async -- there's no legacy synchronous
// C API to wrangle here.
//
// Scope (see plan / issue #3 for the full rationale): standard VNC
// Authentication (security type 2), no-auth (type 1), and Apple's Screen
// Sharing scheme (type 30, Diffie-Hellman + AES -- see
// vnc_apple_dh_auth.h; not part of RFC 6143, implemented against the
// well-established reverse-engineered wire format). Raw + CopyRect +
// Hextile + ZRLE + Tight encodings. No dynamic resize. Clipboard sync
// (Latin-1 only, per RFB's ServerCutText/ClientCutText) and remote cursor
// shape sync (the Cursor pseudo-encoding) are supported.
//
// Tight decoding gap: the real protocol allows the server to skip zlib
// compression entirely for very small Basic-mode payloads; this decoder
// always attempts to zlib-inflate them, so a server that takes that
// shortcut on a given rectangle would have that one rectangle fail rather
// than decode. This is intentionally not special-cased (the exact trigger
// condition/wire signaling for it could not be verified with confidence
// against the RFC text alone, and it only affects rare, tiny rectangles --
// solid or near-solid tiny areas are virtually always sent as Fill instead
// in practice) -- see docs/PROGRESS.md.
class VncSessionBackend : public SessionBackend
{
Q_OBJECT
public:
explicit VncSessionBackend(const Profile& profile, QObject* parent = nullptr);
~VncSessionBackend() override;
// Pure, state-free helpers exposed as public statics purely so tests
// can exercise them without a live connection.
static QByteArray vncAuthResponse(const QByteArray& challenge, const QString& password);
static QByteArray desKeyFromPassword(const QString& password);
static QString mapSocketError(QAbstractSocket::SocketError error, const QString& rawDetail);
static quint32 keysymForQtKey(int key, const QString& text);
public slots:
void connectSession(const SessionConnectOptions& options) override;
void disconnectSession() override;
void reconnectSession(const SessionConnectOptions& options) override;
void sendInput(const QString& input) override;
void confirmHostKey(bool trustHost) override;
void updateTerminalSize(int columns, int rows) override;
void sendKeyEvent(int key,
quint32 nativeScanCode,
const QString& text,
bool pressed,
int modifiers) override;
void sendMouseMoveEvent(int x, int y) override;
void sendMouseButtonEvent(int x, int y, int button, bool pressed) override;
void sendMouseWheelEvent(int x, int y, int deltaX, int deltaY) override;
void setClipboardText(const QString& text) override;
private slots:
void onSocketConnected();
void onSocketReadyRead();
void onSocketDisconnected();
void onSocketErrorOccurred(QAbstractSocket::SocketError error);
private:
enum class RfbState {
Idle,
WaitingProtocolVersion,
WaitingSecurityTypeCount,
WaitingSecurityTypeList,
WaitingSecurityTypeV33,
WaitingSecurityFailureReasonLength,
WaitingSecurityFailureReason,
WaitingVncAuthChallenge,
WaitingSecurityResult,
WaitingSecurityResultReasonLength,
WaitingSecurityResultReason,
WaitingServerInitHeader,
WaitingServerName,
WaitingServerMessageType,
WaitingFramebufferUpdateHeader,
WaitingRectangleHeader,
WaitingRawPixelData,
WaitingCopyRectSource,
WaitingCursorPixelData,
WaitingHextileTileSubencoding,
WaitingHextileTileMeta,
WaitingHextileSubrectData,
WaitingHextileRawTileData,
WaitingZrleCompressedLength,
WaitingZrleCompressedData,
WaitingTightCompressionControl,
WaitingTightFillColor,
WaitingTightFilterId,
WaitingTightLengthByte,
WaitingTightPayload,
WaitingAppleAuthGeneratorLength,
WaitingAppleAuthGeneratorBytes,
WaitingAppleAuthPrimeLength,
WaitingAppleAuthPrimeAndServerKey,
WaitingSetColourMapHeader,
WaitingSetColourMapData,
WaitingServerCutTextHeader,
WaitingServerCutTextData,
};
struct PendingRectangle {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
qint32 encoding = 0;
};
QTcpSocket* m_socket;
SessionState m_state;
RfbState m_rfbState;
QByteArray m_recvBuffer;
bool m_userInitiatedDisconnect;
bool m_reconnectPending;
SessionConnectOptions m_reconnectOptions;
SessionConnectOptions m_activeOptions;
int m_negotiatedMinorVersion;
int m_securityTypeCount;
QByteArray m_offeredSecurityTypes;
quint8 m_chosenSecurityType;
quint32 m_pendingLength;
QImage m_framebuffer;
int m_pendingRectanglesRemaining;
PendingRectangle m_currentRectangle;
quint8 m_pointerButtonMask;
int m_lastPointerX;
int m_lastPointerY;
// Hextile decode state (RFC 6143 SS7.7.4): tile-cursor position relative
// to the current rectangle's origin, plus the background/foreground
// colors, which persist across tiles within one rectangle whenever a
// tile doesn't re-specify them.
int m_hextileTileX;
int m_hextileTileY;
QRgb m_hextileBackground;
QRgb m_hextileForeground;
quint8 m_hextileSubencoding;
int m_hextileSubrectsRemaining;
bool m_hextileSubrectsColoured;
// ZRLE's zlib stream (RFC 6143 SS7.7.6) persists for the whole
// connection, not per-rectangle or per-update -- lazily initialized on
// the first ZRLE rectangle, torn down and reset on every fresh
// connect/reconnect via resetProtocolState(). z_stream_s is only
// forward-declared here so <zlib.h> doesn't leak into every includer of
// this header; the full type is only needed in the .cpp.
z_stream_s* m_zrleInflateStream;
bool m_zrleInflateInitialized;
// Tight decode state (RFC 6143 SS7.7.4). Unlike ZRLE, Tight's "Basic"
// compression mode has 4 independent persistent zlib streams (chosen
// per-rectangle by 2 bits of the compression-control byte), each with
// its own lifecycle -- reset individually via the control byte's low 4
// bits, otherwise persisting like ZRLE's single stream.
std::array<z_stream_s*, 4> m_tightInflateStreams;
std::array<bool, 4> m_tightInflateInitialized;
quint8 m_tightCompressionMode; // compression-control byte >> 4
quint8 m_tightFilterId;
int m_tightLengthByteIndex;
// Apple Screen Sharing authentication state (security type 30, not
// part of RFC 6143 -- see vnc_apple_dh_auth.h). m_appleAuthGenerator
// must persist across the generator-length/generator-bytes state
// hop; m_appleAuthPrimeLength must persist from when it's first read
// until the combined prime+server-public-key buffer (2x that length)
// has fully arrived, since m_pendingLength gets reused to track that
// combined byte count in the meantime.
QByteArray m_appleAuthGenerator;
quint32 m_appleAuthPrimeLength;
void setState(SessionState state, const QString& message);
void resetProtocolState();
void processReceiveBuffer();
bool haveBytes(int count) const;
void sendVersionReply();
void sendClientInit();
void sendSetPixelFormatAndEncodings();
void requestFramebufferUpdate(bool incremental);
void failConnection(const QString& displayMessage, const QString& rawMessage);
void finishHandshakeIntoRunningState();
void onRectangleFinished();
void sendPointerEvent();
void sendWheelClick(quint8 wheelBit);
void sendClientCutText(const QString& text);
QRect currentHextileTileRect() const;
void advanceHextileTile();
bool inflateTightStream(int streamIndex, const QByteArray& compressed, QByteArray* decompressed);
};
#endif