plaintext.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. #include <Windows.h>
  2. #include <array>
  3. #include <cstdio>
  4. #include "../../core/logging/log.h"
  5. #include "../../middleware/secure_channel/runtime.h"
  6. #include "../../state/matchmaking/matchmaking_state.h"
  7. #include "../../state/runtime/runtime.h"
  8. #include "encrypted/internal.h"
  9. #include "internal.h"
  10. namespace sunrise::server::bap::plaintext {
  11. namespace {
  12. /** Protobuf field tag for the length-delimited session token. */
  13. constexpr std::byte kSessionTokenTag{0x0A};
  14. /** Protobuf length byte for the 32-byte session token. */
  15. constexpr std::byte kSessionTokenSize{0x20};
  16. /** Protobuf field tag for the varint protocol version. */
  17. constexpr std::byte kProtocolVersionTag{0x10};
  18. /** Supported server-hello protocol version. */
  19. constexpr std::byte kProtocolVersion{0x01};
  20. /** Fixed protobuf offsets derived from the two one-byte field headers. */
  21. constexpr std::size_t kTokenOffset = 2;
  22. constexpr std::size_t kProtocolTagOffset = kTokenOffset + state::kSessionTokenSize;
  23. constexpr std::size_t kProtocolVersionOffset = kProtocolTagOffset + 1;
  24. constexpr std::size_t kServerHelloRequestSize = kProtocolVersionOffset + 1;
  25. /** Protocol direction bit applied to the final receive-nonce byte. */
  26. constexpr std::byte kReceiveDirectionMask{0x01};
  27. /**
  28. * Logs one refused plaintext request and names the check that refused it.
  29. * A refused server hello leaves its link stuck in `_authenticating` until the activity setup
  30. * times out. That timeout names no service, so this line is the only record of it.
  31. * @param session Session the request arrived on, owned by the connection.
  32. * @param service Numeric request service.
  33. * @param reason Short name of the check that refused.
  34. */
  35. void report_refusal(const Session& session, std::uint16_t service, const char* reason) noexcept {
  36. std::array<char, core::log::kLineCapacity> line{};
  37. const int written = std::snprintf(line.data(),
  38. line.size(),
  39. "ev=bap svc=%u stage=plaintext result=fail conn=%u "
  40. "reason=%s",
  41. static_cast<unsigned>(service),
  42. session.id,
  43. reason);
  44. if (written > 0) {
  45. core::log::write(core::log::Channel::server,
  46. core::log::Level::warn,
  47. {line.data(), static_cast<std::size_t>(written)});
  48. }
  49. }
  50. /**
  51. * Logs a server-hello body that does not match the primary link's documented framing.
  52. * Only the framing bytes are named. The token they wrap is a secret and never reaches the log.
  53. * @param session Session the hello arrived on, owned by the connection.
  54. * @param body Plaintext service body.
  55. */
  56. void report_hello_shape(const Session& session, std::span<const std::byte> body) noexcept {
  57. const auto at = [body](std::size_t index) noexcept {
  58. return index < body.size() ? std::to_integer<unsigned>(body[index]) : 0U;
  59. };
  60. std::array<char, core::log::kLineCapacity> line{};
  61. const int written = std::snprintf(line.data(),
  62. line.size(),
  63. "ev=bap svc=25 stage=plaintext result=shape_accepted "
  64. "conn=%u len=%zu b0=%02X b1=%02X b34=%02X b35=%02X",
  65. session.id,
  66. body.size(),
  67. at(0),
  68. at(1),
  69. at(kProtocolTagOffset),
  70. at(kProtocolVersionOffset));
  71. if (written > 0) {
  72. core::log::write(core::log::Channel::server,
  73. core::log::Level::warn,
  74. {line.data(), static_cast<std::size_t>(written)});
  75. }
  76. }
  77. /**
  78. * Checks the server-hello framing, which is fixed and carries no secret.
  79. * @param body Plaintext service body.
  80. * @return True when the length, both field tags and the protocol version all match.
  81. */
  82. [[nodiscard]] bool valid_hello_shape(std::span<const std::byte> body) noexcept {
  83. return body.size() == kServerHelloRequestSize && body[0] == kSessionTokenTag
  84. && body[1] == kSessionTokenSize && body[kProtocolTagOffset] == kProtocolVersionTag
  85. && body[kProtocolVersionOffset] == kProtocolVersion;
  86. }
  87. /**
  88. * Compares the echoed session token against the one SignOn issued.
  89. * @param body Plaintext service body whose framing is already checked.
  90. * @param signOn Active SignOn token state.
  91. * @return True when every token byte matches.
  92. */
  93. [[nodiscard]] bool hello_token_matches(std::span<const std::byte> body,
  94. const state::SignOnState& signOn) noexcept {
  95. for (std::size_t index = 0; index < signOn.sessionToken.size(); ++index) {
  96. if (body[kTokenOffset + index] != signOn.sessionToken[index]) {
  97. return false;
  98. }
  99. }
  100. return true;
  101. }
  102. /**
  103. * Copies process BAP keys into one authenticated connection session.
  104. * @param session Crypto state owned by the connection.
  105. * @param bap Process BAP key and nonce material.
  106. */
  107. void arm_encryption(Session& session, const state::BapState& bap) noexcept {
  108. session.sendNonce = bap.nonce;
  109. session.receiveNonce = bap.nonce;
  110. session.receiveNonce.back() ^= kReceiveDirectionMask;
  111. session.authenticated = true;
  112. }
  113. /**
  114. * Answers one plaintext request through the service table the encrypted path uses.
  115. * The route decides the reply just as it does for a decrypted frame, and one owing no response
  116. * writes nothing. Body side effects are not staged. The transports carrying them are encrypted.
  117. * @param session Queuez, activity and matchmaking bindings owned by the connection.
  118. * @param scratch Transform buffers owned by the lock, kept off the Client thread stack.
  119. * @param request Parsed plaintext request header and borrowed body.
  120. * @param response Whole-frame storage owned by the caller.
  121. * @param written Gets the encoded response size in bytes.
  122. * @return True when the route owes nothing, or its whole response is encoded.
  123. */
  124. [[nodiscard]] bool consume_service(Session& session,
  125. Scratch& scratch,
  126. const middleware::bap::RequestFrame& request,
  127. std::span<std::byte> response,
  128. std::size_t& written) noexcept {
  129. written = 0;
  130. encrypted::ServiceRoute route;
  131. (void)encrypted::routing::resolve(request.messageId, route);
  132. if (route.responseMode != encrypted::ResponseMode::reply) {
  133. core::log::write(core::log::Channel::server, core::log::Level::info, route.successEvent);
  134. return true;
  135. }
  136. encrypted::ServiceOutcome outcome{};
  137. std::size_t bodySize = 0;
  138. // A codec that refuses answers with an empty body. The Client matches only the head of its
  139. // pending ring, so one unanswered request jams that ring for the rest of the run.
  140. if (!encrypted::body::process(route,
  141. session.queuez,
  142. session.activity,
  143. session.matchmakingContext,
  144. request.body,
  145. scratch.responseBody,
  146. bodySize,
  147. outcome)) {
  148. bodySize = 0;
  149. }
  150. const bool encoded =
  151. middleware::bap::encode_response(route.response,
  152. request.taskId,
  153. request.frameType,
  154. std::span(scratch.responseBody).first(bodySize),
  155. response,
  156. written);
  157. SecureZeroMemory(scratch.responseBody.data(), bodySize);
  158. SecureZeroMemory(&outcome, sizeof outcome);
  159. if (!encoded) {
  160. report_refusal(session, request.messageId, "encode");
  161. return false;
  162. }
  163. core::log::write(core::log::Channel::server, core::log::Level::info, route.successEvent);
  164. return true;
  165. }
  166. } // namespace
  167. /**
  168. * Handles plaintext channel start and server hello, and routes every other plaintext service.
  169. * @param session Auth and nonce state owned by the connection.
  170. * @param scratch Transform buffers owned by the lock, kept off the Client thread stack.
  171. * @param response Whole-frame storage owned by the caller.
  172. * @param written Gets the encoded response size in bytes.
  173. * @return True when the service owes no reply, or its response is encoded.
  174. */
  175. bool consume(Session& session,
  176. Scratch& scratch,
  177. const middleware::bap::OuterFrame& outer,
  178. std::span<std::byte> response,
  179. std::size_t& written) noexcept {
  180. written = 0;
  181. middleware::bap::RequestFrame frame;
  182. if (!middleware::bap::parse_request_payload(outer.payload, outer.frameType, frame)) {
  183. report_refusal(session, 0, "parse");
  184. return false;
  185. }
  186. // The channel-start body is a nonce the Client checks only the length of, so echoing whatever
  187. // arrived is always the correct reply.
  188. if (frame.messageId == static_cast<std::uint16_t>(middleware::bap::RequestService::start)) {
  189. const bool encoded =
  190. middleware::bap::encode_response(middleware::bap::ResponseService::start,
  191. frame.taskId,
  192. frame.frameType,
  193. frame.body,
  194. response,
  195. written);
  196. if (encoded) {
  197. core::log::write(core::log::Channel::server,
  198. core::log::Level::info,
  199. "ev=bap svc=30 rsp=31 result=ok");
  200. }
  201. return encoded;
  202. }
  203. if (frame.messageId
  204. != static_cast<std::uint16_t>(middleware::bap::RequestService::serverHello)) {
  205. return consume_service(session, scratch, frame, response, written);
  206. }
  207. const auto& signOnState = state::sign_on();
  208. const auto& bapState = state::bap();
  209. // Every service 25 is answered without reading the body, and that reaches the Tower on both
  210. // links. The reply is built from State alone, so the body is logged and never refused. The
  211. // activity host's hello uses other framing, and refusing it stranded that link in
  212. // `_authenticating` with no service named anywhere in the log.
  213. if (session.authenticated) {
  214. report_refusal(session, frame.messageId, "reauthenticated");
  215. }
  216. if (!valid_hello_shape(frame.body)) {
  217. report_hello_shape(session, frame.body);
  218. } else if (!hello_token_matches(frame.body, signOnState)) {
  219. report_refusal(session, frame.messageId, "token_mismatch_accepted");
  220. }
  221. // A repeat hello replaces its context, so the old one goes back before the new one is taken.
  222. // Holding both at once drains the small pool on the second hello of a session.
  223. if (session.matchmakingContext.generation != state::matchmaking::kInvalidGeneration) {
  224. (void)state::matchmaking::release_context(session.matchmakingContext);
  225. session.matchmakingContext = {};
  226. }
  227. state::matchmaking::ContextHandle matchmakingContext{};
  228. if (!state::matchmaking::acquire_context(matchmakingContext)) {
  229. // The svc-26 envelope is built from State alone. Refusing over a context the reply never
  230. // reads strands the link in its authenticating step with no service named anywhere.
  231. report_refusal(session, frame.messageId, "context_unavailable");
  232. }
  233. std::array<std::byte, middleware::secure_channel::kServerHelloEnvelopeSize> envelope{};
  234. std::size_t envelopeSize = 0;
  235. const bool encoded =
  236. middleware::secure_channel::encode_server_hello(
  237. signOnState, bapState, envelope, envelopeSize)
  238. && middleware::bap::encode_response(middleware::bap::ResponseService::serverHello,
  239. frame.taskId,
  240. frame.frameType,
  241. std::span(envelope).first(envelopeSize),
  242. response,
  243. written);
  244. SecureZeroMemory(envelope.data(), envelope.size());
  245. if (!encoded) {
  246. (void)state::matchmaking::release_context(matchmakingContext);
  247. report_refusal(session, frame.messageId, "encode");
  248. return false;
  249. }
  250. // Publish the context and counters only after the whole svc-26 response exists. A context that
  251. // was not free publishes as invalid instead of costing the reply.
  252. session.matchmakingContext = matchmakingContext;
  253. arm_encryption(session, bapState);
  254. core::log::write(
  255. core::log::Channel::server, core::log::Level::info, "ev=bap svc=25 rsp=26 result=ok");
  256. return true;
  257. }
  258. } // namespace sunrise::server::bap::plaintext