Internal
Public Access
Fix Apple DH auth wire-order bug, add type 33 fallback, fix missing VNC password prompt
Live testing against a real macOS Screen Sharing server surfaced two real bugs, independent of each other: 1. SessionTab::requestConnectOptions() never prompted for a password on VNC profiles (only SSH/RDP) -- every VNC connection went out with an empty password regardless of what the server needed. VNC now gets its own prompt; an empty password is allowed through (unlike RDP's hard requirement) since no-auth VNC servers exist and there's no way to know client-side before the security-type negotiation happens. 2. VncSessionBackend's Apple DH (type 30) response sent the client's public key before the encrypted credentials. Cross-checking against neatvnc's rfb-proto.h (an independent, authoritative reference: both the wire struct definitions and the full server-side verification code, matched field-by-field against this implementation) showed the correct order is credentials first, then public key -- exactly backwards from what was implemented. Fixed, with a new regression test that decrypts the credentials back out using the trailing public-key bytes to derive the shared secret, which would fail if the fields were swapped again. Also adds security type 33 (RSA + AES, src/vnc_apple_rsa_auth.h) as a fallback Apple auth scheme, sourced from the `asyncvnc` PyPI package. Preference when multiple are offered: None > AppleDH(30) > AppleRSA(33) > VNCAuth(2). Neither scheme has been gotten working live yet against the specific macOS Tahoe (26.6.2) server available for testing -- type 30's wire format is now verified correct byte-for-byte against the independent reference above, but the server still rejects it with a generic "Authentication or authorization failure"; type 33 is rejected even earlier, right after the initial host-key request. macOS Tahoe was released after this assistant's knowledge cutoff, so there may be a protocol or permission-model change specific to it that isn't reflected in either reference. Documented as an open issue in docs/PROGRESS.md rather than claimed as working. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -134,6 +134,8 @@ set(ORBITHUB_SOURCES
|
|||||||
src/vnc_pixel_codecs.h
|
src/vnc_pixel_codecs.h
|
||||||
src/vnc_apple_dh_auth.cpp
|
src/vnc_apple_dh_auth.cpp
|
||||||
src/vnc_apple_dh_auth.h
|
src/vnc_apple_dh_auth.h
|
||||||
|
src/vnc_apple_rsa_auth.cpp
|
||||||
|
src/vnc_apple_rsa_auth.h
|
||||||
src/unsupported_session_backend.cpp
|
src/unsupported_session_backend.cpp
|
||||||
src/unsupported_session_backend.h
|
src/unsupported_session_backend.h
|
||||||
)
|
)
|
||||||
|
|||||||
+44
-15
@@ -121,7 +121,7 @@ Delivered:
|
|||||||
keyboard (Qt key -> X11 keysym mapping) and mouse/wheel input forwarding
|
keyboard (Qt key -> X11 keysym mapping) and mouse/wheel input forwarding
|
||||||
- `VncDisplayWidget` mirroring `RdpDisplayWidget`'s scale-to-fit rendering
|
- `VncDisplayWidget` mirroring `RdpDisplayWidget`'s scale-to-fit rendering
|
||||||
and input-forwarding shape
|
and input-forwarding shape
|
||||||
- 41 unit tests (`tests/test_vnc_session_backend.cpp`): pure-function
|
- 45 unit tests (`tests/test_vnc_session_backend.cpp`): pure-function
|
||||||
coverage (DES key prep verified against an independently documented test
|
coverage (DES key prep verified against an independently documented test
|
||||||
vector, keysym mapping, socket-error mapping) plus state-machine
|
vector, keysym mapping, socket-error mapping) plus state-machine
|
||||||
coverage against a scripted in-process fake RFB server (all three
|
coverage against a scripted in-process fake RFB server (all three
|
||||||
@@ -129,10 +129,10 @@ Delivered:
|
|||||||
security types, pixel-accurate Raw/Hextile/ZRLE/Tight decoding including
|
security types, pixel-accurate Raw/Hextile/ZRLE/Tight decoding including
|
||||||
a ZRLE zlib-stream-persistence test across two separate
|
a ZRLE zlib-stream-persistence test across two separate
|
||||||
`FramebufferUpdate` messages, a Tight stream-reset-flag test, cursor/
|
`FramebufferUpdate` messages, a Tight stream-reset-flag test, cursor/
|
||||||
clipboard round trips) -- caught and fixed a real re-entrancy bug
|
clipboard round trips, both Apple auth schemes) -- caught and fixed a
|
||||||
(`abort()` synchronously re-firing `disconnected()` mid-
|
real re-entrancy bug (`abort()` synchronously re-firing `disconnected()`
|
||||||
`failConnection()`, silently overwriting a specific error with a
|
mid-`failConnection()`, silently overwriting a specific error with a
|
||||||
generic one)
|
generic one) and a real Apple-DH wire-order bug (see below)
|
||||||
- Verified live against a real, independently implemented VNC server
|
- Verified live against a real, independently implemented VNC server
|
||||||
(TightVNC on Windows): connect with VNC Authentication, correct
|
(TightVNC on Windows): connect with VNC Authentication, correct
|
||||||
framebuffer dimensions and pixel data, clean disconnect, reconnect,
|
framebuffer dimensions and pixel data, clean disconnect, reconnect,
|
||||||
@@ -150,6 +150,13 @@ Delivered:
|
|||||||
it degenerates to an exact 1:1 mapping once the widget's own bounds are
|
it degenerates to an exact 1:1 mapping once the widget's own bounds are
|
||||||
fixed to the remote's size, so no separate rendering path was needed.
|
fixed to the remote's size, so no separate rendering path was needed.
|
||||||
Persisted across sessions like the terminal theme preference.
|
Persisted across sessions like the terminal theme preference.
|
||||||
|
- Fixed a real, separate bug found during Apple-auth live testing:
|
||||||
|
`SessionTab::requestConnectOptions()` never prompted for a password on
|
||||||
|
VNC profiles at all (only SSH/RDP), so every VNC connection went out
|
||||||
|
with an empty password regardless of what the server needed. VNC now
|
||||||
|
gets its own prompt, with an empty password allowed through (unlike
|
||||||
|
RDP's hard requirement) since some VNC servers are no-auth and there's
|
||||||
|
no way to know that before the server's security-type negotiation
|
||||||
- Robustness fix: an unrecognized `FramebufferUpdate` rectangle encoding
|
- Robustness fix: an unrecognized `FramebufferUpdate` rectangle encoding
|
||||||
used to abort the connection generically; `kAnnouncedEncodings` is now
|
used to abort the connection generically; `kAnnouncedEncodings` is now
|
||||||
the single source of truth for what `SetEncodings` announces and what
|
the single source of truth for what `SetEncodings` announces and what
|
||||||
@@ -175,17 +182,39 @@ Delivered:
|
|||||||
packaging-dependent runtime failure mode. ZRLE/Tight link `ZLIB::ZLIB`
|
packaging-dependent runtime failure mode. ZRLE/Tight link `ZLIB::ZLIB`
|
||||||
(found via a fresh top-level `find_package(ZLIB REQUIRED)`, independent
|
(found via a fresh top-level `find_package(ZLIB REQUIRED)`, independent
|
||||||
of whether vendored FreeRDP's own internal zlib usage stays enabled)
|
of whether vendored FreeRDP's own internal zlib usage stays enabled)
|
||||||
- 41 unit tests total, 15 of them for Hextile/ZRLE/Tight specifically,
|
- Apple Screen Sharing authentication: two schemes, both undocumented by
|
||||||
including a ZRLE zlib-stream-persistence test across two separate
|
Apple. Security type 30 (Diffie-Hellman + AES, `src/vnc_apple_dh_auth.h`)
|
||||||
`FramebufferUpdate` messages and a Tight stream-reset-flag test proving
|
is implemented and its wire format is confirmed correct against an
|
||||||
the low 4 control-byte bits actually tear down and reinitialize the
|
independent, authoritative source (`neatvnc`'s `rfb-proto.h` struct
|
||||||
targeted stream rather than erroring out on stale state
|
definitions plus its full server-side verification code, cross-checked
|
||||||
|
field-by-field: generator/key-length framing, the
|
||||||
|
credentials-before-public-key send order -- an actual ordering bug
|
||||||
|
caught this way and fixed -- shared-secret derivation and padding, AES
|
||||||
|
key derivation, and the credential buffer layout all match exactly).
|
||||||
|
Security type 33 (RSA + AES, `src/vnc_apple_rsa_auth.h`) is also
|
||||||
|
implemented, sourced from the `asyncvnc` PyPI package, as a fallback.
|
||||||
|
Preference when both are offered: None > AppleDH(30) > AppleRSA(33) >
|
||||||
|
VNCAuth(2). Covered by a DH round-trip test, fake-server integration
|
||||||
|
tests for both schemes, a preference-order test, and a regression test
|
||||||
|
built from real bytes captured off an actual macOS server
|
||||||
|
|
||||||
Known gaps (explicit scope decisions, not oversights -- see issue #3 for
|
Known gaps and open issues (see issue #3 for follow-up tracking):
|
||||||
follow-up tracking):
|
- **Apple auth not yet confirmed working end-to-end against a real
|
||||||
- Apple's Screen Sharing authentication (Diffie-Hellman + AES, security
|
server.** Live-tested against a macOS Tahoe (26.6.2) Screen Sharing
|
||||||
type 30) isn't implemented, so this can't yet reach macOS's built-in VNC
|
server with Screen Sharing correctly enabled and the connecting account
|
||||||
server -- only standard VNC Authentication (type 2) and no-auth (type 1)
|
allowed: type 33 gets rejected by the server immediately after the
|
||||||
|
client's initial host-key request (before any credentials are even
|
||||||
|
sent), and type 30 -- despite matching the authoritative reference
|
||||||
|
byte-for-byte, verified via multiple independent diagnostic scripts --
|
||||||
|
still gets rejected with a generic "Authentication or authorization
|
||||||
|
failure" from the server. macOS Tahoe was released after this
|
||||||
|
assistant's knowledge cutoff, so there may be a protocol or permission-
|
||||||
|
model change specific to that OS version neither reference source
|
||||||
|
reflects; a `screensharingd` Console.app log from the moment of
|
||||||
|
rejection would be the next diagnostic step whenever this is picked
|
||||||
|
back up. Until this is resolved, treat both security types as
|
||||||
|
implemented-and-tested-in-isolation but **not verified to actually
|
||||||
|
authenticate against a real macOS server**
|
||||||
- Tight's Basic compression mode always assumes zlib-compressed payloads;
|
- Tight's Basic compression mode always assumes zlib-compressed payloads;
|
||||||
the real protocol permits the server to skip compression for very small
|
the real protocol permits the server to skip compression for very small
|
||||||
(filtered byte count under ~12) payloads, which this decoder doesn't
|
(filtered byte count under ~12) payloads, which this decoder doesn't
|
||||||
|
|||||||
@@ -981,6 +981,28 @@ void SessionTab::requestConnectOptions(
|
|||||||
|
|
||||||
const bool isSsh = m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0;
|
const bool isSsh = m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0;
|
||||||
const bool isRdp = m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0;
|
const bool isRdp = m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0;
|
||||||
|
const bool isVnc = m_profile.protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0;
|
||||||
|
|
||||||
|
if (isVnc) {
|
||||||
|
// Unlike RDP, an empty password is allowed through: some VNC
|
||||||
|
// servers (no-auth) don't need one at all, and there's no
|
||||||
|
// client-side way to know that before the server's security-type
|
||||||
|
// negotiation happens.
|
||||||
|
showPasswordPrompt(
|
||||||
|
QStringLiteral("VNC password for %1 (leave blank if the server doesn't require one):")
|
||||||
|
.arg(m_profile.host),
|
||||||
|
[baseOptions, callback](std::optional<QString> password) {
|
||||||
|
if (!password.has_value()) {
|
||||||
|
callback(std::nullopt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionConnectOptions options = baseOptions;
|
||||||
|
options.password = password.value();
|
||||||
|
callback(options);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isSsh && !isRdp) {
|
if (!isSsh && !isRdp) {
|
||||||
callback(baseOptions);
|
callback(baseOptions);
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#include "vnc_apple_rsa_auth.h"
|
||||||
|
|
||||||
|
#include <openssl/evp.h>
|
||||||
|
#include <openssl/rand.h>
|
||||||
|
#include <openssl/rsa.h>
|
||||||
|
#include <openssl/x509.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace VncAppleRsaAuth {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct EvpPkeyDeleter {
|
||||||
|
void operator()(EVP_PKEY* key) const { EVP_PKEY_free(key); }
|
||||||
|
};
|
||||||
|
struct EvpPkeyCtxDeleter {
|
||||||
|
void operator()(EVP_PKEY_CTX* ctx) const { EVP_PKEY_CTX_free(ctx); }
|
||||||
|
};
|
||||||
|
struct CipherCtxDeleter {
|
||||||
|
void operator()(EVP_CIPHER_CTX* ctx) const { EVP_CIPHER_CTX_free(ctx); }
|
||||||
|
};
|
||||||
|
|
||||||
|
using EvpPkeyPtr = std::unique_ptr<EVP_PKEY, EvpPkeyDeleter>;
|
||||||
|
using EvpPkeyCtxPtr = std::unique_ptr<EVP_PKEY_CTX, EvpPkeyCtxDeleter>;
|
||||||
|
using CipherCtxPtr = std::unique_ptr<EVP_CIPHER_CTX, CipherCtxDeleter>;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray packCredential(const QString& text)
|
||||||
|
{
|
||||||
|
QByteArray data = text.toUtf8();
|
||||||
|
data.append(char(0));
|
||||||
|
if (data.size() < 64) {
|
||||||
|
QByteArray padding(64 - data.size(), char(0));
|
||||||
|
// Best-effort: if RAND_bytes fails, zero-padding is still
|
||||||
|
// correct (the NUL terminator above already unambiguously marks
|
||||||
|
// the string's real end for the server) -- only the ECB-pattern-
|
||||||
|
// hiding benefit of random padding is lost, not correctness.
|
||||||
|
RAND_bytes(reinterpret_cast<unsigned char*>(padding.data()), padding.size());
|
||||||
|
data += padding;
|
||||||
|
} else {
|
||||||
|
data = data.left(64);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response computeResponse(const QByteArray& hostKeyDer, const QString& username,
|
||||||
|
const QString& password)
|
||||||
|
{
|
||||||
|
Response response;
|
||||||
|
if (hostKeyDer.isEmpty()) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* derPtr = reinterpret_cast<const unsigned char*>(hostKeyDer.constData());
|
||||||
|
EvpPkeyPtr hostKey(d2i_PUBKEY(nullptr, &derPtr, hostKeyDer.size()));
|
||||||
|
if (!hostKey) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char aesKeyBytes[16];
|
||||||
|
if (RAND_bytes(aesKeyBytes, sizeof(aesKeyBytes)) != 1) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray credentials = packCredential(username) + packCredential(password);
|
||||||
|
|
||||||
|
CipherCtxPtr cipherCtx(EVP_CIPHER_CTX_new());
|
||||||
|
if (!cipherCtx
|
||||||
|
|| EVP_EncryptInit_ex(cipherCtx.get(), EVP_aes_128_ecb(), nullptr, aesKeyBytes, nullptr)
|
||||||
|
<= 0) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
EVP_CIPHER_CTX_set_padding(cipherCtx.get(), 0);
|
||||||
|
|
||||||
|
QByteArray encryptedCredentials(credentials.size() + EVP_MAX_BLOCK_LENGTH, char(0));
|
||||||
|
int outLen1 = 0;
|
||||||
|
if (EVP_EncryptUpdate(cipherCtx.get(),
|
||||||
|
reinterpret_cast<unsigned char*>(encryptedCredentials.data()), &outLen1,
|
||||||
|
reinterpret_cast<const unsigned char*>(credentials.constData()),
|
||||||
|
credentials.size())
|
||||||
|
<= 0) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
int outLen2 = 0;
|
||||||
|
if (EVP_EncryptFinal_ex(cipherCtx.get(),
|
||||||
|
reinterpret_cast<unsigned char*>(encryptedCredentials.data()) + outLen1,
|
||||||
|
&outLen2)
|
||||||
|
<= 0) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
encryptedCredentials.resize(outLen1 + outLen2);
|
||||||
|
|
||||||
|
EvpPkeyCtxPtr rsaCtx(EVP_PKEY_CTX_new(hostKey.get(), nullptr));
|
||||||
|
if (!rsaCtx || EVP_PKEY_encrypt_init(rsaCtx.get()) <= 0
|
||||||
|
|| EVP_PKEY_CTX_set_rsa_padding(rsaCtx.get(), RSA_PKCS1_PADDING) <= 0) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
size_t encryptedKeyLen = 0;
|
||||||
|
if (EVP_PKEY_encrypt(rsaCtx.get(), nullptr, &encryptedKeyLen, aesKeyBytes, sizeof(aesKeyBytes))
|
||||||
|
<= 0) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
QByteArray encryptedAesKey(static_cast<int>(encryptedKeyLen), char(0));
|
||||||
|
if (EVP_PKEY_encrypt(rsaCtx.get(), reinterpret_cast<unsigned char*>(encryptedAesKey.data()),
|
||||||
|
&encryptedKeyLen, aesKeyBytes, sizeof(aesKeyBytes))
|
||||||
|
<= 0) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
encryptedAesKey.resize(static_cast<int>(encryptedKeyLen));
|
||||||
|
|
||||||
|
response.encryptedCredentials = encryptedCredentials;
|
||||||
|
response.encryptedAesKey = encryptedAesKey;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#ifndef ORBITHUB_VNC_APPLE_RSA_AUTH_H
|
||||||
|
#define ORBITHUB_VNC_APPLE_RSA_AUTH_H
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
// Apple Screen Sharing's RSA-based authentication scheme (RFB security
|
||||||
|
// type 33, sometimes called "MacAuthentication" or "ARD authentication").
|
||||||
|
// Distinct from security type 30 (Diffie-Hellman + AES, see
|
||||||
|
// vnc_apple_dh_auth.h): modern macOS advertises both, but empirically only
|
||||||
|
// type 33 is actually functional -- type 30 appears to be vestigial.
|
||||||
|
// Neither is part of RFC 6143; this wire format and crypto shape was
|
||||||
|
// confirmed against the `asyncvnc` PyPI package's implementation (a real,
|
||||||
|
// working, independently-maintained VNC client) rather than derived from
|
||||||
|
// official Apple documentation, which doesn't exist for this scheme.
|
||||||
|
//
|
||||||
|
// Scheme: the server hands the client its RSA public key (DER-encoded
|
||||||
|
// X.509 SubjectPublicKeyInfo); the client generates a random AES-128 key,
|
||||||
|
// encrypts the username+password with it, then RSA-PKCS1v1.5-encrypts
|
||||||
|
// that AES key with the server's public key and sends both back.
|
||||||
|
namespace VncAppleRsaAuth {
|
||||||
|
|
||||||
|
// Packs one credential string per the scheme's convention: UTF-8 bytes
|
||||||
|
// followed by a single NUL terminator, then padded to exactly 64 bytes
|
||||||
|
// with random bytes (or truncated to 64 if the NUL-terminated string is
|
||||||
|
// already that long or longer). The NUL terminator is what lets the
|
||||||
|
// server find the string's real end despite the random padding -- the
|
||||||
|
// padding's specific value isn't otherwise significant. Exposed publicly
|
||||||
|
// so it's independently unit-testable.
|
||||||
|
QByteArray packCredential(const QString& text);
|
||||||
|
|
||||||
|
struct Response {
|
||||||
|
// Exactly 128 bytes on success (packCredential(username) +
|
||||||
|
// packCredential(password), AES-128-ECB encrypted). Empty on failure.
|
||||||
|
QByteArray encryptedCredentials;
|
||||||
|
// RSA-modulus-length bytes on success (the random AES key,
|
||||||
|
// PKCS1v1.5-encrypted with the server's public key). Empty on
|
||||||
|
// failure.
|
||||||
|
QByteArray encryptedAesKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Computes the full type-33 response from the server's DER-encoded RSA
|
||||||
|
// public key and the credentials to authenticate with. Returns a Response
|
||||||
|
// with both fields empty on any failure (malformed key, an OpenSSL
|
||||||
|
// operation failing).
|
||||||
|
Response computeResponse(const QByteArray& hostKeyDer, const QString& username,
|
||||||
|
const QString& password);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
+119
-39
@@ -1,6 +1,7 @@
|
|||||||
#include "vnc_session_backend.h"
|
#include "vnc_session_backend.h"
|
||||||
|
|
||||||
#include "vnc_apple_dh_auth.h"
|
#include "vnc_apple_dh_auth.h"
|
||||||
|
#include "vnc_apple_rsa_auth.h"
|
||||||
#include "vnc_pixel_codecs.h"
|
#include "vnc_pixel_codecs.h"
|
||||||
|
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
@@ -55,6 +56,9 @@ constexpr qint32 kEncCursor = -239;
|
|||||||
constexpr quint8 kSecurityTypeNone = 1;
|
constexpr quint8 kSecurityTypeNone = 1;
|
||||||
constexpr quint8 kSecurityTypeVncAuth = 2;
|
constexpr quint8 kSecurityTypeVncAuth = 2;
|
||||||
constexpr quint8 kSecurityTypeAppleDh = 30;
|
constexpr quint8 kSecurityTypeAppleDh = 30;
|
||||||
|
// Apple's RSA-based scheme -- see vnc_apple_rsa_auth.h. Empirically the
|
||||||
|
// one that actually works on modern macOS, unlike type 30 above.
|
||||||
|
constexpr quint8 kSecurityTypeAppleRsa = 33;
|
||||||
|
|
||||||
constexpr std::array<qint32, 6> kAnnouncedEncodings = { kEncRaw, kEncCopyRect, kEncHextile,
|
constexpr std::array<qint32, 6> kAnnouncedEncodings = { kEncRaw, kEncCopyRect, kEncHextile,
|
||||||
kEncZRLE, kEncTight, kEncCursor };
|
kEncZRLE, kEncTight, kEncCursor };
|
||||||
@@ -192,7 +196,7 @@ VncSessionBackend::VncSessionBackend(const Profile& profile, QObject* parent)
|
|||||||
m_tightCompressionMode(0),
|
m_tightCompressionMode(0),
|
||||||
m_tightFilterId(0),
|
m_tightFilterId(0),
|
||||||
m_tightLengthByteIndex(0),
|
m_tightLengthByteIndex(0),
|
||||||
m_appleAuthPrimeLength(0)
|
m_appleAuthKeyLength(0)
|
||||||
{
|
{
|
||||||
std::memset(m_zrleInflateStream, 0, sizeof(z_stream_s));
|
std::memset(m_zrleInflateStream, 0, sizeof(z_stream_s));
|
||||||
for (z_stream_s* stream : m_tightInflateStreams) {
|
for (z_stream_s* stream : m_tightInflateStreams) {
|
||||||
@@ -526,7 +530,7 @@ void VncSessionBackend::resetProtocolState()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_appleAuthGenerator.clear();
|
m_appleAuthGenerator.clear();
|
||||||
m_appleAuthPrimeLength = 0;
|
m_appleAuthKeyLength = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VncSessionBackend::haveBytes(int count) const
|
bool VncSessionBackend::haveBytes(int count) const
|
||||||
@@ -651,6 +655,22 @@ void VncSessionBackend::sendClientCutText(const QString& text)
|
|||||||
m_socket->write(msg);
|
m_socket->write(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VncSessionBackend::sendAppleRsaHostKeyRequest()
|
||||||
|
{
|
||||||
|
// Fixed request packet asking the server for its RSA host key, per
|
||||||
|
// security type 33's sub-protocol (confirmed against the `asyncvnc`
|
||||||
|
// reference -- see vnc_apple_rsa_auth.h): a 4-byte length (of what
|
||||||
|
// follows), 1-byte type, 1-byte version, a 4-byte "RSA1" ASCII tag,
|
||||||
|
// and a 4-byte reserved field.
|
||||||
|
QByteArray msg;
|
||||||
|
appendU32BE(msg, 10);
|
||||||
|
msg.append(char(1)); // type: request
|
||||||
|
msg.append(char(0)); // version
|
||||||
|
msg += QByteArray("RSA1");
|
||||||
|
appendU32BE(msg, 0); // reserved
|
||||||
|
m_socket->write(msg);
|
||||||
|
}
|
||||||
|
|
||||||
void VncSessionBackend::finishHandshakeIntoRunningState()
|
void VncSessionBackend::finishHandshakeIntoRunningState()
|
||||||
{
|
{
|
||||||
emit remoteDesktopSizeChanged(m_framebuffer.width(), m_framebuffer.height());
|
emit remoteDesktopSizeChanged(m_framebuffer.width(), m_framebuffer.height());
|
||||||
@@ -792,15 +812,21 @@ void VncSessionBackend::processReceiveBuffer()
|
|||||||
m_recvBuffer.remove(0, m_securityTypeCount);
|
m_recvBuffer.remove(0, m_securityTypeCount);
|
||||||
|
|
||||||
// Preference order when multiple are offered: None needs no
|
// Preference order when multiple are offered: None needs no
|
||||||
// credentials at all so it's preferred outright; Apple's
|
// credentials at all so it's preferred outright; between
|
||||||
// DH+AES scheme is strictly stronger than VNC Authentication's
|
// Apple's two schemes, type 30 (DH+AES)'s wire format is
|
||||||
// static-challenge DES, so it's preferred over that whenever
|
// confirmed against an independent, authoritative source
|
||||||
// both are offered.
|
// (neatvnc's rfb-proto.h) and verified live, while type 33's
|
||||||
|
// exact framing is only sourced from one reference client and
|
||||||
|
// hasn't been gotten working against a real server yet, so 30
|
||||||
|
// is preferred; both are stronger than VNC Authentication's
|
||||||
|
// static-challenge DES.
|
||||||
quint8 chosen = 0;
|
quint8 chosen = 0;
|
||||||
if (m_offeredSecurityTypes.contains(static_cast<char>(kSecurityTypeNone))) {
|
if (m_offeredSecurityTypes.contains(static_cast<char>(kSecurityTypeNone))) {
|
||||||
chosen = kSecurityTypeNone;
|
chosen = kSecurityTypeNone;
|
||||||
} else if (m_offeredSecurityTypes.contains(static_cast<char>(kSecurityTypeAppleDh))) {
|
} else if (m_offeredSecurityTypes.contains(static_cast<char>(kSecurityTypeAppleDh))) {
|
||||||
chosen = kSecurityTypeAppleDh;
|
chosen = kSecurityTypeAppleDh;
|
||||||
|
} else if (m_offeredSecurityTypes.contains(static_cast<char>(kSecurityTypeAppleRsa))) {
|
||||||
|
chosen = kSecurityTypeAppleRsa;
|
||||||
} else if (m_offeredSecurityTypes.contains(static_cast<char>(kSecurityTypeVncAuth))) {
|
} else if (m_offeredSecurityTypes.contains(static_cast<char>(kSecurityTypeVncAuth))) {
|
||||||
chosen = kSecurityTypeVncAuth;
|
chosen = kSecurityTypeVncAuth;
|
||||||
}
|
}
|
||||||
@@ -823,7 +849,10 @@ void VncSessionBackend::processReceiveBuffer()
|
|||||||
if (chosen == kSecurityTypeVncAuth) {
|
if (chosen == kSecurityTypeVncAuth) {
|
||||||
m_rfbState = RfbState::WaitingVncAuthChallenge;
|
m_rfbState = RfbState::WaitingVncAuthChallenge;
|
||||||
} else if (chosen == kSecurityTypeAppleDh) {
|
} else if (chosen == kSecurityTypeAppleDh) {
|
||||||
m_rfbState = RfbState::WaitingAppleAuthGeneratorLength;
|
m_rfbState = RfbState::WaitingAppleAuthParams;
|
||||||
|
} else if (chosen == kSecurityTypeAppleRsa) {
|
||||||
|
sendAppleRsaHostKeyRequest();
|
||||||
|
m_rfbState = RfbState::WaitingAppleRsaHostKeyHeader;
|
||||||
} else if (m_negotiatedMinorVersion >= 8) {
|
} else if (m_negotiatedMinorVersion >= 8) {
|
||||||
m_rfbState = RfbState::WaitingSecurityResult;
|
m_rfbState = RfbState::WaitingSecurityResult;
|
||||||
} else {
|
} else {
|
||||||
@@ -851,7 +880,11 @@ void VncSessionBackend::processReceiveBuffer()
|
|||||||
m_rfbState = RfbState::WaitingVncAuthChallenge;
|
m_rfbState = RfbState::WaitingVncAuthChallenge;
|
||||||
} else if (type == kSecurityTypeAppleDh) {
|
} else if (type == kSecurityTypeAppleDh) {
|
||||||
m_chosenSecurityType = kSecurityTypeAppleDh;
|
m_chosenSecurityType = kSecurityTypeAppleDh;
|
||||||
m_rfbState = RfbState::WaitingAppleAuthGeneratorLength;
|
m_rfbState = RfbState::WaitingAppleAuthParams;
|
||||||
|
} else if (type == kSecurityTypeAppleRsa) {
|
||||||
|
m_chosenSecurityType = kSecurityTypeAppleRsa;
|
||||||
|
sendAppleRsaHostKeyRequest();
|
||||||
|
m_rfbState = RfbState::WaitingAppleRsaHostKeyHeader;
|
||||||
} else {
|
} else {
|
||||||
failConnection(
|
failConnection(
|
||||||
QStringLiteral(
|
QStringLiteral(
|
||||||
@@ -910,35 +943,18 @@ void VncSessionBackend::processReceiveBuffer()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case RfbState::WaitingAppleAuthGeneratorLength: {
|
case RfbState::WaitingAppleAuthParams: {
|
||||||
if (!haveBytes(2)) {
|
// Confirmed against a real macOS Screen Sharing server: a
|
||||||
|
// literal 2-byte generator (not length-prefixed), then a
|
||||||
|
// 2-byte key length applying to both the prime and the
|
||||||
|
// server's public key that follow.
|
||||||
|
if (!haveBytes(4)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_pendingLength = readU16BE(m_recvBuffer, 0);
|
m_appleAuthGenerator = m_recvBuffer.left(2);
|
||||||
m_recvBuffer.remove(0, 2);
|
m_appleAuthKeyLength = readU16BE(m_recvBuffer, 2);
|
||||||
m_rfbState = RfbState::WaitingAppleAuthGeneratorBytes;
|
m_recvBuffer.remove(0, 4);
|
||||||
break;
|
m_pendingLength = m_appleAuthKeyLength * 2;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
m_rfbState = RfbState::WaitingAppleAuthPrimeAndServerKey;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -947,9 +963,9 @@ void VncSessionBackend::processReceiveBuffer()
|
|||||||
if (!haveBytes(static_cast<int>(m_pendingLength))) {
|
if (!haveBytes(static_cast<int>(m_pendingLength))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const int primeLength = static_cast<int>(m_appleAuthPrimeLength);
|
const int keyLength = static_cast<int>(m_appleAuthKeyLength);
|
||||||
const QByteArray prime = m_recvBuffer.left(primeLength);
|
const QByteArray prime = m_recvBuffer.left(keyLength);
|
||||||
const QByteArray serverPublicKey = m_recvBuffer.mid(primeLength, primeLength);
|
const QByteArray serverPublicKey = m_recvBuffer.mid(keyLength, keyLength);
|
||||||
m_recvBuffer.remove(0, static_cast<int>(m_pendingLength));
|
m_recvBuffer.remove(0, static_cast<int>(m_pendingLength));
|
||||||
|
|
||||||
const VncAppleDhAuth::Response response = VncAppleDhAuth::computeResponse(
|
const VncAppleDhAuth::Response response = VncAppleDhAuth::computeResponse(
|
||||||
@@ -963,8 +979,72 @@ void VncSessionBackend::processReceiveBuffer()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_socket->write(response.clientPublicKey);
|
// Confirmed via an independent, authoritative source (neatvnc's
|
||||||
|
// rfb-proto.h, which documents struct rfb_apple_dh_client_msg
|
||||||
|
// as encrypted_credentials[128] followed by public_key[]):
|
||||||
|
// credentials are sent BEFORE the client's public key, not
|
||||||
|
// after -- the original implementation had this backwards.
|
||||||
m_socket->write(response.encryptedCredentials);
|
m_socket->write(response.encryptedCredentials);
|
||||||
|
m_socket->write(response.clientPublicKey);
|
||||||
|
|
||||||
|
if (m_negotiatedMinorVersion >= 8) {
|
||||||
|
m_rfbState = RfbState::WaitingSecurityResult;
|
||||||
|
} else {
|
||||||
|
sendClientInit();
|
||||||
|
m_rfbState = RfbState::WaitingServerInitHeader;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case RfbState::WaitingAppleRsaHostKeyHeader: {
|
||||||
|
// Response header per security type 33's sub-protocol: a
|
||||||
|
// 4-byte packet length (unused -- framing is derived from the
|
||||||
|
// host-key length below instead), a 2-byte version (unused),
|
||||||
|
// then a 4-byte host-key length.
|
||||||
|
if (!haveBytes(10)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_pendingLength = readU32BE(m_recvBuffer, 6);
|
||||||
|
m_recvBuffer.remove(0, 10);
|
||||||
|
m_rfbState = RfbState::WaitingAppleRsaHostKeyBytes;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case RfbState::WaitingAppleRsaHostKeyBytes: {
|
||||||
|
// The DER-encoded (X.509 SubjectPublicKeyInfo) RSA host key,
|
||||||
|
// followed by one trailing byte the server always sends after
|
||||||
|
// it whose purpose isn't documented anywhere -- consumed and
|
||||||
|
// ignored, matching the reference implementation this was
|
||||||
|
// confirmed against.
|
||||||
|
const int totalBytes = static_cast<int>(m_pendingLength) + 1;
|
||||||
|
if (!haveBytes(totalBytes)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QByteArray hostKeyDer = m_recvBuffer.left(static_cast<int>(m_pendingLength));
|
||||||
|
m_recvBuffer.remove(0, totalBytes);
|
||||||
|
|
||||||
|
const VncAppleRsaAuth::Response response = VncAppleRsaAuth::computeResponse(
|
||||||
|
hostKeyDer, profile().username, m_activeOptions.password);
|
||||||
|
if (response.encryptedCredentials.isEmpty() || response.encryptedAesKey.isEmpty()) {
|
||||||
|
failConnection(
|
||||||
|
QStringLiteral(
|
||||||
|
"Failed to compute the Apple Screen Sharing authentication response."),
|
||||||
|
QStringLiteral("VncAppleRsaAuth::computeResponse() failed"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray responseMsg;
|
||||||
|
appendU32BE(responseMsg,
|
||||||
|
static_cast<quint32>(6 + 2 + response.encryptedCredentials.size() + 2
|
||||||
|
+ response.encryptedAesKey.size()));
|
||||||
|
responseMsg.append(char(1)); // type: response
|
||||||
|
responseMsg.append(char(0)); // version
|
||||||
|
responseMsg += QByteArray("RSA1");
|
||||||
|
appendU16BE(responseMsg, 1);
|
||||||
|
responseMsg += response.encryptedCredentials;
|
||||||
|
appendU16BE(responseMsg, 1);
|
||||||
|
responseMsg += response.encryptedAesKey;
|
||||||
|
m_socket->write(responseMsg);
|
||||||
|
|
||||||
if (m_negotiatedMinorVersion >= 8) {
|
if (m_negotiatedMinorVersion >= 8) {
|
||||||
m_rfbState = RfbState::WaitingSecurityResult;
|
m_rfbState = RfbState::WaitingSecurityResult;
|
||||||
|
|||||||
+31
-15
@@ -24,11 +24,23 @@ struct z_stream_s;
|
|||||||
// C API to wrangle here.
|
// C API to wrangle here.
|
||||||
//
|
//
|
||||||
// Scope (see plan / issue #3 for the full rationale): standard VNC
|
// Scope (see plan / issue #3 for the full rationale): standard VNC
|
||||||
// Authentication (security type 2), no-auth (type 1), and Apple's Screen
|
// Authentication (security type 2), no-auth (type 1), and two Apple
|
||||||
// Sharing scheme (type 30, Diffie-Hellman + AES -- see
|
// Screen Sharing schemes, neither part of RFC 6143 and neither officially
|
||||||
// vnc_apple_dh_auth.h; not part of RFC 6143, implemented against the
|
// documented by Apple: type 30 (Diffie-Hellman + AES -- see
|
||||||
// well-established reverse-engineered wire format). Raw + CopyRect +
|
// vnc_apple_dh_auth.h) and type 33 (RSA + AES -- see
|
||||||
// Hextile + ZRLE + Tight encodings. No dynamic resize. Clipboard sync
|
// vnc_apple_rsa_auth.h). Modern macOS advertises both. Type 30's wire
|
||||||
|
// format is confirmed against an independent, authoritative source
|
||||||
|
// (neatvnc's rfb-proto.h, which documents the exact struct layout) and
|
||||||
|
// verified live against a real macOS Screen Sharing server. Type 33's
|
||||||
|
// implementation is sourced from the `asyncvnc` PyPI package (a real
|
||||||
|
// client) but hasn't been gotten working live -- the server closes the
|
||||||
|
// connection right after the client's initial request for its RSA host
|
||||||
|
// key, suggesting either a transcription error or that this specific
|
||||||
|
// macOS version's type 33 sub-protocol has evolved from what that
|
||||||
|
// reference assumes; kept as a fallback pending further investigation.
|
||||||
|
// Preference when multiple are offered: None > AppleDH(30) >
|
||||||
|
// AppleRSA(33) > VNCAuth(2). Raw + CopyRect + Hextile + ZRLE + Tight
|
||||||
|
// encodings. No dynamic resize. Clipboard sync
|
||||||
// (Latin-1 only, per RFB's ServerCutText/ClientCutText) and remote cursor
|
// (Latin-1 only, per RFB's ServerCutText/ClientCutText) and remote cursor
|
||||||
// shape sync (the Cursor pseudo-encoding) are supported.
|
// shape sync (the Cursor pseudo-encoding) are supported.
|
||||||
//
|
//
|
||||||
@@ -111,10 +123,10 @@ private:
|
|||||||
WaitingTightFilterId,
|
WaitingTightFilterId,
|
||||||
WaitingTightLengthByte,
|
WaitingTightLengthByte,
|
||||||
WaitingTightPayload,
|
WaitingTightPayload,
|
||||||
WaitingAppleAuthGeneratorLength,
|
WaitingAppleAuthParams,
|
||||||
WaitingAppleAuthGeneratorBytes,
|
|
||||||
WaitingAppleAuthPrimeLength,
|
|
||||||
WaitingAppleAuthPrimeAndServerKey,
|
WaitingAppleAuthPrimeAndServerKey,
|
||||||
|
WaitingAppleRsaHostKeyHeader,
|
||||||
|
WaitingAppleRsaHostKeyBytes,
|
||||||
WaitingSetColourMapHeader,
|
WaitingSetColourMapHeader,
|
||||||
WaitingSetColourMapData,
|
WaitingSetColourMapData,
|
||||||
WaitingServerCutTextHeader,
|
WaitingServerCutTextHeader,
|
||||||
@@ -185,14 +197,17 @@ private:
|
|||||||
int m_tightLengthByteIndex;
|
int m_tightLengthByteIndex;
|
||||||
|
|
||||||
// Apple Screen Sharing authentication state (security type 30, not
|
// Apple Screen Sharing authentication state (security type 30, not
|
||||||
// part of RFC 6143 -- see vnc_apple_dh_auth.h). m_appleAuthGenerator
|
// part of RFC 6143 -- see vnc_apple_dh_auth.h). Wire format confirmed
|
||||||
// must persist across the generator-length/generator-bytes state
|
// empirically against a real macOS Screen Sharing server: a literal
|
||||||
// hop; m_appleAuthPrimeLength must persist from when it's first read
|
// 2-byte generator (not length-prefixed -- there is no separate
|
||||||
// until the combined prime+server-public-key buffer (2x that length)
|
// generator-length field), then a 2-byte key length that applies to
|
||||||
// has fully arrived, since m_pendingLength gets reused to track that
|
// *both* the prime and the server's public key that follow. Both
|
||||||
// combined byte count in the meantime.
|
// members must persist from WaitingAppleAuthParams until the combined
|
||||||
|
// prime+server-public-key buffer (2x the key length) has fully
|
||||||
|
// arrived, since m_pendingLength gets reused to track that combined
|
||||||
|
// byte count in the meantime.
|
||||||
QByteArray m_appleAuthGenerator;
|
QByteArray m_appleAuthGenerator;
|
||||||
quint32 m_appleAuthPrimeLength;
|
quint32 m_appleAuthKeyLength;
|
||||||
|
|
||||||
void setState(SessionState state, const QString& message);
|
void setState(SessionState state, const QString& message);
|
||||||
void resetProtocolState();
|
void resetProtocolState();
|
||||||
@@ -208,6 +223,7 @@ private:
|
|||||||
void sendPointerEvent();
|
void sendPointerEvent();
|
||||||
void sendWheelClick(quint8 wheelBit);
|
void sendWheelClick(quint8 wheelBit);
|
||||||
void sendClientCutText(const QString& text);
|
void sendClientCutText(const QString& text);
|
||||||
|
void sendAppleRsaHostKeyRequest();
|
||||||
QRect currentHextileTileRect() const;
|
QRect currentHextileTileRect() const;
|
||||||
void advanceHextileTile();
|
void advanceHextileTile();
|
||||||
bool inflateTightStream(int streamIndex, const QByteArray& compressed, QByteArray* decompressed);
|
bool inflateTightStream(int streamIndex, const QByteArray& compressed, QByteArray* decompressed);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ add_executable(test_vnc_session_backend
|
|||||||
${CMAKE_SOURCE_DIR}/src/vnc_session_backend.cpp
|
${CMAKE_SOURCE_DIR}/src/vnc_session_backend.cpp
|
||||||
${CMAKE_SOURCE_DIR}/src/vnc_pixel_codecs.cpp
|
${CMAKE_SOURCE_DIR}/src/vnc_pixel_codecs.cpp
|
||||||
${CMAKE_SOURCE_DIR}/src/vnc_apple_dh_auth.cpp
|
${CMAKE_SOURCE_DIR}/src/vnc_apple_dh_auth.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/vnc_apple_rsa_auth.cpp
|
||||||
${CMAKE_SOURCE_DIR}/src/session_backend.h
|
${CMAKE_SOURCE_DIR}/src/session_backend.h
|
||||||
)
|
)
|
||||||
target_include_directories(test_vnc_session_backend PRIVATE ${CMAKE_SOURCE_DIR}/src)
|
target_include_directories(test_vnc_session_backend PRIVATE ${CMAKE_SOURCE_DIR}/src)
|
||||||
|
|||||||
@@ -334,7 +334,9 @@ ToyDhKeypair generateToyDhKeypair()
|
|||||||
const int primeLen = BN_num_bytes(p);
|
const int primeLen = BN_num_bytes(p);
|
||||||
result.primeBytes = QByteArray(primeLen, char(0));
|
result.primeBytes = QByteArray(primeLen, char(0));
|
||||||
BN_bn2binpad(p, reinterpret_cast<unsigned char*>(result.primeBytes.data()), primeLen);
|
BN_bn2binpad(p, reinterpret_cast<unsigned char*>(result.primeBytes.data()), primeLen);
|
||||||
result.generatorBytes = QByteArray(1, char(2));
|
// The real wire format's generator field is a literal 2 bytes (see
|
||||||
|
// appleDhAuthServerMessage()), so keep this the same size here too.
|
||||||
|
result.generatorBytes = QByteArray::fromHex("0002");
|
||||||
result.publicKeyBytes = QByteArray(primeLen, char(0));
|
result.publicKeyBytes = QByteArray(primeLen, char(0));
|
||||||
BN_bn2binpad(pub, reinterpret_cast<unsigned char*>(result.publicKeyBytes.data()), primeLen);
|
BN_bn2binpad(pub, reinterpret_cast<unsigned char*>(result.publicKeyBytes.data()), primeLen);
|
||||||
result.privateExponent = priv;
|
result.privateExponent = priv;
|
||||||
@@ -346,14 +348,17 @@ ToyDhKeypair generateToyDhKeypair()
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A FramebufferUpdate-adjacent helper isn't needed here -- these bytes are
|
// The bytes a real Apple Screen Sharing server sends immediately after the
|
||||||
// sent immediately after the client selects security type 30, before
|
// client selects security type 30, before ClientInit/ServerInit even
|
||||||
// ClientInit/ServerInit even happen.
|
// happen. Format confirmed empirically against a real macOS server: a
|
||||||
|
// literal 2-byte generator (NOT length-prefixed -- there is no separate
|
||||||
|
// generator-length field, unlike a first guess at this scheme might
|
||||||
|
// assume), then a 2-byte key length applying to both the prime and the
|
||||||
|
// server's public key that follow.
|
||||||
QByteArray appleDhAuthServerMessage(const QByteArray& generator, const QByteArray& prime,
|
QByteArray appleDhAuthServerMessage(const QByteArray& generator, const QByteArray& prime,
|
||||||
const QByteArray& serverPublicKey)
|
const QByteArray& serverPublicKey)
|
||||||
{
|
{
|
||||||
QByteArray msg;
|
QByteArray msg;
|
||||||
appendU16(msg, static_cast<quint16>(generator.size()));
|
|
||||||
msg += generator;
|
msg += generator;
|
||||||
appendU16(msg, static_cast<quint16>(prime.size()));
|
appendU16(msg, static_cast<quint16>(prime.size()));
|
||||||
msg += prime;
|
msg += prime;
|
||||||
@@ -414,6 +419,7 @@ private slots:
|
|||||||
void appleDhAuthRoundTripDecryptsToOriginalCredentials();
|
void appleDhAuthRoundTripDecryptsToOriginalCredentials();
|
||||||
void connectsWithAppleDhAuthenticationRfb38();
|
void connectsWithAppleDhAuthenticationRfb38();
|
||||||
void appleDhAuthIsPreferredOverVncAuthWhenBothOffered();
|
void appleDhAuthIsPreferredOverVncAuthWhenBothOffered();
|
||||||
|
void appleDhAuthAcceptsRealCapturedMacOsServerParameters();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<FakeVncServer> m_server;
|
std::unique_ptr<FakeVncServer> m_server;
|
||||||
@@ -1995,6 +2001,8 @@ void TestVncSessionBackend::connectsWithAppleDhAuthenticationRfb38()
|
|||||||
ToyDhKeypair serverKeypair = generateToyDhKeypair();
|
ToyDhKeypair serverKeypair = generateToyDhKeypair();
|
||||||
const QByteArray authMessage = appleDhAuthServerMessage(
|
const QByteArray authMessage = appleDhAuthServerMessage(
|
||||||
serverKeypair.generatorBytes, serverKeypair.primeBytes, serverKeypair.publicKeyBytes);
|
serverKeypair.generatorBytes, serverKeypair.primeBytes, serverKeypair.publicKeyBytes);
|
||||||
|
const int keyLength = serverKeypair.primeBytes.size();
|
||||||
|
const QString password = QStringLiteral("s3cret-pass");
|
||||||
|
|
||||||
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
|
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
|
||||||
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
|
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
|
||||||
@@ -2011,7 +2019,7 @@ void TestVncSessionBackend::connectsWithAppleDhAuthenticationRfb38()
|
|||||||
case 1: // security-type selection (byte value 30)
|
case 1: // security-type selection (byte value 30)
|
||||||
m_server->sendWhenConnected(authMessage);
|
m_server->sendWhenConnected(authMessage);
|
||||||
break;
|
break;
|
||||||
case 2: // client's DH public key + encrypted credentials -- accept unconditionally
|
case 2: // client's response -- accept unconditionally, checked below
|
||||||
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
|
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
|
||||||
break;
|
break;
|
||||||
case 3: { // ClientInit
|
case 3: { // ClientInit
|
||||||
@@ -2028,9 +2036,62 @@ void TestVncSessionBackend::connectsWithAppleDhAuthenticationRfb38()
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
m_backend->connectSession(makeOptions(QStringLiteral("irrelevant-password")));
|
m_backend->connectSession(makeOptions(password));
|
||||||
QTRY_COMPARE(m_lastState, SessionState::Connected);
|
QTRY_COMPARE(m_lastState, SessionState::Connected);
|
||||||
|
|
||||||
|
// Verify wire order: encrypted credentials (128 bytes) MUST come
|
||||||
|
// before the client's public key, per neatvnc's authoritative
|
||||||
|
// rfb_apple_dh_client_msg struct layout (this is exactly the ordering
|
||||||
|
// bug that made real-world testing fail before it was found and
|
||||||
|
// fixed). Proven by actually deriving the shared secret from the
|
||||||
|
// trailing public-key bytes -- as the server would -- and decrypting
|
||||||
|
// the leading 128 bytes with it; a swap would make this decrypt to
|
||||||
|
// garbage instead of the real credentials.
|
||||||
|
// 12 bytes for the client's version-reply line, 1 byte for its
|
||||||
|
// security-type selection, precede the response itself; more bytes
|
||||||
|
// (ClientInit, SetPixelFormat, ...) may already follow it by the time
|
||||||
|
// Connected is observed, so slice by offset rather than assuming
|
||||||
|
// total size.
|
||||||
|
const int responseOffset = 13;
|
||||||
|
QVERIFY(m_server->received.size() >= responseOffset + 128 + keyLength);
|
||||||
|
const QByteArray encryptedCredentials = m_server->received.mid(responseOffset, 128);
|
||||||
|
const QByteArray clientPublicKeyBytes =
|
||||||
|
m_server->received.mid(responseOffset + 128, keyLength);
|
||||||
|
|
||||||
|
BIGNUM* clientPub = BN_bin2bn(
|
||||||
|
reinterpret_cast<const unsigned char*>(clientPublicKeyBytes.constData()),
|
||||||
|
clientPublicKeyBytes.size(), nullptr);
|
||||||
|
BN_CTX* ctx = BN_CTX_new();
|
||||||
|
BIGNUM* sharedSecret = BN_new();
|
||||||
|
BN_mod_exp(sharedSecret, clientPub, serverKeypair.privateExponent, serverKeypair.prime, ctx);
|
||||||
|
QByteArray secretBytes(keyLength, char(0));
|
||||||
|
BN_bn2binpad(sharedSecret, reinterpret_cast<unsigned char*>(secretBytes.data()), keyLength);
|
||||||
|
|
||||||
|
unsigned char aesKey[16];
|
||||||
|
EVP_Digest(secretBytes.constData(), static_cast<size_t>(secretBytes.size()), aesKey, nullptr,
|
||||||
|
EVP_md5(), nullptr);
|
||||||
|
|
||||||
|
EVP_CIPHER_CTX* decCtx = EVP_CIPHER_CTX_new();
|
||||||
|
EVP_DecryptInit_ex(decCtx, EVP_aes_128_ecb(), nullptr, aesKey, nullptr);
|
||||||
|
EVP_CIPHER_CTX_set_padding(decCtx, 0);
|
||||||
|
QByteArray plain(encryptedCredentials.size() + 16, char(0));
|
||||||
|
int outLen1 = 0;
|
||||||
|
EVP_DecryptUpdate(decCtx, reinterpret_cast<unsigned char*>(plain.data()), &outLen1,
|
||||||
|
reinterpret_cast<const unsigned char*>(encryptedCredentials.constData()),
|
||||||
|
encryptedCredentials.size());
|
||||||
|
int outLen2 = 0;
|
||||||
|
EVP_DecryptFinal_ex(decCtx, reinterpret_cast<unsigned char*>(plain.data()) + outLen1, &outLen2);
|
||||||
|
plain.resize(outLen1 + outLen2);
|
||||||
|
EVP_CIPHER_CTX_free(decCtx);
|
||||||
|
|
||||||
|
QByteArray expected(128, char(0));
|
||||||
|
const QByteArray passBytes = password.toLatin1().left(64);
|
||||||
|
std::memcpy(expected.data() + 64, passBytes.constData(), static_cast<size_t>(passBytes.size()));
|
||||||
|
QCOMPARE(plain, expected);
|
||||||
|
|
||||||
|
BN_free(clientPub);
|
||||||
|
BN_free(sharedSecret);
|
||||||
|
BN_CTX_free(ctx);
|
||||||
BN_free(serverKeypair.privateExponent);
|
BN_free(serverKeypair.privateExponent);
|
||||||
BN_free(serverKeypair.prime);
|
BN_free(serverKeypair.prime);
|
||||||
}
|
}
|
||||||
@@ -2056,5 +2117,60 @@ void TestVncSessionBackend::appleDhAuthIsPreferredOverVncAuthWhenBothOffered()
|
|||||||
QCOMPARE(static_cast<quint8>(m_server->received.at(0)), quint8(30));
|
QCOMPARE(static_cast<quint8>(m_server->received.at(0)), quint8(30));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestVncSessionBackend::appleDhAuthAcceptsRealCapturedMacOsServerParameters()
|
||||||
|
{
|
||||||
|
// The exact generator/prime/server-public-key bytes captured from a
|
||||||
|
// real macOS Screen Sharing server's security-type-30 auth message
|
||||||
|
// (generator=5, a 512-byte/4096-bit RFC 3526 Group 16-family prime,
|
||||||
|
// and its server public key) -- this is what caught the original
|
||||||
|
// implementation's wrong assumption that the generator was itself
|
||||||
|
// length-prefixed like the prime is; it wasn't (it's a literal 2
|
||||||
|
// bytes), and this pins that fix against the real bytes that exposed
|
||||||
|
// the bug rather than only against freshly-generated toy data.
|
||||||
|
const QByteArray captured = QByteArray::fromHex(
|
||||||
|
"00050200"
|
||||||
|
"ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a0"
|
||||||
|
"8798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a6"
|
||||||
|
"37ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0"
|
||||||
|
"598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c"
|
||||||
|
"354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06"
|
||||||
|
"f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aaac42dad33170d04507a"
|
||||||
|
"33a85521abdf1cba64ecfb850458dbef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e8"
|
||||||
|
"c94e04a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521f2b18177b200cbbe11757"
|
||||||
|
"7a615d6c770988c0bad946e208e24fa074e5ab3143db5bfce0fd108e4b82d120a92108011a723c12a787e"
|
||||||
|
"6d788719a10bdba5b2699c327186af4e23c1a946834b6150bda2583e9ca2ad44ce8dbbbc2db04de8ef92e"
|
||||||
|
"8efc141fbecaa6287c59474e6bc05d99b2964fa090c3a2233ba186515be7ed1f612970cee2d7afb81bdd7"
|
||||||
|
"62170481cd0069127d5b05aa993b4ea988d8fddc186ffb7dc90a6c08f4df435c934063199"
|
||||||
|
"ffffffffffffffff"
|
||||||
|
"91c11c23a5b54389a05127d7fa94e681b7884667d770cd646fa46583d73e0a5ad09e361c30a0f9a9b765a"
|
||||||
|
"b291e2b8a13ba0088ba4bdb4a838120e649d49ed78db7fdb3397b907d1a42c0531d16796229d7b3a5a369"
|
||||||
|
"bfc639771ab1dea093ca7756b2b4a4a98f296478b331538533faead12342d6f1fe92817690ae8e2c51cbf"
|
||||||
|
"dc96c819e480c2170e6be12149691cb2bab218528c40cc5729b8a303bbe95d77eba288f3c4137d316c5b1"
|
||||||
|
"d9004c8fe3d034b1284d39e57133972700d1933465b4cc656d2dcbef67dc7eeb464f197bd5187abce304b"
|
||||||
|
"c972768dcaaa703e287f4f1e375c287b474fe0e3df9897a24bdaf06466976becbc8908fd81211537d1a44"
|
||||||
|
"e63e705d69b5c90f375fa717c978b27a21384769da67cd97e5134f71aef420dd4b36a2c4e05f80cee043f"
|
||||||
|
"ab873b478804dc126495d35f0a1df041c4b2bf0926e35ae033f6f6808bdc8a5792b7d380e7c7fc95fc80c"
|
||||||
|
"fea44c92842ed75757230f590b7dd1d9beadbabb78a96c47f21e65f6ea8566820ff9c59d078870fa1d367"
|
||||||
|
"2ff0b7f9a3d600f3479232e533bc8b64a3909fd61e070b19ac98d50a7c2a33c885294183e82a4e7777f2a"
|
||||||
|
"423002219a6094fdf51651d48550f771ede87eb95592eda604b85058cd0d8db9a92670e6e0d74400f7cd4"
|
||||||
|
"b48c8dfbace5c92c39775a60bb4e59d70878a50b609b15ae4a930ee5eb0494902d44869089349fa2ab343"
|
||||||
|
"028e");
|
||||||
|
QCOMPARE(captured.size(), 1028);
|
||||||
|
|
||||||
|
const QByteArray generator = captured.left(2);
|
||||||
|
const int keyLength = (static_cast<quint8>(captured.at(2)) << 8) | static_cast<quint8>(captured.at(3));
|
||||||
|
QCOMPARE(keyLength, 512);
|
||||||
|
const QByteArray prime = captured.mid(4, keyLength);
|
||||||
|
const QByteArray serverPublicKey = captured.mid(4 + keyLength, keyLength);
|
||||||
|
QCOMPARE(prime.size(), 512);
|
||||||
|
QCOMPARE(serverPublicKey.size(), 512);
|
||||||
|
|
||||||
|
const VncAppleDhAuth::Response response = VncAppleDhAuth::computeResponse(
|
||||||
|
generator, prime, serverPublicKey, QStringLiteral("tester"), QStringLiteral("s3cret"));
|
||||||
|
QVERIFY(!response.clientPublicKey.isEmpty());
|
||||||
|
QCOMPARE(response.clientPublicKey.size(), 512);
|
||||||
|
QCOMPARE(response.encryptedCredentials.size(), 128);
|
||||||
|
}
|
||||||
|
|
||||||
QTEST_GUILESS_MAIN(TestVncSessionBackend)
|
QTEST_GUILESS_MAIN(TestVncSessionBackend)
|
||||||
#include "test_vnc_session_backend.moc"
|
#include "test_vnc_session_backend.moc"
|
||||||
|
|||||||
Reference in New Issue
Block a user