winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }