diff --git a/DOCKER_README.md b/DOCKER_README.md index 7ac9f5c273..b912f0f3a3 100644 --- a/DOCKER_README.md +++ b/DOCKER_README.md @@ -52,8 +52,9 @@ Create and run the container (substitute your ``): ```bash docker run -d \ + --device /dev/dri/ \ --name= \ - --restart=unless-stopped + --restart=unless-stopped \ -e PUID= \ -e PGID= \ -e TZ= \ @@ -86,6 +87,25 @@ services: - "47998-48000:47998-48000/udp" ``` +### Using podman run +Create and run the container (substitute your ``): + +```bash +podman run -d \ + --device /dev/dri/ \ + --name= \ + --restart=unless-stopped \ + --userns=keep-id \ + -e PUID= \ + -e PGID= \ + -e TZ= \ + -v :/config \ + -p 47984-47990:47984-47990/tcp \ + -p 48010:48010 \ + -p 47998-48000:47998-48000/udp \ + +``` + ### Parameters You must substitute the `` with your own settings. @@ -132,8 +152,9 @@ The architectures supported by these images are shown in the table below. | tag suffix | amd64/x86_64 | arm64/aarch64 | |-----------------|--------------|---------------| | archlinux | ✅ | ❌ | +| debian-bookworm | ✅ | ✅ | | debian-bullseye | ✅ | ✅ | -| fedora-36 | ✅ | ✅ | -| fedora-37 | ✅ | ✅ | +| fedora-38 | ✅ | ✅ | +| fedora-39 | ✅ | ✅ | | ubuntu-20.04 | ✅ | ✅ | | ubuntu-22.04 | ✅ | ✅ | diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst index 4913ca611f..08fb5834ac 100644 --- a/docs/source/about/advanced_usage.rst +++ b/docs/source/about/advanced_usage.rst @@ -838,6 +838,64 @@ keybindings external_ip = 123.456.789.12 +`lan_encryption_mode `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + This determines when encryption will be used when streaming over your local network. + + .. warning:: Encryption can reduce streaming performance, particularly on less powerful hosts and clients. + +**Choices** + +.. table:: + :widths: auto + + ===== =========== + Value Description + ===== =========== + 0 encryption will not be used + 1 encryption will be used if the client supports it + 2 encryption is mandatory and unencrypted connections are rejected + ===== =========== + +**Default** + ``0`` + +**Example** + .. code-block:: text + + lan_encryption_mode = 0 + +`wan_encryption_mode `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + This determines when encryption will be used when streaming over the Internet. + + .. warning:: Encryption can reduce streaming performance, particularly on less powerful hosts and clients. + +**Choices** + +.. table:: + :widths: auto + + ===== =========== + Value Description + ===== =========== + 0 encryption will not be used + 1 encryption will be used if the client supports it + 2 encryption is mandatory and unencrypted connections are rejected + ===== =========== + +**Default** + ``1`` + +**Example** + .. code-block:: text + + wan_encryption_mode = 1 + `ping_timeout `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/config.cpp b/src/config.cpp index cde646a771..eb25f58a7d 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -376,7 +376,10 @@ namespace config { APPS_JSON_PATH, 20, // fecPercentage - 1 // channels + 1, // channels + + ENCRYPTION_MODE_NEVER, // lan_encryption_mode + ENCRYPTION_MODE_OPPORTUNISTIC, // wan_encryption_mode }; nvhttp_t nvhttp { @@ -1016,6 +1019,9 @@ namespace config { int_between_f(vars, "channels", stream.channels, { 1, std::numeric_limits::max() }); + int_between_f(vars, "lan_encryption_mode", stream.lan_encryption_mode, { 0, 2 }); + int_between_f(vars, "wan_encryption_mode", stream.wan_encryption_mode, { 0, 2 }); + path_f(vars, "file_apps", stream.file_apps); int_between_f(vars, "fec_percentage", stream.fec_percentage, { 1, 255 }); diff --git a/src/config.h b/src/config.h index 44b8997462..ba0ee8a37f 100644 --- a/src/config.h +++ b/src/config.h @@ -76,6 +76,10 @@ namespace config { bool install_steam_drivers; }; + constexpr int ENCRYPTION_MODE_NEVER = 0; // Never use video encryption, even if the client supports it + constexpr int ENCRYPTION_MODE_OPPORTUNISTIC = 1; // Use video encryption if available, but stream without it if not supported + constexpr int ENCRYPTION_MODE_MANDATORY = 2; // Always use video encryption and refuse clients that can't encrypt + struct stream_t { std::chrono::milliseconds ping_timeout; @@ -85,6 +89,10 @@ namespace config { // max unique instances of video and audio streams int channels; + + // Video encryption settings for LAN and WAN streams + int lan_encryption_mode; + int wan_encryption_mode; }; struct nvhttp_t { diff --git a/src/crypto.cpp b/src/crypto.cpp index 26af3e7df4..e92e6e9e7d 100644 --- a/src/crypto.cpp +++ b/src/crypto.cpp @@ -152,10 +152,11 @@ namespace crypto { auto cipher = tagged_cipher.substr(tag_size); auto tag = tagged_cipher.substr(0, tag_size); - plaintext.resize((cipher.size() + 15) / 16 * 16); + plaintext.resize(round_to_pkcs7_padded(cipher.size())); - int size; - if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &size, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) { + int update_outlen, final_outlen; + + if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &update_outlen, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) { return -1; } @@ -163,12 +164,11 @@ namespace crypto { return -1; } - int len = size; - if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + size, &len) != 1) { + if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + update_outlen, &final_outlen) != 1) { return -1; } - plaintext.resize(size + len); + plaintext.resize(update_outlen + final_outlen); return 0; } @@ -187,16 +187,15 @@ namespace crypto { auto tag = tagged_cipher; auto cipher = tag + tag_size; - int len; - int size = round_to_pkcs7_padded(plaintext.size()); + int update_outlen, final_outlen; // Encrypt into the caller's buffer - if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &size, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { + if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &update_outlen, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { return -1; } // GCM encryption won't ever fill ciphertext here but we have to call it anyway - if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + size, &len) != 1) { + if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + update_outlen, &final_outlen) != 1) { return -1; } @@ -204,13 +203,11 @@ namespace crypto { return -1; } - return len + size; + return update_outlen + final_outlen; } int ecb_t::decrypt(const std::string_view &cipher, std::vector &plaintext) { - int len; - auto fg = util::fail_guard([this]() { EVP_CIPHER_CTX_reset(decrypt_ctx.get()); }); @@ -221,19 +218,19 @@ namespace crypto { } EVP_CIPHER_CTX_set_padding(decrypt_ctx.get(), padding); + plaintext.resize(round_to_pkcs7_padded(cipher.size())); + + int update_outlen, final_outlen; - plaintext.resize((cipher.size() + 15) / 16 * 16); - auto size = (int) plaintext.size(); - // Decrypt into the caller's buffer, leaving room for the auth tag to be prepended - if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &size, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) { + if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &update_outlen, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) { return -1; } - if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data(), &len) != 1) { + if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + update_outlen, &final_outlen) != 1) { return -1; } - plaintext.resize(len + size); + plaintext.resize(update_outlen + final_outlen); return 0; } @@ -249,22 +246,20 @@ namespace crypto { } EVP_CIPHER_CTX_set_padding(encrypt_ctx.get(), padding); + cipher.resize(round_to_pkcs7_padded(plaintext.size())); - int len; - - cipher.resize((plaintext.size() + 15) / 16 * 16); - auto size = (int) cipher.size(); + int update_outlen, final_outlen; // Encrypt into the caller's buffer - if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher.data(), &size, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { + if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher.data(), &update_outlen, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { return -1; } - if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher.data() + size, &len) != 1) { + if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher.data() + update_outlen, &final_outlen) != 1) { return -1; } - cipher.resize(len + size); + cipher.resize(update_outlen + final_outlen); return 0; } @@ -280,20 +275,18 @@ namespace crypto { return false; } - int len; - - int size = plaintext.size(); // round_to_pkcs7_padded(plaintext.size()); + int update_outlen, final_outlen; // Encrypt into the caller's buffer - if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &size, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { + if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &update_outlen, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { return -1; } - if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + size, &len) != 1) { + if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + update_outlen, &final_outlen) != 1) { return -1; } - return size + len; + return update_outlen + final_outlen; } ecb_t::ecb_t(const aes_t &key, bool padding): @@ -309,7 +302,7 @@ namespace crypto { aes_t gen_aes_key(const std::array &salt, const std::string_view &pin) { - aes_t key; + aes_t key(16); std::string salt_pin; salt_pin.reserve(salt.size() + pin.size()); diff --git a/src/crypto.h b/src/crypto.h index b75d013ca1..eb355f576c 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -23,7 +23,7 @@ namespace crypto { using sha256_t = std::array; - using aes_t = std::array; + using aes_t = std::vector; using x509_t = util::safe_ptr; using x509_store_t = util::safe_ptr; using x509_store_ctx_t = util::safe_ptr; diff --git a/src/network.cpp b/src/network.cpp index c843cfc055..5778c8db9a 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -44,7 +44,7 @@ namespace net { net_e from_address(const std::string_view &view) { - auto addr = ip::make_address(view); + auto addr = normalize_address(ip::make_address(view)); if (addr.is_v6()) { for (auto &range : pc_ips_v6) { diff --git a/src/network.h b/src/network.h index b54f63ce7e..57bc65e8fc 100644 --- a/src/network.h +++ b/src/network.h @@ -58,6 +58,15 @@ namespace net { std::string_view af_to_any_address_string(af_e af); + /** + * @brief Converts an address to a normalized form. + * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses. + * @param address The address to normalize. + * @return Normalized address. + */ + boost::asio::ip::address + normalize_address(boost::asio::ip::address address); + /** * @brief Returns the given address in normalized string form. * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses. diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 93f72edbdf..120045439d 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -299,8 +299,10 @@ namespace nvhttp { make_launch_session(bool host_audio, const args_t &args) { rtsp_stream::launch_session_t launch_session; + auto rikey = util::from_hex_vec(get_arg(args, "rikey"), true); + std::copy(rikey.cbegin(), rikey.cend(), std::back_inserter(launch_session.gcm_key)); + launch_session.host_audio = host_audio; - launch_session.gcm_key = util::from_hex(get_arg(args, "rikey"), true); std::stringstream mode = std::stringstream(get_arg(args, "mode", "0x0x0")); // Split mode by the char "x", to populate width/height/fps int x = 0; @@ -324,11 +326,10 @@ namespace nvhttp { launch_session.av_ping_payload = util::hex_vec(raw_payload); RAND_bytes((unsigned char *) &launch_session.control_connect_data, sizeof(launch_session.control_connect_data)); + launch_session.iv.resize(16); uint32_t prepend_iv = util::endian::big(util::from_view(get_arg(args, "rikeyid"))); auto prepend_iv_p = (uint8_t *) &prepend_iv; - - auto next = std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session.iv)); - std::fill(next, std::end(launch_session.iv), 0); + std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session.iv)); return launch_session; } diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index 543d6c38f2..27697c2dc9 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -99,12 +99,22 @@ namespace platf { fs::path appdata() { - const char *homedir; - if ((homedir = getenv("HOME")) == nullptr) { - homedir = getpwuid(geteuid())->pw_dir; + const char *dir; + + // May be set if running under a systemd service with the ConfigurationDirectory= option set. + if ((dir = getenv("CONFIGURATION_DIRECTORY")) != nullptr) { + return fs::path { dir } / "sunshine"sv; + } + // Otherwise, follow the XDG base directory specification: + // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + if ((dir = getenv("XDG_CONFIG_HOME")) != nullptr) { + return fs::path { dir } / "sunshine"sv; + } + if ((dir = getenv("HOME")) == nullptr) { + dir = getpwuid(geteuid())->pw_dir; } - return fs::path { homedir } / ".config/sunshine"sv; + return fs::path { dir } / ".config/sunshine"sv; } std::string diff --git a/src/rtsp.cpp b/src/rtsp.cpp index 21a98745e5..e92e177ac2 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -5,6 +5,7 @@ #define BOOST_BIND_GLOBAL_PLACEHOLDERS extern "C" { +#include #include } @@ -517,14 +518,42 @@ namespace rtsp_stream { std::stringstream ss; // Tell the client about our supported features - ss << "a=x-ss-general.featureFlags: " << (uint32_t) platf::get_capabilities() << std::endl; + ss << "a=x-ss-general.featureFlags:" << (uint32_t) platf::get_capabilities() << std::endl; - if (video::active_hevc_mode != 1) { - ss << "sprop-parameter-sets=AAAAAU"sv << std::endl; + // Always request new control stream encryption if the client supports it + uint32_t encryption_flags_supported = SS_ENC_CONTROL_V2 | SS_ENC_AUDIO; + uint32_t encryption_flags_requested = SS_ENC_CONTROL_V2; + + // Determine the encryption desired for this remote endpoint + auto nettype = net::from_address(sock.remote_endpoint().address().to_string()); + int encryption_mode; + if (nettype == net::net_e::PC || nettype == net::net_e::LAN) { + encryption_mode = config::stream.lan_encryption_mode; } + else { + encryption_mode = config::stream.wan_encryption_mode; + } + if (encryption_mode != config::ENCRYPTION_MODE_NEVER) { + // Advertise support for video encryption if it's not disabled + encryption_flags_supported |= SS_ENC_VIDEO; + + // If it's mandatory, also request it to enable use if the client + // didn't explicitly opt in, but it otherwise has support. + if (encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { + encryption_flags_requested |= SS_ENC_VIDEO | SS_ENC_AUDIO; + } + } + + // Report supported and required encryption flags + ss << "a=x-ss-general.encryptionSupported:" << encryption_flags_supported << std::endl; + ss << "a=x-ss-general.encryptionRequested:" << encryption_flags_requested << std::endl; if (video::last_encoder_probe_supported_ref_frames_invalidation) { - ss << "x-nv-video[0].refPicInvalidation=1"sv << std::endl; + ss << "a=x-nv-video[0].refPicInvalidation:1"sv << std::endl; + } + + if (video::active_hevc_mode != 1) { + ss << "sprop-parameter-sets=AAAAAU"sv << std::endl; } if (video::active_av1_mode != 1) { @@ -705,6 +734,7 @@ namespace rtsp_stream { args.try_emplace("x-nv-vqos[0].qosTrafficType"sv, "5"sv); args.try_emplace("x-nv-aqos.qosTrafficType"sv, "4"sv); args.try_emplace("x-ml-video.configuredBitrateKbps"sv, "0"sv); + args.try_emplace("x-ss-general.encryptionEnabled"sv, "0"sv); stream::config_t config; @@ -721,10 +751,15 @@ namespace rtsp_stream { config.controlProtocolType = util::from_view(args.at("x-nv-general.useReliableUdp"sv)); config.packetsize = util::from_view(args.at("x-nv-video[0].packetSize"sv)); config.minRequiredFecPackets = util::from_view(args.at("x-nv-vqos[0].fec.minRequiredFecPackets"sv)); - config.nvFeatureFlags = util::from_view(args.at("x-nv-general.featureFlags"sv)); config.mlFeatureFlags = util::from_view(args.at("x-ml-general.featureFlags"sv)); config.audioQosType = util::from_view(args.at("x-nv-aqos.qosTrafficType"sv)); config.videoQosType = util::from_view(args.at("x-nv-vqos[0].qosTrafficType"sv)); + config.encryptionFlagsEnabled = util::from_view(args.at("x-ss-general.encryptionEnabled"sv)); + + // Legacy clients use nvFeatureFlags to indicate support for audio encryption + if (util::from_view(args.at("x-nv-general.featureFlags"sv)) & 0x20) { + config.encryptionFlagsEnabled |= SS_ENC_AUDIO; + } config.monitor.height = util::from_view(args.at("x-nv-video[0].clientViewportHt"sv)); config.monitor.width = util::from_view(args.at("x-nv-video[0].clientViewportWd"sv)); @@ -796,7 +831,24 @@ namespace rtsp_stream { return; } - auto session = stream::session::alloc(config, launch_session->gcm_key, launch_session->iv, launch_session->av_ping_payload, launch_session->control_connect_data); + // Check that any required encryption is enabled + auto nettype = net::from_address(sock.remote_endpoint().address().to_string()); + int encryption_mode; + if (nettype == net::net_e::PC || nettype == net::net_e::LAN) { + encryption_mode = config::stream.lan_encryption_mode; + } + else { + encryption_mode = config::stream.wan_encryption_mode; + } + if (encryption_mode == config::ENCRYPTION_MODE_MANDATORY && + (config.encryptionFlagsEnabled & (SS_ENC_VIDEO | SS_ENC_AUDIO)) != (SS_ENC_VIDEO | SS_ENC_AUDIO)) { + BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv; + + respond(sock, &option, 403, "Forbidden", req->sequenceNumber, {}); + return; + } + + auto session = stream::session::alloc(config, *launch_session); auto slot = server->accept(session); if (!slot) { diff --git a/src/stream.cpp b/src/stream.cpp index 54bed0ab17..2e0c0f521a 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -121,6 +121,17 @@ namespace stream { NV_VIDEO_PACKET packet; }; + struct video_packet_enc_prefix_t { + video_packet_raw_t * + payload() { + return (video_packet_raw_t *) (this + 1); + } + + std::uint8_t iv[12]; // 12-byte IV is ideal for AES-GCM + std::uint32_t unused; + std::uint8_t tag[16]; + }; + struct audio_packet_raw_t { uint8_t * payload() { @@ -235,14 +246,14 @@ namespace stream { // return bytes written on success // return -1 on error static inline int - encode_audio(int featureSet, const audio::buffer_t &plaintext, audio_packet_t &destination, std::uint32_t avRiKeyIv, crypto::cipher::cbc_t &cbc) { + encode_audio(bool encrypted, const audio::buffer_t &plaintext, audio_packet_t &destination, std::uint32_t avRiKeyIv, crypto::cipher::cbc_t &cbc) { // If encryption isn't enabled - if (!(featureSet & 0x20)) { + if (!encrypted) { std::copy(std::begin(plaintext), std::end(plaintext), destination->payload()); return plaintext.size(); } - crypto::aes_t iv {}; + crypto::aes_t iv(16); *(std::uint32_t *) iv.data() = util::endian::big(avRiKeyIv + destination->rtp.sequenceNumber); return cbc.encrypt(std::string_view { (char *) std::begin(plaintext), plaintext.size() }, destination->payload(), &iv); @@ -278,8 +289,15 @@ namespace stream { void iterate(std::chrono::milliseconds timeout); + /** + * @brief Calls the handler for a given control stream message. + * @param type The message type. + * @param session The session the message was received on. + * @param payload The payload of the message. + * @param reinjected `true` if this message is being reprocessed after decryption. + */ void - call(std::uint16_t type, session_t *session, const std::string_view &payload); + call(std::uint16_t type, session_t *session, const std::string_view &payload, bool reinjected); void map(uint16_t type, std::function cb) { @@ -354,6 +372,9 @@ namespace stream { int lowseq; udp::endpoint peer; + std::optional cipher; + std::uint64_t gcm_iv_counter; + safe::mail_raw_t::event_t idr_events; safe::mail_raw_t::event_t> invalidate_ref_frames_events; @@ -379,13 +400,13 @@ namespace stream { struct { crypto::cipher::gcm_t cipher; - crypto::aes_t iv; + crypto::aes_t legacy_input_enc_iv; // Only used when the client doesn't support full control stream encryption - uint32_t connect_data; // Used for new clients with ML_FF_SESSION_ID_V1 + std::uint32_t connect_data; // Used for new clients with ML_FF_SESSION_ID_V1 std::string expected_peer_address; // Only used for legacy clients without ML_FF_SESSION_ID_V1 net::peer_t peer; - std::uint8_t seq; + std::uint32_t seq; platf::feedback_queue_t feedback_queue; safe::mail_raw_t::event_t hdr_queue; @@ -414,9 +435,29 @@ namespace stream { return plaintext; } - crypto::aes_t iv {}; auto seq = session->control.seq++; - iv[0] = seq; + + crypto::aes_t iv; + if (session->config.encryptionFlagsEnabled & SS_ENC_CONTROL_V2) { + // We use the deterministic IV construction algorithm specified in NIST SP 800-38D + // Section 8.2.1. The sequence number is our "invocation" field and the 'CH' in the + // high bytes is the "fixed" field. Because each client provides their own unique + // key, our values in the fixed field need only uniquely identify each independent + // use of the client's key with AES-GCM in our code. + // + // The sequence number is 32 bits long which allows for 2^32 control stream messages + // to be sent to each client before the IV repeats. + iv.resize(12); + std::copy_n((uint8_t *) &seq, sizeof(seq), std::begin(iv)); + iv[10] = 'H'; // Host originated + iv[11] = 'C'; // Control stream + } + else { + // Nvidia's old style encryption uses a 16-byte IV + iv.resize(16); + + iv[0] = (std::uint8_t) seq; + } auto packet = (control_encrypted_p) tagged_cipher.data(); @@ -503,8 +544,21 @@ namespace stream { return nullptr; } + /** + * @brief Calls the handler for a given control stream message. + * @param type The message type. + * @param session The session the message was received on. + * @param payload The payload of the message. + * @param reinjected `true` if this message is being reprocessed after decryption. + */ void - control_server_t::call(std::uint16_t type, session_t *session, const std::string_view &payload) { + control_server_t::call(std::uint16_t type, session_t *session, const std::string_view &payload, bool reinjected) { + // If we are using the encrypted control stream protocol, drop any messages that come off the wire unencrypted + if (session->config.controlProtocolType == 13 && !reinjected && type != packetTypes[IDX_ENCRYPTED]) { + BOOST_LOG(error) << "Dropping unencrypted message on encrypted control stream: "sv << util::hex(type).to_string_view(); + return; + } + auto cb = _map_type_cb.find(type); if (cb == std::end(_map_type_cb)) { BOOST_LOG(debug) @@ -541,7 +595,7 @@ namespace stream { auto type = *(std::uint16_t *) packet->data; std::string_view payload { (char *) packet->data + sizeof(type), packet->dataLength - sizeof(type) }; - call(type, session, payload); + call(type, session, payload, false); } break; case ENET_EVENT_TYPE_CONNECT: BOOST_LOG(info) << "CLIENT CONNECTED"sv; @@ -568,16 +622,17 @@ namespace stream { size_t percentage; size_t blocksize; + size_t prefixsize; util::buffer_t shards; char * data(size_t el) { - return &shards[el * blocksize]; + return &shards[(el + 1) * prefixsize + el * blocksize]; } - std::string_view - operator[](size_t el) const { - return { &shards[el * blocksize], blocksize }; + char * + prefix(size_t el) { + return &shards[el * (prefixsize + blocksize)]; } size_t @@ -587,7 +642,7 @@ namespace stream { }; static fec_t - encode(const std::string_view &payload, size_t blocksize, size_t fecpercentage, size_t minparityshards) { + encode(const std::string_view &payload, size_t blocksize, size_t fecpercentage, size_t minparityshards, size_t prefixsize) { auto payload_size = payload.size(); auto pad = payload_size % blocksize != 0; @@ -614,15 +669,21 @@ namespace stream { fecpercentage = 0; } - util::buffer_t shards { nr_shards * blocksize }; + util::buffer_t shards { nr_shards * (blocksize + prefixsize) }; util::buffer_t shards_p { nr_shards }; - // copy payload + padding - auto next = std::copy(std::begin(payload), std::end(payload), std::begin(shards)); - std::fill(next, std::end(shards), 0); // padding with zero - + auto next = std::begin(payload); for (auto x = 0; x < nr_shards; ++x) { - shards_p[x] = (uint8_t *) &shards[x * blocksize]; + shards_p[x] = (uint8_t *) &shards[(x + 1) * prefixsize + x * blocksize]; + + auto copy_len = std::min(blocksize, std::end(payload) - next); + std::copy_n(next, copy_len, shards_p[x]); + if (copy_len < blocksize) { + // Zero any additional space after the end of the payload + std::fill_n(shards_p[x] + copy_len, blocksize - copy_len, 0); + } + + next += copy_len; } if (data_shards + parity_shards <= DATA_SHARDS_MAX) { @@ -637,6 +698,7 @@ namespace stream { nr_shards, fecpercentage, blocksize, + prefixsize, std::move(shards) }; } @@ -881,7 +943,7 @@ namespace stream { std::vector plaintext; auto &cipher = session->control.cipher; - auto &iv = session->control.iv; + auto &iv = session->control.legacy_input_enc_iv; if (cipher.decrypt(tagged_cipher, plaintext, &iv)) { // something went wrong :( @@ -891,7 +953,7 @@ namespace stream { return; } - if (tagged_cipher_length >= 16 + sizeof(crypto::aes_t)) { + if (tagged_cipher_length >= 16 + iv.size()) { std::copy(payload.end() - 16, payload.end(), std::begin(iv)); } @@ -915,11 +977,27 @@ namespace stream { std::string_view tagged_cipher { (char *) header->payload(), (size_t) tagged_cipher_length }; auto &cipher = session->control.cipher; - crypto::aes_t iv {}; - iv[0] = (std::uint8_t) seq; + crypto::aes_t iv; + if (session->config.encryptionFlagsEnabled & SS_ENC_CONTROL_V2) { + // We use the deterministic IV construction algorithm specified in NIST SP 800-38D + // Section 8.2.1. The sequence number is our "invocation" field and the 'CC' in the + // high bytes is the "fixed" field. Because each client provides their own unique + // key, our values in the fixed field need only uniquely identify each independent + // use of the client's key with AES-GCM in our code. + // + // The sequence number is 32 bits long which allows for 2^32 control stream messages + // to be received from each client before the IV repeats. + iv.resize(12); + std::copy_n((uint8_t *) &seq, sizeof(seq), std::begin(iv)); + iv[10] = 'C'; // Client originated + iv[11] = 'C'; // Control stream + } + else { + // Nvidia's old style encryption uses a 16-byte IV + iv.resize(16); - // update control sequence - ++session->control.seq; + iv[0] = (std::uint8_t) seq; + } std::vector plaintext; if (cipher.decrypt(tagged_cipher, plaintext, &iv)) { @@ -946,7 +1024,7 @@ namespace stream { input::passthrough(session->input, std::move(plaintext)); } else { - server->call(type, session, next_payload); + server->call(type, session, next_payload, true); } }); @@ -1301,7 +1379,9 @@ namespace stream { } } - auto shards = fec::encode(current_payload, blocksize, fecPercentage, session->config.minRequiredFecPackets); + // If video encryption is enabled, we allocate space for the encryption header before each shard + auto shards = fec::encode(current_payload, blocksize, fecPercentage, session->config.minRequiredFecPackets, + session->video.cipher ? sizeof(video_packet_enc_prefix_t) : 0); // set FEC info now that we know for sure what our percentage will be for this frame for (auto x = 0; x < shards.size(); ++x) { @@ -1322,12 +1402,34 @@ namespace stream { inspect->packet.multiFecBlocks = (blockIndex << 4) | lastBlockIndex; inspect->packet.frameIndex = packet->frame_index(); + + // Encrypt this shard if video encryption is enabled + if (session->video.cipher) { + // We use the deterministic IV construction algorithm specified in NIST SP 800-38D + // Section 8.2.1. The sequence number is our "invocation" field and the 'V' in the + // high bytes is the "fixed" field. Because each client provides their own unique + // key, our values in the fixed field need only uniquely identify each independent + // use of the client's key with AES-GCM in our code. + // + // The IV counter is 64 bits long which allows for 2^64 encrypted video packets + // to be sent to each client before the IV repeats. + crypto::aes_t iv(12); + std::copy_n((uint8_t *) &session->video.gcm_iv_counter, sizeof(session->video.gcm_iv_counter), std::begin(iv)); + iv[11] = 'V'; // Video stream + session->video.gcm_iv_counter++; + + // Encrypt the target buffer in place + auto *prefix = (video_packet_enc_prefix_t *) shards.prefix(x); + prefix->unused = 0; + std::copy(std::begin(iv), std::end(iv), prefix->iv); + session->video.cipher->encrypt(std::string_view { (char *) inspect, (size_t) blocksize }, prefix->tag, &iv); + } } auto peer_address = session->video.peer.address(); auto batch_info = platf::batched_send_info_t { shards.shards.begin(), - shards.blocksize, + shards.prefixsize + shards.blocksize, shards.nr_shards, (uintptr_t) sock.native_handle(), peer_address, @@ -1341,8 +1443,8 @@ namespace stream { BOOST_LOG(verbose) << "Falling back to unbatched send"sv; for (auto x = 0; x < shards.size(); ++x) { auto send_info = platf::send_info_t { - shards[x].data(), - shards[x].size(), + shards.prefix(x), + shards.prefixsize + shards.blocksize, (uintptr_t) sock.native_handle(), peer_address, session->video.peer.port(), @@ -1415,7 +1517,7 @@ namespace stream { // For now, encode_audio needs it to be the proper sequenceNumber audio_packet->rtp.sequenceNumber = sequenceNumber; - auto bytes = encode_audio(session->config.nvFeatureFlags, packet_data, audio_packet, session->audio.avRiKeyId, session->audio.cipher); + auto bytes = encode_audio(session->config.encryptionFlagsEnabled & SS_ENC_AUDIO, packet_data, audio_packet, session->audio.avRiKeyId, session->audio.cipher); if (bytes < 0) { BOOST_LOG(error) << "Couldn't encode audio packet"sv; break; @@ -1779,7 +1881,7 @@ namespace stream { } std::shared_ptr - alloc(config_t &config, crypto::aes_t &gcm_key, crypto::aes_t &iv, std::string_view av_ping_payload, uint32_t control_connect_data) { + alloc(config_t &config, rtsp_stream::launch_session_t &launch_session) { auto session = std::make_shared(); auto mail = std::make_shared(); @@ -1788,18 +1890,25 @@ namespace stream { session->config = config; - session->control.connect_data = control_connect_data; + session->control.connect_data = launch_session.control_connect_data; session->control.feedback_queue = mail->queue(mail::gamepad_feedback); session->control.hdr_queue = mail->event(mail::hdr); - session->control.iv = iv; + session->control.legacy_input_enc_iv = launch_session.iv; session->control.cipher = crypto::cipher::gcm_t { - gcm_key, false + launch_session.gcm_key, false }; session->video.idr_events = mail->event(mail::idr); session->video.invalidate_ref_frames_events = mail->event>(mail::invalidate_ref_frames); session->video.lowseq = 0; - session->video.ping_payload = av_ping_payload; + session->video.ping_payload = launch_session.av_ping_payload; + if (config.encryptionFlagsEnabled & SS_ENC_VIDEO) { + BOOST_LOG(info) << "Video encryption enabled"sv; + session->video.cipher = crypto::cipher::gcm_t { + launch_session.gcm_key, false + }; + session->video.gcm_iv_counter = 0; + } constexpr auto max_block_size = crypto::cipher::round_to_pkcs7_padded(2048); @@ -1826,11 +1935,11 @@ namespace stream { session->audio.fec_packet->fecHeader.ssrc = 0; session->audio.cipher = crypto::cipher::cbc_t { - gcm_key, true + launch_session.gcm_key, true }; - session->audio.ping_payload = av_ping_payload; - session->audio.avRiKeyId = util::endian::big(*(std::uint32_t *) iv.data()); + session->audio.ping_payload = launch_session.av_ping_payload; + session->audio.avRiKeyId = util::endian::big(*(std::uint32_t *) launch_session.iv.data()); session->audio.sequenceNumber = 0; session->audio.timestamp = 0; diff --git a/src/stream.h b/src/stream.h index ca87168ef2..1e2dcd4ea5 100644 --- a/src/stream.h +++ b/src/stream.h @@ -22,12 +22,13 @@ namespace stream { int packetsize; int minRequiredFecPackets; - int nvFeatureFlags; int mlFeatureFlags; int controlProtocolType; int audioQosType; int videoQosType; + uint32_t encryptionFlagsEnabled; + std::optional gcmap; }; @@ -40,7 +41,7 @@ namespace stream { }; std::shared_ptr - alloc(config_t &config, crypto::aes_t &gcm_key, crypto::aes_t &iv, std::string_view av_ping_payload, uint32_t control_connect_data); + alloc(config_t &config, rtsp_stream::launch_session_t &launch_session); int start(session_t &session, const std::string &addr_string); void diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 6046d6d426..71e01ae2e0 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -668,6 +668,46 @@

+ +
+ + +
+ This determines when encryption will be used when streaming over your local network.
+ Encryption can reduce streaming performance, particularly on less powerful hosts and clients. +
+
+ + +
+ + +
+ This determines when encryption will be used when streaming over the Internet.
+ Encryption can reduce streaming performance, particularly on less powerful hosts and clients. +
+
+
@@ -1155,6 +1195,8 @@

"origin_web_ui_allowed": "lan", "upnp": "disabled", "external_ip": "", + "lan_encryption_mode": 0, + "wan_encryption_mode": 1, "ping_timeout": 10000, }, },