Files
orbithub/src/vnc_apple_dh_auth.cpp
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

220 lines
7.2 KiB
C++

#include "vnc_apple_dh_auth.h"
#include <openssl/bn.h>
#include <openssl/core_names.h>
#include <openssl/evp.h>
#include <openssl/param_build.h>
#include <cstring>
#include <memory>
namespace VncAppleDhAuth {
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 BnDeleter {
void operator()(BIGNUM* bn) const { BN_free(bn); }
};
struct ParamBldDeleter {
void operator()(OSSL_PARAM_BLD* bld) const { OSSL_PARAM_BLD_free(bld); }
};
struct ParamDeleter {
void operator()(OSSL_PARAM* params) const { OSSL_PARAM_free(params); }
};
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 BnPtr = std::unique_ptr<BIGNUM, BnDeleter>;
using ParamBldPtr = std::unique_ptr<OSSL_PARAM_BLD, ParamBldDeleter>;
using ParamPtr = std::unique_ptr<OSSL_PARAM, ParamDeleter>;
using CipherCtxPtr = std::unique_ptr<EVP_CIPHER_CTX, CipherCtxDeleter>;
BnPtr bnFromBytes(const QByteArray& bytes)
{
return BnPtr(
BN_bin2bn(reinterpret_cast<const unsigned char*>(bytes.constData()), bytes.size(), nullptr));
}
// Builds an EVP_PKEY holding just the DH domain parameters (generator,
// prime) -- used both as the basis for our own keygen and, with a public
// value added, to represent the server's public key for derive().
EvpPkeyPtr buildDomainParams(const BIGNUM* g, const BIGNUM* p)
{
ParamBldPtr bld(OSSL_PARAM_BLD_new());
if (!bld) {
return nullptr;
}
if (OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_FFC_P, p) <= 0
|| OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_FFC_G, g) <= 0) {
return nullptr;
}
ParamPtr params(OSSL_PARAM_BLD_to_param(bld.get()));
if (!params) {
return nullptr;
}
EvpPkeyCtxPtr ctx(EVP_PKEY_CTX_new_from_name(nullptr, "DH", nullptr));
if (!ctx || EVP_PKEY_fromdata_init(ctx.get()) <= 0) {
return nullptr;
}
EVP_PKEY* rawKey = nullptr;
if (EVP_PKEY_fromdata(ctx.get(), &rawKey, EVP_PKEY_KEY_PARAMETERS, params.get()) <= 0) {
return nullptr;
}
return EvpPkeyPtr(rawKey);
}
EvpPkeyPtr buildPeerPublicKey(const BIGNUM* g, const BIGNUM* p, const BIGNUM* pub)
{
ParamBldPtr bld(OSSL_PARAM_BLD_new());
if (!bld) {
return nullptr;
}
if (OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_FFC_P, p) <= 0
|| OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_FFC_G, g) <= 0
|| OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_PUB_KEY, pub) <= 0) {
return nullptr;
}
ParamPtr params(OSSL_PARAM_BLD_to_param(bld.get()));
if (!params) {
return nullptr;
}
EvpPkeyCtxPtr ctx(EVP_PKEY_CTX_new_from_name(nullptr, "DH", nullptr));
if (!ctx || EVP_PKEY_fromdata_init(ctx.get()) <= 0) {
return nullptr;
}
EVP_PKEY* rawKey = nullptr;
if (EVP_PKEY_fromdata(ctx.get(), &rawKey, EVP_PKEY_PUBLIC_KEY, params.get()) <= 0) {
return nullptr;
}
return EvpPkeyPtr(rawKey);
}
}
Response computeResponse(const QByteArray& generator, const QByteArray& prime,
const QByteArray& serverPublicKey, const QString& username,
const QString& password)
{
Response response;
if (generator.isEmpty() || prime.isEmpty() || serverPublicKey.isEmpty()) {
return response;
}
BnPtr g = bnFromBytes(generator);
BnPtr p = bnFromBytes(prime);
BnPtr serverPub = bnFromBytes(serverPublicKey);
if (!g || !p || !serverPub) {
return response;
}
EvpPkeyPtr domainParams = buildDomainParams(g.get(), p.get());
if (!domainParams) {
return response;
}
// Generate our own ephemeral DH keypair against the same domain
// parameters the server offered.
EvpPkeyCtxPtr keygenCtx(EVP_PKEY_CTX_new_from_pkey(nullptr, domainParams.get(), nullptr));
if (!keygenCtx || EVP_PKEY_keygen_init(keygenCtx.get()) <= 0) {
return response;
}
EVP_PKEY* rawOurKey = nullptr;
if (EVP_PKEY_keygen(keygenCtx.get(), &rawOurKey) <= 0) {
return response;
}
EvpPkeyPtr ourKey(rawOurKey);
BIGNUM* ourPubRaw = nullptr;
if (EVP_PKEY_get_bn_param(ourKey.get(), OSSL_PKEY_PARAM_PUB_KEY, &ourPubRaw) <= 0
|| ourPubRaw == nullptr) {
return response;
}
BnPtr ourPub(ourPubRaw);
QByteArray clientPublicKey(prime.size(), char(0));
if (BN_bn2binpad(ourPub.get(), reinterpret_cast<unsigned char*>(clientPublicKey.data()),
prime.size())
< 0) {
return response;
}
EvpPkeyPtr peerKey = buildPeerPublicKey(g.get(), p.get(), serverPub.get());
if (!peerKey) {
return response;
}
EvpPkeyCtxPtr deriveCtx(EVP_PKEY_CTX_new_from_pkey(nullptr, ourKey.get(), nullptr));
if (!deriveCtx || EVP_PKEY_derive_init(deriveCtx.get()) <= 0
|| EVP_PKEY_derive_set_peer(deriveCtx.get(), peerKey.get()) <= 0) {
return response;
}
size_t secretLen = 0;
if (EVP_PKEY_derive(deriveCtx.get(), nullptr, &secretLen) <= 0 || secretLen == 0) {
return response;
}
QByteArray secret(static_cast<int>(secretLen), char(0));
if (EVP_PKEY_derive(deriveCtx.get(), reinterpret_cast<unsigned char*>(secret.data()),
&secretLen)
<= 0) {
return response;
}
secret.resize(static_cast<int>(secretLen));
unsigned char aesKey[16];
if (EVP_Digest(secret.constData(), static_cast<size_t>(secret.size()), aesKey, nullptr,
EVP_md5(), nullptr)
<= 0) {
return response;
}
// 64 bytes username + 64 bytes password, NUL-padded/truncated.
QByteArray credentials(128, char(0));
const QByteArray userBytes = username.toLatin1().left(64);
const QByteArray passBytes = password.toLatin1().left(64);
std::memcpy(credentials.data(), userBytes.constData(),
static_cast<size_t>(userBytes.size()));
std::memcpy(credentials.data() + 64, passBytes.constData(),
static_cast<size_t>(passBytes.size()));
CipherCtxPtr cipherCtx(EVP_CIPHER_CTX_new());
if (!cipherCtx
|| EVP_EncryptInit_ex(cipherCtx.get(), EVP_aes_128_ecb(), nullptr, aesKey, nullptr) <= 0) {
return response;
}
EVP_CIPHER_CTX_set_padding(cipherCtx.get(), 0);
QByteArray ciphertext(credentials.size() + EVP_MAX_BLOCK_LENGTH, char(0));
int outLen1 = 0;
if (EVP_EncryptUpdate(cipherCtx.get(), reinterpret_cast<unsigned char*>(ciphertext.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*>(ciphertext.data()) + outLen1, &outLen2)
<= 0) {
return response;
}
ciphertext.resize(outLen1 + outLen2);
response.clientPublicKey = clientPublicKey;
response.encryptedCredentials = ciphertext;
return response;
}
}