Settings.cpp (16768B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "Public/winget/Settings.h" 5 #include "Public/AppInstallerLogging.h" 6 #include "Public/AppInstallerRuntime.h" 7 #include "Public/AppInstallerStrings.h" 8 #include "Public/AppInstallerSHA256.h" 9 #include "Public/winget/Yaml.h" 10 11 namespace AppInstaller::Settings 12 { 13 using namespace std::string_view_literals; 14 using namespace Runtime; 15 using namespace Utility; 16 17 namespace 18 { 19 void ValidateSettingNamePath(const std::filesystem::path& name) 20 { 21 THROW_HR_IF(E_INVALIDARG, !name.has_relative_path()); 22 THROW_HR_IF(E_INVALIDARG, name.has_root_path()); 23 THROW_HR_IF(E_INVALIDARG, !name.has_filename()); 24 } 25 26 void LogSettingAction(std::string_view action, const StreamDefinition& def) 27 { 28 AICLI_LOG(Core, Verbose, << "Setting action: " << action << ", Type: " << ToString(def.Type) << ", Name: " << def.Name); 29 } 30 31 #ifndef WINGET_DISABLE_FOR_FUZZING 32 // A settings container backed by the ApplicationDataContainer functionality. 33 struct ApplicationDataSettingsContainer : public details::ISettingsContainer 34 { 35 using Container = winrt::Windows::Storage::ApplicationDataContainer; 36 37 ApplicationDataSettingsContainer(const Container& container, const std::filesystem::path& name) 38 { 39 m_parentContainer = GetRelativeContainer(container, name.parent_path()); 40 m_settingName = winrt::to_hstring(name.filename().c_str()); 41 } 42 43 static Container GetRelativeContainer(const Container& container, const std::filesystem::path& offset) 44 { 45 auto result = container; 46 47 for (const auto& part : offset) 48 { 49 auto partHstring = winrt::to_hstring(part.c_str()); 50 result = result.CreateContainer(partHstring, winrt::Windows::Storage::ApplicationDataCreateDisposition::Always); 51 } 52 53 return result; 54 } 55 56 std::unique_ptr<std::istream> Get() override 57 { 58 auto settingsValues = m_parentContainer.Values(); 59 if (settingsValues.HasKey(m_settingName)) 60 { 61 auto value = winrt::unbox_value<winrt::hstring>(settingsValues.Lookup(m_settingName)); 62 return std::make_unique<std::istringstream>(Utility::ConvertToUTF8(value.c_str())); 63 } 64 else 65 { 66 return {}; 67 } 68 } 69 70 bool Set(std::string_view value) override 71 { 72 m_parentContainer.Values().Insert(m_settingName, winrt::box_value(winrt::to_hstring(value))); 73 return true; 74 } 75 76 void Remove() override 77 { 78 m_parentContainer.Values().Remove(m_settingName); 79 } 80 81 std::filesystem::path PathTo() override 82 { 83 THROW_HR(E_UNEXPECTED); 84 } 85 86 private: 87 Container m_parentContainer = nullptr; 88 winrt::hstring m_settingName; 89 }; 90 #endif 91 92 // A settings container backed by the filesystem. 93 struct FileSettingsContainer : public details::ISettingsContainer 94 { 95 FileSettingsContainer(std::filesystem::path root, const std::filesystem::path& name) : m_settingFile(std::move(root)) 96 { 97 m_settingFile /= name; 98 } 99 100 std::unique_ptr<std::istream> Get() override 101 { 102 if (std::filesystem::exists(m_settingFile)) 103 { 104 auto result = std::make_unique<std::ifstream>(m_settingFile); 105 THROW_LAST_ERROR_IF(result->fail()); 106 return result; 107 } 108 else 109 { 110 return {}; 111 } 112 } 113 114 bool Set(std::string_view value) override 115 { 116 EnsureParentPath(); 117 118 std::ofstream stream(m_settingFile, std::ios_base::out | std::ios_base::binary | std::ios_base::trunc); 119 THROW_LAST_ERROR_IF(stream.fail()); 120 stream << value << std::flush; 121 THROW_LAST_ERROR_IF(stream.fail()); 122 123 return true; 124 } 125 126 void Remove() override 127 { 128 std::filesystem::remove(m_settingFile); 129 } 130 131 std::filesystem::path PathTo() override 132 { 133 return m_settingFile; 134 } 135 136 private: 137 void EnsureParentPath() 138 { 139 std::filesystem::create_directories(m_settingFile.parent_path()); 140 } 141 142 std::filesystem::path m_settingFile; 143 }; 144 145 // A settings container that manages safely writing to its value with exchange semantics. 146 // Only allows Set to succeed if the hash value of the setting is the same as the last time it was read. 147 struct ExchangeSettingsContainer : public details::ISettingsContainer 148 { 149 ExchangeSettingsContainer(std::unique_ptr<ISettingsContainer>&& container, const std::string_view& name) : 150 m_container(std::move(container)), m_name(name) {} 151 152 std::unique_ptr<std::istream> Get() override 153 { 154 return GetInternal(m_hash); 155 } 156 157 bool Set(std::string_view value) override 158 { 159 THROW_HR_IF(E_UNEXPECTED, value.size() > std::numeric_limits<uint32_t>::max()); 160 161 // If Set is called without ever reading the value, then we can assume that caller wants 162 // to overwrite it regardless. Also, we don't have any previous value to compare against 163 // anyway so the only other option would be to always reject it. 164 if (m_hash) 165 { 166 std::optional<SHA256::HashBuffer> currentHash; 167 std::ignore = GetInternal(currentHash); 168 169 if (currentHash && !SHA256::AreEqual(m_hash.value(), currentHash.value())) 170 { 171 AICLI_LOG(Core, Verbose, << "Setting value for '" << m_name << "' has changed since last read; rejecting Set"); 172 return false; 173 } 174 } 175 176 SHA256::HashBuffer newHash = SHA256::ComputeHash(reinterpret_cast<const uint8_t*>(value.data()), static_cast<uint32_t>(value.size())); 177 if (m_container->Set(value)) 178 { 179 m_hash = std::move(newHash); 180 return true; 181 } 182 else 183 { 184 return false; 185 } 186 } 187 188 void Remove() override 189 { 190 m_container->Remove(); 191 m_hash.reset(); 192 } 193 194 std::filesystem::path PathTo() override 195 { 196 return m_container->PathTo(); 197 } 198 199 protected: 200 std::string_view m_name; 201 std::optional<SHA256::HashBuffer> m_hash; 202 203 private: 204 std::unique_ptr<std::istream> GetInternal(std::optional<SHA256::HashBuffer>& hashStorage) 205 { 206 std::unique_ptr<std::istream> stream = m_container->Get(); 207 208 if (!stream) 209 { 210 // If no stream exists, then no hashing needs to be done. 211 // Return an empty hash vector to indicate the attempted read but no result. 212 hashStorage.emplace(); 213 return stream; 214 } 215 216 std::string streamContents = Utility::ReadEntireStream(*stream); 217 THROW_HR_IF(E_UNEXPECTED, streamContents.size() > std::numeric_limits<uint32_t>::max()); 218 219 hashStorage = SHA256::ComputeHash(reinterpret_cast<const uint8_t*>(streamContents.c_str()), static_cast<uint32_t>(streamContents.size())); 220 221 // Return a stream over the contents that we read in and hashed, to prevent a race. 222 return std::make_unique<std::istringstream>(streamContents); 223 } 224 225 std::unique_ptr<ISettingsContainer> m_container; 226 }; 227 228 // A settings container wrapper that enforces security. 229 struct SecureSettingsContainer : public ExchangeSettingsContainer 230 { 231 constexpr static std::string_view NodeName_Sha256 = "SHA256"sv; 232 233 SecureSettingsContainer(std::unique_ptr<ISettingsContainer>&& container, const std::string_view& name) : 234 ExchangeSettingsContainer(std::move(container), name), m_secure(GetPathTo(PathName::SecureSettingsForRead), name) {} 235 236 private: 237 struct VerificationData 238 { 239 bool Found = false; 240 SHA256::HashBuffer Hash; 241 }; 242 243 VerificationData GetVerificationData() 244 { 245 std::unique_ptr<std::istream> stream = m_secure.Get(); 246 247 if (!stream) 248 { 249 return {}; 250 } 251 252 std::string streamContents = Utility::ReadEntireStream(*stream); 253 254 YAML::Node document; 255 try 256 { 257 document = YAML::Load(streamContents); 258 } 259 catch (const std::runtime_error& e) 260 { 261 AICLI_LOG(Core, Error, << "Secure setting metadata for '" << m_name << "' contained invalid YAML (" << e.what() << "):\n" << streamContents); 262 return {}; 263 } 264 265 std::string hashString; 266 267 try 268 { 269 hashString = document[NodeName_Sha256].as<std::string>(); 270 } 271 catch (const std::runtime_error& e) 272 { 273 AICLI_LOG(Core, Error, << "Secure setting metadata for '" << m_name << "' contained invalid YAML (" << e.what() << "):\n" << streamContents); 274 return {}; 275 } 276 277 VerificationData result; 278 result.Found = true; 279 result.Hash = SHA256::ConvertToBytes(hashString); 280 281 return result; 282 } 283 284 void SetVerificationData(VerificationData data) 285 { 286 YAML::Emitter out; 287 out << YAML::BeginMap; 288 out << YAML::Key << NodeName_Sha256 << YAML::Value << SHA256::ConvertToString(data.Hash); 289 out << YAML::EndMap; 290 291 m_secure.Set(out.str()); 292 } 293 294 public: 295 std::unique_ptr<std::istream> Get() override 296 { 297 std::unique_ptr<std::istream> stream = ExchangeSettingsContainer::Get(); 298 299 if (!stream) 300 { 301 // If no stream exists, then no verification needs to be done. 302 return stream; 303 } 304 305 VerificationData verData = GetVerificationData(); 306 307 // This case should be very rare, so a very identifiable error is helpful. 308 // Plus the text for this one is fairly on point for what has happened. 309 THROW_HR_IF(SPAPI_E_FILE_HASH_NOT_IN_CATALOG, !verData.Found); 310 311 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_DATA_CHECKSUM_ERROR), !SHA256::AreEqual(m_hash.value(), verData.Hash)); 312 313 // ExchangeSettingsContainer already produces an in memory stream that we can use. 314 return stream; 315 } 316 317 bool Set(std::string_view value) override 318 { 319 // Force the creation of the secure settings location with appropriate ACLs 320 GetPathTo(PathName::SecureSettingsForWrite); 321 322 bool exchangeResult = ExchangeSettingsContainer::Set(value); 323 324 if (exchangeResult) 325 { 326 VerificationData verData; 327 verData.Hash = m_hash.value(); 328 329 SetVerificationData(verData); 330 } 331 332 return exchangeResult; 333 } 334 335 void Remove() override 336 { 337 ExchangeSettingsContainer::Remove(); 338 m_secure.Remove(); 339 } 340 341 std::filesystem::path PathTo() override 342 { 343 THROW_HR(E_UNEXPECTED); 344 } 345 346 private: 347 FileSettingsContainer m_secure; 348 }; 349 350 std::unique_ptr<details::ISettingsContainer> GetRawSettingsContainer(Type type, const std::string_view& name) 351 { 352 #ifndef WINGET_DISABLE_FOR_FUZZING 353 if (IsRunningInPackagedContext()) 354 { 355 switch (type) 356 { 357 case Type::Standard: 358 return std::make_unique<ApplicationDataSettingsContainer>( 359 ApplicationDataSettingsContainer::GetRelativeContainer( 360 winrt::Windows::Storage::ApplicationData::Current().LocalSettings(), GetPathTo(PathName::StandardSettings)), 361 name); 362 default: 363 THROW_HR(E_UNEXPECTED); 364 } 365 } 366 else 367 #endif 368 { 369 switch (type) 370 { 371 case Type::Standard: 372 return std::make_unique<FileSettingsContainer>(GetPathTo(PathName::StandardSettings), name); 373 default: 374 THROW_HR(E_UNEXPECTED); 375 } 376 } 377 } 378 379 // The default is not a raw container, so we wrap some of the underlying containers to enable higher order behaviors. 380 std::unique_ptr<details::ISettingsContainer> GetSettingsContainer(Type type, const std::string_view& name) 381 { 382 switch (type) 383 { 384 case Type::Standard: 385 // Standard settings should use exchange semantics to prevent overwrites 386 return std::make_unique<ExchangeSettingsContainer>(GetRawSettingsContainer(type, name), name); 387 388 case Type::UserFile: 389 // User file settings are not typically modified by us, so there is no need for exchange 390 return std::make_unique<FileSettingsContainer>(GetPathTo(PathName::UserFileSettings), name); 391 392 case Type::Secure: 393 // Secure settings add hash verification on reads on top of exchange semantics 394 return std::make_unique<SecureSettingsContainer>(GetRawSettingsContainer(Type::Standard, name), name); 395 396 default: 397 THROW_HR(E_UNEXPECTED); 398 } 399 } 400 401 std::unique_ptr<details::ISettingsContainer> GetSettingsContainer(const StreamDefinition& streamDefinition) 402 { 403 return GetSettingsContainer(streamDefinition.Type, streamDefinition.Name); 404 } 405 } 406 407 std::string_view ToString(Type type) 408 { 409 switch (type) 410 { 411 case Type::Standard: 412 return "Standard"sv; 413 case Type::UserFile: 414 return "UserFile"sv; 415 case Type::Secure: 416 return "Secure"sv; 417 default: 418 THROW_HR(E_UNEXPECTED); 419 } 420 } 421 422 Stream::Stream(const StreamDefinition& streamDefinition) : 423 m_streamDefinition(streamDefinition), m_container(GetSettingsContainer(streamDefinition)) 424 { 425 ValidateSettingNamePath(m_streamDefinition.Name); 426 } 427 428 std::unique_ptr<std::istream> Stream::Get() 429 { 430 LogSettingAction("Get", m_streamDefinition); 431 return m_container->Get(); 432 } 433 434 [[nodiscard]] bool Stream::Set(std::string_view value) 435 { 436 LogSettingAction("Set", m_streamDefinition); 437 return m_container->Set(value); 438 } 439 440 void Stream::Remove() 441 { 442 LogSettingAction("Remove", m_streamDefinition); 443 m_container->Remove(); 444 } 445 446 std::string_view Stream::GetName() const 447 { 448 return m_streamDefinition.Name; 449 } 450 451 std::filesystem::path Stream::GetPath() const 452 { 453 return m_container->PathTo(); 454 } 455 }