state_runtime.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. #include <Windows.h>
  2. #include <algorithm>
  3. #include <array>
  4. #include <bcrypt.h>
  5. #include <cstddef>
  6. #include <cstdint>
  7. #include <cstdio>
  8. #include <limits>
  9. #include <span>
  10. #include "../../core/logging/log.h"
  11. #include "../../core/settings/settings.h"
  12. #include "../activity/defaults/activity_defaults_validation.h"
  13. #include "../build_data/runtime.h"
  14. #include "../record_claims/record_claims.h"
  15. #include "equipment/configured_equipment_identity.h"
  16. #include "runtime.h"
  17. #include "state.h"
  18. #include "storage/internal.h"
  19. namespace sunrise::state {
  20. namespace runtime::storage {
  21. State g_state;
  22. SRWLOCK g_stateLock{SRWLOCK_INIT};
  23. } // namespace runtime::storage
  24. namespace {
  25. /** Network-order IPv4 loopback returned by the in-process SignOn route. */
  26. constexpr std::uint32_t kLoopbackAddress = 0x7F000001;
  27. /** Default one-hour lifetime for generated SignOn session tokens. */
  28. constexpr std::uint32_t kDefaultTokenLifetimeSeconds = 3600;
  29. /** Family 5 uses the largest signed 64-bit value as its process-global object key. */
  30. constexpr std::uint64_t kGlobalFamily5Soid =
  31. static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)());
  32. /**
  33. * Fills fixed secret storage with Windows system randomness.
  34. * @tparam Size Required secret byte count.
  35. * @param output Secret storage to overwrite.
  36. * @return True when Windows generates every byte.
  37. */
  38. template <std::size_t Size>
  39. [[nodiscard]] bool randomize(std::array<std::byte, Size>& output) noexcept {
  40. return BCryptGenRandom(nullptr,
  41. reinterpret_cast<PUCHAR>(output.data()),
  42. static_cast<ULONG>(output.size()),
  43. BCRYPT_USE_SYSTEM_PREFERRED_RNG)
  44. >= 0;
  45. }
  46. /** @return True when any authored or already-seeded account identity owns one SOID. */
  47. [[nodiscard]] bool identity_uses_soid(const AccountState& accountState,
  48. std::uint64_t soid) noexcept {
  49. if (soid == 0 || accountState.primarySoid == soid) {
  50. return true;
  51. }
  52. for (std::size_t index = 0; index < accountState.profileItemCount; ++index) {
  53. if (accountState.profileItems[index].instanceSoid == soid) {
  54. return true;
  55. }
  56. }
  57. for (std::size_t characterIndex = 0; characterIndex < accountState.characterCount;
  58. ++characterIndex) {
  59. const CharacterState& character = accountState.characters[characterIndex];
  60. if (character.soid == soid) {
  61. return true;
  62. }
  63. for (const std::optional<account::inventory::Item>& item : character.equipment.slots) {
  64. if (item.has_value() && item->instanceSoid == soid) {
  65. return true;
  66. }
  67. }
  68. for (std::size_t index = 0; index < character.inventory.count; ++index) {
  69. if (character.inventory.values[index].instanceSoid == soid) {
  70. return true;
  71. }
  72. }
  73. }
  74. return false;
  75. }
  76. /** Seeds canonical character row generations before installed build data is needed. */
  77. [[nodiscard]] bool seed_inventory_runtime_fields(AccountState& accountState) noexcept {
  78. if (!account::valid_authored(accountState)) {
  79. return false;
  80. }
  81. for (std::size_t characterIndex = 0; characterIndex < accountState.characterCount;
  82. ++characterIndex) {
  83. CharacterState& character = accountState.characters[characterIndex];
  84. std::uint32_t next = 0;
  85. for (std::optional<account::inventory::Item>& item : character.equipment.slots) {
  86. if (item.has_value()) {
  87. item->mutationSerial = static_cast<std::int32_t>(next++);
  88. }
  89. }
  90. for (std::size_t index = 0; index < character.inventory.count; ++index) {
  91. character.inventory.values[index].mutationSerial = static_cast<std::int32_t>(next++);
  92. }
  93. character.nextInventorySerial = next;
  94. }
  95. return account::valid(accountState);
  96. }
  97. /**
  98. * Canonicalizes only profile rows which the installed socket UI materializes as action sources.
  99. * @param accountState Account canonicalized in place.
  100. * @return True when every profile row canonicalizes.
  101. */
  102. [[nodiscard]] bool canonicalize_profile_item_identities(AccountState& accountState) noexcept {
  103. if (!account::valid(accountState)) {
  104. return false;
  105. }
  106. if (accountState.profileItemCount == 0) {
  107. // Nothing to canonicalize, so the socket relation is not needed. Demanding it here would
  108. // refuse the first account snapshot of an account that owns no profile stack at all, and
  109. // an empty account family never becomes active.
  110. return true;
  111. }
  112. if (!build_data::socket_plug_rules_ready()) {
  113. return false;
  114. }
  115. std::array<bool, account::inventory::kProfileItemCapacity> actionSources{};
  116. std::size_t actionSourceCount = 0;
  117. for (std::size_t index = 0; index < accountState.profileItemCount; ++index) {
  118. const account::inventory::ProfileItem& profileItem = accountState.profileItems[index];
  119. build_data::items::Definition item{};
  120. build_data::items::details::Definition detail{};
  121. build_data::inventory::buckets::Descriptor bucket{};
  122. if (!build_data::find_item_definition_hash(profileItem.definitionHash, item)
  123. || item.definitionHash != profileItem.definitionHash
  124. || !build_data::find_configured_item_detail(item.definitionIndex, detail)
  125. || detail.definitionIndex != item.definitionIndex
  126. || detail.definitionHash != item.definitionHash || detail.bucketId != item.bucketId
  127. || detail.instancedDefinitionState
  128. != build_data::items::details::InstancedDefinitionState::stackable
  129. || !build_data::find_inventory_bucket_descriptor(item.bucketId, bucket)
  130. || bucket.arraySelector != build_data::inventory::buckets::ArraySelector::profile) {
  131. return false;
  132. }
  133. actionSources[index] =
  134. build_data::is_profile_action_source(item.definitionIndex, item.bucketId);
  135. if (actionSources[index]
  136. && ++actionSourceCount > account::inventory::kProfileActionSourceCapacity) {
  137. return false;
  138. }
  139. }
  140. // Currency, material, and consumable rows are native non-instanced stacks. Clear any stale
  141. // runtime key before allocating action-source identities so it cannot reserve the namespace.
  142. for (std::size_t index = 0; index < accountState.profileItemCount; ++index) {
  143. if (!actionSources[index]) {
  144. accountState.profileItems[index].instanceSoid = 0;
  145. }
  146. }
  147. std::uint64_t nextProfileSoid = account::inventory::kFirstProfileItemInstanceSoid;
  148. for (std::size_t index = 0; index < accountState.profileItemCount; ++index) {
  149. account::inventory::ProfileItem& item = accountState.profileItems[index];
  150. if (!actionSources[index] || item.instanceSoid != 0) {
  151. continue;
  152. }
  153. while (identity_uses_soid(accountState, nextProfileSoid)) {
  154. if (nextProfileSoid == (std::numeric_limits<std::uint64_t>::max)()) {
  155. return false;
  156. }
  157. ++nextProfileSoid;
  158. }
  159. item.instanceSoid = nextProfileSoid;
  160. if (nextProfileSoid != (std::numeric_limits<std::uint64_t>::max)()) {
  161. ++nextProfileSoid;
  162. }
  163. }
  164. return account::valid(accountState);
  165. }
  166. } // namespace
  167. /**
  168. * Loads build data and generates secrets with Sunrise's authored activity defaults.
  169. * @param module Loaded Sunrise module, or null to disable disk persistence.
  170. * @param initialAccount Empty State, or a complete checked account from Core settings.
  171. * @return True when the cached data passes its checks and every secret gets random bytes.
  172. */
  173. bool initialize(void* module, const AccountState& initialAccount) noexcept {
  174. return initialize(module, initialAccount, activity::defaults::authored());
  175. }
  176. /**
  177. * Loads build data and publishes fixed activity defaults in one step.
  178. * @param module Loaded Sunrise module, or null to disable disk persistence.
  179. * @param initialAccount Empty State, or a complete checked account from Core settings.
  180. * @param activityDefaults Complete local fallback policy from immutable Core settings.
  181. * @return True when account, defaults, cached data, and generated secrets are valid.
  182. */
  183. bool initialize(void* module,
  184. const AccountState& initialAccount,
  185. const activity::defaults::ActivityDefaults& activityDefaults) noexcept {
  186. AccountState runtimeAccount = initialAccount;
  187. if (!seed_inventory_runtime_fields(runtimeAccount)
  188. || !activity::defaults::valid(activityDefaults)) {
  189. return false;
  190. }
  191. if (!build_data::initialize(module, runtime::equipment::configured_hash(runtimeAccount))) {
  192. return false;
  193. }
  194. // Claims are held beside the build data cache, so a restart keeps what the client already
  195. // shows as Acquired. A missing file is a first run, not a failure.
  196. (void)record_claims::initialize(module);
  197. // A cache hit already has the complete plug relation, so publish canonical profile identities
  198. // in the first State image. On a first cache build, snapshot preparation repeats this step
  199. // after package extraction has published the relation.
  200. if (build_data::socket_plug_rules_ready()
  201. && !canonicalize_profile_item_identities(runtimeAccount)) {
  202. build_data::shutdown();
  203. return false;
  204. }
  205. {
  206. // The account key is authored, and a truncated one is consistent enough to go unnoticed.
  207. std::array<char, 96> line{};
  208. const int written =
  209. std::snprintf(line.data(),
  210. line.size(),
  211. "ev=account stage=identity primary=0x%016llX characters=%zu",
  212. static_cast<unsigned long long>(runtimeAccount.primarySoid),
  213. runtimeAccount.characterCount);
  214. if (written > 0) {
  215. core::log::write(core::log::Channel::state,
  216. core::log::Level::info,
  217. {line.data(), static_cast<std::size_t>(written)});
  218. }
  219. }
  220. State initialized{};
  221. if (!randomize(initialized.signOn.encryptionKey)
  222. || !randomize(initialized.signOn.authenticationKey)
  223. || !randomize(initialized.signOn.sessionToken) || !randomize(initialized.bap.nonce)
  224. || !randomize(initialized.bap.sessionKey) || !randomize(initialized.bap.envelopeIv)) {
  225. SecureZeroMemory(&initialized, sizeof initialized);
  226. build_data::shutdown();
  227. return false;
  228. }
  229. initialized.signOn.relayAddress = kLoopbackAddress;
  230. // The published relay port is the one the listener binds, so both move with one setting.
  231. initialized.signOn.relayPort = core::settings::get().server.bapPort;
  232. initialized.signOn.tokenLifetimeSeconds = kDefaultTokenLifetimeSeconds;
  233. initialized.account = runtimeAccount;
  234. initialized.activity.defaults = activityDefaults;
  235. initialized.investment.family5.objectSoid = kGlobalFamily5Soid;
  236. // Only the override lists come from settings. Identity and gate stay owned by State.
  237. const Family5State& authored = core::settings::get().initialFamily5;
  238. initialized.investment.family5.flags = authored.flags;
  239. initialized.investment.family5.flagCount = authored.flagCount;
  240. initialized.investment.family5.values = authored.values;
  241. initialized.investment.family5.valueCount = authored.valueCount;
  242. // The arm is account-wide and rides the first ws-503, which goes out before any pick. Nothing
  243. // is selected at boot, so it is armed when any authored character carries the bypass. The
  244. // per-character objB byte is the other half, and it still decides which character it opens.
  245. for (std::size_t index = 0; index < runtimeAccount.characterCount; ++index) {
  246. if (runtimeAccount.characters[index].contentBypass) {
  247. initialized.investment.family5.contentGateArm = true;
  248. break;
  249. }
  250. }
  251. // Publish one complete State only after every generated secret is valid.
  252. AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
  253. runtime::storage::g_state = initialized;
  254. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  255. SecureZeroMemory(&initialized, sizeof initialized);
  256. return true;
  257. }
  258. /** Securely erases State, including activity destinations and matchmaking descriptors. */
  259. void shutdown() noexcept {
  260. AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
  261. SecureZeroMemory(&runtime::storage::g_state, sizeof runtime::storage::g_state);
  262. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  263. build_data::shutdown();
  264. }
  265. /** @return Immutable generated SignOn session fields. */
  266. const SignOnState& sign_on() noexcept {
  267. return runtime::storage::g_state.signOn;
  268. }
  269. /** Ensures every native profile action source has one unique runtime item-instance key. */
  270. bool ensure_profile_item_identities() noexcept {
  271. AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
  272. AccountState candidate = runtime::storage::g_state.account;
  273. const bool ready = canonicalize_profile_item_identities(candidate);
  274. if (ready) {
  275. runtime::storage::g_state.account = candidate;
  276. }
  277. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  278. return ready;
  279. }
  280. /**
  281. * Publishes the bootstrap content-id token read from the installed client.
  282. * @param token Exactly 16 native bytes.
  283. * @return True when the complete token is kept for this process.
  284. */
  285. bool publish_bootstrap_token(std::span<const std::byte> token) noexcept {
  286. SignOnState& signOn = runtime::storage::g_state.signOn;
  287. if (token.size() != signOn.bootstrapToken.size()) {
  288. return false;
  289. }
  290. std::copy(token.begin(), token.end(), signOn.bootstrapToken.begin());
  291. signOn.bootstrapTokenPresent = true;
  292. return true;
  293. }
  294. /** @return Immutable generated BAP session fields. */
  295. const BapState& bap() noexcept {
  296. return runtime::storage::g_state.bap;
  297. }
  298. /** @return A copy of the evaluated content state, read under the lock. */
  299. InvestmentState investment_snapshot() noexcept {
  300. AcquireSRWLockShared(&runtime::storage::g_stateLock);
  301. const InvestmentState snapshot = runtime::storage::g_state.investment;
  302. ReleaseSRWLockShared(&runtime::storage::g_stateLock);
  303. return snapshot;
  304. }
  305. } // namespace sunrise::state