diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst index 4913ca611f8..08fb5834ace 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 cde646a7718..eb25f58a7da 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 44b89974627..ba0ee8a37fd 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/rtsp.cpp b/src/rtsp.cpp index 14123b3df9f..e92e177ac2c 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -524,6 +524,26 @@ namespace rtsp_stream { 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; @@ -811,6 +831,23 @@ namespace rtsp_stream { return; } + // 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); diff --git a/src/stream.cpp b/src/stream.cpp index e57d5996456..a3ea360057d 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() { @@ -354,6 +365,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; @@ -588,16 +602,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 @@ -607,7 +622,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; @@ -634,15 +649,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) { @@ -657,6 +678,7 @@ namespace stream { nr_shards, fecpercentage, blocksize, + prefixsize, std::move(shards) }; } @@ -1337,7 +1359,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) { @@ -1358,12 +1382,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, @@ -1377,8 +1423,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(), @@ -1836,6 +1882,13 @@ namespace stream { session->video.invalidate_ref_frames_events = mail->event>(mail::invalidate_ref_frames); session->video.lowseq = 0; 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); diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 6046d6d4266..71e01ae2e0c 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, }, },