fix(vnc): support RA2_256 Raspberry Pi connections

This commit is contained in:
victorwon2001 authored and Kang committed 2026-10-01 15:28:38 +08:00
1 parent 642c0de88b
commit 64d592cf41
35 files changed
+1847 -144

No files matched your search

+39
View File
@@ -950,6 +950,17 @@ dependencies = [
"error-code",
]
[[package]]
name = "cmac"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa"
dependencies = [
"cipher 0.4.4",
"dbl",
"digest 0.10.7",
]
[[package]]
name = "cmake"
version = "0.1.58"
@@ -1500,6 +1511,15 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
[[package]]
name = "dbl"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9"
dependencies = [
"generic-array 0.14.7",
]
[[package]]
name = "dbus"
version = "0.9.12"
@@ -1586,6 +1606,7 @@ dependencies = [
"const-oid 0.9.6",
"der_derive 0.7.3",
"flagset",
"pem-rfc7468 0.7.0",
"zeroize",
]
@@ -1900,6 +1921,19 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "eax"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28"
dependencies = [
"aead 0.5.2",
"cipher 0.4.4",
"cmac",
"ctr 0.9.2",
"subtle",
]
[[package]]
name = "ecdsa"
version = "0.16.9"
@@ -9507,10 +9541,15 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
name = "vnc-rs"
version = "0.5.3"
dependencies = [
"aes 0.8.4",
"async_io_stream",
"eax",
"flate2",
"futures",
"image",
"rand 0.8.8",
"rsa 0.9.10",
"sha2 0.10.9",
"thiserror 1.0.69",
"tokio",
"tokio-stream",
+1
View File
@@ -899,6 +899,7 @@ e+JpiSq66Z6GIt0801skPh20jxOO3F52SoX1IeO5D5PXfZrfSZlw6S8c7bwyp2FHxDewRx
config: ConnectionType::Vnc {
host: "example.com".to_string(),
port: 5900,
username: String::new(),
security: VncSecuritySettings::default(),
display: VncDisplaySettings::default(),
clipboard: VncClipboardSettings::default(),
+9
View File
@@ -60,6 +60,15 @@ pub async fn vnc_reconnect(
state.inner().clone().reconnect(app, &session_id).await
}
#[tauri::command]
pub async fn respond_vnc_server_key(
state: tauri::State<'_, Arc<VncSessionManager>>,
request_id: String,
accepted: bool,
) -> AppResult<()> {
state.respond_server_key(&request_id, accepted).await
}
#[tauri::command]
pub async fn close_vnc_session(
app: tauri::AppHandle,
+4
View File
@@ -724,6 +724,8 @@ pub enum ConnectionType {
#[serde(default = "default_vnc_port")]
port: u16,
#[serde(default)]
username: String,
#[serde(default)]
security: VncSecuritySettings,
#[serde(default)]
display: VncDisplaySettings,
@@ -2721,6 +2723,7 @@ mod tests {
let ConnectionType::Vnc {
port,
username,
security,
display,
clipboard,
@@ -2734,6 +2737,7 @@ mod tests {
};
assert_eq!(port, 5900);
assert!(username.is_empty());
assert_eq!(security.mode, "auto");
assert_eq!(display.scale_mode, "fit");
assert!(clipboard.enabled);
+186 -25
View File
@@ -2,17 +2,18 @@ use crate::config::{self, ConnectionAuth, ConnectionNetwork, ConnectionType};
use crate::core::network::open_tcp_transport;
use crate::error::{AppError, AppResult};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use tauri::async_runtime::JoinHandle;
use tauri::ipc::{Channel, InvokeResponseBody};
use tauri::{AppHandle, Emitter};
use tokio::sync::{Mutex, mpsc, watch};
use tokio::sync::{Mutex, mpsc, oneshot, watch};
use tokio::time::{Duration, timeout};
use vnc::{
ClientKeyEvent, ClientMouseEvent, PixelFormat, Rect, Screen, VncClient, VncConnector,
VncEncoding, VncError, VncEvent, VncLimits, VncSecurityPolicy, X11Event,
VncEncoding, VncError, VncEvent, VncLimits, VncSecurityPolicy, VncServerKey, X11Event,
};
use zeroize::Zeroizing;
@@ -28,6 +29,8 @@ const WORKER_COMMAND_CHANNEL_CAPACITY: usize = 256;
const MAX_PENDING_FRAMES: usize = 2;
const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
const RA2_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(75);
const SERVER_KEY_PROMPT_TIMEOUT: Duration = Duration::from_secs(60);
const WORKER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(3);
const EVENT_POLL_INTERVAL: Duration = Duration::from_millis(8);
const UPDATE_REQUEST_INTERVAL: Duration = Duration::from_millis(16);
@@ -64,6 +67,18 @@ pub struct VncStateEvent {
pub error_kind: Option<VncErrorKind>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VncServerKeyVerifyEvent {
pub request_id: String,
pub session_id: String,
pub host: String,
pub port: u16,
pub fingerprint: String,
pub key_bits: u32,
pub known_host_status: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VncClipboardEvent {
@@ -94,6 +109,12 @@ enum VncWorkerCommand {
FullRefresh,
}
struct VncServerKeyPending {
session_id: String,
generation: u64,
responder: oneshot::Sender<bool>,
}
#[derive(Debug, Clone)]
pub struct VncConnectConfig {
pub session_id: String,
@@ -101,6 +122,7 @@ pub struct VncConnectConfig {
pub name: String,
pub host: String,
pub port: u16,
pub username: String,
pub password: Option<String>,
pub security_mode: String,
pub scale_mode: String,
@@ -200,16 +222,19 @@ pub struct VncSession {
cancel_sender: Mutex<Option<watch::Sender<bool>>>,
worker: Mutex<Option<JoinHandle<()>>>,
close_requested: AtomicBool,
pending_server_keys: Arc<Mutex<HashMap<String, VncServerKeyPending>>>,
}
pub struct VncSessionManager {
sessions: Mutex<HashMap<String, Arc<VncSession>>>,
pending_server_keys: Arc<Mutex<HashMap<String, VncServerKeyPending>>>,
}
impl VncSessionManager {
pub fn new() -> Self {
Self {
sessions: Mutex::new(HashMap::new()),
pending_server_keys: Arc::new(Mutex::new(HashMap::new())),
}
}
@@ -234,6 +259,7 @@ impl VncSessionManager {
cancel_sender: Mutex::new(None),
worker: Mutex::new(None),
close_requested: AtomicBool::new(false),
pending_server_keys: self.pending_server_keys.clone(),
});
self.sessions
.lock()
@@ -312,6 +338,8 @@ impl VncSessionManager {
pub async fn reconnect(self: &Arc<Self>, app: AppHandle, session_id: &str) -> AppResult<()> {
let session = self.get(session_id).await?;
self.cancel_pending_server_keys_for_session(session_id)
.await;
stop_worker(&session).await;
session.close_requested.store(false, Ordering::Release);
*session.framebuffer.lock().await = None;
@@ -327,6 +355,8 @@ impl VncSessionManager {
return Ok(());
};
session.close_requested.store(true, Ordering::Release);
self.cancel_pending_server_keys_for_session(session_id)
.await;
stop_worker(&session).await;
session.frame_attach_id.fetch_add(1, Ordering::AcqRel);
*session.frame_channel.lock().await = None;
@@ -338,6 +368,36 @@ impl VncSessionManager {
Ok(())
}
pub async fn respond_server_key(&self, request_id: &str, accepted: bool) -> AppResult<()> {
let Some(pending) = self.pending_server_keys.lock().await.remove(request_id) else {
return Err(AppError::Cancelled(format!(
"No pending VNC server key request with id '{request_id}'"
)));
};
if let Ok(session) = self.get(&pending.session_id).await
&& session.generation.load(Ordering::Acquire) != pending.generation
{
return Ok(());
}
let _ = pending.responder.send(accepted);
Ok(())
}
async fn cancel_pending_server_keys_for_session(&self, session_id: &str) {
let mut pending = self.pending_server_keys.lock().await;
let request_ids = pending
.iter()
.filter_map(|(request_id, request)| {
(request.session_id == session_id).then(|| request_id.clone())
})
.collect::<Vec<_>>();
for request_id in request_ids {
if let Some(request) = pending.remove(&request_id) {
let _ = request.responder.send(false);
}
}
}
pub async fn close_all(&self, app: &AppHandle) {
let ids = self
.sessions
@@ -470,7 +530,7 @@ async fn run_protocol_generation(
set_state(session, VncSessionState::Authenticating, None, None).await;
emit_state(app, session, VncSessionState::Authenticating, None, None);
let password = Zeroizing::new(session.config.password.clone().unwrap_or_default());
if password.as_bytes().len() > 8 {
if session.config.security_mode == "vnc-auth" && password.len() > 8 {
return Err((
VncErrorKind::Authentication,
"Classic VNC authentication passwords must be 8 bytes or fewer".to_string(),
@@ -493,12 +553,24 @@ async fn run_protocol_generation(
..VncLimits::default()
};
let auth_password = password.to_string();
let has_credentials = session.config.password.is_some();
let handshake_timeout = if session.config.security_mode == "auto" && has_credentials {
RA2_HANDSHAKE_TIMEOUT
} else {
HANDSHAKE_TIMEOUT
};
let verifier_app = app.clone();
let verifier_session = session.clone();
let state = VncConnector::new(stream)
.set_auth_method(async move { Ok(auth_password) })
.set_security_policy(security_policy(
&session.config.security_mode,
session.config.password.is_some(),
))
.set_credentials_available(has_credentials)
.set_username(session.config.username.clone())
.set_server_key_verifier(move |server_key| {
let app = verifier_app.clone();
let session = verifier_session.clone();
async move { verify_vnc_server_key(&app, &session, generation, &server_key).await }
})
.set_security_policy(security_policy(&session.config.security_mode))
.set_pixel_format(PixelFormat::rgba())
.set_limits(limits)
.add_encoding(VncEncoding::DesktopSizePseudo)
@@ -508,7 +580,7 @@ async fn run_protocol_generation(
.allow_shared(session.config.shared)
.build()
.map_err(classify_vnc_error)?;
let client = timeout(HANDSHAKE_TIMEOUT, state.try_start())
let client = timeout(handshake_timeout, state.try_start())
.await
.map_err(|_| {
(
@@ -575,15 +647,90 @@ fn vnc_connect_target(config: &VncConnectConfig) -> (&str, u16) {
(config.host.as_str(), config.port)
}
fn security_policy(mode: &str, has_password: bool) -> VncSecurityPolicy {
fn security_policy(mode: &str) -> VncSecurityPolicy {
match mode {
"none" => VncSecurityPolicy::NoneOnly,
"vnc-auth" => VncSecurityPolicy::VncAuthOnly,
_ if has_password => VncSecurityPolicy::VncAuthOnly,
_ => VncSecurityPolicy::NoneOnly,
_ => VncSecurityPolicy::Auto,
}
}
async fn verify_vnc_server_key(
app: &AppHandle,
session: &Arc<VncSession>,
generation: u64,
server_key: &VncServerKey,
) -> Result<(), VncError> {
if session.generation.load(Ordering::Acquire) != generation {
return Err(VncError::Ra2ServerKeyRejected(
"stale VNC connection generation".to_string(),
));
}
let fingerprint = format!(
"SHA256:{}",
hex::encode(Sha256::digest(server_key.encoded()))
);
let known_host_status = crate::storage::check_vnc_known_host(
&session.config.host,
session.config.port,
&fingerprint,
)
.map_err(|error| VncError::Ra2ServerKeyRejected(error.to_string()))?;
if known_host_status == crate::storage::KnownHostCheck::Match {
return Ok(());
}
let request_id = uuid::Uuid::new_v4().to_string();
let (tx, rx) = oneshot::channel();
session.pending_server_keys.lock().await.insert(
request_id.clone(),
VncServerKeyPending {
session_id: session.config.session_id.clone(),
generation,
responder: tx,
},
);
let payload = VncServerKeyVerifyEvent {
request_id: request_id.clone(),
session_id: session.config.session_id.clone(),
host: session.config.host.clone(),
port: session.config.port,
fingerprint: fingerprint.clone(),
key_bits: server_key.bits(),
known_host_status: match known_host_status {
crate::storage::KnownHostCheck::Match => "match",
crate::storage::KnownHostCheck::HostSeen => "changed",
crate::storage::KnownHostCheck::UnknownHost => "unknown",
}
.to_string(),
};
let _ = app.emit("vnc-server-key-verify", payload);
let accepted = match timeout(SERVER_KEY_PROMPT_TIMEOUT, rx).await {
Ok(Ok(accepted)) => accepted,
_ => {
session.pending_server_keys.lock().await.remove(&request_id);
return Err(VncError::Ra2ServerKeyRejected(
"verification timed out".to_string(),
));
}
};
if session.generation.load(Ordering::Acquire) != generation {
return Err(VncError::Ra2ServerKeyRejected(
"stale VNC connection generation".to_string(),
));
}
if !accepted {
return Err(VncError::Ra2ServerKeyRejected(
"user rejected the server key".to_string(),
));
}
crate::storage::upsert_vnc_known_host(&session.config.host, session.config.port, &fingerprint)
.map_err(|error| VncError::Ra2ServerKeyRejected(error.to_string()))?;
Ok(())
}
async fn handle_vnc_event(
app: &AppHandle,
session: &Arc<VncSession>,
@@ -910,18 +1057,24 @@ fn classify_vnc_error(error: VncError) -> (VncErrorKind, String, bool) {
"VNC authentication failed".to_string(),
false,
),
VncError::CredentialTooLong { .. } | VncError::Ra2ServerKeyRejected(_) => {
(VncErrorKind::Authentication, error.to_string(), false)
}
VncError::UnsupportedSecurityType
| VncError::RequiredSecurityTypeUnavailable(_)
| VncError::InvalidSecurityType(_) => (
VncErrorKind::Authentication,
format!(
"The VNC server requires an unsupported security type. Currently supported: None and VNC Authentication. Details: {error}"
"The VNC server requires an unsupported security type. Currently supported: None, VNC Authentication, and RA2_256 in Auto mode. Details: {error}"
),
false,
),
VncError::InvalidEncoding(_) | VncError::InvalidImageData => {
(VncErrorKind::Encoding, error.to_string(), false)
}
VncError::IoError(ref source) if source.kind() == std::io::ErrorKind::InvalidData => {
(VncErrorKind::Protocol, error.to_string(), false)
}
VncError::IoError(_) => (VncErrorKind::Transport, error.to_string(), true),
VncError::LimitExceeded { .. }
| VncError::InvalidDimensions
@@ -929,7 +1082,16 @@ fn classify_vnc_error(error: VncError) -> (VncErrorKind, String, bool) {
| VncError::WrongPixelFormat
| VncError::WrongServerMessage
| VncError::InvalidSecurityResult(_)
| VncError::SecurityFailure(_) => (VncErrorKind::Protocol, error.to_string(), false),
| VncError::SecurityFailure(_)
| VncError::InvalidRa2KeyLength { .. }
| VncError::InvalidRa2PublicKey
| VncError::InvalidRa2EncryptedRandomLength { .. }
| VncError::InvalidRa2RandomLength(_)
| VncError::Ra2ServerHashMismatch
| VncError::InvalidRa2Subtype(_)
| VncError::Ra2ServerKeyVerifierRequired
| VncError::InvalidRa2RecordLimit(_)
| VncError::Ra2Crypto(_) => (VncErrorKind::Protocol, error.to_string(), false),
_ => (VncErrorKind::Internal, error.to_string(), true),
}
}
@@ -979,6 +1141,7 @@ pub fn load_saved_vnc_config(app: &AppHandle, connection_id: &str) -> AppResult<
let ConnectionType::Vnc {
host,
port,
username,
security,
display,
clipboard,
@@ -996,8 +1159,9 @@ pub fn load_saved_vnc_config(app: &AppHandle, connection_id: &str) -> AppResult<
"VNC Authentication requires a password".to_string(),
));
}
if let Some(password) = password.as_ref()
&& password.as_bytes().len() > 8
if security.mode == "vnc-auth"
&& let Some(password) = password.as_ref()
&& password.len() > 8
{
return Err(AppError::Config(
"Classic VNC authentication passwords must be 8 bytes or fewer".to_string(),
@@ -1010,6 +1174,7 @@ pub fn load_saved_vnc_config(app: &AppHandle, connection_id: &str) -> AppResult<
name: connection.name,
host,
port,
username,
password,
security_mode: security.mode,
scale_mode: display.scale_mode,
@@ -1090,16 +1255,9 @@ mod tests {
#[test]
fn security_mode_maps_to_fail_closed_policy() {
assert_eq!(security_policy("none", true), VncSecurityPolicy::NoneOnly);
assert_eq!(
security_policy("vnc-auth", false),
VncSecurityPolicy::VncAuthOnly
);
assert_eq!(
security_policy("auto", true),
VncSecurityPolicy::VncAuthOnly
);
assert_eq!(security_policy("auto", false), VncSecurityPolicy::NoneOnly);
assert_eq!(security_policy("none"), VncSecurityPolicy::NoneOnly);
assert_eq!(security_policy("vnc-auth"), VncSecurityPolicy::VncAuthOnly);
assert_eq!(security_policy("auto"), VncSecurityPolicy::Auto);
}
#[test]
@@ -1110,6 +1268,7 @@ mod tests {
name: "IPv6 VNC".to_string(),
host: "::1".to_string(),
port: 5900,
username: String::new(),
password: None,
security_mode: "none".to_string(),
scale_mode: "fit".to_string(),
@@ -1133,6 +1292,7 @@ mod tests {
name: "Test VNC".to_string(),
host: "127.0.0.1".to_string(),
port: 5900,
username: String::new(),
password: None,
security_mode: "none".to_string(),
scale_mode: "fit".to_string(),
@@ -1156,6 +1316,7 @@ mod tests {
cancel_sender: Mutex::new(None),
worker: Mutex::new(None),
close_requested: AtomicBool::new(false),
pending_server_keys: Arc::new(Mutex::new(HashMap::new())),
});
queue_or_send_frame(&session, vec![1]).await;
+1
View File
@@ -236,6 +236,7 @@ pub fn run() {
cmd::vnc::vnc_input_batch,
cmd::vnc::vnc_set_clipboard_text,
cmd::vnc::vnc_reconnect,
cmd::vnc::respond_vnc_server_key,
cmd::vnc::close_vnc_session,
cmd::session::cancel_session_creation,
cmd::session::list_serial_ports,
+4
View File
@@ -166,6 +166,8 @@ impl Storage {
txn.open_table(KNOWN_HOSTS_TABLE).map_err(storage_error)?;
txn.open_table(RDP_KNOWN_HOSTS_TABLE)
.map_err(storage_error)?;
txn.open_table(VNC_KNOWN_HOSTS_TABLE)
.map_err(storage_error)?;
txn.open_table(NOTE_FOLDERS_TABLE).map_err(storage_error)?;
txn.open_table(NOTES_TABLE).map_err(storage_error)?;
txn.open_table(NOTE_SUMMARIES_TABLE)
@@ -193,6 +195,8 @@ pub(super) fn open_all_v3_tables(txn: &redb::WriteTransaction) -> AppResult<()>
txn.open_table(KNOWN_HOSTS_TABLE).map_err(storage_error)?;
txn.open_table(RDP_KNOWN_HOSTS_TABLE)
.map_err(storage_error)?;
txn.open_table(VNC_KNOWN_HOSTS_TABLE)
.map_err(storage_error)?;
txn.open_table(COMMAND_HISTORY_TABLE)
.map_err(storage_error)?;
txn.open_table(NOTE_FOLDERS_TABLE).map_err(storage_error)?;
+18 -1
View File
@@ -13,6 +13,7 @@ mod sessions;
mod settings_impl;
mod tables;
mod util;
mod vnc_known_hosts;
#[cfg(test)]
mod tests;
@@ -26,7 +27,7 @@ pub use tables::{
IDX_CONNECTIONS_BY_GROUP_TABLE, IDX_CONNECTIONS_BY_LAST_USED_TABLE,
IDX_CONNECTIONS_BY_PROTOCOL_TABLE, KNOWN_HOSTS_TABLE, META_TABLE, NOTE_FOLDERS_TABLE,
NOTES_TABLE, OTP_ACCOUNTS_TABLE, PROXIES_TABLE, RDP_KNOWN_HOSTS_TABLE, SETTINGS_TABLE,
TUNNELS_TABLE,
TUNNELS_TABLE, VNC_KNOWN_HOSTS_TABLE,
};
use crate::error::{AppError, AppResult};
@@ -341,6 +342,22 @@ pub(crate) fn upsert_rdp_known_host(
storage()?.upsert_rdp_known_host(host, port, sha256_fingerprint, certificate)
}
pub(crate) fn check_vnc_known_host(
host: &str,
port: u16,
sha256_fingerprint: &str,
) -> AppResult<KnownHostCheck> {
storage()?.check_vnc_known_host(host, port, sha256_fingerprint)
}
pub(crate) fn upsert_vnc_known_host(
host: &str,
port: u16,
sha256_fingerprint: &str,
) -> AppResult<()> {
storage()?.upsert_vnc_known_host(host, port, sha256_fingerprint)
}
pub(crate) fn load_master_key_token() -> AppResult<Option<String>> {
storage()?.load_master_key_token()
}
+12
View File
@@ -26,6 +26,7 @@ pub(super) const TUNNEL_PREFIX: &str = "tunnels/";
pub(super) const KNOWN_HOST_PREFIX: &str = "known_hosts/";
pub(super) const KNOWN_HOST_RAW_PREFIX: &str = "known_hosts/raw/";
pub(super) const RDP_KNOWN_HOST_PREFIX: &str = "rdp_known_hosts/";
pub(super) const VNC_KNOWN_HOST_PREFIX: &str = "vnc_known_hosts/";
pub(super) const COMMAND_HISTORY_PREFIX: &str = "command_history/";
pub(super) const NOTE_FOLDER_PREFIX: &str = "note_folders/";
pub(super) const NOTE_DOCUMENT_PREFIX: &str = "notes/";
@@ -45,6 +46,8 @@ pub const TUNNELS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("tu
pub const KNOWN_HOSTS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("known_hosts");
pub const RDP_KNOWN_HOSTS_TABLE: TableDefinition<&str, &[u8]> =
TableDefinition::new("rdp_known_hosts");
pub const VNC_KNOWN_HOSTS_TABLE: TableDefinition<&str, &[u8]> =
TableDefinition::new("vnc_known_hosts");
pub const COMMAND_HISTORY_TABLE: TableDefinition<&str, &[u8]> =
TableDefinition::new("command_history");
pub const NOTE_FOLDERS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("note_folders");
@@ -127,6 +130,15 @@ pub(super) struct RdpKnownHostRecord {
pub(super) updated_at_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct VncKnownHostRecord {
pub(super) host: String,
pub(super) port: u16,
pub(super) sha256_fingerprint: String,
pub(super) created_at_ms: u64,
pub(super) updated_at_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct HistoryStoreFileV2 {
pub(super) version: u32,
+27
View File
@@ -492,3 +492,30 @@ fn known_hosts_management_clear_removes_ssh_records_without_touching_rdp() {
);
let _ = fs::remove_dir_all(dir);
}
#[test]
fn vnc_known_hosts_distinguish_unknown_match_and_changed_keys() {
let (dir, storage) = test_storage("vnc-known-hosts");
assert_eq!(
storage
.check_vnc_known_host("pi.local", 5900, "SHA256:first")
.expect("check unknown"),
KnownHostCheck::UnknownHost
);
storage
.upsert_vnc_known_host("pi.local", 5900, "SHA256:first")
.expect("save fingerprint");
assert_eq!(
storage
.check_vnc_known_host("PI.LOCAL", 5900, "sha256:FIRST")
.expect("check match"),
KnownHostCheck::Match
);
assert_eq!(
storage
.check_vnc_known_host("pi.local", 5900, "SHA256:changed")
.expect("check changed"),
KnownHostCheck::HostSeen
);
let _ = fs::remove_dir_all(dir);
}
+56
View File
@@ -0,0 +1,56 @@
use crate::error::AppResult;
use super::history::history_id;
use super::tables::*;
use super::util::*;
use super::{KnownHostCheck, Storage};
impl Storage {
pub fn check_vnc_known_host(
&self,
host: &str,
port: u16,
sha256_fingerprint: &str,
) -> AppResult<KnownHostCheck> {
let Some(record) = self.read_json::<VncKnownHostRecord>(
VNC_KNOWN_HOSTS_TABLE,
&vnc_known_host_key(host, port),
)?
else {
return Ok(KnownHostCheck::UnknownHost);
};
if record
.sha256_fingerprint
.eq_ignore_ascii_case(sha256_fingerprint)
{
Ok(KnownHostCheck::Match)
} else {
Ok(KnownHostCheck::HostSeen)
}
}
pub fn upsert_vnc_known_host(
&self,
host: &str,
port: u16,
sha256_fingerprint: &str,
) -> AppResult<()> {
let now = current_time_ms();
let key = vnc_known_host_key(host, port);
let existing: Option<VncKnownHostRecord> = self.read_json(VNC_KNOWN_HOSTS_TABLE, &key)?;
let record = VncKnownHostRecord {
host: host.to_string(),
port,
sha256_fingerprint: sha256_fingerprint.to_string(),
created_at_ms: existing.map_or(now, |record| record.created_at_ms),
updated_at_ms: now,
};
self.write_json(VNC_KNOWN_HOSTS_TABLE, &key, &record)
}
}
pub(super) fn vnc_known_host_key(host: &str, port: u16) -> String {
let host_identifier = format!("{}:{}", host.trim().to_ascii_lowercase(), port);
format!("{VNC_KNOWN_HOST_PREFIX}{}", history_id(&host_identifier))
}
+1
View File
@@ -47,6 +47,7 @@ This directory contains local copies of upstream crates used by NyaTerm's RDP st
- Adds configurable limits for strings, dimensions, rectangles, encoded/decoded payloads, clipboard data, and channels.
- Reduces internal channel capacity and adds deterministic protocol/parser regression tests.
- Adds an explicit security policy so NyaTerm can fail closed for `none`, `vnc-auth`, or password-aware `auto`.
- Adds RA2_256 (security type 129) with bounded RSA key exchange, AES-EAX encrypted records, username/password subtypes, and a pre-credential server-key verification callback used by NyaTerm's TOFU flow.
- Integration status: used by NyaTerm's VNC direct-TCP manager and React pane. Raw is the required fallback; compressed encodings must remain gated by fork tests and interoperability checks before being advertised.
## Update Method
+5
View File
@@ -18,6 +18,11 @@ name = "vnc"
path = "src/lib.rs"
[dependencies]
aes = "0.8"
eax = "0.5"
rand = "0.8"
rsa = "0.9"
sha2 = "0.10"
#error
thiserror = "^1"
flate2 = "^1"
+4 -1
View File
@@ -12,7 +12,10 @@ NyaTerm vendors this crate to harden its network parser before application
integration. Local changes forbid unsafe Rust, replace network-controlled panic
and undefined-behavior paths with typed errors, add bounded protocol limits,
reduce queue sizes, add explicit security-selection policy, and add
deterministic handshake/parser regression tests.
deterministic handshake/parser regression tests. The fork also implements
RA2_256 (security type 129) with RSA key exchange, AES-EAX records, bounded
credentials, and an application-provided server-key verifier that runs before
credentials are sent.
This fork is used by NyaTerm's direct-TCP VNC manager and React pane. Raw must
remain the required fallback. ZRLE/Tight support should only be advertised after
+27 -6
View File
@@ -11,6 +11,7 @@ pub(super) enum SecurityType {
VncAuth = 2,
RA2 = 5,
RA2ne = 6,
RA2_256 = 129,
Tight = 16,
Ultra = 17,
Tls = 18,
@@ -30,6 +31,7 @@ impl TryFrom<u8> for SecurityType {
2 => Ok(Self::VncAuth),
5 => Ok(Self::RA2),
6 => Ok(Self::RA2ne),
129 => Ok(Self::RA2_256),
16 => Ok(Self::Tight),
17 => Ok(Self::Ultra),
18 => Ok(Self::Tls),
@@ -108,7 +110,17 @@ impl SecurityType {
}
let mut sec_types = Vec::with_capacity(num as usize);
for _ in 0..num {
sec_types.push(reader.read_u8().await?.try_into()?);
let raw = reader.read_u8().await?;
match raw.try_into() {
Ok(security_type) => sec_types.push(security_type),
Err(VncError::InvalidSecurityType(_)) => {
tracing::trace!(
raw,
"Ignoring unsupported advertised VNC security type"
);
}
Err(error) => return Err(error),
}
}
tracing::trace!("Server supported security type: {:?}", sec_types);
Ok(sec_types)
@@ -235,15 +247,24 @@ mod tests {
}
#[tokio::test]
async fn rejects_unknown_security_type_and_result() {
let mut security = &[1_u8, 99][..];
let error = SecurityType::read(&mut security, &VncVersion::RFB38, &VncLimits::default())
async fn ignores_unknown_security_types_in_rfb38_lists_but_rejects_unknown_results() {
let mut security = &[3_u8, 99, 129, 2][..];
let types = SecurityType::read(&mut security, &VncVersion::RFB38, &VncLimits::default())
.await
.unwrap_err();
assert!(matches!(error, VncError::InvalidSecurityType(99)));
.unwrap();
assert_eq!(types, vec![SecurityType::RA2_256, SecurityType::VncAuth]);
assert!(matches!(
AuthResult::try_from(7),
Err(VncError::InvalidSecurityResult(7))
));
}
#[tokio::test]
async fn rfb33_keeps_unknown_single_security_type_fail_closed() {
let mut security = &99_u32.to_be_bytes()[..];
let error = SecurityType::read(&mut security, &VncVersion::RFB33, &VncLimits::default())
.await
.unwrap_err();
assert!(matches!(error, VncError::InvalidSecurityType(99)));
}
}
+259 -103
View File
@@ -1,9 +1,11 @@
use super::{
auth::{read_security_failure, AuthHelper, AuthResult, SecurityType},
connection::VncClient,
security::ra2::{self, ServerKeyVerifier, VncServerKey},
};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
use tracing::{info, trace};
@@ -33,7 +35,7 @@ where
{
pub fn try_start(
self,
) -> Pin<Box<dyn Future<Output = Result<Self, VncError>> + Send + Sync + 'static>> {
) -> Pin<Box<dyn Future<Output = Result<Self, VncError>> + Send + 'static>> {
Box::pin(async move {
match self {
VncState::Handshake(mut connector) => {
@@ -64,120 +66,145 @@ where
)
.await?;
if connector.security_policy == VncSecurityPolicy::NoneOnly
&& !security_types.contains(&SecurityType::None)
{
return Err(VncError::RequiredSecurityTypeUnavailable("none"));
}
if connector.security_policy == VncSecurityPolicy::VncAuthOnly
&& !security_types.contains(&SecurityType::VncAuth)
{
return Err(VncError::RequiredSecurityTypeUnavailable("vnc-auth"));
}
let prefer_none = match connector.security_policy {
VncSecurityPolicy::Auto => connector.auth_methond.is_none(),
VncSecurityPolicy::NoneOnly => true,
VncSecurityPolicy::VncAuthOnly => false,
let selected = match connector.security_policy {
VncSecurityPolicy::NoneOnly => {
if !security_types.contains(&SecurityType::None) {
return Err(VncError::RequiredSecurityTypeUnavailable("none"));
}
SecurityType::None
}
VncSecurityPolicy::VncAuthOnly => {
if !security_types.contains(&SecurityType::VncAuth) {
return Err(VncError::RequiredSecurityTypeUnavailable("vnc-auth"));
}
SecurityType::VncAuth
}
VncSecurityPolicy::Auto if connector.credentials_available => {
if connector.rfb_version != VncVersion::RFB33
&& security_types.contains(&SecurityType::RA2_256)
{
SecurityType::RA2_256
} else if security_types.contains(&SecurityType::VncAuth) {
SecurityType::VncAuth
} else {
return Err(VncError::UnsupportedSecurityType);
}
}
VncSecurityPolicy::Auto => {
if security_types.contains(&SecurityType::None) {
SecurityType::None
} else {
return Err(VncError::UnsupportedSecurityType);
}
}
};
if prefer_none && security_types.contains(&SecurityType::None) {
match connector.rfb_version {
VncVersion::RFB33 => {
// If the security-type is 1, for no authentication, the server does not
// send the SecurityResult message but proceeds directly to the
// initialization messages (Section 7.3).
info!("No auth needed in vnc3.3");
}
VncVersion::RFB37 => {
// After the security handshake, if the security-type is 1, for no
// authentication, the server does not send the SecurityResult message
// but proceeds directly to the initialization messages (Section 7.3).
info!("No auth needed in vnc3.7");
SecurityType::write(&SecurityType::None, &mut connector.stream)
.await?;
}
VncVersion::RFB38 => {
info!("No auth needed in vnc3.8");
SecurityType::write(&SecurityType::None, &mut connector.stream)
.await?;
let result: AuthResult =
connector.stream.read_u32().await?.try_into()?;
if result == AuthResult::Failed {
let reason = read_security_failure(
&mut connector.stream,
&connector.limits,
)
.await?;
return Err(VncError::SecurityFailure(reason));
let client = match selected {
SecurityType::None => {
match connector.rfb_version {
VncVersion::RFB33 => {
info!("No auth needed in vnc3.3");
}
VncVersion::RFB37 => {
info!("No auth needed in vnc3.7");
SecurityType::write(&selected, &mut connector.stream).await?;
}
VncVersion::RFB38 => {
info!("No auth needed in vnc3.8");
SecurityType::write(&selected, &mut connector.stream).await?;
let result: AuthResult =
connector.stream.read_u32().await?.try_into()?;
if result == AuthResult::Failed {
let reason = read_security_failure(
&mut connector.stream,
&connector.limits,
)
.await?;
return Err(VncError::SecurityFailure(reason));
}
}
}
VncClient::new(
connector.stream,
connector.allow_shared,
connector.pixel_format,
connector.encodings,
connector.limits,
)
.await?
}
} else {
// choose a auth method
if security_types.contains(&SecurityType::VncAuth) {
if connector.rfb_version != VncVersion::RFB33 {
// In the security handshake (Section 7.1.2), rather than a two-way
// negotiation, the server decides the security type and sends a single
// word:
// +--------------+--------------+---------------+
// | No. of bytes | Type [Value] | Description |
// +--------------+--------------+---------------+
// | 4 | U32 | security-type |
// +--------------+--------------+---------------+
// The security-type may only take the value 0, 1, or 2. A value of 0
// means that the connection has failed and is followed by a string
// giving the reason, as described in Section 7.1.2.
SecurityType::write(&SecurityType::VncAuth, &mut connector.stream)
.await?;
SecurityType::VncAuth => {
let credential = connector
.auth_methond
.take()
.ok_or(VncError::NoPassword)?
.await?;
if credential.len() > 8 {
return Err(VncError::CredentialTooLong {
field: "classic VNC password",
actual: credential.len(),
limit: 8,
});
}
} else {
return Err(VncError::UnsupportedSecurityType);
}
// get password
if connector.auth_methond.is_none() {
return Err(VncError::NoPassword);
}
let credential = connector
.auth_methond
.take()
.ok_or(VncError::NoPassword)?
.await?;
// auth
let auth = AuthHelper::read(&mut connector.stream, &credential).await?;
auth.write(&mut connector.stream).await?;
let result = auth.finish(&mut connector.stream).await?;
if let AuthResult::Failed = result {
if let VncVersion::RFB37 = connector.rfb_version {
// In VNC Authentication (Section 7.2.2), if the authentication fails,
// the server sends the SecurityResult message, but does not send an
// error message before closing the connection.
return Err(VncError::WrongPassword);
} else {
if connector.rfb_version != VncVersion::RFB33 {
SecurityType::write(&selected, &mut connector.stream).await?;
}
let auth = AuthHelper::read(&mut connector.stream, &credential).await?;
auth.write(&mut connector.stream).await?;
let result = auth.finish(&mut connector.stream).await?;
if let AuthResult::Failed = result {
if let VncVersion::RFB37 = connector.rfb_version {
return Err(VncError::WrongPassword);
}
let reason =
read_security_failure(&mut connector.stream, &connector.limits)
.await?;
return Err(VncError::SecurityFailure(reason));
}
VncClient::new(
connector.stream,
connector.allow_shared,
connector.pixel_format,
connector.encodings,
connector.limits,
)
.await?
}
}
SecurityType::RA2_256 => {
let verifier = connector
.server_key_verifier
.clone()
.ok_or(VncError::Ra2ServerKeyVerifierRequired)?;
let credential = connector
.auth_methond
.take()
.ok_or(VncError::NoPassword)?
.await?;
let username = connector.username.take().unwrap_or_default();
ra2::validate_credentials(&username, &credential)?;
SecurityType::write(&selected, &mut connector.stream).await?;
let stream = ra2::authenticate(
connector.stream,
username,
credential,
verifier,
connector.limits,
connector.rfb_version,
)
.await?;
VncClient::new(
stream,
connector.allow_shared,
connector.pixel_format,
connector.encodings,
connector.limits,
)
.await?
}
_ => return Err(VncError::UnsupportedSecurityType),
};
info!("auth done, client connected");
Ok(VncState::Connected(
VncClient::new(
connector.stream,
connector.allow_shared,
connector.pixel_format,
connector.encodings,
connector.limits,
)
.await?,
))
Ok(VncState::Connected(client))
}
VncState::Connected(_) => Err(VncError::ConnectError),
}
@@ -201,6 +228,9 @@ where
{
stream: S,
auth_methond: Option<F>,
credentials_available: bool,
username: Option<String>,
server_key_verifier: Option<ServerKeyVerifier>,
security_policy: VncSecurityPolicy,
rfb_version: VncVersion,
allow_shared: bool,
@@ -245,6 +275,9 @@ where
Self {
stream,
auth_methond: None,
credentials_available: false,
username: None,
server_key_verifier: None,
security_policy: VncSecurityPolicy::Auto,
allow_shared: true,
rfb_version: VncVersion::RFB38,
@@ -295,6 +328,26 @@ where
///
pub fn set_auth_method(mut self, auth_callback: F) -> Self {
self.auth_methond = Some(auth_callback);
self.credentials_available = true;
self
}
pub fn set_credentials_available(mut self, available: bool) -> Self {
self.credentials_available = available;
self
}
pub fn set_username(mut self, username: impl Into<String>) -> Self {
self.username = Some(username.into());
self
}
pub fn set_server_key_verifier<V, Fut>(mut self, verifier: V) -> Self
where
V: Fn(VncServerKey) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<(), VncError>> + Send + 'static,
{
self.server_key_verifier = Some(Arc::new(move |key| Box::pin(verifier(key))));
self
}
@@ -406,7 +459,10 @@ mod tests {
.unwrap()
}
async fn write_server_init(server: &mut DuplexStream) {
async fn write_server_init<S>(server: &mut S)
where
S: AsyncRead + AsyncWrite + Unpin,
{
server.write_all(&2_u16.to_be_bytes()).await.unwrap();
server.write_all(&2_u16.to_be_bytes()).await.unwrap();
server
@@ -615,6 +671,49 @@ mod tests {
server_task.await.unwrap();
}
async fn run_ra2_handshake(subtype: u8, expected_username: &'static str) {
let (client, mut server) = tokio::io::duplex(16 * 1024);
let limits = VncLimits::default();
let server_task = tokio::spawn(async move {
server.write_all(b"RFB 003.008\n").await.unwrap();
let mut version = [0_u8; 12];
server.read_exact(&mut version).await.unwrap();
assert_eq!(&version, b"RFB 003.008\n");
server.write_all(&[4, 99, 1, 2, 129]).await.unwrap();
assert_eq!(server.read_u8().await.unwrap(), 129);
let mut encrypted = crate::client::security::ra2::accept_test_handshake(
server,
subtype,
expected_username,
"raspberry",
limits,
)
.await;
assert_eq!(encrypted.read_u8().await.unwrap(), 1);
write_server_init(&mut encrypted).await;
});
let state = VncConnector::new(client)
.set_auth_method(async { Ok("raspberry".to_owned()) })
.set_credentials_available(true)
.set_username("pi")
.set_server_key_verifier(|key| async move {
assert!(key.bits() >= 2048);
assert!(!key.encoded().is_empty());
Ok(())
})
.set_security_policy(VncSecurityPolicy::Auto)
.add_encoding(VncEncoding::Raw)
.build()
.unwrap()
.try_start()
.await
.unwrap();
state.finish().unwrap().close().await.unwrap();
server_task.await.unwrap();
}
#[tokio::test]
async fn none_security_33_37_38() {
run_none_handshake(VncVersion::RFB33).await;
@@ -622,6 +721,63 @@ mod tests {
run_none_handshake(VncVersion::RFB38).await;
}
#[tokio::test]
async fn ra2_256_auto_prefers_encrypted_auth_and_supports_both_subtypes() {
run_ra2_handshake(1, "pi").await;
run_ra2_handshake(2, "").await;
}
#[tokio::test]
async fn auto_with_credentials_never_downgrades_to_none() {
let (client, mut server) = tokio::io::duplex(128);
tokio::spawn(async move {
server.write_all(b"RFB 003.008\n").await.unwrap();
let mut version = [0_u8; 12];
server.read_exact(&mut version).await.unwrap();
server.write_all(&[1, 1]).await.unwrap();
});
let result = VncConnector::new(client)
.set_auth_method(async { Ok("password".to_owned()) })
.set_credentials_available(true)
.set_security_policy(VncSecurityPolicy::Auto)
.add_encoding(VncEncoding::Raw)
.build()
.unwrap()
.try_start()
.await;
assert!(matches!(result, Err(VncError::UnsupportedSecurityType)));
}
#[tokio::test]
async fn classic_vnc_auth_keeps_the_eight_byte_limit() {
let (client, mut server) = tokio::io::duplex(128);
tokio::spawn(async move {
server.write_all(b"RFB 003.008\n").await.unwrap();
let mut version = [0_u8; 12];
server.read_exact(&mut version).await.unwrap();
server.write_all(&[1, 2]).await.unwrap();
});
let result = VncConnector::new(client)
.set_auth_method(async { Ok("123456789".to_owned()) })
.set_credentials_available(true)
.set_security_policy(VncSecurityPolicy::Auto)
.add_encoding(VncEncoding::Raw)
.build()
.unwrap()
.try_start()
.await;
assert!(matches!(
result,
Err(VncError::CredentialTooLong {
field: "classic VNC password",
actual: 9,
limit: 8
})
));
}
#[tokio::test]
async fn scripted_rfb_none_decodes_zrle_tight_and_jpeg_as_raw() {
let zrle = run_scripted_framebuffer(
+1
View File
@@ -6,3 +6,4 @@ mod security;
pub use connection::VncClient;
pub use connector::{VncConnector, VncSecurityPolicy};
pub use security::ra2::VncServerKey;
+2
View File
@@ -1 +1,3 @@
pub(crate) mod des;
pub(crate) mod ra2;
+854
View File
@@ -0,0 +1,854 @@
use std::{
future::Future,
io,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use aes::Aes256;
use eax::{
aead::{generic_array::GenericArray, AeadInPlace, KeyInit},
Eax,
};
use rand::{rngs::OsRng, RngCore};
use rsa::{traits::PublicKeyParts, BigUint, Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey};
use sha2::{Digest, Sha256};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf};
use crate::{VncError, VncLimits, VncVersion};
use super::super::auth::read_security_failure;
type Aes256Eax = Eax<Aes256>;
const RA2_RANDOM_BYTES: usize = 16;
const RA2_HASH_BYTES: usize = 32;
const RA2_MAC_BYTES: usize = 16;
const CLIENT_RSA_BITS: usize = 2048;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VncServerKey {
bits: u32,
encoded: Vec<u8>,
}
impl VncServerKey {
pub fn bits(&self) -> u32 {
self.bits
}
pub fn encoded(&self) -> &[u8] {
&self.encoded
}
}
pub(crate) type ServerKeyVerifyFuture =
Pin<Box<dyn Future<Output = Result<(), VncError>> + Send + 'static>>;
pub(crate) type ServerKeyVerifier =
Arc<dyn Fn(VncServerKey) -> ServerKeyVerifyFuture + Send + Sync + 'static>;
pub(crate) fn validate_credentials(username: &str, password: &str) -> Result<(), VncError> {
validate_credential("RA2 username", username)?;
validate_credential("RA2 password", password)
}
fn validate_credential(field: &'static str, value: &str) -> Result<(), VncError> {
let len = value.len();
if len > usize::from(u8::MAX) {
return Err(VncError::CredentialTooLong {
field,
actual: len,
limit: usize::from(u8::MAX),
});
}
Ok(())
}
pub(crate) async fn authenticate<S>(
mut stream: S,
username: String,
password: String,
verifier: ServerKeyVerifier,
limits: VncLimits,
version: VncVersion,
) -> Result<Ra2Stream<S>, VncError>
where
S: AsyncRead + AsyncWrite + Unpin,
{
validate_credentials(&username, &password)?;
let (server_public_key, server_wire) = read_public_key(&mut stream, &limits).await?;
verifier(VncServerKey {
bits: u32::try_from(server_public_key.n().bits()).unwrap_or(u32::MAX),
encoded: server_wire.clone(),
})
.await?;
let mut rng = OsRng;
let client_private_key = RsaPrivateKey::new(&mut rng, CLIENT_RSA_BITS)
.map_err(|_| VncError::Ra2Crypto("failed to generate client RSA key"))?;
let client_public_key = RsaPublicKey::from(&client_private_key);
let client_wire = encode_public_key(&client_public_key)?;
stream.write_all(&client_wire).await?;
let encrypted_server_random_len = usize::from(stream.read_u16().await?);
let expected_server_random_len = client_private_key.size();
if encrypted_server_random_len != expected_server_random_len {
return Err(VncError::InvalidRa2EncryptedRandomLength {
actual: encrypted_server_random_len,
expected: expected_server_random_len,
});
}
let mut encrypted_server_random = vec![0; encrypted_server_random_len];
stream.read_exact(&mut encrypted_server_random).await?;
let server_random = client_private_key
.decrypt(Pkcs1v15Encrypt, &encrypted_server_random)
.map_err(|_| VncError::Ra2Crypto("failed to decrypt server random"))?;
if server_random.len() != RA2_RANDOM_BYTES {
return Err(VncError::InvalidRa2RandomLength(server_random.len()));
}
let mut client_random = [0_u8; RA2_RANDOM_BYTES];
rng.fill_bytes(&mut client_random);
let encrypted_client_random = server_public_key
.encrypt(&mut rng, Pkcs1v15Encrypt, &client_random)
.map_err(|_| VncError::Ra2Crypto("failed to encrypt client random"))?;
let encrypted_client_random_len =
u16::try_from(encrypted_client_random.len()).map_err(|_| VncError::LimitExceeded {
field: "RA2 encrypted client random",
actual: encrypted_client_random.len() as u64,
limit: u64::from(u16::MAX),
})?;
stream.write_u16(encrypted_client_random_len).await?;
stream.write_all(&encrypted_client_random).await?;
let client_session_key = sha256_concat(&server_random, &client_random);
let server_session_key = sha256_concat(&client_random, &server_random);
let expected_server_hash = sha256_concat(&server_wire, &client_wire);
let client_hash = sha256_concat(&client_wire, &server_wire);
let mut encrypted = Ra2Stream::new(
stream,
client_session_key,
server_session_key,
limits.max_ra2_record_bytes,
)?;
let mut server_hash = [0_u8; RA2_HASH_BYTES];
encrypted.read_exact(&mut server_hash).await?;
if server_hash != expected_server_hash {
return Err(VncError::Ra2ServerHashMismatch);
}
encrypted.write_all(&client_hash).await?;
let subtype = encrypted.read_u8().await?;
let mut credentials = Vec::with_capacity(username.len() + password.len() + 2);
match subtype {
1 => {
credentials.push(username.len() as u8);
credentials.extend_from_slice(username.as_bytes());
}
2 => credentials.push(0),
invalid => return Err(VncError::InvalidRa2Subtype(invalid)),
}
credentials.push(password.len() as u8);
credentials.extend_from_slice(password.as_bytes());
encrypted.write_all(&credentials).await?;
let result = encrypted.read_u32().await?;
match result {
0 => Ok(encrypted),
1 => {
if version == VncVersion::RFB38 {
let reason = read_security_failure(&mut encrypted, &limits).await?;
Err(VncError::SecurityFailure(reason))
} else {
Err(VncError::WrongPassword)
}
}
invalid => Err(VncError::InvalidSecurityResult(invalid)),
}
}
async fn read_public_key<S>(
stream: &mut S,
limits: &VncLimits,
) -> Result<(RsaPublicKey, Vec<u8>), VncError>
where
S: AsyncRead + Unpin,
{
let bits = stream.read_u32().await?;
if bits < limits.min_ra2_key_bits || bits > limits.max_ra2_key_bits {
return Err(VncError::InvalidRa2KeyLength {
actual: bits,
min: limits.min_ra2_key_bits,
max: limits.max_ra2_key_bits,
});
}
let key_bytes_u32 = bits
.checked_add(7)
.ok_or(VncError::IntegerOverflow("RA2 key byte length"))?
/ 8;
let key_bytes = usize::try_from(key_bytes_u32)
.map_err(|_| VncError::IntegerOverflow("RA2 key byte length"))?;
let mut modulus = vec![0_u8; key_bytes];
let mut exponent = vec![0_u8; key_bytes];
stream.read_exact(&mut modulus).await?;
stream.read_exact(&mut exponent).await?;
let public_key = RsaPublicKey::new(
BigUint::from_bytes_be(&modulus),
BigUint::from_bytes_be(&exponent),
)
.map_err(|_| VncError::InvalidRa2PublicKey)?;
let mut wire = Vec::with_capacity(4 + key_bytes * 2);
wire.extend_from_slice(&bits.to_be_bytes());
wire.extend_from_slice(&modulus);
wire.extend_from_slice(&exponent);
Ok((public_key, wire))
}
fn encode_public_key(key: &RsaPublicKey) -> Result<Vec<u8>, VncError> {
let bits = u32::try_from(key.n().bits()).map_err(|_| VncError::InvalidRa2PublicKey)?;
let key_bytes = usize::try_from(
bits.checked_add(7)
.ok_or(VncError::IntegerOverflow("RA2 key byte length"))?
/ 8,
)
.map_err(|_| VncError::IntegerOverflow("RA2 key byte length"))?;
let modulus = key.n().to_bytes_be();
let exponent = key.e().to_bytes_be();
if modulus.len() > key_bytes || exponent.len() > key_bytes {
return Err(VncError::InvalidRa2PublicKey);
}
let mut wire = Vec::with_capacity(4 + key_bytes * 2);
wire.extend_from_slice(&bits.to_be_bytes());
wire.resize(4 + key_bytes - modulus.len(), 0);
wire.extend_from_slice(&modulus);
wire.resize(4 + key_bytes * 2 - exponent.len(), 0);
wire.extend_from_slice(&exponent);
Ok(wire)
}
fn sha256_concat(left: &[u8], right: &[u8]) -> [u8; RA2_HASH_BYTES] {
let mut hasher = Sha256::new();
hasher.update(left);
hasher.update(right);
hasher.finalize().into()
}
pub(crate) struct Ra2Stream<S> {
stream: S,
write_cipher: Aes256Eax,
read_cipher: Aes256Eax,
write_counter: [u8; 16],
read_counter: [u8; 16],
max_record_bytes: usize,
read_header: [u8; 2],
read_header_pos: usize,
read_body: Vec<u8>,
read_body_pos: usize,
read_plain: Vec<u8>,
read_plain_pos: usize,
write_pending: Vec<u8>,
write_pending_pos: usize,
}
impl<S> Ra2Stream<S> {
fn new(
stream: S,
write_key: [u8; 32],
read_key: [u8; 32],
max_record_bytes: usize,
) -> Result<Self, VncError> {
if max_record_bytes == 0 || max_record_bytes > usize::from(u16::MAX) {
return Err(VncError::InvalidRa2RecordLimit(max_record_bytes));
}
let write_cipher = Aes256Eax::new_from_slice(&write_key)
.map_err(|_| VncError::Ra2Crypto("invalid client session key"))?;
let read_cipher = Aes256Eax::new_from_slice(&read_key)
.map_err(|_| VncError::Ra2Crypto("invalid server session key"))?;
Ok(Self {
stream,
write_cipher,
read_cipher,
write_counter: [0; 16],
read_counter: [0; 16],
max_record_bytes,
read_header: [0; 2],
read_header_pos: 0,
read_body: Vec::new(),
read_body_pos: 0,
read_plain: Vec::new(),
read_plain_pos: 0,
write_pending: Vec::new(),
write_pending_pos: 0,
})
}
fn decrypt_ready_record(&mut self) -> io::Result<()> {
let ciphertext_len = usize::from(u16::from_be_bytes(self.read_header));
let (ciphertext, tag_bytes) = self.read_body.split_at_mut(ciphertext_len);
let tag = GenericArray::clone_from_slice(tag_bytes);
self.read_cipher
.decrypt_in_place_detached(
GenericArray::from_slice(&self.read_counter),
&self.read_header,
ciphertext,
&tag,
)
.map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"RA2 AES-EAX authentication failed",
)
})?;
increment_counter(&mut self.read_counter);
self.read_plain.clear();
self.read_plain.extend_from_slice(ciphertext);
self.read_plain_pos = 0;
self.read_header_pos = 0;
self.read_body.clear();
self.read_body_pos = 0;
Ok(())
}
fn prepare_write_record(&mut self, plaintext: &[u8]) -> io::Result<usize> {
let len = plaintext.len().min(self.max_record_bytes);
let header = (len as u16).to_be_bytes();
let mut ciphertext = plaintext[..len].to_vec();
let tag = self
.write_cipher
.encrypt_in_place_detached(
GenericArray::from_slice(&self.write_counter),
&header,
&mut ciphertext,
)
.map_err(|_| io::Error::other("RA2 AES-EAX encryption failed"))?;
increment_counter(&mut self.write_counter);
self.write_pending.clear();
self.write_pending.reserve(2 + len + RA2_MAC_BYTES);
self.write_pending.extend_from_slice(&header);
self.write_pending.extend_from_slice(&ciphertext);
self.write_pending.extend_from_slice(&tag);
self.write_pending_pos = 0;
Ok(len)
}
fn poll_drain_write_pending(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<()>>
where
S: AsyncWrite + Unpin,
{
while self.write_pending_pos < self.write_pending.len() {
let written = match Pin::new(&mut self.stream)
.poll_write(cx, &self.write_pending[self.write_pending_pos..])
{
Poll::Pending => return Poll::Pending,
Poll::Ready(Ok(0)) => {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::WriteZero,
"failed to write RA2 record",
)));
}
Poll::Ready(Ok(written)) => written,
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
};
self.write_pending_pos += written;
}
self.write_pending.clear();
self.write_pending_pos = 0;
Poll::Ready(Ok(()))
}
}
impl<S> AsyncRead for Ra2Stream<S>
where
S: AsyncRead + AsyncWrite + Unpin,
{
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
output: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let this = self.get_mut();
loop {
if this.read_plain_pos < this.read_plain.len() {
let remaining = &this.read_plain[this.read_plain_pos..];
let count = remaining.len().min(output.remaining());
output.put_slice(&remaining[..count]);
this.read_plain_pos += count;
if this.read_plain_pos == this.read_plain.len() {
this.read_plain.clear();
this.read_plain_pos = 0;
}
return Poll::Ready(Ok(()));
}
if output.remaining() == 0 {
return Poll::Ready(Ok(()));
}
if !this.write_pending.is_empty() {
match this.poll_drain_write_pending(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
Poll::Ready(Ok(())) => {}
}
}
if this.read_header_pos < this.read_header.len() {
let start = this.read_header_pos;
let mut buffer = ReadBuf::new(&mut this.read_header[start..]);
match Pin::new(&mut this.stream).poll_read(cx, &mut buffer) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
Poll::Ready(Ok(())) => {
let read = buffer.filled().len();
if read == 0 {
if this.read_header_pos == 0 {
return Poll::Ready(Ok(()));
}
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"truncated RA2 record header",
)));
}
this.read_header_pos += read;
if this.read_header_pos < this.read_header.len() {
continue;
}
let ciphertext_len = usize::from(u16::from_be_bytes(this.read_header));
if ciphertext_len > this.max_record_bytes {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"RA2 record exceeds configured limit: {ciphertext_len} > {}",
this.max_record_bytes
),
)));
}
this.read_body.resize(ciphertext_len + RA2_MAC_BYTES, 0);
this.read_body_pos = 0;
}
}
}
if this.read_body_pos < this.read_body.len() {
let start = this.read_body_pos;
let mut buffer = ReadBuf::new(&mut this.read_body[start..]);
match Pin::new(&mut this.stream).poll_read(cx, &mut buffer) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
Poll::Ready(Ok(())) => {
let read = buffer.filled().len();
if read == 0 {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"truncated RA2 encrypted record",
)));
}
this.read_body_pos += read;
if this.read_body_pos < this.read_body.len() {
continue;
}
}
}
}
if let Err(error) = this.decrypt_ready_record() {
return Poll::Ready(Err(error));
}
}
}
}
impl<S> AsyncWrite for Ra2Stream<S>
where
S: AsyncWrite + Unpin,
{
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
input: &[u8],
) -> Poll<io::Result<usize>> {
let this = self.get_mut();
if !this.write_pending.is_empty() {
match this.poll_drain_write_pending(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
Poll::Ready(Ok(())) => {}
}
}
if input.is_empty() {
return Poll::Ready(Ok(0));
}
let consumed = match this.prepare_write_record(input) {
Ok(consumed) => consumed,
Err(error) => return Poll::Ready(Err(error)),
};
let written = match Pin::new(&mut this.stream)
.poll_write(cx, &this.write_pending[this.write_pending_pos..])
{
Poll::Pending => return Poll::Ready(Ok(consumed)),
Poll::Ready(Ok(0)) => {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::WriteZero,
"failed to write RA2 record",
)));
}
Poll::Ready(Ok(written)) => written,
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
};
this.write_pending_pos += written;
if this.write_pending_pos == this.write_pending.len() {
this.write_pending.clear();
this.write_pending_pos = 0;
}
Poll::Ready(Ok(consumed))
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let this = self.get_mut();
if !this.write_pending.is_empty() {
match this.poll_drain_write_pending(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
Poll::Ready(Ok(())) => {}
}
}
Pin::new(&mut this.stream).poll_flush(cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let this = self.get_mut();
if !this.write_pending.is_empty() {
match this.poll_drain_write_pending(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
Poll::Ready(Ok(())) => {}
}
}
Pin::new(&mut this.stream).poll_shutdown(cx)
}
}
fn increment_counter(counter: &mut [u8; 16]) {
for byte in counter {
let (next, overflow) = byte.overflowing_add(1);
*byte = next;
if !overflow {
break;
}
}
}
#[cfg(test)]
async fn establish_test_encryption<S>(
mut stream: S,
limits: VncLimits,
corrupt_server_hash: bool,
) -> Ra2Stream<S>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let mut rng = OsRng;
let server_private_key = RsaPrivateKey::new(&mut rng, CLIENT_RSA_BITS).unwrap();
let server_public_key = RsaPublicKey::from(&server_private_key);
let server_wire = encode_public_key(&server_public_key).unwrap();
stream.write_all(&server_wire).await.unwrap();
let (client_public_key, client_wire) = read_public_key(&mut stream, &limits).await.unwrap();
let server_random = [0x31_u8; RA2_RANDOM_BYTES];
let encrypted_server_random = client_public_key
.encrypt(&mut rng, Pkcs1v15Encrypt, &server_random)
.unwrap();
stream
.write_u16(encrypted_server_random.len() as u16)
.await
.unwrap();
stream.write_all(&encrypted_server_random).await.unwrap();
let encrypted_client_random_len = usize::from(stream.read_u16().await.unwrap());
let mut encrypted_client_random = vec![0_u8; encrypted_client_random_len];
stream
.read_exact(&mut encrypted_client_random)
.await
.unwrap();
let client_random = server_private_key
.decrypt(Pkcs1v15Encrypt, &encrypted_client_random)
.unwrap();
assert_eq!(client_random.len(), RA2_RANDOM_BYTES);
let client_session_key = sha256_concat(&server_random, &client_random);
let server_session_key = sha256_concat(&client_random, &server_random);
let mut server_hash = sha256_concat(&server_wire, &client_wire);
let expected_client_hash = sha256_concat(&client_wire, &server_wire);
let mut encrypted = Ra2Stream::new(
stream,
server_session_key,
client_session_key,
limits.max_ra2_record_bytes,
)
.unwrap();
if corrupt_server_hash {
server_hash[0] ^= 0x80;
}
encrypted.write_all(&server_hash).await.unwrap();
if corrupt_server_hash {
return encrypted;
}
let mut client_hash = [0_u8; RA2_HASH_BYTES];
encrypted.read_exact(&mut client_hash).await.unwrap();
assert_eq!(client_hash, expected_client_hash);
encrypted
}
#[cfg(test)]
pub(crate) async fn accept_test_handshake<S>(
stream: S,
subtype: u8,
expected_username: &str,
expected_password: &str,
limits: VncLimits,
) -> Ra2Stream<S>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let mut encrypted = establish_test_encryption(stream, limits, false).await;
encrypted.write_u8(subtype).await.unwrap();
let username_len = usize::from(encrypted.read_u8().await.unwrap());
let mut username = vec![0_u8; username_len];
encrypted.read_exact(&mut username).await.unwrap();
let password_len = usize::from(encrypted.read_u8().await.unwrap());
let mut password = vec![0_u8; password_len];
encrypted.read_exact(&mut password).await.unwrap();
assert_eq!(username, expected_username.as_bytes());
assert_eq!(password, expected_password.as_bytes());
encrypted.write_u32(0).await.unwrap();
encrypted
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct PartialThenPendingWriter {
bytes: Vec<u8>,
write_polls: usize,
}
impl AsyncWrite for PartialThenPendingWriter {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
input: &[u8],
) -> Poll<io::Result<usize>> {
self.write_polls += 1;
match self.write_polls {
1 => {
let written = input.len().min(3);
self.bytes.extend_from_slice(&input[..written]);
Poll::Ready(Ok(written))
}
2 => {
cx.waker().wake_by_ref();
Poll::Pending
}
_ => {
self.bytes.extend_from_slice(input);
Poll::Ready(Ok(input.len()))
}
}
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
}
fn paired_streams<S1, S2>(
left: S1,
right: S2,
max_record_bytes: usize,
) -> (Ra2Stream<S1>, Ra2Stream<S2>) {
let left_to_right = [0x11; 32];
let right_to_left = [0x22; 32];
(
Ra2Stream::new(left, left_to_right, right_to_left, max_record_bytes).unwrap(),
Ra2Stream::new(right, right_to_left, left_to_right, max_record_bytes).unwrap(),
)
}
#[test]
fn credentials_use_utf8_byte_limits() {
assert!(validate_credentials("用户", "密码").is_ok());
let error = validate_credentials(&"é".repeat(128), "password").unwrap_err();
assert!(matches!(
error,
VncError::CredentialTooLong {
field: "RA2 username",
actual: 256,
limit: 255
}
));
}
#[test]
fn encrypted_stream_backpressure_does_not_consume_replacement_input() {
let writer = PartialThenPendingWriter::default();
let mut stream = Ra2Stream::new(writer, [0x55; 32], [0x66; 32], 64).unwrap();
let waker = futures::task::noop_waker_ref();
let mut cx = Context::from_waker(waker);
assert!(matches!(
Pin::new(&mut stream).poll_write(&mut cx, b"first"),
Poll::Ready(Ok(5))
));
assert!(matches!(
Pin::new(&mut stream).poll_write(&mut cx, b"cancelled"),
Poll::Pending
));
assert!(matches!(
Pin::new(&mut stream).poll_write(&mut cx, b"x"),
Poll::Ready(Ok(1))
));
assert!(matches!(
Pin::new(&mut stream).poll_flush(&mut cx),
Poll::Ready(Ok(()))
));
let first_record_len = 2 + 5 + RA2_MAC_BYTES;
let bytes = &stream.stream.bytes;
assert_eq!(bytes.len(), first_record_len + 2 + 1 + RA2_MAC_BYTES);
assert_eq!(&bytes[..2], &5_u16.to_be_bytes());
assert_eq!(
&bytes[first_record_len..first_record_len + 2],
&1_u16.to_be_bytes()
);
}
#[tokio::test]
async fn rejects_out_of_range_server_rsa_key_before_allocation() {
let mut input = &9_000_u32.to_be_bytes()[..];
let error = read_public_key(&mut input, &VncLimits::default())
.await
.unwrap_err();
assert!(matches!(
error,
VncError::InvalidRa2KeyLength {
actual: 9_000,
min: 1_024,
max: 8_192
}
));
}
#[tokio::test]
async fn authenticate_rejects_server_hash_mismatch() {
let (client, server) = tokio::io::duplex(16 * 1024);
let limits = VncLimits::default();
let server_task = tokio::spawn(async move {
let _encrypted = establish_test_encryption(server, limits, true).await;
});
let verifier: ServerKeyVerifier = Arc::new(|_| Box::pin(async { Ok(()) }));
let error = match authenticate(
client,
"pi".to_string(),
"raspberry".to_string(),
verifier,
limits,
VncVersion::RFB38,
)
.await
{
Ok(_) => panic!("corrupted server key hash must fail closed"),
Err(error) => error,
};
assert!(matches!(error, VncError::Ra2ServerHashMismatch));
server_task.await.unwrap();
}
#[tokio::test]
async fn authenticate_rejects_unknown_subtype() {
let (client, server) = tokio::io::duplex(16 * 1024);
let limits = VncLimits::default();
let server_task = tokio::spawn(async move {
let mut encrypted = establish_test_encryption(server, limits, false).await;
encrypted.write_u8(3).await.unwrap();
});
let verifier: ServerKeyVerifier = Arc::new(|_| Box::pin(async { Ok(()) }));
let error = match authenticate(
client,
"pi".to_string(),
"raspberry".to_string(),
verifier,
limits,
VncVersion::RFB38,
)
.await
{
Ok(_) => panic!("unknown RA2 subtype must fail closed"),
Err(error) => error,
};
assert!(matches!(error, VncError::InvalidRa2Subtype(3)));
server_task.await.unwrap();
}
#[tokio::test]
async fn encrypted_stream_splits_and_coalesces_independently_of_rfb_reads() {
let (left_io, right_io) = tokio::io::duplex(4096);
let (mut left, mut right) = paired_streams(left_io, right_io, 5);
let sender = tokio::spawn(async move {
left.write_all(b"abcdefgh").await.unwrap();
left.write_all(b"ijklmnop").await.unwrap();
left
});
let mut first = [0_u8; 3];
right.read_exact(&mut first).await.unwrap();
assert_eq!(&first, b"abc");
let mut rest = [0_u8; 13];
right.read_exact(&mut rest).await.unwrap();
assert_eq!(&rest, b"defghijklmnop");
let mut left = sender.await.unwrap();
right.write_all(b"reply-one").await.unwrap();
right.write_all(b"reply-two").await.unwrap();
let mut reply = [0_u8; 18];
left.read_exact(&mut reply).await.unwrap();
assert_eq!(&reply, b"reply-onereply-two");
}
#[tokio::test]
async fn encrypted_stream_rejects_bad_mac() {
let (mut raw_writer, raw_reader) = tokio::io::duplex(128);
let key = [0x44; 32];
let mut reader = Ra2Stream::new(raw_reader, [0x33; 32], key, 64).unwrap();
let cipher = Aes256Eax::new_from_slice(&key).unwrap();
let header = 3_u16.to_be_bytes();
let mut ciphertext = b"bad".to_vec();
let tag = cipher
.encrypt_in_place_detached(GenericArray::from_slice(&[0; 16]), &header, &mut ciphertext)
.unwrap();
raw_writer.write_all(&header).await.unwrap();
raw_writer.write_all(&ciphertext).await.unwrap();
let mut bad_tag = tag.to_vec();
bad_tag[0] ^= 0x80;
raw_writer.write_all(&bad_tag).await.unwrap();
let mut output = [0_u8; 3];
let error = reader.read_exact(&mut output).await.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
}
}
+26
View File
@@ -19,6 +19,32 @@ pub enum VncError {
RequiredSecurityTypeUnavailable(&'static str),
#[error("Wrong password")]
WrongPassword,
#[error("VNC credential is too long for {field}: {actual} > {limit} bytes")]
CredentialTooLong {
field: &'static str,
actual: usize,
limit: usize,
},
#[error("RA2 server key length is outside the supported range: {actual} bits (expected {min}..={max})")]
InvalidRa2KeyLength { actual: u32, min: u32, max: u32 },
#[error("Invalid RA2 RSA public key")]
InvalidRa2PublicKey,
#[error("Invalid RA2 encrypted random length: {actual} bytes, expected {expected}")]
InvalidRa2EncryptedRandomLength { actual: usize, expected: usize },
#[error("Invalid RA2 decrypted random length: {0} bytes")]
InvalidRa2RandomLength(usize),
#[error("RA2 server public-key hash verification failed")]
Ra2ServerHashMismatch,
#[error("Unknown RA2 authentication subtype: {0}")]
InvalidRa2Subtype(u8),
#[error("RA2 server key verification callback is required")]
Ra2ServerKeyVerifierRequired,
#[error("RA2 server key was rejected: {0}")]
Ra2ServerKeyRejected(String),
#[error("Invalid RA2 record size limit: {0}")]
InvalidRa2RecordLimit(usize),
#[error("RA2 cryptographic operation failed: {0}")]
Ra2Crypto(&'static str),
#[error("Server rejected the connection: {0}")]
SecurityFailure(String),
#[error("Protocol limit exceeded for {field}: {actual} > {limit}")]
+1 -1
View File
@@ -217,7 +217,7 @@ pub mod event;
pub mod limits;
pub use client::VncClient;
pub use client::{VncConnector, VncSecurityPolicy};
pub use client::{VncConnector, VncSecurityPolicy, VncServerKey};
pub use config::*;
pub use error::*;
pub use event::*;
+6
View File
@@ -3,6 +3,9 @@
pub struct VncLimits {
pub max_server_name_bytes: usize,
pub max_failure_reason_bytes: usize,
pub min_ra2_key_bits: u32,
pub max_ra2_key_bits: u32,
pub max_ra2_record_bytes: usize,
pub max_clipboard_bytes: usize,
pub max_framebuffer_width: u16,
pub max_framebuffer_height: u16,
@@ -18,6 +21,9 @@ impl Default for VncLimits {
Self {
max_server_name_bytes: 64 * 1024,
max_failure_reason_bytes: 64 * 1024,
min_ra2_key_bits: 1_024,
max_ra2_key_bits: 8_192,
max_ra2_record_bytes: usize::from(u16::MAX),
max_clipboard_bytes: 16 * 1024 * 1024,
max_framebuffer_width: 16_384,
max_framebuffer_height: 16_384,