diff --git a/Cargo.lock b/Cargo.lock index 6c48c5a51..07a2033cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2416,6 +2416,7 @@ dependencies = [ "tracing", "tracing-subscriber", "url", + "uuid", "whoami", "windows 0.58.0", "winit", diff --git a/crates/ironrdp-client/Cargo.toml b/crates/ironrdp-client/Cargo.toml index f6b57045f..03ca572fb 100644 --- a/crates/ironrdp-client/Cargo.toml +++ b/crates/ironrdp-client/Cargo.toml @@ -41,6 +41,7 @@ ironrdp = { workspace = true, features = [ "rdpsnd", "cliprdr", "displaycontrol", + "connector" ] } ironrdp-cliprdr-native.workspace = true ironrdp-rdpsnd-native.workspace = true @@ -77,6 +78,7 @@ reqwest = "0.12" url = "2.5" raw-window-handle = "0.6.2" ironrdp-core = { workspace = true, features = ["alloc"] } +uuid = { version = "1.11.0"} [target.'cfg(windows)'.dependencies] windows = { workspace = true, features = ["Win32_Foundation"] } diff --git a/crates/ironrdp-client/src/config.rs b/crates/ironrdp-client/src/config.rs index e05af8f30..2595d8e0e 100644 --- a/crates/ironrdp-client/src/config.rs +++ b/crates/ironrdp-client/src/config.rs @@ -1,13 +1,12 @@ -use core::num::ParseIntError; -use core::str::FromStr; -use std::io; - use anyhow::Context as _; use clap::clap_derive::ValueEnum; use clap::Parser; +use core::num::ParseIntError; +use core::str::FromStr; use ironrdp::connector::{self, Credentials}; use ironrdp::pdu::rdp::capability_sets::MajorPlatformType; use ironrdp::pdu::rdp::client_info::PerformanceFlags; +use std::io; use tap::prelude::*; const DEFAULT_WIDTH: u16 = 1920; @@ -316,6 +315,8 @@ impl Config { whoami::Platform::Android => MajorPlatformType::ANDROID, _ => MajorPlatformType::UNSPECIFIED, }, + hardware_id: None, + license_cache: None, no_server_pointer: args.no_server_pointer, autologon: args.autologon, request_data: None, diff --git a/crates/ironrdp-connector/src/connection.rs b/crates/ironrdp-connector/src/connection.rs index 323f73ccc..c42eabdc5 100644 --- a/crates/ironrdp-connector/src/connection.rs +++ b/crates/ironrdp-connector/src/connection.rs @@ -1,16 +1,16 @@ use core::mem; -use std::borrow::Cow; -use std::net::SocketAddr; - use ironrdp_core::{decode, encode_vec, Encode, WriteBuf}; use ironrdp_pdu::rdp::client_info::{OptionalSystemTime, TimezoneInfo}; use ironrdp_pdu::x224::X224; use ironrdp_pdu::{gcc, mcs, nego, rdp, PduHint}; use ironrdp_svc::{StaticChannelSet, StaticVirtualChannel, SvcClientProcessor}; +use std::borrow::Cow; +use std::net::SocketAddr; +use std::sync::Arc; use crate::channel_connection::{ChannelConnectionSequence, ChannelConnectionState}; use crate::connection_activation::{ConnectionActivationSequence, ConnectionActivationState}; -use crate::license_exchange::LicenseExchangeSequence; +use crate::license_exchange::{LicenseExchangeSequence, NoopLicenseCache}; use crate::{ encode_x224_packet, Config, ConnectorError, ConnectorErrorExt as _, ConnectorResult, DesktopSize, Sequence, State, Written, @@ -481,6 +481,11 @@ impl Sequence for ClientConnector { io_channel_id, self.config.credentials.username().unwrap_or("").to_owned(), self.config.domain.clone(), + self.config.hardware_id.unwrap_or_default(), + self.config + .license_cache + .clone() + .unwrap_or_else(|| Arc::new(NoopLicenseCache)), ), }, ), diff --git a/crates/ironrdp-connector/src/lib.rs b/crates/ironrdp-connector/src/lib.rs index 82645a2ad..f4015d1fb 100644 --- a/crates/ironrdp-connector/src/lib.rs +++ b/crates/ironrdp-connector/src/lib.rs @@ -17,12 +17,12 @@ pub mod credssp; mod license_exchange; mod server_name; -use core::any::Any; -use core::fmt; - +pub use crate::license_exchange::LicenseCache; pub use channel_connection::{ChannelConnectionSequence, ChannelConnectionState}; pub use connection::{encode_send_data_request, ClientConnector, ClientConnectorState, ConnectionResult}; pub use connection_finalization::{ConnectionFinalizationSequence, ConnectionFinalizationState}; +use core::any::Any; +use core::fmt; use ironrdp_core::{encode_buf, encode_vec, Encode, WriteBuf}; use ironrdp_pdu::nego::NegoRequestData; use ironrdp_pdu::rdp::capability_sets; @@ -32,6 +32,7 @@ use ironrdp_pdu::{gcc, x224, PduHint}; pub use license_exchange::{LicenseExchangeSequence, LicenseExchangeState}; pub use server_name::ServerName; pub use sspi; +use std::sync::Arc; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] @@ -161,6 +162,10 @@ pub struct Config { pub dig_product_id: String, pub client_dir: String, pub platform: capability_sets::MajorPlatformType, + /// Unique identifier for the computer + /// + /// Each 32-bit integer contains client hardware-specific data helping the server uniquely identify the client. + pub hardware_id: Option<[u32; 4]>, /// Optional data for the x224 connection request. /// /// Fallbacks to a sensible default depending on the provided credentials: @@ -170,6 +175,7 @@ pub struct Config { pub request_data: Option, /// If true, the INFO_AUTOLOGON flag is set in the [`ClientInfoPdu`](ironrdp_pdu::rdp::ClientInfoPdu) pub autologon: bool, + pub license_cache: Option>, // FIXME(@CBenoit): these are client-only options, not part of the connector. pub no_server_pointer: bool, diff --git a/crates/ironrdp-connector/src/license_exchange.rs b/crates/ironrdp-connector/src/license_exchange.rs index dc2a8030b..28b60bfee 100644 --- a/crates/ironrdp-connector/src/license_exchange.rs +++ b/crates/ironrdp-connector/src/license_exchange.rs @@ -1,12 +1,13 @@ +use super::{legacy, ConnectorError, ConnectorErrorExt}; +use crate::{encode_send_data_request, ConnectorResult, ConnectorResultExt as _, Sequence, State, Written}; +use core::fmt::Debug; use core::{fmt, mem}; - use ironrdp_core::WriteBuf; -use ironrdp_pdu::rdp::server_license::{self, LicensePdu, ServerLicenseError}; +use ironrdp_pdu::rdp::server_license::{self, LicenseInformation, LicensePdu, ServerLicenseError}; use ironrdp_pdu::PduHint; use rand_core::{OsRng, RngCore as _}; - -use super::legacy; -use crate::{encode_send_data_request, ConnectorResult, ConnectorResultExt as _, Sequence, State, Written}; +use std::str; +use std::sync::Arc; #[derive(Default, Debug)] #[non_exhaustive] @@ -57,15 +58,43 @@ pub struct LicenseExchangeSequence { pub io_channel_id: u16, pub username: String, pub domain: Option, + pub hardware_id: [u32; 4], + pub license_cache: Arc, +} + +pub trait LicenseCache: Sync + Send + Debug { + fn get_license(&self, license_info: LicenseInformation) -> ConnectorResult>>; + fn store_license(&self, license_info: LicenseInformation) -> ConnectorResult<()>; +} + +#[derive(Debug)] +pub(crate) struct NoopLicenseCache; + +impl LicenseCache for NoopLicenseCache { + fn get_license(&self, _license_info: LicenseInformation) -> ConnectorResult>> { + Ok(None) + } + + fn store_license(&self, _license_info: LicenseInformation) -> ConnectorResult<()> { + Ok(()) + } } impl LicenseExchangeSequence { - pub fn new(io_channel_id: u16, username: String, domain: Option) -> Self { + pub fn new( + io_channel_id: u16, + username: String, + domain: Option, + hardware_id: [u32; 4], + license_cache: Arc, + ) -> Self { Self { state: LicenseExchangeState::NewLicenseRequest, io_channel_id, username, domain, + hardware_id, + license_cache, } } } @@ -107,52 +136,102 @@ impl Sequence for LicenseExchangeSequence { let mut premaster_secret = [0u8; server_license::PREMASTER_SECRET_SIZE]; OsRng.fill_bytes(&mut premaster_secret); - match server_license::ClientNewLicenseRequest::from_server_license_request( - &license_request, - &client_random, - &premaster_secret, - &self.username, - self.domain.as_deref().unwrap_or(""), - ) { - Ok((new_license_request, encryption_data)) => { - trace!(?encryption_data, "Successfully generated Client New License Request"); - info!(message = ?new_license_request, "Send"); - - let written = encode_send_data_request::( - send_data_indication_ctx.initiator_id, - send_data_indication_ctx.channel_id, - &new_license_request.into(), - output, - )?; - - ( - Written::from_size(written)?, - LicenseExchangeState::PlatformChallenge { encryption_data }, - ) + let license_info = license_request + .scope_list + .iter() + .filter_map(|scope| { + self.license_cache + .get_license(LicenseInformation { + version: license_request.product_info.version, + scope: scope.0.clone(), + company_name: license_request.product_info.company_name.clone(), + product_id: license_request.product_info.product_id.clone(), + license_info: vec![], + }) + .transpose() + }) + .next() + .transpose()?; + + if let Some(info) = license_info { + match server_license::ClientLicenseInfo::from_server_license_request( + &license_request, + &client_random, + &premaster_secret, + self.hardware_id, + info, + ) { + Ok((client_license_info, encryption_data)) => { + trace!(?encryption_data, "Successfully generated Client License Info"); + info!(message = ?client_license_info, "Send"); + + let written = encode_send_data_request::( + send_data_indication_ctx.initiator_id, + send_data_indication_ctx.channel_id, + &client_license_info.into(), + output, + )?; + + trace!(?written, "Written ClientLicenseInfo"); + + ( + Written::from_size(written)?, + LicenseExchangeState::PlatformChallenge { encryption_data }, + ) + } + Err(err) => { + return Err(custom_err!("ClientNewLicenseRequest", err)); + } } - Err(error) => { - if let ServerLicenseError::InvalidX509Certificate { - source: error, - cert_der, - } = &error - { - struct BytesHexFormatter<'a>(&'a [u8]); - - impl fmt::Display for BytesHexFormatter<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "0x")?; - self.0.iter().try_for_each(|byte| write!(f, "{byte:02X}")) + } else { + let hwid = self.hardware_id; + match server_license::ClientNewLicenseRequest::from_server_license_request( + &license_request, + &client_random, + &premaster_secret, + &self.username, + &format!("{:X}-{:X}-{:X}-{:X}", hwid[0], hwid[1], hwid[2], hwid[3]), + ) { + Ok((new_license_request, encryption_data)) => { + trace!(?encryption_data, "Successfully generated Client New License Request"); + info!(message = ?new_license_request, "Send"); + + let written = encode_send_data_request::( + send_data_indication_ctx.initiator_id, + send_data_indication_ctx.channel_id, + &new_license_request.into(), + output, + )?; + + ( + Written::from_size(written)?, + LicenseExchangeState::PlatformChallenge { encryption_data }, + ) + } + Err(error) => { + if let ServerLicenseError::InvalidX509Certificate { + source: error, + cert_der, + } = &error + { + struct BytesHexFormatter<'a>(&'a [u8]); + + impl fmt::Display for BytesHexFormatter<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0x")?; + self.0.iter().try_for_each(|byte| write!(f, "{byte:02X}")) + } } + + error!( + %error, + cert_der = %BytesHexFormatter(cert_der), + "Unsupported or invalid X509 certificate received during license exchange step" + ); } - error!( - %error, - cert_der = %BytesHexFormatter(cert_der), - "Unsupported or invalid X509 certificate received during license exchange step" - ); + return Err(custom_err!("ClientNewLicenseRequest", error)); } - - return Err(custom_err!("ClientNewLicenseRequest", error)); } } } @@ -188,7 +267,7 @@ impl Sequence for LicenseExchangeSequence { let challenge_response = server_license::ClientPlatformChallengeResponse::from_server_platform_challenge( &challenge, - self.domain.as_deref().unwrap_or(""), + self.hardware_id, &encryption_data, ) .map_err(|e| custom_err!("ClientPlatformChallengeResponse", e))?; @@ -242,6 +321,12 @@ impl Sequence for LicenseExchangeSequence { .map_err(|e| custom_err!("license verification", e))?; debug!("License verified with success"); + + let license_info = upgrade_license + .new_license_info(&encryption_data) + .map_err(ConnectorError::decode)?; + + self.license_cache.store_license(license_info)? } LicensePdu::LicensingErrorMessage(error_message) => { if error_message.error_code != server_license::LicenseErrorCode::StatusValidClient { diff --git a/crates/ironrdp-pdu/src/rdp/server_license.rs b/crates/ironrdp-pdu/src/rdp/server_license.rs index 19215a1e2..071fceb2a 100644 --- a/crates/ironrdp-pdu/src/rdp/server_license.rs +++ b/crates/ironrdp-pdu/src/rdp/server_license.rs @@ -11,11 +11,13 @@ use num_traits::{FromPrimitive, ToPrimitive}; use thiserror::Error; use crate::rdp::headers::{BasicSecurityHeader, BasicSecurityHeaderFlags, BASIC_SECURITY_HEADER_SIZE}; +pub use crate::rdp::server_license::client_license_info::ClientLicenseInfo; use crate::PduError; #[cfg(test)] mod tests; +mod client_license_info; mod client_new_license_request; mod client_platform_challenge_response; mod licensing_error_message; @@ -30,7 +32,7 @@ pub use self::client_platform_challenge_response::{ pub use self::licensing_error_message::{LicenseErrorCode, LicensingErrorMessage, LicensingStateTransition}; pub use self::server_license_request::{cert, ProductInfo, Scope, ServerCertificate, ServerLicenseRequest}; pub use self::server_platform_challenge::ServerPlatformChallenge; -pub use self::server_upgrade_license::{NewLicenseInformation, ServerUpgradeLicense}; +pub use self::server_upgrade_license::{LicenseInformation, ServerUpgradeLicense}; pub const PREAMBLE_SIZE: usize = 4; pub const PREMASTER_SECRET_SIZE: usize = 48; @@ -341,6 +343,7 @@ fn compute_mac_data(mac_salt_key: &[u8], data: &[u8]) -> Vec { #[derive(Debug, PartialEq)] pub enum LicensePdu { ClientNewLicenseRequest(ClientNewLicenseRequest), + ClientLicenseInfo(ClientLicenseInfo), ClientPlatformChallengeResponse(ClientPlatformChallengeResponse), ServerLicenseRequest(ServerLicenseRequest), ServerPlatformChallenge(ServerPlatformChallenge), @@ -375,6 +378,7 @@ impl Encode for LicensePdu { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { match self { Self::ClientNewLicenseRequest(ref pdu) => pdu.encode(dst), + Self::ClientLicenseInfo(ref pdu) => pdu.encode(dst), Self::ClientPlatformChallengeResponse(ref pdu) => pdu.encode(dst), Self::ServerLicenseRequest(ref pdu) => pdu.encode(dst), Self::ServerPlatformChallenge(ref pdu) => pdu.encode(dst), @@ -386,6 +390,7 @@ impl Encode for LicensePdu { fn name(&self) -> &'static str { match self { Self::ClientNewLicenseRequest(pdu) => pdu.name(), + Self::ClientLicenseInfo(pdu) => pdu.name(), Self::ClientPlatformChallengeResponse(pdu) => pdu.name(), Self::ServerLicenseRequest(pdu) => pdu.name(), Self::ServerPlatformChallenge(pdu) => pdu.name(), @@ -397,6 +402,7 @@ impl Encode for LicensePdu { fn size(&self) -> usize { match self { Self::ClientNewLicenseRequest(pdu) => pdu.size(), + Self::ClientLicenseInfo(pdu) => pdu.size(), Self::ClientPlatformChallengeResponse(pdu) => pdu.size(), Self::ServerLicenseRequest(pdu) => pdu.size(), Self::ServerPlatformChallenge(pdu) => pdu.size(), @@ -412,6 +418,12 @@ impl From for LicensePdu { } } +impl From for LicensePdu { + fn from(pdu: ClientLicenseInfo) -> Self { + Self::ClientLicenseInfo(pdu) + } +} + impl From for LicensePdu { fn from(pdu: ClientPlatformChallengeResponse) -> Self { Self::ClientPlatformChallengeResponse(pdu) diff --git a/crates/ironrdp-pdu/src/rdp/server_license/client_license_info.rs b/crates/ironrdp-pdu/src/rdp/server_license/client_license_info.rs new file mode 100644 index 000000000..d8a610b5a --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/server_license/client_license_info.rs @@ -0,0 +1,208 @@ +use crate::crypto::rc4::Rc4; +use crate::crypto::rsa::encrypt_with_public_key; +use crate::rdp::headers::{BasicSecurityHeader, BasicSecurityHeaderFlags}; +use crate::rdp::server_license::client_new_license_request::{compute_master_secret, compute_session_key_blob}; +use crate::rdp::server_license::client_platform_challenge_response::CLIENT_HARDWARE_IDENTIFICATION_SIZE; +use crate::rdp::server_license::{ + compute_mac_data, BlobHeader, BlobType, LicenseEncryptionData, LicenseHeader, PreambleFlags, PreambleType, + PreambleVersion, ServerLicenseError, ServerLicenseRequest, KEY_EXCHANGE_ALGORITHM_RSA, MAC_SIZE, PLATFORM_ID, + PREAMBLE_SIZE, RANDOM_NUMBER_SIZE, +}; +use byteorder::{LittleEndian, WriteBytesExt}; +use ironrdp_core::{ + ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use md5::Digest; +use std::io; + +const LICENSE_INFO_STATIC_FIELDS_SIZE: usize = 20; + +/// [2.2.2.3] Client License Info (CLIENT_LICENSE_INFO) +/// +/// [2.2.2.3]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpele/9407b2eb-f180-4827-9488-cdbff4a5d4ea +#[derive(Debug, PartialEq, Eq)] +pub struct ClientLicenseInfo { + pub license_header: LicenseHeader, + pub client_random: Vec, + pub encrypted_premaster_secret: Vec, + pub license_info: Vec, + pub encrypted_hwid: Vec, + pub mac_data: Vec, +} + +impl ClientLicenseInfo { + const NAME: &'static str = "ClientLicenseInfo"; + + pub fn from_server_license_request( + license_request: &ServerLicenseRequest, + client_random: &[u8], + premaster_secret: &[u8], + hardware_data: [u32; 4], + license_info: Vec, + ) -> Result<(Self, LicenseEncryptionData), ServerLicenseError> { + let public_key = license_request.get_public_key()? + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, + "attempted to retrieve the server public key from a server license request message that does not have a certificate"))?; + let encrypted_premaster_secret = encrypt_with_public_key(premaster_secret, &public_key)?; + + let master_secret = compute_master_secret( + premaster_secret, + client_random, + license_request.server_random.as_slice(), + ); + let session_key_blob = compute_session_key_blob( + master_secret.as_slice(), + client_random, + license_request.server_random.as_slice(), + ); + let mac_salt_key = &session_key_blob[..16]; + + let mut md5 = md5::Md5::new(); + md5.update( + [ + &session_key_blob[16..32], + client_random, + license_request.server_random.as_slice(), + ] + .concat() + .as_slice(), + ); + let license_key = md5.finalize().to_vec(); + + let mut hardware_id = Vec::with_capacity(CLIENT_HARDWARE_IDENTIFICATION_SIZE); + hardware_id.write_u32::(PLATFORM_ID)?; + for data in hardware_data { + hardware_id.write_u32::(data)?; + } + + let mut rc4 = Rc4::new(&license_key); + let encrypted_hwid = rc4.process(&hardware_id); + + let mac_data = compute_mac_data(mac_salt_key, &hardware_id); + + let size = RANDOM_NUMBER_SIZE + + PREAMBLE_SIZE + + LICENSE_INFO_STATIC_FIELDS_SIZE + + encrypted_premaster_secret.len() + + license_info.len() + + encrypted_hwid.len() + + MAC_SIZE; + + let license_header = LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, + }, + preamble_message_type: PreambleType::LicenseInfo, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: (size) as u16, + }; + + Ok(( + Self { + license_header, + client_random: Vec::from(client_random), + encrypted_premaster_secret, + license_info, + encrypted_hwid, + mac_data, + }, + LicenseEncryptionData { + premaster_secret: Vec::from(premaster_secret), + mac_salt_key: Vec::from(mac_salt_key), + license_key, + }, + )) + } +} + +impl ClientLicenseInfo { + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + self.license_header.encode(dst)?; + + dst.write_u32(KEY_EXCHANGE_ALGORITHM_RSA); + dst.write_u32(PLATFORM_ID); + dst.write_slice(&self.client_random); + + BlobHeader::new(BlobType::RANDOM, self.encrypted_premaster_secret.len()).encode(dst)?; + dst.write_slice(&self.encrypted_premaster_secret); + + BlobHeader::new(BlobType::DATA, self.license_info.len()).encode(dst)?; + dst.write_slice(&self.license_info); + + BlobHeader::new(BlobType::ENCRYPTED_DATA, self.encrypted_hwid.len()).encode(dst)?; + dst.write_slice(&self.encrypted_hwid); + + dst.write_slice(&self.mac_data); + + Ok(()) + } + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn size(&self) -> usize { + self.license_header.size() + + LICENSE_INFO_STATIC_FIELDS_SIZE + + RANDOM_NUMBER_SIZE + + self.encrypted_premaster_secret.len() + + self.license_info.len() + + self.encrypted_hwid.len() + + MAC_SIZE + } +} + +impl ClientLicenseInfo { + pub fn decode(license_header: LicenseHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + if license_header.preamble_message_type != PreambleType::LicenseInfo { + return Err(invalid_field_err!("preambleMessageType", "unexpected preamble type")); + } + + let key_exchange_algorithm = src.read_u32(); + if key_exchange_algorithm != KEY_EXCHANGE_ALGORITHM_RSA { + return Err(invalid_field_err!("keyExchangeAlgo", "invalid key exchange algorithm")); + } + + // We can ignore platform ID + let _platform_id = src.read_u32(); + + ensure_size!(in: src, size: RANDOM_NUMBER_SIZE); + let client_random = src.read_slice(RANDOM_NUMBER_SIZE).into(); + + let premaster_secret_blob_header = BlobHeader::decode(src)?; + if premaster_secret_blob_header.blob_type != BlobType::RANDOM { + return Err(invalid_field_err!("blobType", "invalid blob type")); + } + ensure_size!(in: src, size: premaster_secret_blob_header.length); + let encrypted_premaster_secret = src.read_slice(premaster_secret_blob_header.length).into(); + + let license_info_blob_header = BlobHeader::decode(src)?; + if license_info_blob_header.blob_type != BlobType::DATA { + return Err(invalid_field_err!("blobType", "invalid blob type")); + } + ensure_size!(in: src, size: license_info_blob_header.length); + let license_info = src.read_slice(license_info_blob_header.length).into(); + + let encrypted_hwid_blob_header = BlobHeader::decode(src)?; + if encrypted_hwid_blob_header.blob_type != BlobType::DATA { + return Err(invalid_field_err!("blobType", "invalid blob type")); + } + ensure_size!(in: src, size: encrypted_hwid_blob_header.length); + let encrypted_hwid = src.read_slice(encrypted_hwid_blob_header.length).into(); + + ensure_size!(in: src, size: MAC_SIZE); + let mac_data = src.read_slice(MAC_SIZE).into(); + + Ok(Self { + license_header, + client_random, + encrypted_premaster_secret, + license_info, + encrypted_hwid, + mac_data, + }) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/server_license/client_new_license_request.rs b/crates/ironrdp-pdu/src/rdp/server_license/client_new_license_request.rs index f1d9a14b2..4d1930a91 100644 --- a/crates/ironrdp-pdu/src/rdp/server_license/client_new_license_request.rs +++ b/crates/ironrdp-pdu/src/rdp/server_license/client_new_license_request.rs @@ -231,7 +231,7 @@ fn salted_hash(salt: &[u8], salt_first: &[u8], salt_second: &[u8], input: &[u8]) } // According to https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpele/88061224-4a2f-4a28-a52e-e896b75ed2d3 -fn compute_master_secret(premaster_secret: &[u8], client_random: &[u8], server_random: &[u8]) -> Vec { +pub(crate) fn compute_master_secret(premaster_secret: &[u8], client_random: &[u8], server_random: &[u8]) -> Vec { [ salted_hash(premaster_secret, client_random, server_random, b"A"), salted_hash(premaster_secret, client_random, server_random, b"BB"), @@ -241,7 +241,7 @@ fn compute_master_secret(premaster_secret: &[u8], client_random: &[u8], server_r } // According to https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpele/88061224-4a2f-4a28-a52e-e896b75ed2d3 -fn compute_session_key_blob(master_secret: &[u8], client_random: &[u8], server_random: &[u8]) -> Vec { +pub(crate) fn compute_session_key_blob(master_secret: &[u8], client_random: &[u8], server_random: &[u8]) -> Vec { [ salted_hash(master_secret, server_random, client_random, b"A"), salted_hash(master_secret, server_random, client_random, b"BB"), diff --git a/crates/ironrdp-pdu/src/rdp/server_license/client_platform_challenge_response.rs b/crates/ironrdp-pdu/src/rdp/server_license/client_platform_challenge_response.rs index 1e7e039ff..9498e3513 100644 --- a/crates/ironrdp-pdu/src/rdp/server_license/client_platform_challenge_response.rs +++ b/crates/ironrdp-pdu/src/rdp/server_license/client_platform_challenge_response.rs @@ -8,7 +8,6 @@ use ironrdp_core::{ cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, }; -use md5::Digest; use num_derive::{FromPrimitive, ToPrimitive}; use num_traits::{FromPrimitive as _, ToPrimitive as _}; @@ -22,7 +21,7 @@ use crate::crypto::rc4::Rc4; const RESPONSE_DATA_VERSION: u16 = 0x100; const RESPONSE_DATA_STATIC_FIELDS_SIZE: usize = 8; -const CLIENT_HARDWARE_IDENTIFICATION_SIZE: usize = 20; +pub(crate) const CLIENT_HARDWARE_IDENTIFICATION_SIZE: usize = 20; /// [2.2.2.5] Client Platform Challenge Response (CLIENT_PLATFORM_CHALLENGE_RESPONSE) /// @@ -40,7 +39,7 @@ impl ClientPlatformChallengeResponse { pub fn from_server_platform_challenge( platform_challenge: &ServerPlatformChallenge, - hostname: &str, + hardware_data: [u32; 4], encryption_data: &LicenseEncryptionData, ) -> Result { let mut rc4 = Rc4::new(&encryption_data.license_key); @@ -61,12 +60,10 @@ impl ClientPlatformChallengeResponse { challenge_response_data.write_all(&decrypted_challenge)?; let mut hardware_id = Vec::with_capacity(CLIENT_HARDWARE_IDENTIFICATION_SIZE); - let mut md5 = md5::Md5::new(); - md5.update(hostname.as_bytes()); - let hardware_data = &md5.finalize(); - hardware_id.write_u32::(PLATFORM_ID)?; - hardware_id.write_all(hardware_data)?; + for data in hardware_data { + hardware_id.write_u32::(data)?; + } let mut rc4 = Rc4::new(&encryption_data.license_key); let encrypted_hwid = rc4.process(&hardware_id); diff --git a/crates/ironrdp-pdu/src/rdp/server_license/client_platform_challenge_response/test.rs b/crates/ironrdp-pdu/src/rdp/server_license/client_platform_challenge_response/test.rs index 6416a0edc..4b057bea5 100644 --- a/crates/ironrdp-pdu/src/rdp/server_license/client_platform_challenge_response/test.rs +++ b/crates/ironrdp-pdu/src/rdp/server_license/client_platform_challenge_response/test.rs @@ -1,11 +1,10 @@ -use ironrdp_core::{decode, encode_vec}; -use lazy_static::lazy_static; - use super::*; use crate::rdp::server_license::{ BasicSecurityHeader, BasicSecurityHeaderFlags, LicenseHeader, LicensePdu, PreambleFlags, PreambleType, PreambleVersion, BASIC_SECURITY_HEADER_SIZE, PREAMBLE_SIZE, }; +use ironrdp_core::{decode, encode_vec}; +use lazy_static::lazy_static; const PLATFORM_CHALLENGE_RESPONSE_DATA_BUFFER: [u8; 18] = [ 0x00, 0x01, // version @@ -184,13 +183,10 @@ fn challenge_response_creates_from_server_challenge_and_encryption_data_correctl ], }; + let hardware_data = vec![0u8; 16]; let mut hardware_id = Vec::with_capacity(CLIENT_HARDWARE_IDENTIFICATION_SIZE); - let mut md5 = md5::Md5::new(); - md5.update(b"sample-hostname"); - let hardware_data = &md5.finalize(); - hardware_id.write_u32::(PLATFORM_ID).unwrap(); - hardware_id.write_all(hardware_data).unwrap(); + hardware_id.write_all(&hardware_data).unwrap(); let mut rc4 = Rc4::new(&encryption_data.license_key); let encrypted_hwid = rc4.process(&hardware_id); @@ -226,12 +222,9 @@ fn challenge_response_creates_from_server_challenge_and_encryption_data_correctl mac_data, }; - let challenge_response = ClientPlatformChallengeResponse::from_server_platform_challenge( - &server_challenge, - "sample-hostname", - &encryption_data, - ) - .unwrap(); + let challenge_response = + ClientPlatformChallengeResponse::from_server_platform_challenge(&server_challenge, [0u32; 4], &encryption_data) + .unwrap(); assert_eq!(challenge_response, correct_challenge_response); } diff --git a/crates/ironrdp-pdu/src/rdp/server_license/server_upgrade_license.rs b/crates/ironrdp-pdu/src/rdp/server_license/server_upgrade_license.rs index 6751b9129..6504f86d8 100644 --- a/crates/ironrdp-pdu/src/rdp/server_license/server_upgrade_license.rs +++ b/crates/ironrdp-pdu/src/rdp/server_license/server_upgrade_license.rs @@ -14,7 +14,7 @@ use crate::crypto::rc4::Rc4; use crate::utils; use crate::utils::CharacterSet; -const NEW_LICENSE_INFO_STATIC_FIELDS_SIZE: usize = 20; +const LICENSE_INFO_STATIC_FIELDS_SIZE: usize = 20; /// [2.2.2.6] Server Upgrade License (SERVER_UPGRADE_LICENSE) /// @@ -28,8 +28,7 @@ pub struct ServerUpgradeLicense { impl ServerUpgradeLicense { pub fn verify_server_license(&self, encryption_data: &LicenseEncryptionData) -> Result<(), ServerLicenseError> { - let mut rc4 = Rc4::new(encryption_data.license_key.as_slice()); - let decrypted_license_info = rc4.process(self.encrypted_license_info.as_slice()); + let decrypted_license_info = self.decrypted_license_info(encryption_data); let mac_data = super::compute_mac_data(encryption_data.mac_salt_key.as_slice(), decrypted_license_info.as_ref()); @@ -39,6 +38,16 @@ impl ServerUpgradeLicense { Ok(()) } + + pub fn new_license_info(&self, encryption_data: &LicenseEncryptionData) -> DecodeResult { + let data = self.decrypted_license_info(encryption_data); + LicenseInformation::decode(&mut ReadCursor::new(&data)) + } + + fn decrypted_license_info(&self, encryption_data: &LicenseEncryptionData) -> Vec { + let mut rc4 = Rc4::new(encryption_data.license_key.as_slice()); + rc4.process(self.encrypted_license_info.as_slice()) + } } impl ServerUpgradeLicense { @@ -94,8 +103,8 @@ impl ServerUpgradeLicense { } } -#[derive(Debug, PartialEq, Eq)] -pub struct NewLicenseInformation { +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct LicenseInformation { pub version: u32, pub scope: String, pub company_name: String, @@ -103,13 +112,13 @@ pub struct NewLicenseInformation { pub license_info: Vec, } -impl NewLicenseInformation { - const NAME: &'static str = "NewLicenseInformation"; +impl LicenseInformation { + const NAME: &'static str = "LicenseInformation"; - const FIXED_PART_SIZE: usize = NEW_LICENSE_INFO_STATIC_FIELDS_SIZE; + const FIXED_PART_SIZE: usize = LICENSE_INFO_STATIC_FIELDS_SIZE; } -impl Encode for NewLicenseInformation { +impl Encode for LicenseInformation { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { ensure_size!(in: dst, size: self.size()); @@ -151,7 +160,7 @@ impl Encode for NewLicenseInformation { } } -impl<'de> Decode<'de> for NewLicenseInformation { +impl<'de> Decode<'de> for LicenseInformation { fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { ensure_fixed_part_size!(in: src); diff --git a/crates/ironrdp-pdu/src/rdp/server_license/server_upgrade_license/tests.rs b/crates/ironrdp-pdu/src/rdp/server_license/server_upgrade_license/tests.rs index 2a398fef3..6aa58641d 100644 --- a/crates/ironrdp-pdu/src/rdp/server_license/server_upgrade_license/tests.rs +++ b/crates/ironrdp-pdu/src/rdp/server_license/server_upgrade_license/tests.rs @@ -245,7 +245,7 @@ const NEW_LICENSE_INFORMATION_BUFFER: [u8; 2031] = [ ]; lazy_static! { - pub static ref NEW_LICENSE_INFORMATION: NewLicenseInformation = NewLicenseInformation { + pub static ref NEW_LICENSE_INFORMATION: LicenseInformation = LicenseInformation { version: 0x0006_0000, scope: "microsoft.com".to_owned(), company_name: "Microsoft Corporation".to_owned(), diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index f4416c1c0..2105ae67f 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -864,6 +864,8 @@ fn build_config( pointer_software_rendering: false, performance_flags: PerformanceFlags::default(), desktop_scale_factor: 0, + hardware_id: None, + license_cache: None, } } diff --git a/crates/ironrdp/examples/screenshot.rs b/crates/ironrdp/examples/screenshot.rs index c36835091..1e5da29db 100644 --- a/crates/ironrdp/examples/screenshot.rs +++ b/crates/ironrdp/examples/screenshot.rs @@ -20,13 +20,9 @@ #[macro_use] extern crate tracing; -use core::time::Duration; -use std::io::Write as _; -use std::net::TcpStream; -use std::path::PathBuf; - use anyhow::Context as _; use connector::Credentials; +use core::time::Duration; use ironrdp::connector; use ironrdp::connector::ConnectionResult; use ironrdp::pdu::gcc::KeyboardType; @@ -35,6 +31,9 @@ use ironrdp::session::image::DecodedImage; use ironrdp::session::{ActiveStage, ActiveStageOutput}; use ironrdp_pdu::rdp::client_info::PerformanceFlags; use sspi::network_client::reqwest_network_client::ReqwestNetworkClient; +use std::io::Write as _; +use std::net::TcpStream; +use std::path::PathBuf; use tokio_rustls::rustls; const HELP: &str = "\ @@ -212,6 +211,8 @@ fn build_config(username: String, password: String, domain: Option) -> c pointer_software_rendering: true, performance_flags: PerformanceFlags::default(), desktop_scale_factor: 0, + hardware_id: None, + license_cache: None, } } diff --git a/ffi/src/connector/config.rs b/ffi/src/connector/config.rs index d7bc1e7a9..6888339be 100644 --- a/ffi/src/connector/config.rs +++ b/ffi/src/connector/config.rs @@ -196,6 +196,8 @@ pub mod ffi { pointer_software_rendering: self.pointer_software_rendering.unwrap_or(false), performance_flags: self.performance_flags.ok_or("performance flag is missing")?, desktop_scale_factor: 0, + hardware_id: None, + license_cache: None, }; tracing::debug!(config=?inner_config, "Built config"); Ok(Box::new(Config(inner_config)))