Files
RemoteDesk/client/crates/client-core/src/secure_pipe.rs
T
曾志威 5ef362ba22
ci / rust (push) Canceled after 0s
ci / native-client (push) Canceled after 0s
ci / package-windows-client (push) Canceled after 0s
ci / linux-agent (push) Canceled after 0s
ci / edge-service (push) Canceled after 0s
ci / coturn-pop (push) Canceled after 0s
ci / package-windows-host (push) Canceled after 0s
migrate runtime and packaging to Rust WebRTC
2026-08-14 16:08:28 +08:00

1186 lines
42 KiB
Rust

use crate::{ChallengeMacVerification, HelperProcessIdentity, PipePeerVerifier, SessionMaterial};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use hmac::{Hmac, KeyInit as _, Mac as _};
use serde::{Deserialize, Serialize};
use sha2::{Digest as _, Sha256};
#[cfg(windows)]
use std::time::Instant;
use std::{
fmt,
fs::File,
io::{self, Read, Write},
path::Path,
time::Duration,
};
use zeroize::{Zeroize as _, Zeroizing};
const PIPE_PROTOCOL_VERSION: u8 = 1;
const PIPE_KEY_BYTES: usize = 32;
const PIPE_NONCE_BYTES: usize = 32;
const MAX_PIPE_FRAME_BYTES: usize = 64 * 1024;
const CONNECT_POLL_INTERVAL: Duration = Duration::from_millis(10);
const CLIENT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const AUTH_DOMAIN: &[u8] = b"RemoteDesk secure pipe auth v1\0";
const SESSION_DOMAIN: &[u8] = b"RemoteDesk secure pipe session v1\0";
const SESSION_ACK_DOMAIN: &[u8] = b"RemoteDesk secure pipe session ack v1\0";
pub const PIPE_BOOTSTRAP_KEY_ENV: &str = "REMOTEDESK_PIPE_BOOTSTRAP_KEY";
type HmacSha256 = Hmac<Sha256>;
#[derive(Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RdpViewerLaunchConfig {
pub schema_version: u8,
pub target: String,
pub username: String,
pub width: u16,
pub height: u16,
pub use_multimon: bool,
#[serde(default)]
pub monitor_indices: Vec<u8>,
pub fullscreen: bool,
pub redirect_clipboard: bool,
pub session_id: String,
pub certificate_sha256: String,
pub credential_ref: Option<String>,
}
pub struct SecurePipeBootstrap {
pipe_name: String,
encoded_key: Zeroizing<String>,
}
impl fmt::Debug for SecurePipeBootstrap {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SecurePipeBootstrap")
.field("pipe_name", &self.pipe_name)
.field("encoded_key", &"[REDACTED]")
.finish()
}
}
impl SecurePipeBootstrap {
#[must_use]
pub fn pipe_name(&self) -> &str {
&self.pipe_name
}
#[must_use]
pub fn encoded_key(&self) -> &str {
&self.encoded_key
}
}
pub struct SecurePipeServer {
#[cfg(windows)]
pipe: File,
key: Zeroizing<Vec<u8>>,
}
pub struct SecurePipeClientSession {
#[cfg(windows)]
pipe: File,
#[cfg(windows)]
deadline: Instant,
key: Zeroizing<Vec<u8>>,
nonce: Vec<u8>,
payload: Vec<u8>,
}
impl fmt::Debug for SecurePipeClientSession {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SecurePipeClientSession")
.field("key", &"[REDACTED]")
.field("payload", &"[REDACTED]")
.finish_non_exhaustive()
}
}
impl SecurePipeClientSession {
#[must_use]
pub fn payload(&self) -> &[u8] {
&self.payload
}
#[cfg(windows)]
pub fn acknowledge(mut self) -> io::Result<()> {
ensure_handshake_deadline(self.deadline)?;
let digest = payload_digest(&self.payload);
let mac = session_ack_mac(&self.key, &self.nonce, &digest);
write_json_frame_until(
&mut self.pipe,
&SessionAckMessage {
protocol_version: PIPE_PROTOCOL_VERSION,
payload_sha256: URL_SAFE_NO_PAD.encode(digest),
mac: URL_SAFE_NO_PAD.encode(mac),
},
self.deadline,
)?;
self.pipe.flush()
}
#[cfg(not(windows))]
pub fn acknowledge(self) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"secure Named Pipes require Windows",
))
}
}
impl fmt::Debug for SecurePipeServer {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SecurePipeServer")
.field("key", &"[REDACTED]")
.finish_non_exhaustive()
}
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct ChallengeMessage {
protocol_version: u8,
nonce: String,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct AuthenticationMessage {
protocol_version: u8,
pid: u32,
creation_token: u64,
build_hash: String,
mac: String,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct SessionMessage {
protocol_version: u8,
payload: String,
mac: String,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct SessionAckMessage {
protocol_version: u8,
payload_sha256: String,
mac: String,
}
impl SecurePipeServer {
#[cfg(windows)]
pub fn create(session_id: &str) -> io::Result<(Self, SecurePipeBootstrap)> {
if session_id.is_empty()
|| session_id.len() > 64
|| !session_id
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid secure pipe session identifier",
));
}
let mut key = Zeroizing::new(vec![0_u8; PIPE_KEY_BYTES]);
getrandom::fill(&mut key).map_err(io::Error::other)?;
let mut suffix = [0_u8; 16];
getrandom::fill(&mut suffix).map_err(io::Error::other)?;
let pipe_name = format!(
r"\\.\pipe\RemoteDesk\rdp-{session_id}-{}",
URL_SAFE_NO_PAD.encode(suffix)
);
let pipe = windows_platform::create_current_user_pipe(&pipe_name)?;
let bootstrap = SecurePipeBootstrap {
pipe_name,
encoded_key: Zeroizing::new(URL_SAFE_NO_PAD.encode(&*key)),
};
Ok((Self { pipe, key }, bootstrap))
}
#[cfg(not(windows))]
pub fn create(_session_id: &str) -> io::Result<(Self, SecurePipeBootstrap)> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"secure Named Pipes require Windows",
))
}
#[cfg(windows)]
pub fn authenticate_and_send(
mut self,
expected: HelperProcessIdentity,
payload: &[u8],
timeout: Duration,
) -> io::Result<()> {
if payload.len() > MAX_PIPE_FRAME_BYTES / 2 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"secure pipe session payload is too large",
));
}
let deadline = Instant::now()
.checked_add(timeout)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid pipe timeout"))?;
windows_platform::wait_for_client(&self.pipe, timeout)?;
windows_platform::set_nonblocking(&self.pipe)?;
let pipe_pid = windows_platform::pipe_client_pid(&self.pipe)?;
if pipe_pid != expected.pid {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"secure pipe client PID does not match the spawned helper",
));
}
let mut nonce = [0_u8; PIPE_NONCE_BYTES];
getrandom::fill(&mut nonce).map_err(io::Error::other)?;
write_json_frame_until(
&mut self.pipe,
&ChallengeMessage {
protocol_version: PIPE_PROTOCOL_VERSION,
nonce: URL_SAFE_NO_PAD.encode(nonce),
},
deadline,
)?;
let auth: AuthenticationMessage = read_json_frame_until(&mut self.pipe, deadline)?;
if auth.protocol_version != PIPE_PROTOCOL_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"secure pipe protocol version mismatch",
));
}
let claimed = HelperProcessIdentity::new(auth.pid, auth.creation_token, auth.build_hash);
let mac = URL_SAFE_NO_PAD
.decode(auth.mac)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid pipe auth MAC"))?;
let mac_verified = verify_identity_mac(&self.key, AUTH_DOMAIN, &nonce, &claimed, &mac);
let mut verifier = PipePeerVerifier::new(expected);
verifier
.authenticate(claimed, mac_verified)
.map_err(|error| io::Error::new(io::ErrorKind::PermissionDenied, error))?;
let material = SessionMaterial::new(payload.to_vec());
let authenticated_payload = verifier
.session_material(&material)
.map_err(|error| io::Error::new(io::ErrorKind::PermissionDenied, error))?;
let session_mac = session_mac(&self.key, &nonce, authenticated_payload);
write_json_frame_until(
&mut self.pipe,
&SessionMessage {
protocol_version: PIPE_PROTOCOL_VERSION,
payload: URL_SAFE_NO_PAD.encode(authenticated_payload),
mac: URL_SAFE_NO_PAD.encode(session_mac),
},
deadline,
)?;
self.pipe.flush()?;
let acknowledgement: SessionAckMessage = read_json_frame_until(&mut self.pipe, deadline)?;
if acknowledgement.protocol_version != PIPE_PROTOCOL_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"secure pipe acknowledgement version mismatch",
));
}
let acknowledged_digest = URL_SAFE_NO_PAD
.decode(acknowledgement.payload_sha256)
.map_err(|_| {
io::Error::new(io::ErrorKind::InvalidData, "invalid pipe payload digest")
})?;
let expected_digest = payload_digest(authenticated_payload);
if acknowledged_digest.as_slice() != expected_digest.as_slice() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"secure pipe payload acknowledgement does not match",
));
}
let acknowledgement_mac = URL_SAFE_NO_PAD
.decode(acknowledgement.mac)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid pipe ACK MAC"))?;
if !verify_session_ack_mac(&self.key, &nonce, &expected_digest, &acknowledgement_mac) {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"secure pipe acknowledgement MAC was rejected",
));
}
ensure_handshake_deadline(deadline)?;
Ok(())
}
}
#[cfg(windows)]
pub fn receive_secure_pipe_session(pipe_name: &str) -> io::Result<SecurePipeClientSession> {
if !pipe_name.starts_with(r"\\.\pipe\RemoteDesk\rdp-") || pipe_name.len() > 256 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid RemoteDesk secure pipe name",
));
}
let encoded_key = std::env::var(PIPE_BOOTSTRAP_KEY_ENV)
.map_err(|_| io::Error::new(io::ErrorKind::PermissionDenied, "pipe key is missing"))?;
// The helper reads the one-time bootstrap before starting any worker thread.
unsafe { std::env::remove_var(PIPE_BOOTSTRAP_KEY_ENV) };
let mut encoded_key = Zeroizing::new(encoded_key);
let key = Zeroizing::new(
URL_SAFE_NO_PAD
.decode(encoded_key.as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "pipe key is invalid"))?,
);
encoded_key.zeroize();
if key.len() != PIPE_KEY_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"pipe key has an invalid length",
));
}
receive_secure_pipe_session_with_key(pipe_name, key)
}
#[cfg(windows)]
fn receive_secure_pipe_session_with_key(
pipe_name: &str,
key: Zeroizing<Vec<u8>>,
) -> io::Result<SecurePipeClientSession> {
if !pipe_name.starts_with(r"\\.\pipe\RemoteDesk\rdp-") || pipe_name.len() > 256 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid RemoteDesk secure pipe name",
));
}
if key.len() != PIPE_KEY_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"pipe key has an invalid length",
));
}
let deadline = Instant::now()
.checked_add(CLIENT_CONNECT_TIMEOUT)
.expect("fixed secure pipe timeout fits Instant");
let mut pipe = windows_platform::connect_client(pipe_name, CLIENT_CONNECT_TIMEOUT)?;
let challenge: ChallengeMessage = read_json_frame_until(&mut pipe, deadline)?;
if challenge.protocol_version != PIPE_PROTOCOL_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"secure pipe protocol version mismatch",
));
}
let nonce = URL_SAFE_NO_PAD
.decode(challenge.nonce)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid pipe nonce"))?;
if nonce.len() != PIPE_NONCE_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"pipe nonce has an invalid length",
));
}
let identity = current_process_identity()?;
let auth_mac = identity_mac(&key, AUTH_DOMAIN, &nonce, &identity);
write_json_frame_until(
&mut pipe,
&AuthenticationMessage {
protocol_version: PIPE_PROTOCOL_VERSION,
pid: identity.pid,
creation_token: identity.creation_token,
build_hash: identity.build_hash.clone(),
mac: URL_SAFE_NO_PAD.encode(auth_mac),
},
deadline,
)?;
let session: SessionMessage = read_json_frame_until(&mut pipe, deadline)?;
if session.protocol_version != PIPE_PROTOCOL_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"secure pipe protocol version mismatch",
));
}
let payload = URL_SAFE_NO_PAD
.decode(session.payload)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid pipe payload"))?;
let mac = URL_SAFE_NO_PAD
.decode(session.mac)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid pipe session MAC"))?;
if !verify_session_mac(&key, &nonce, &payload, &mac) {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"secure pipe session MAC was rejected",
));
}
Ok(SecurePipeClientSession {
pipe,
deadline,
key,
nonce,
payload,
})
}
#[cfg(not(windows))]
pub fn receive_secure_pipe_session(_pipe_name: &str) -> io::Result<SecurePipeClientSession> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"secure Named Pipes require Windows",
))
}
pub fn receive_secure_pipe_payload(pipe_name: &str) -> io::Result<Vec<u8>> {
let session = receive_secure_pipe_session(pipe_name)?;
let payload = session.payload().to_vec();
session.acknowledge()?;
Ok(payload)
}
pub fn current_process_identity() -> io::Result<HelperProcessIdentity> {
#[cfg(windows)]
{
let executable = std::env::current_exe()?;
process_identity(std::process::id(), &executable)
}
#[cfg(not(windows))]
{
Err(io::Error::new(
io::ErrorKind::Unsupported,
"Windows process identity is unavailable",
))
}
}
pub fn process_identity(pid: u32, expected_executable: &Path) -> io::Result<HelperProcessIdentity> {
#[cfg(windows)]
{
windows_platform::process_identity(pid, expected_executable)
}
#[cfg(not(windows))]
{
let _ = (pid, expected_executable);
Err(io::Error::new(
io::ErrorKind::Unsupported,
"Windows process identity is unavailable",
))
}
}
fn identity_mac(
key: &[u8],
domain: &[u8],
nonce: &[u8],
identity: &HelperProcessIdentity,
) -> Vec<u8> {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts arbitrary key lengths");
update_identity_mac(&mut mac, domain, nonce, identity);
mac.finalize().into_bytes().to_vec()
}
fn update_identity_mac(
mac: &mut HmacSha256,
domain: &[u8],
nonce: &[u8],
identity: &HelperProcessIdentity,
) {
mac.update(domain);
mac.update(nonce);
mac.update(&identity.pid.to_le_bytes());
mac.update(&identity.creation_token.to_le_bytes());
mac.update(identity.build_hash.as_bytes());
}
fn verify_identity_mac(
key: &[u8],
domain: &[u8],
nonce: &[u8],
identity: &HelperProcessIdentity,
candidate: &[u8],
) -> ChallengeMacVerification {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts arbitrary key lengths");
update_identity_mac(&mut mac, domain, nonce, identity);
if mac.verify_slice(candidate).is_ok() {
ChallengeMacVerification::Verified
} else {
ChallengeMacVerification::Rejected
}
}
fn session_mac(key: &[u8], nonce: &[u8], payload: &[u8]) -> Vec<u8> {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts arbitrary key lengths");
update_session_mac(&mut mac, nonce, payload);
mac.finalize().into_bytes().to_vec()
}
fn verify_session_mac(key: &[u8], nonce: &[u8], payload: &[u8], candidate: &[u8]) -> bool {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts arbitrary key lengths");
update_session_mac(&mut mac, nonce, payload);
mac.verify_slice(candidate).is_ok()
}
fn update_session_mac(mac: &mut HmacSha256, nonce: &[u8], payload: &[u8]) {
mac.update(SESSION_DOMAIN);
mac.update(nonce);
mac.update(&(payload.len() as u64).to_le_bytes());
mac.update(payload);
}
fn payload_digest(payload: &[u8]) -> [u8; 32] {
Sha256::digest(payload).into()
}
fn session_ack_mac(key: &[u8], nonce: &[u8], digest: &[u8; 32]) -> Vec<u8> {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts arbitrary key lengths");
update_session_ack_mac(&mut mac, nonce, digest);
mac.finalize().into_bytes().to_vec()
}
fn verify_session_ack_mac(key: &[u8], nonce: &[u8], digest: &[u8; 32], candidate: &[u8]) -> bool {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts arbitrary key lengths");
update_session_ack_mac(&mut mac, nonce, digest);
mac.verify_slice(candidate).is_ok()
}
fn update_session_ack_mac(mac: &mut HmacSha256, nonce: &[u8], digest: &[u8; 32]) {
mac.update(SESSION_ACK_DOMAIN);
mac.update(nonce);
mac.update(digest);
}
#[cfg(test)]
fn write_json_frame<T: Serialize>(writer: &mut impl Write, value: &T) -> io::Result<()> {
let encoded = serde_json::to_vec(value).map_err(io::Error::other)?;
if encoded.is_empty() || encoded.len() > MAX_PIPE_FRAME_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"secure pipe frame size is invalid",
));
}
let length = u32::try_from(encoded.len())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "pipe frame is too large"))?;
writer.write_all(&length.to_le_bytes())?;
writer.write_all(&encoded)
}
#[cfg(windows)]
fn write_json_frame_until<T: Serialize>(
writer: &mut impl Write,
value: &T,
deadline: Instant,
) -> io::Result<()> {
let encoded = serde_json::to_vec(value).map_err(io::Error::other)?;
if encoded.is_empty() || encoded.len() > MAX_PIPE_FRAME_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"secure pipe frame size is invalid",
));
}
let length = u32::try_from(encoded.len())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "pipe frame is too large"))?;
let mut frame = Vec::with_capacity(4 + encoded.len());
frame.extend_from_slice(&length.to_le_bytes());
frame.extend_from_slice(&encoded);
let mut offset = 0;
while offset < frame.len() {
ensure_handshake_deadline(deadline)?;
match writer.write(&frame[offset..]) {
Ok(0) => {
return Err(io::Error::new(
io::ErrorKind::WriteZero,
"secure pipe write made no progress",
));
}
Ok(count) => offset += count,
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
Err(error) if windows_platform::read_is_pending(&error) => {
std::thread::sleep(CONNECT_POLL_INTERVAL);
}
Err(error) => return Err(error),
}
}
Ok(())
}
#[cfg(test)]
fn read_json_frame<T: for<'de> Deserialize<'de>>(reader: &mut impl Read) -> io::Result<T> {
let mut length = [0_u8; 4];
reader.read_exact(&mut length)?;
let length = usize::try_from(u32::from_le_bytes(length))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid pipe frame length"))?;
if length == 0 || length > MAX_PIPE_FRAME_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"secure pipe frame size is invalid",
));
}
let mut encoded = vec![0_u8; length];
reader.read_exact(&mut encoded)?;
serde_json::from_slice(&encoded).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"secure pipe frame is not valid protocol JSON",
)
})
}
#[cfg(windows)]
fn read_json_frame_until<T: for<'de> Deserialize<'de>>(
reader: &mut impl Read,
deadline: Instant,
) -> io::Result<T> {
let mut length = [0_u8; 4];
read_exact_until(reader, &mut length, deadline)?;
let length = usize::try_from(u32::from_le_bytes(length))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid pipe frame length"))?;
if length == 0 || length > MAX_PIPE_FRAME_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"secure pipe frame size is invalid",
));
}
let mut encoded = vec![0_u8; length];
read_exact_until(reader, &mut encoded, deadline)?;
serde_json::from_slice(&encoded).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"secure pipe frame is not valid protocol JSON",
)
})
}
#[cfg(windows)]
fn read_exact_until(
reader: &mut impl Read,
buffer: &mut [u8],
deadline: Instant,
) -> io::Result<()> {
let mut offset = 0;
while offset < buffer.len() {
ensure_handshake_deadline(deadline)?;
match reader.read(&mut buffer[offset..]) {
// In PIPE_NOWAIT mode Windows can report a successful zero-byte
// read while the connected peer has not produced data yet.
Ok(0) => std::thread::sleep(CONNECT_POLL_INTERVAL),
Ok(count) => offset += count,
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
Err(error) if windows_platform::read_is_pending(&error) => {
std::thread::sleep(CONNECT_POLL_INTERVAL);
}
Err(error) => return Err(error),
}
}
Ok(())
}
#[cfg(windows)]
fn ensure_handshake_deadline(deadline: Instant) -> io::Result<()> {
if Instant::now() >= deadline {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
"secure pipe handshake timed out",
));
}
Ok(())
}
#[cfg(windows)]
mod windows_platform {
use super::{CONNECT_POLL_INTERVAL, HelperProcessIdentity, Sha256};
use sha2::Digest as _;
use std::{
ffi::c_void,
fs::{self, File, OpenOptions},
io, mem,
os::windows::io::{AsRawHandle as _, FromRawHandle as _, RawHandle},
path::Path,
ptr, thread,
time::{Duration, Instant},
};
use windows_sys::Win32::{
Foundation::{
ERROR_INSUFFICIENT_BUFFER, ERROR_NO_DATA, ERROR_PIPE_CONNECTED, ERROR_PIPE_LISTENING,
FILETIME, HANDLE, INVALID_HANDLE_VALUE, LocalFree,
},
Security::{
Authorization::{
ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
SDDL_REVISION_1,
},
GetTokenInformation, PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES, TOKEN_QUERY,
TOKEN_USER, TokenUser,
},
Storage::FileSystem::{FILE_FLAG_FIRST_PIPE_INSTANCE, PIPE_ACCESS_DUPLEX},
System::{
Pipes::{
ConnectNamedPipe, CreateNamedPipeW, GetNamedPipeClientProcessId, PIPE_NOWAIT,
PIPE_READMODE_BYTE, PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_BYTE,
SetNamedPipeHandleState,
},
Threading::{
GetCurrentProcess, GetProcessTimes, OpenProcess, OpenProcessToken,
PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION, QueryFullProcessImageNameW,
},
},
};
pub fn create_current_user_pipe(pipe_name: &str) -> io::Result<File> {
let sid = current_user_sid()?;
let sddl = wide(&format!("D:P(A;;GA;;;{sid})"));
let mut descriptor: PSECURITY_DESCRIPTOR = ptr::null_mut();
// SAFETY: pointers reference writable output storage and a terminated SDDL string.
if unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
sddl.as_ptr(),
SDDL_REVISION_1,
&mut descriptor,
ptr::null_mut(),
)
} == 0
{
return Err(io::Error::last_os_error());
}
let attributes = SECURITY_ATTRIBUTES {
nLength: u32::try_from(mem::size_of::<SECURITY_ATTRIBUTES>())
.expect("SECURITY_ATTRIBUTES size fits u32"),
lpSecurityDescriptor: descriptor.cast(),
bInheritHandle: 0,
};
let pipe_name = wide(pipe_name);
// SAFETY: arguments are initialized and the security descriptor remains alive for the call.
let handle = unsafe {
CreateNamedPipeW(
pipe_name.as_ptr(),
PIPE_ACCESS_DUPLEX | FILE_FLAG_FIRST_PIPE_INSTANCE,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_NOWAIT | PIPE_REJECT_REMOTE_CLIENTS,
1,
64 * 1024,
64 * 1024,
5_000,
&attributes,
)
};
// SAFETY: the descriptor was allocated by LocalAlloc inside the conversion API.
unsafe { LocalFree(descriptor.cast()) };
if handle == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
// SAFETY: CreateNamedPipeW returned an owned kernel handle.
Ok(unsafe { File::from_raw_handle(handle as RawHandle) })
}
pub fn wait_for_client(pipe: &File, timeout: Duration) -> io::Result<()> {
let handle = pipe.as_raw_handle() as HANDLE;
let deadline = Instant::now() + timeout;
loop {
// SAFETY: handle is a live Named Pipe server handle and no OVERLAPPED is used.
if unsafe { ConnectNamedPipe(handle, ptr::null_mut()) } != 0 {
break;
}
let error = io::Error::last_os_error();
match error.raw_os_error().map(|value| value as u32) {
Some(ERROR_PIPE_CONNECTED) => break,
Some(ERROR_PIPE_LISTENING) if Instant::now() < deadline => {
thread::sleep(CONNECT_POLL_INTERVAL);
}
Some(ERROR_PIPE_LISTENING) => {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
"native helper did not connect to its secure pipe",
));
}
_ => return Err(error),
}
}
Ok(())
}
pub fn pipe_client_pid(pipe: &File) -> io::Result<u32> {
let mut pid = 0_u32;
// SAFETY: handle is a connected Named Pipe server and pid is writable.
if unsafe { GetNamedPipeClientProcessId(pipe.as_raw_handle() as HANDLE, &mut pid) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(pid)
}
pub fn connect_client(pipe_name: &str, timeout: Duration) -> io::Result<File> {
let deadline = Instant::now() + timeout;
loop {
match OpenOptions::new().read(true).write(true).open(pipe_name) {
Ok(pipe) => {
set_nonblocking(&pipe)?;
return Ok(pipe);
}
Err(error)
if matches!(
error.kind(),
io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied
) && Instant::now() < deadline =>
{
thread::sleep(CONNECT_POLL_INTERVAL);
}
Err(error) => return Err(error),
}
}
}
pub fn set_nonblocking(pipe: &File) -> io::Result<()> {
let mode = PIPE_READMODE_BYTE | PIPE_NOWAIT;
// SAFETY: handle is a live Named Pipe client and mode points to initialized storage.
if unsafe {
SetNamedPipeHandleState(
pipe.as_raw_handle() as HANDLE,
&mode,
ptr::null(),
ptr::null(),
)
} == 0
{
return Err(io::Error::last_os_error());
}
Ok(())
}
pub fn read_is_pending(error: &io::Error) -> bool {
error.kind() == io::ErrorKind::WouldBlock
|| error.raw_os_error().map(|value| value as u32) == Some(ERROR_NO_DATA)
}
pub fn process_identity(
pid: u32,
expected_executable: &Path,
) -> io::Result<HelperProcessIdentity> {
// SAFETY: OpenProcess returns a new owned handle or null.
let process = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
if process.is_null() {
return Err(io::Error::last_os_error());
}
// SAFETY: OpenProcess returned an owned process handle.
let process = unsafe { File::from_raw_handle(process as RawHandle) };
let handle = process.as_raw_handle() as HANDLE;
let mut creation = FILETIME::default();
let mut exit = FILETIME::default();
let mut kernel = FILETIME::default();
let mut user = FILETIME::default();
// SAFETY: all output pointers are valid for writes and handle is a process handle.
if unsafe { GetProcessTimes(handle, &mut creation, &mut exit, &mut kernel, &mut user) } == 0
{
return Err(io::Error::last_os_error());
}
let creation_token =
(u64::from(creation.dwHighDateTime) << 32) | u64::from(creation.dwLowDateTime);
let mut image = vec![0_u16; 32_768];
let mut image_len = u32::try_from(image.len()).expect("image buffer length fits u32");
// SAFETY: image is writable for image_len UTF-16 code units.
if unsafe {
QueryFullProcessImageNameW(
handle,
PROCESS_NAME_WIN32,
image.as_mut_ptr(),
&mut image_len,
)
} == 0
{
return Err(io::Error::last_os_error());
}
image.truncate(image_len as usize);
let actual_executable =
std::path::PathBuf::from(String::from_utf16(&image).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"helper image path is not valid UTF-16",
)
})?);
let expected = fs::canonicalize(expected_executable)?;
let actual = fs::canonicalize(&actual_executable)?;
if !actual
.to_string_lossy()
.eq_ignore_ascii_case(&expected.to_string_lossy())
{
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"secure pipe client image does not match the launched helper",
));
}
let build_hash = Sha256::digest(fs::read(actual)?).iter().fold(
String::with_capacity(64),
|mut output, byte| {
use std::fmt::Write as _;
write!(output, "{byte:02x}").expect("writing to a String cannot fail");
output
},
);
Ok(HelperProcessIdentity::new(pid, creation_token, build_hash))
}
fn current_user_sid() -> io::Result<String> {
let mut token: HANDLE = ptr::null_mut();
// SAFETY: token points to writable handle storage; pseudo process handle is valid.
if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 {
return Err(io::Error::last_os_error());
}
// SAFETY: OpenProcessToken returned an owned token handle.
let token = unsafe { File::from_raw_handle(token as RawHandle) };
let mut required = 0_u32;
// SAFETY: null buffer with zero length is the documented size query.
unsafe {
GetTokenInformation(
token.as_raw_handle() as HANDLE,
TokenUser,
ptr::null_mut(),
0,
&mut required,
)
};
let size_error = io::Error::last_os_error();
if size_error.raw_os_error().map(|value| value as u32) != Some(ERROR_INSUFFICIENT_BUFFER)
|| required == 0
{
return Err(size_error);
}
let word = mem::size_of::<usize>();
let words = (required as usize).div_ceil(word);
let mut buffer = vec![0_usize; words];
// SAFETY: buffer has at least required writable bytes and required is preserved.
if unsafe {
GetTokenInformation(
token.as_raw_handle() as HANDLE,
TokenUser,
buffer.as_mut_ptr().cast::<c_void>(),
required,
&mut required,
)
} == 0
{
return Err(io::Error::last_os_error());
}
// SAFETY: TOKEN_USER is the documented structure returned for TokenUser.
let token_user = unsafe { &*buffer.as_ptr().cast::<TOKEN_USER>() };
let mut sid_string = ptr::null_mut();
// SAFETY: SID comes from a successful TokenUser query and output pointer is writable.
if unsafe { ConvertSidToStringSidW(token_user.User.Sid, &mut sid_string) } == 0 {
return Err(io::Error::last_os_error());
}
let mut length = 0_usize;
// SAFETY: conversion returns a null-terminated LocalAlloc UTF-16 string.
unsafe {
while *sid_string.add(length) != 0 {
length += 1;
}
}
// SAFETY: the preceding scan established a valid initialized slice.
let sid = String::from_utf16(unsafe { std::slice::from_raw_parts(sid_string, length) })
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "current SID is invalid"));
// SAFETY: sid_string was allocated by ConvertSidToStringSidW.
unsafe { LocalFree(sid_string.cast()) };
sid
}
fn wide(value: &str) -> Vec<u16> {
value.encode_utf16().chain([0]).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_codec_is_bounded_and_strict() {
let message = ChallengeMessage {
protocol_version: 1,
nonce: "nonce".to_owned(),
};
let mut bytes = Vec::new();
write_json_frame(&mut bytes, &message).unwrap();
let decoded: ChallengeMessage = read_json_frame(&mut bytes.as_slice()).unwrap();
assert_eq!(decoded.protocol_version, 1);
assert_eq!(decoded.nonce, "nonce");
let oversized = u32::try_from(MAX_PIPE_FRAME_BYTES + 1)
.unwrap()
.to_le_bytes();
assert!(read_json_frame::<ChallengeMessage>(&mut oversized.as_slice()).is_err());
}
#[test]
fn challenge_and_payload_macs_bind_all_fields() {
let key = [7_u8; PIPE_KEY_BYTES];
let nonce = [9_u8; PIPE_NONCE_BYTES];
let identity = HelperProcessIdentity::new(42, 100, "build-a");
let mac = identity_mac(&key, AUTH_DOMAIN, &nonce, &identity);
assert_eq!(
verify_identity_mac(&key, AUTH_DOMAIN, &nonce, &identity, &mac),
ChallengeMacVerification::Verified
);
assert_eq!(
verify_identity_mac(
&key,
AUTH_DOMAIN,
&nonce,
&HelperProcessIdentity::new(42, 101, "build-a"),
&mac,
),
ChallengeMacVerification::Rejected
);
assert_ne!(
session_mac(&key, &nonce, b"one"),
session_mac(&key, &nonce, b"two")
);
let session_signature = session_mac(&key, &nonce, b"payload");
assert!(verify_session_mac(
&key,
&nonce,
b"payload",
&session_signature
));
assert!(!verify_session_mac(
&key,
&nonce,
b"tampered",
&session_signature
));
let digest = payload_digest(b"payload");
let acknowledgement = session_ack_mac(&key, &nonce, &digest);
assert!(verify_session_ack_mac(
&key,
&nonce,
&digest,
&acknowledgement
));
assert!(!verify_session_ack_mac(
&key,
&nonce,
&payload_digest(b"other payload"),
&acknowledgement
));
let mut tampered_acknowledgement = acknowledgement;
tampered_acknowledgement[0] ^= 1;
assert!(!verify_session_ack_mac(
&key,
&nonce,
&digest,
&tampered_acknowledgement
));
}
#[test]
fn launch_config_rejects_unknown_fields() {
let value = serde_json::json!({
"schema_version": 2,
"target": "host",
"username": "user",
"width": 1920,
"height": 1080,
"use_multimon": false,
"fullscreen": false,
"redirect_clipboard": true,
"session_id": "1-2-3",
"certificate_sha256": "ab".repeat(32),
"credential_ref": null,
"password": "must-not-be-accepted"
});
assert!(serde_json::from_value::<RdpViewerLaunchConfig>(value).is_err());
}
#[cfg(windows)]
#[test]
fn real_windows_pipe_authenticates_process_and_payload() {
let session_id = format!("test-{}-1", std::process::id());
let (server, bootstrap) = SecurePipeServer::create(&session_id).unwrap();
let key = URL_SAFE_NO_PAD
.decode(bootstrap.encoded_key().as_bytes())
.unwrap();
let pipe_name = bootstrap.pipe_name().to_owned();
let expected = current_process_identity().unwrap();
let client = std::thread::spawn(move || {
let session =
receive_secure_pipe_session_with_key(&pipe_name, Zeroizing::new(key)).unwrap();
let payload = session.payload().to_vec();
session.acknowledge().unwrap();
payload
});
server
.authenticate_and_send(expected, b"authenticated-session", Duration::from_secs(2))
.unwrap();
assert_eq!(client.join().unwrap(), b"authenticated-session");
}
#[cfg(windows)]
#[test]
fn real_windows_pipe_rejects_mismatched_payload_acknowledgement() {
let session_id = format!("test-{}-2", std::process::id());
let (server, bootstrap) = SecurePipeServer::create(&session_id).unwrap();
let key = URL_SAFE_NO_PAD
.decode(bootstrap.encoded_key().as_bytes())
.unwrap();
let pipe_name = bootstrap.pipe_name().to_owned();
let expected = current_process_identity().unwrap();
let client = std::thread::spawn(move || {
let mut session =
receive_secure_pipe_session_with_key(&pipe_name, Zeroizing::new(key)).unwrap();
session.payload[0] ^= 1;
session.acknowledge().unwrap();
});
let error = server
.authenticate_and_send(expected, b"authenticated-session", Duration::from_secs(2))
.unwrap_err();
client.join().unwrap();
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
assert!(error.to_string().contains("does not match"));
}
#[cfg(windows)]
#[test]
fn real_windows_pipe_rejects_tampered_acknowledgement_mac() {
let session_id = format!("test-{}-3", std::process::id());
let (server, bootstrap) = SecurePipeServer::create(&session_id).unwrap();
let key = URL_SAFE_NO_PAD
.decode(bootstrap.encoded_key().as_bytes())
.unwrap();
let pipe_name = bootstrap.pipe_name().to_owned();
let expected = current_process_identity().unwrap();
let client = std::thread::spawn(move || {
let mut session =
receive_secure_pipe_session_with_key(&pipe_name, Zeroizing::new(key)).unwrap();
session.key[0] ^= 1;
session.acknowledge().unwrap();
});
let error = server
.authenticate_and_send(expected, b"authenticated-session", Duration::from_secs(2))
.unwrap_err();
client.join().unwrap();
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
assert!(error.to_string().contains("MAC was rejected"));
}
#[cfg(windows)]
#[test]
fn real_windows_pipe_times_out_after_connected_client_stalls() {
let session_id = format!("test-{}-4", std::process::id());
let (server, bootstrap) = SecurePipeServer::create(&session_id).unwrap();
let pipe_name = bootstrap.pipe_name().to_owned();
let expected = current_process_identity().unwrap();
let (connected_sender, connected_receiver) = std::sync::mpsc::channel();
let client = std::thread::spawn(move || {
let _pipe = windows_platform::connect_client(&pipe_name, Duration::from_secs(2))
.expect("test client connects");
connected_sender.send(()).unwrap();
std::thread::sleep(Duration::from_secs(2));
});
connected_receiver
.recv_timeout(Duration::from_secs(2))
.unwrap();
let error = server
.authenticate_and_send(
expected,
b"authenticated-session",
Duration::from_millis(100),
)
.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::TimedOut);
client.join().unwrap();
}
}