commit 12f100c7920c0d971fbcdd8279f4162b132a18cb parent 80d8a63cd27ecc822d10e23d59cca5cd0c0e0890 Author: JohnMcPMS <johnmcp@microsoft.com> Date: Thu, 13 May 2021 22:14:26 -0700 Ensure chain of trust with packaged index (#959) This change better ensures the chain of trust when using a packaged index by: 1. Ensuring that the index package does not change family names 2. Ensuring that the default source is Store signed 3. Using signed data from the package to verify the manifest files that we download separately Diffstat:
39 files changed, 566 insertions(+), 105 deletions(-)
diff --git a/src/AppInstallerCLI/AppInstallerCLI.vcxproj b/src/AppInstallerCLI/AppInstallerCLI.vcxproj @@ -184,7 +184,7 @@ <Link> <SubSystem>Console</SubSystem> <GenerateWindowsMetadata>false</GenerateWindowsMetadata> - <AdditionalDependencies Condition="'$(Configuration)'=='Debug'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)'=='Debug'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> </Link> <Manifest> <AdditionalManifestFiles Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">$(ProjectDir)..\manifest\shared.manifest %(AdditionalManifestFiles)</AdditionalManifestFiles> @@ -246,7 +246,7 @@ <EnableCOMDATFolding>true</EnableCOMDATFolding> <OptimizeReferences>true</OptimizeReferences> <GenerateWindowsMetadata>false</GenerateWindowsMetadata> - <AdditionalDependencies Condition="'$(Configuration)'=='Release'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)'=='Release'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> </Link> <Manifest> <AdditionalManifestFiles Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">$(ProjectDir)..\manifest\shared.manifest %(AdditionalManifestFiles)</AdditionalManifestFiles> diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -241,7 +241,7 @@ namespace AppInstaller::CLI::Workflow if (context.Contains(Execution::Data::PackageVersion) && context.Get<Execution::Data::PackageVersion>()->GetSource() != nullptr && - SourceTrustLevel::Trusted == context.Get<Execution::Data::PackageVersion>()->GetSource()->GetDetails().TrustLevel) + WI_IsFlagSet(context.Get<Execution::Data::PackageVersion>()->GetSource()->GetDetails().TrustLevel, SourceTrustLevel::Trusted)) { context.SetFlags(Execution::ContextFlag::InstallerTrusted); } diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -116,7 +116,7 @@ <Link> <SubSystem>Console</SubSystem> <GenerateWindowsMetadata>false</GenerateWindowsMetadata> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> </Link> <Manifest> <AdditionalManifestFiles Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(ProjectDir)..\manifest\shared.manifest</AdditionalManifestFiles> @@ -132,7 +132,7 @@ <TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</TreatWarningAsError> </ClCompile> <Link> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> </Link> <Manifest> <AdditionalManifestFiles Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(ProjectDir)..\manifest\shared.manifest</AdditionalManifestFiles> @@ -157,8 +157,8 @@ <EnableCOMDATFolding>true</EnableCOMDATFolding> <OptimizeReferences>true</OptimizeReferences> <GenerateWindowsMetadata>false</GenerateWindowsMetadata> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies)</AdditionalDependencies> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|x64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|x64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> </Link> <Manifest> <AdditionalManifestFiles Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(ProjectDir)..\manifest\shared.manifest</AdditionalManifestFiles> diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -29,10 +29,10 @@ using namespace AppInstaller::Utility; SQLiteIndex CreateTestIndex(const std::string& filePath, std::optional<Schema::Version> version = {}) { - // If no specific version requested, then use generator to run against all versions. + // If no specific version requested, then use generator to run against the last 3 versions. if (!version) { - version = GENERATE(Schema::Version{ 1, 0 }, Schema::Version{ 1, 1 }, Schema::Version::Latest()); + version = GENERATE(Schema::Version{ 1, 1 }, Schema::Version{ 1, 2 }, Schema::Version::Latest()); } return SQLiteIndex::CreateNew(filePath, version.value()); @@ -42,25 +42,25 @@ Schema::Version TestPrepareForRead(SQLiteIndex& index) { // This will only be called for tests that want to support cross version checks. // Based on the version of the incoming, we only want to generate versions less or equal to it. - if (index.GetVersion() == Schema::Version{ 1, 0 }) + if (index.GetVersion() == Schema::Version{ 1, 1 }) { // Nothing to do here } - else if (index.GetVersion() == Schema::Version{ 1, 1 }) + else if (index.GetVersion() == Schema::Version{ 1, 2 }) { - auto changeVersion = GENERATE(false, true); + Schema::Version version = GENERATE(Schema::Version{ 1, 1 }, Schema::Version{ 1, 2 }); - if (changeVersion) + if (version != Schema::Version{ 1, 2 }) { - index.ForceVersion(Schema::Version{ 1, 0 }); - return { 1, 0 }; + index.ForceVersion(version); + return version; } } - else if (index.GetVersion() == Schema::Version{ 1, 2 }) + else if (index.GetVersion() == Schema::Version{ 1, 3 }) { - Schema::Version version = GENERATE(Schema::Version{ 1, 0 }, Schema::Version{ 1, 1 }, Schema::Version{ 1, 2 }); + Schema::Version version = GENERATE(Schema::Version{ 1, 1 }, Schema::Version{ 1, 2 }, Schema::Version{ 1, 3 }); - if (version != Schema::Version{ 1, 2 }) + if (version != Schema::Version{ 1, 3 }) { index.ForceVersion(version); return version; @@ -239,6 +239,12 @@ bool IsManifestMetadataSupported(const SQLiteIndex& index, const Schema::Version return (index.GetVersion() >= Schema::Version{ 1, 1 } && testVersion >= Schema::Version{ 1, 1 }); } +bool AreManifestHashesSupported(const SQLiteIndex& index, const Schema::Version& testVersion) +{ + UNSCOPED_INFO("Index " << index.GetVersion() << " | Test " << testVersion); + return (index.GetVersion() >= Schema::Version{ 1, 3 } && testVersion >= Schema::Version{ 1, 3 }); +} + std::string GetPropertyStringByKey(const SQLiteIndex& index, SQLite::rowid_t id, PackageVersionProperty property, std::string_view version, std::string_view channel) { auto manifestId = index.GetManifestIdByKey(id, version, channel); @@ -2130,3 +2136,61 @@ TEST_CASE("SQLiteIndex_NormNameAndPublisher_Complex", "[sqliteindex]") REQUIRE(results.Matches.empty()); } } + +TEST_CASE("SQLiteIndex_ManifestHash_Present", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + uint8_t data[4] = { 1, 2, 3, 4 }; + SHA256::HashBuffer hash = SHA256::ComputeHash(data, sizeof(data)); + + SQLiteIndex index = CreateTestIndex(tempFile); + + Manifest manifest; + manifest.Id = "Foo"; + manifest.Version = "Bar"; + manifest.StreamSha256 = hash; + index.AddManifest(manifest, "path"); + + Schema::Version testVersion = TestPrepareForRead(index); + + auto results = index.Search({}); + REQUIRE(results.Matches.size() == 1); + + auto hashResult = index.GetPropertyByManifestId(results.Matches[0].first, PackageVersionProperty::ManifestSHA256Hash); + + if (AreManifestHashesSupported(index, testVersion)) + { + REQUIRE(hashResult); + auto hashResultBytes = SHA256::ConvertToBytes(hashResult.value()); + REQUIRE(hash.size() == hashResultBytes.size()); + REQUIRE(std::equal(hash.begin(), hash.end(), hashResultBytes.begin())); + } + else + { + REQUIRE(!hashResult); + } +} + +TEST_CASE("SQLiteIndex_ManifestHash_Missing", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = CreateTestIndex(tempFile); + + Manifest manifest; + manifest.Id = "Foo"; + manifest.Version = "Bar"; + index.AddManifest(manifest, "path"); + + Schema::Version testVersion = TestPrepareForRead(index); + + auto results = index.Search({}); + REQUIRE(results.Matches.size() == 1); + + auto hashResult = index.GetPropertyByManifestId(results.Matches[0].first, PackageVersionProperty::ManifestSHA256Hash); + + REQUIRE(!hashResult); +} diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -33,7 +33,8 @@ bool operator==(const MultiValue& a, const MultiValue& b) TEST_CASE("ReadPreviewGoodManifestAndVerifyContents", "[ManifestValidation]") { - Manifest manifest = YamlParser::CreateFromPath(TestDataFile("Manifest-Good.yaml")); + auto manifestFile = TestDataFile("Manifest-Good.yaml"); + Manifest manifest = YamlParser::CreateFromPath(manifestFile); REQUIRE(manifest.Id == "microsoft.msixsdk"); REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageName>() == "MSIX SDK"); @@ -119,6 +120,13 @@ TEST_CASE("ReadPreviewGoodManifestAndVerifyContents", "[ManifestValidation]") REQUIRE(localization1.Get<Localization::Description>() == "El proyecto MSIX SDK es habilita desarrolladores de diferentes"); REQUIRE(localization1.Get<Localization::PackageUrl>() == "https://github.com/microsoft/msix-packaging/es-MX"); REQUIRE(localization1.Get<Localization::LicenseUrl>() == "https://github.com/microsoft/msix-packaging/blob/master/LICENSE-es-MX"); + + // Stream hash + std::ifstream stream(manifestFile, std::ios_base::in | std::ios_base::binary); + REQUIRE(!stream.fail()); + auto manifestHash = SHA256::ComputeHash(stream); + REQUIRE(manifestHash.size() == manifest.StreamSha256.size()); + REQUIRE(std::equal(manifestHash.begin(), manifestHash.end(), manifest.StreamSha256.begin())); } TEST_CASE("ReadGoodManifestWithSpaces", "[ManifestValidation]") diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp @@ -139,6 +139,8 @@ namespace AppInstaller return "Unsupported MIME type returned by rest source"; case APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION: return "Invalid rest source contract version"; + case APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE: + return "The source data is corrupted or tampered"; default: return "Unknown Error Code"; } diff --git a/src/AppInstallerCommonCore/Manifest/YamlParser.cpp b/src/AppInstallerCommonCore/Manifest/YamlParser.cpp @@ -446,6 +446,12 @@ namespace AppInstaller::Manifest::YamlParser OutputYamlDoc(manifestDoc, mergedManifestPath); } + // If there is only one input file, use its hash for the stream + if (input.size() == 1) + { + manifest.StreamSha256 = std::move(input[0].StreamSha256); + } + return resultErrors; } } @@ -476,7 +482,7 @@ namespace AppInstaller::Manifest::YamlParser else { YamlManifestInfo doc; - doc.Root = YAML::Load(inputPath); + doc.Root = YAML::Load(inputPath, doc.StreamSha256); doc.FileName = inputPath.filename().u8string(); docList.emplace_back(std::move(doc)); } diff --git a/src/AppInstallerCommonCore/MsixInfo.cpp b/src/AppInstallerCommonCore/MsixInfo.cpp @@ -345,7 +345,7 @@ namespace AppInstaller::Msix return signatureContent; } - std::string MsixInfo::GetPackageFullName() + std::wstring MsixInfo::GetPackageFullNameWide() { ComPtr<IAppxManifestPackageId> packageId; if (m_isBundle) @@ -364,7 +364,12 @@ namespace AppInstaller::Msix wil::unique_cotaskmem_string fullName; THROW_IF_FAILED(packageId->GetPackageFullName(&fullName)); - return Utility::ConvertToUTF8(fullName.get()); + return { fullName.get() }; + } + + std::string MsixInfo::GetPackageFullName() + { + return Utility::ConvertToUTF8(GetPackageFullNameWide()); } bool MsixInfo::IsNewerThan(const std::filesystem::path& otherManifest) diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -75,6 +75,7 @@ #define APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL ((HRESULT)0x8a15003C) #define APPINSTALLER_CLI_ERROR_RESTSOURCE_UNSUPPORTED_MIME_TYPE ((HRESULT)0x8a15003D) #define APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION ((HRESULT)0x8a15003E) +#define APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE ((HRESULT)0x8a15003F) namespace AppInstaller { diff --git a/src/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h b/src/AppInstallerCommonCore/Public/AppInstallerMsixInfo.h @@ -62,6 +62,7 @@ namespace AppInstaller::Msix std::vector<byte> GetSignature(); // Gets the package full name. + std::wstring GetPackageFullNameWide(); std::string GetPackageFullName(); // Gets a value indicating whether the referenced info is newer than the given manifest. diff --git a/src/AppInstallerCommonCore/Public/AppInstallerSHA256.h b/src/AppInstallerCommonCore/Public/AppInstallerSHA256.h @@ -19,6 +19,8 @@ namespace AppInstaller::Utility { { public: using HashBuffer = std::vector<uint8_t>; + constexpr static size_t HashBufferSizeInBytes = 32; + constexpr static size_t HashStringSizeInChars = 64; SHA256(); diff --git a/src/AppInstallerCommonCore/Public/winget/Manifest.h b/src/AppInstallerCommonCore/Public/winget/Manifest.h @@ -2,6 +2,7 @@ // Licensed under the MIT License. #pragma once #include <AppInstallerStrings.h> +#include <AppInstallerSHA256.h> #include <winget/ManifestInstaller.h> #include <winget/ManifestLocalization.h> @@ -43,5 +44,8 @@ namespace AppInstaller::Manifest // Get all commands across installers std::vector<string_t> GetAggregatedCommands() const; + + // If not empty, the SHA256 hash of the manifest stream itself. + Utility::SHA256::HashBuffer StreamSha256; }; } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestYamlParser.h b/src/AppInstallerCommonCore/Public/winget/ManifestYamlParser.h @@ -4,6 +4,7 @@ #include <winget/ManifestValidation.h> #include <winget/Manifest.h> #include <winget/Yaml.h> +#include <AppInstallerSHA256.h> #include <filesystem> @@ -17,6 +18,9 @@ namespace AppInstaller::Manifest::YamlParser // File name of the manifest file if applicable for error reporting std::string FileName; + // The SHA256 hash of the stream + Utility::SHA256::HashBuffer StreamSha256; + ManifestTypeEnum ManifestType = ManifestTypeEnum::Preview; }; diff --git a/src/AppInstallerCommonCore/Public/winget/Yaml.h b/src/AppInstallerCommonCore/Public/winget/Yaml.h @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include <AppInstallerSHA256.h> + #include <fstream> #include <map> #include <memory> @@ -155,6 +157,7 @@ namespace AppInstaller::YAML Node Load(std::string_view input); Node Load(const std::string& input); Node Load(const std::filesystem::path& input); + Node Load(const std::filesystem::path& input, Utility::SHA256::HashBuffer& hashOut); // Any emitter event. // Not using enum class to enable existing code to function. diff --git a/src/AppInstallerCommonCore/SHA256.cpp b/src/AppInstallerCommonCore/SHA256.cpp @@ -91,26 +91,26 @@ namespace AppInstaller::Utility { std::string SHA256::ConvertToString(const HashBuffer& hashBuffer) { - if (hashBuffer.size() != 32) + if (hashBuffer.size() != HashBufferSizeInBytes) { THROW_HR_MSG(E_INVALIDARG, "Invalid SHA256 size when SHA256::ConvertToString() is called."); } - char resultBuffer[65]; + char resultBuffer[HashStringSizeInChars + 1]; - for (size_t i = 0; i < 32; i++) + for (size_t i = 0; i < HashBufferSizeInBytes; i++) { sprintf_s(resultBuffer + i * 2, 3, "%02x", hashBuffer[i]); } - resultBuffer[64] = '\0'; + resultBuffer[HashStringSizeInChars] = '\0'; return std::string(resultBuffer); } std::vector<uint8_t> SHA256::ConvertToBytes(const std::string& hashStr) { - if (hashStr.size() != 64) + if (hashStr.size() != HashStringSizeInChars) { THROW_HR_MSG(E_INVALIDARG, "Invalid SHA256 size when SHA256::ConvertToBytes() is called."); } @@ -118,9 +118,9 @@ namespace AppInstaller::Utility { auto hashCStr = hashStr.c_str(); std::vector<uint8_t> resultBuffer; - resultBuffer.resize(32); + resultBuffer.resize(HashBufferSizeInBytes); - for (size_t i = 0; i < 32; i++) + for (size_t i = 0; i < HashBufferSizeInBytes; i++) { sscanf_s(hashCStr + 2 * i, "%02hhx", &resultBuffer[i]); } diff --git a/src/AppInstallerCommonCore/Yaml.cpp b/src/AppInstallerCommonCore/Yaml.cpp @@ -286,9 +286,9 @@ namespace AppInstaller::YAML return Load(static_cast<std::string_view>(input)); } - Node Load(std::istream& input) + Node Load(std::istream& input, Utility::SHA256::HashBuffer* hashOut) { - Wrapper::Parser parser(input); + Wrapper::Parser parser(input, hashOut); Wrapper::Document document = parser.Load(); if (document.HasRoot()) @@ -301,11 +301,21 @@ namespace AppInstaller::YAML } } - Node Load(const std::filesystem::path& input) + Node Load(const std::filesystem::path& input, Utility::SHA256::HashBuffer* hashOut) { std::ifstream stream(input, std::ios_base::in | std::ios_base::binary); THROW_LAST_ERROR_IF(stream.fail()); - return Load(stream); + return Load(stream, hashOut); + } + + Node Load(const std::filesystem::path& input) + { + return Load(input, nullptr); + } + + Node Load(const std::filesystem::path& input, Utility::SHA256::HashBuffer& hashOut) + { + return Load(input, &hashOut); } Emitter::Emitter() : diff --git a/src/AppInstallerCommonCore/YamlWrapper.cpp b/src/AppInstallerCommonCore/YamlWrapper.cpp @@ -247,12 +247,17 @@ namespace AppInstaller::YAML::Wrapper yaml_parser_set_input_string(&m_parser, reinterpret_cast<const unsigned char*>(m_input.c_str()), m_input.size()); } - Parser::Parser(std::istream& input) : m_token(true) + Parser::Parser(std::istream& input, Utility::SHA256::HashBuffer* hashOut) : m_token(true) { THROW_HR_IF(APPINSTALLER_CLI_ERROR_YAML_INIT_FAILED, !yaml_parser_initialize(&m_parser)); m_input = Utility::ReadEntireStream(input); + if (hashOut) + { + *hashOut = Utility::SHA256::ComputeHash(reinterpret_cast<const uint8_t*>(m_input.data()), static_cast<uint32_t>(m_input.size())); + } + PrepareInput(); yaml_parser_set_input_string(&m_parser, reinterpret_cast<const unsigned char*>(m_input.c_str()), m_input.size()); } diff --git a/src/AppInstallerCommonCore/YamlWrapper.h b/src/AppInstallerCommonCore/YamlWrapper.h @@ -4,6 +4,7 @@ #include <yaml.h> #include "winget/Yaml.h" #include "AppInstallerLanguageUtilities.h" +#include "AppInstallerSHA256.h" #include <iostream> #include <string_view> @@ -67,7 +68,7 @@ namespace AppInstaller::YAML::Wrapper struct Parser { Parser(std::string_view input); - Parser(std::istream& input); + Parser(std::istream& input, Utility::SHA256::HashBuffer* hashOut = nullptr); Parser(const Parser&) = delete; Parser& operator=(const Parser&) = delete; diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -251,6 +251,8 @@ <ClInclude Include="Microsoft\Schema\1_2\NormalizedPackageNameTable.h" /> <ClInclude Include="Microsoft\Schema\1_2\NormalizedPackagePublisherTable.h" /> <ClInclude Include="Microsoft\Schema\1_2\SearchResultsTable.h" /> + <ClInclude Include="Microsoft\Schema\1_3\HashVirtualTable.h" /> + <ClInclude Include="Microsoft\Schema\1_3\Interface.h" /> <ClInclude Include="Microsoft\Schema\ISQLiteIndex.h" /> <ClInclude Include="Microsoft\Schema\MetadataTable.h" /> <ClInclude Include="Microsoft\Schema\Version.h" /> @@ -304,6 +306,7 @@ <ClCompile Include="Microsoft\Schema\1_1\SearchResultsTable_1_1.cpp" /> <ClCompile Include="Microsoft\Schema\1_2\Interface_1_2.cpp" /> <ClCompile Include="Microsoft\Schema\1_2\SearchResultsTable_1_2.cpp" /> + <ClCompile Include="Microsoft\Schema\1_3\Interface_1_3.cpp" /> <ClCompile Include="Microsoft\Schema\MetadataTable.cpp" /> <ClCompile Include="Microsoft\Schema\Version.cpp" /> <ClCompile Include="Microsoft\SQLiteIndex.cpp" /> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -46,6 +46,9 @@ <Filter Include="Rest\Schema\1_0\Json"> <UniqueIdentifier>{6bcbaf7a-289f-4d0b-b128-67bef903745c}</UniqueIdentifier> </Filter> + <Filter Include="Microsoft\Schema\1_3"> + <UniqueIdentifier>{15639b2c-ce61-4a18-995a-a73cf1a5817e}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h"> @@ -207,6 +210,12 @@ <ClInclude Include="Rest\Schema\CommonRestConstants.h"> <Filter>Rest\Schema</Filter> </ClInclude> + <ClInclude Include="Microsoft\Schema\1_3\Interface.h"> + <Filter>Microsoft\Schema\1_3</Filter> + </ClInclude> + <ClInclude Include="Microsoft\Schema\1_3\HashVirtualTable.h"> + <Filter>Microsoft\Schema\1_3</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -317,6 +326,9 @@ <ClCompile Include="Rest\Schema\JsonHelper.cpp"> <Filter>Rest\Schema</Filter> </ClCompile> + <ClCompile Include="Microsoft\Schema\1_3\Interface_1_3.cpp"> + <Filter>Microsoft\Schema\1_3</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp @@ -90,7 +90,7 @@ namespace AppInstaller::Repository::Microsoft auto lock = Synchronization::CrossProcessReaderWriteLock::LockForWrite(CreateNameForCPRWL(details)); - UpdateInternal(packageLocation, details, progress); + UpdateInternal(packageLocation, packageInfo, details, progress); } void Update(const SourceDetails& details, IProgressCallback& progress) override final @@ -98,13 +98,27 @@ namespace AppInstaller::Repository::Microsoft THROW_HR_IF(E_INVALIDARG, details.Type != PreIndexedPackageSourceFactory::Type()); std::string packageLocation = GetPackageLocation(details); + Msix::MsixInfo packageInfo(packageLocation); + + // The package should not be a bundle + THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, packageInfo.GetIsBundle()); + + // Ensure that family name has not changed + THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE, + GetPackageFamilyNameFromDetails(details) != Msix::GetPackageFamilyNameFromFullName(packageInfo.GetPackageFullName())); + + if (progress.IsCancelled()) + { + AICLI_LOG(Repo, Info, << "Cancelling update upon request"); + return; + } auto lock = Synchronization::CrossProcessReaderWriteLock::LockForWrite(CreateNameForCPRWL(details)); - UpdateInternal(packageLocation, details, progress); + UpdateInternal(packageLocation, packageInfo, details, progress); } - virtual void UpdateInternal(std::string packageLocation, const SourceDetails& details, IProgressCallback& progress) = 0; + virtual void UpdateInternal(const std::string& packageLocation, Msix::MsixInfo& packageInfo, const SourceDetails& details, IProgressCallback& progress) = 0; void Remove(const SourceDetails& details, IProgressCallback& progress) override final { @@ -152,22 +166,13 @@ namespace AppInstaller::Repository::Microsoft return std::make_shared<SQLiteIndexSource>(details, GetPackageFamilyNameFromDetails(details), std::move(index), std::move(lock)); } - void UpdateInternal(std::string packageLocation, const SourceDetails& details, IProgressCallback& progress) override + void UpdateInternal(const std::string& packageLocation, Msix::MsixInfo& packageInfo, const SourceDetails& details, IProgressCallback& progress) override { // Check if the package is newer before calling into deployment. // This can save us a lot of time over letting deployment detect same version. auto extension = GetExtensionFromDetails(details); if (extension) { - Msix::MsixInfo packageInfo(packageLocation); - THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, packageInfo.GetIsBundle()); - - if (progress.IsCancelled()) - { - AICLI_LOG(Repo, Info, << "Cancelling update upon request"); - return; - } - if (!packageInfo.IsNewerThan(extension->GetPackageVersion())) { AICLI_LOG(Repo, Info, << "Remote source data was not newer than existing, no update needed"); @@ -204,13 +209,35 @@ namespace AppInstaller::Repository::Microsoft Deployment::AddPackage( uri, winrt::Windows::Management::Deployment::DeploymentOptions::None, - SourceTrustLevel::Trusted == details.TrustLevel, + WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::Trusted), progress); if (download) { - // If successful, delete the file - std::filesystem::remove(tempFile); + try + { + // If successful, delete the file + std::filesystem::remove(tempFile); + } + CATCH_LOG(); + } + + // Ensure origin if necessary + // TODO: Move to checking this before deploying it. That requires significant code to be written though + // as there is no public API to check the origin directly. + if (WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::StoreOrigin)) + { + std::wstring pfn = packageInfo.GetPackageFullNameWide(); + + PackageOrigin origin = PackageOrigin::PackageOrigin_Unknown; + if (SUCCEEDED_WIN32_LOG(GetStagedPackageOrigin(pfn.c_str(), &origin))) + { + if (origin != PackageOrigin::PackageOrigin_Store) + { + Deployment::RemovePackage(Utility::ConvertToUTF8(pfn), progress); + THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE); + } + } } } @@ -260,21 +287,12 @@ namespace AppInstaller::Repository::Microsoft return std::make_shared<SQLiteIndexSource>(details, GetPackageFamilyNameFromDetails(details), std::move(index), std::move(lock)); } - void UpdateInternal(std::string packageLocation, const SourceDetails& details, IProgressCallback& progress) override + void UpdateInternal(const std::string&, Msix::MsixInfo& packageInfo, const SourceDetails& details, IProgressCallback& progress) override { // We will extract the manifest and index files directly to this location std::filesystem::path packageState = GetStatePathFromDetails(details); std::filesystem::create_directories(packageState); - Msix::MsixInfo packageInfo(packageLocation); - THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, packageInfo.GetIsBundle()); - - if (progress.IsCancelled()) - { - AICLI_LOG(Repo, Info, << "Cancelling update upon request"); - return; - } - std::filesystem::path manifestPath = packageState / s_PreIndexedPackageSourceFactory_AppxManifestFileName; std::filesystem::path indexPath = packageState / s_PreIndexedPackageSourceFactory_IndexFileName; diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp @@ -6,10 +6,11 @@ #include <winget/ManifestYamlParser.h> +using namespace AppInstaller::Utility; + + namespace AppInstaller::Repository::Microsoft { - using namespace Utility; - namespace { // The base for the package objects. @@ -67,9 +68,18 @@ namespace AppInstaller::Repository::Microsoft Manifest::Manifest GetManifest() override { std::shared_ptr<const SQLiteIndexSource> source = GetReferenceSource(); + std::optional<std::string> relativePathOpt = source->GetIndex().GetPropertyByManifestId(m_manifestId, PackageVersionProperty::RelativePath); THROW_HR_IF(E_NOT_SET, !relativePathOpt); - return GetManifestFromArgAndRelativePath(source->GetDetails().Arg, relativePathOpt.value()); + + std::optional<std::string> manifestHashString = source->GetIndex().GetPropertyByManifestId(m_manifestId, PackageVersionProperty::ManifestSHA256Hash); + SHA256::HashBuffer manifestSHA256; + if (manifestHashString) + { + manifestSHA256 = SHA256::ConvertToBytes(manifestHashString.value()); + } + + return GetManifestFromArgAndRelativePath(source->GetDetails().Arg, relativePathOpt.value(), manifestSHA256); } std::shared_ptr<const ISource> GetSource() const override @@ -91,7 +101,7 @@ namespace AppInstaller::Repository::Microsoft } private: - static Manifest::Manifest GetManifestFromArgAndRelativePath(const std::string& arg, const std::string& relativePath) + static Manifest::Manifest GetManifestFromArgAndRelativePath(const std::string& arg, const std::string& relativePath, const SHA256::HashBuffer& expectedHash) { std::string fullPath = arg; if (fullPath.back() != '/') @@ -113,7 +123,14 @@ namespace AppInstaller::Repository::Microsoft bool success = false; try { - (void)Utility::DownloadToStream(fullPath, manifestStream, Utility::DownloadType::Manifest, emptyCallback); + auto downloadHash = Utility::DownloadToStream(fullPath, manifestStream, Utility::DownloadType::Manifest, emptyCallback, !expectedHash.empty()); + + if (!expectedHash.empty() && + (!downloadHash || downloadHash->size() != expectedHash.size() || !std::equal(expectedHash.begin(), expectedHash.end(), downloadHash->begin()))) + { + THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE); + } + success = true; } catch (...) @@ -143,7 +160,15 @@ namespace AppInstaller::Repository::Microsoft else { AICLI_LOG(Repo, Info, << "Opening manifest from local file: " << fullPath); - return Manifest::YamlParser::CreateFromPath(fullPath); + Manifest::Manifest result = Manifest::YamlParser::CreateFromPath(fullPath); + + if (!expectedHash.empty() && + (result.StreamSha256.size() != expectedHash.size() || !std::equal(expectedHash.begin(), expectedHash.end(), result.StreamSha256.begin()))) + { + THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE); + } + + return result; } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.h @@ -43,5 +43,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Executes all relevant searches for the query. virtual void PerformQuerySearch(SearchResultsTable& resultsTable, const RequestMatch& query) const; + + // Gets a property already knowing that the manifest id is valid. + virtual std::optional<std::string> GetPropertyByManifestIdInternal(const SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionProperty property) const; }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface_1_0.cpp @@ -498,21 +498,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 return {}; } - switch (property) - { - case AppInstaller::Repository::PackageVersionProperty::Id: - return std::get<0>(ManifestTable::GetValuesById<IdTable>(connection, manifestId)); - case AppInstaller::Repository::PackageVersionProperty::Name: - return std::get<0>(ManifestTable::GetValuesById<NameTable>(connection, manifestId)); - case AppInstaller::Repository::PackageVersionProperty::Version: - return std::get<0>(ManifestTable::GetValuesById<VersionTable>(connection, manifestId)); - case AppInstaller::Repository::PackageVersionProperty::Channel: - return std::get<0>(ManifestTable::GetValuesById<ChannelTable>(connection, manifestId)); - case AppInstaller::Repository::PackageVersionProperty::RelativePath: - return PathPartTable::GetPathById(connection, std::get<0>(ManifestTable::GetIdsById<PathPartTable>(connection, manifestId))); - default: - return {}; - } + return GetPropertyByManifestIdInternal(connection, manifestId, property); } std::vector<std::string> Interface::GetMultiPropertyByManifestId(const SQLite::Connection&, SQLite::rowid_t, PackageVersionMultiProperty) const @@ -602,4 +588,23 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 } } } + + std::optional<std::string> Interface::GetPropertyByManifestIdInternal(const SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionProperty property) const + { + switch (property) + { + case AppInstaller::Repository::PackageVersionProperty::Id: + return std::get<0>(ManifestTable::GetValuesById<IdTable>(connection, manifestId)); + case AppInstaller::Repository::PackageVersionProperty::Name: + return std::get<0>(ManifestTable::GetValuesById<NameTable>(connection, manifestId)); + case AppInstaller::Repository::PackageVersionProperty::Version: + return std::get<0>(ManifestTable::GetValuesById<VersionTable>(connection, manifestId)); + case AppInstaller::Repository::PackageVersionProperty::Channel: + return std::get<0>(ManifestTable::GetValuesById<ChannelTable>(connection, manifestId)); + case AppInstaller::Repository::PackageVersionProperty::RelativePath: + return PathPartTable::GetPathById(connection, std::get<0>(ManifestTable::GetIdsById<PathPartTable>(connection, manifestId))); + default: + return {}; + } + } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.cpp @@ -253,12 +253,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 return result; } - void ManifestTableUpdateValueIdById(SQLite::Connection& connection, std::string_view valueName, SQLite::rowid_t value, SQLite::rowid_t id) + SQLite::Statement ManifestTableUpdateValueIdById_Statement(SQLite::Connection& connection, std::string_view valueName) { SQLite::Builder::StatementBuilder builder; - builder.Update(s_ManifestTable_Table_Name).Set().Column(valueName).Equals(value).Where(SQLite::RowIDName).Equals(id); + builder.Update(s_ManifestTable_Table_Name).Set().Column(valueName).Equals(SQLite::Builder::Unbound).Where(SQLite::RowIDName).Equals(SQLite::Builder::Unbound); - builder.Execute(connection); + return builder.Prepare(connection); } bool ManifestTableCheckConsistency(const SQLite::Connection& connection, const SQLite::Builder::QualifiedColumn& target, bool log) @@ -361,6 +361,20 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 savepoint.Commit(); } + void ManifestTable::AddColumn(SQLite::Connection& connection, AddedColumnInfo value) + { + using namespace SQLite::Builder; + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "addColumnManifestTable_v1_3"); + + StatementBuilder alterTableBuilder; + alterTableBuilder.AlterTable(s_ManifestTable_Table_Name).Add(value.Name, value.Type); + + alterTableBuilder.Execute(connection); + + savepoint.Commit(); + } + void ManifestTable::Create_deprecated(SQLite::Connection& connection, std::initializer_list<ManifestColumnInfo> values) { using namespace SQLite::Builder; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.h @@ -54,8 +54,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 std::string_view valueAlias, bool useLike); - // Update the value of a single column for the manifest with the given rowid. - void ManifestTableUpdateValueIdById(SQLite::Connection& connection, std::string_view valueName, SQLite::rowid_t value, SQLite::rowid_t id); + // Prepares a statement to update the value of a single column for the manifest with the given rowid. + // The first bind value will be the value to set. + // The second bind value will be the manifest rowid to modify. + SQLite::Statement ManifestTableUpdateValueIdById_Statement(SQLite::Connection& connection, std::string_view valueName); // Checks the consistency of the index to ensure that every referenced row exists. // Returns true if index is consistent; false if it is not. @@ -70,6 +72,13 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 bool Unique; }; + // Information on a column being added via ALTER TABLE + struct AddedColumnInfo + { + std::string_view Name; + SQLite::Builder::Type Type; + }; + // A value that is 1:1 with the manifest. struct ManifestOneToOneValue { @@ -86,6 +95,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Creates the table with named indices. static void Create(SQLite::Connection& connection, std::initializer_list<ManifestColumnInfo> values); + // Alters the table, adding the columns provided. + static void AddColumn(SQLite::Connection& connection, AddedColumnInfo value); + // Creates the table with standard primary keys. static void Create_deprecated(SQLite::Connection& connection, std::initializer_list<ManifestColumnInfo> values); @@ -148,9 +160,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Update the value of a single column for the manifest with the given rowid. template <typename Table> - static void UpdateValueIdById(SQLite::Connection& connection, SQLite::rowid_t id, SQLite::rowid_t value) + static void UpdateValueIdById(SQLite::Connection& connection, SQLite::rowid_t id, const typename Table::id_t& value) { - details::ManifestTableUpdateValueIdById(connection, Table::ValueName(), value, id); + auto stmt = details::ManifestTableUpdateValueIdById_Statement(connection, Table::ValueName()); + stmt.Bind(1, value); + stmt.Bind(2, id); + stmt.Execute(); } // Deletes the manifest row with the given rowid. diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_3/HashVirtualTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_3/HashVirtualTable.h @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "SQLiteStatementBuilder.h" + +#include <string_view> + +using namespace std::string_view_literals; + + +namespace AppInstaller::Repository::Microsoft::Schema::V1_3 +{ + // A virtual table used to add a direct column onto the manifest table. + struct HashVirtualTable + { + // The id type (which is actually the value for this virtual table) + using id_t = SQLite::blob_t; + + // The name of the column. + static constexpr std::string_view ValueName() + { + return "hash"sv; + } + + // The name of the column. + static constexpr SQLite::Builder::Type SQLiteType() + { + return SQLite::Builder::Type::Blob; + } + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_3/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_3/Interface.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/Schema/ISQLiteIndex.h" +#include "Microsoft/Schema/1_2/Interface.h" + + +namespace AppInstaller::Repository::Microsoft::Schema::V1_3 +{ + // Interface to this schema version exposed through ISQLiteIndex. + struct Interface : public V1_2::Interface + { + Interface(Utility::NormalizationVersion normVersion = Utility::NormalizationVersion::Initial); + + // Version 1.0 + Schema::Version GetVersion() const override; + void CreateTables(SQLite::Connection& connection) override; + SQLite::rowid_t AddManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) override; + std::pair<bool, SQLite::rowid_t> UpdateManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) override; + + protected: + // Gets a property already knowing that the manifest id is valid. + std::optional<std::string> GetPropertyByManifestIdInternal(const SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionProperty property) const override; + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_3/Interface_1_3.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_3/Interface_1_3.cpp @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Microsoft/Schema/1_3/Interface.h" +#include <AppInstallerSHA256.h> + +#include "Microsoft/Schema/1_0/ManifestTable.h" + +#include "Microsoft/Schema/1_3/HashVirtualTable.h" + + +namespace AppInstaller::Repository::Microsoft::Schema::V1_3 +{ + Interface::Interface(Utility::NormalizationVersion normVersion) : V1_2::Interface(normVersion) + { + } + + Schema::Version Interface::GetVersion() const + { + return { 1, 3 }; + } + + void Interface::CreateTables(SQLite::Connection& connection) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createtables_v1_3"); + + V1_2::Interface::CreateTables(connection); + + V1_0::ManifestTable::AddColumn(connection, { HashVirtualTable::ValueName(), HashVirtualTable::SQLiteType() }); + + savepoint.Commit(); + } + + SQLite::rowid_t Interface::AddManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "addmanifest_v1_3"); + + SQLite::rowid_t manifestId = V1_2::Interface::AddManifest(connection, manifest, relativePath); + + // Set the hash value if provided + if (!manifest.StreamSha256.empty()) + { + THROW_HR_IF(E_INVALIDARG, manifest.StreamSha256.size() != Utility::SHA256::HashBufferSizeInBytes); + V1_0::ManifestTable::UpdateValueIdById<HashVirtualTable>(connection, manifestId, manifest.StreamSha256); + } + + savepoint.Commit(); + + return manifestId; + } + + std::pair<bool, SQLite::rowid_t> Interface::UpdateManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "updatemanifest_v1_3"); + + auto [indexModified, manifestId] = V1_2::Interface::UpdateManifest(connection, manifest, relativePath); + + // Set the hash value if provided + if (!manifest.StreamSha256.empty()) + { + THROW_HR_IF(E_INVALIDARG, manifest.StreamSha256.size() != Utility::SHA256::HashBufferSizeInBytes); + + auto currentHash = std::get<0>(V1_0::ManifestTable::GetIdsById<HashVirtualTable>(connection, manifestId)); + + if (currentHash.size() != Utility::SHA256::HashBufferSizeInBytes || + !std::equal(currentHash.begin(), currentHash.end(), manifest.StreamSha256.begin())) + { + V1_0::ManifestTable::UpdateValueIdById<HashVirtualTable>(connection, manifestId, manifest.StreamSha256); + indexModified = true; + } + } + + savepoint.Commit(); + + return { indexModified, manifestId }; + } + + std::optional<std::string> Interface::GetPropertyByManifestIdInternal(const SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionProperty property) const + { + switch (property) + { + case AppInstaller::Repository::PackageVersionProperty::ManifestSHA256Hash: + { + SQLite::blob_t hash = std::get<0>(V1_0::ManifestTable::GetIdsById<HashVirtualTable>(connection, manifestId)); + return hash.empty() ? std::optional<std::string>{} : Utility::SHA256::ConvertToString(hash); + } + default: + return V1_2::Interface::GetPropertyByManifestIdInternal(connection, manifestId, property); + } + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp @@ -7,6 +7,7 @@ #include "1_0/Interface.h" #include "1_1/Interface.h" #include "1_2/Interface.h" +#include "1_3/Interface.h" namespace AppInstaller::Repository::Microsoft::Schema { @@ -39,11 +40,15 @@ namespace AppInstaller::Repository::Microsoft::Schema { return std::make_unique<V1_1::Interface>(); } - else if (*this == Version{ 1, 2 } || + else if (*this == Version{ 1, 2 }) + { + return std::make_unique<V1_2::Interface>(); + } + else if (*this == Version{ 1, 3 } || this->MajorVersion == 1 || this->IsLatest()) { - return std::make_unique<V1_2::Interface>(); + return std::make_unique<V1_3::Interface>(); } // We do not have the capacity to operate on this schema version diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h @@ -120,6 +120,8 @@ namespace AppInstaller::Repository Version, Channel, RelativePath, + // Returned in hexadecimal format + ManifestSHA256Hash, }; // A property of a package version that can have multiple values. diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h @@ -25,12 +25,15 @@ namespace AppInstaller::Repository }; // Defines the trust level of the source. - enum class SourceTrustLevel + enum class SourceTrustLevel : uint32_t { - None, - Trusted, + None = 0x00000000, + Trusted = 0x00000001, + StoreOrigin = 0x00000002, }; + DEFINE_ENUM_FLAG_OPERATORS(SourceTrustLevel); + std::string_view ToString(SourceOrigin origin); // Interface for retrieving information about a source without acting on it. diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -402,7 +402,7 @@ namespace AppInstaller::Repository details.Arg = s_Source_WingetCommunityDefault_Arg; details.Data = s_Source_WingetCommunityDefault_Data; details.Identifier = s_Source_WingetCommunityDefault_Identifier; - details.TrustLevel = SourceTrustLevel::Trusted; + details.TrustLevel = SourceTrustLevel::Trusted | SourceTrustLevel::StoreOrigin; result.emplace_back(std::move(details)); } @@ -414,7 +414,7 @@ namespace AppInstaller::Repository details.Arg = s_Source_WingetMSStoreDefault_Arg; details.Data = s_Source_WingetMSStoreDefault_Data; details.Identifier = s_Source_WingetMSStoreDefault_Identifier; - details.TrustLevel = SourceTrustLevel::Trusted; + details.TrustLevel = SourceTrustLevel::Trusted | SourceTrustLevel::StoreOrigin; result.emplace_back(std::move(details)); } } diff --git a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp @@ -137,6 +137,9 @@ namespace AppInstaller::Repository::SQLite::Builder case Type::Text: out << "TEXT"; break; + case Type::Blob: + out << "BLOB"; + break; default: THROW_HR(E_UNEXPECTED); } @@ -605,6 +608,31 @@ namespace AppInstaller::Repository::SQLite::Builder return *this; } + StatementBuilder& StatementBuilder::AlterTable(std::string_view table) + { + OutputOperationAndTable(m_stream, "ALTER TABLE", table); + return *this; + } + + StatementBuilder& StatementBuilder::AlterTable(QualifiedTable table) + { + OutputOperationAndTable(m_stream, "ALTER TABLE", table); + return *this; + } + + StatementBuilder& StatementBuilder::AlterTable(std::initializer_list<std::string_view> table) + { + OutputOperationAndTable(m_stream, "ALTER TABLE", table); + return *this; + } + + StatementBuilder& StatementBuilder::Add(std::string_view column, Type type) + { + m_stream << " ADD " << column; + OutputType(m_stream, type); + return *this; + } + StatementBuilder& StatementBuilder::DropTable(std::string_view table) { OutputOperationAndTable(m_stream, "DROP TABLE", table); diff --git a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h @@ -107,6 +107,7 @@ namespace AppInstaller::Repository::SQLite::Builder Int64, RowId = Int64, Text, + Blob, }; // Aggregate functions. @@ -328,6 +329,15 @@ namespace AppInstaller::Repository::SQLite::Builder StatementBuilder& CreateTable(QualifiedTable table); StatementBuilder& CreateTable(std::initializer_list<std::string_view> table); + // Begin an alter table statement. + // The initializer_list form enables the table name to be constructed from multiple parts. + StatementBuilder& AlterTable(std::string_view table); + StatementBuilder& AlterTable(QualifiedTable table); + StatementBuilder& AlterTable(std::initializer_list<std::string_view> table); + + // Complete an alter table statement by adding a column. + StatementBuilder& Add(std::string_view column, Type type); + // Begin an table deletion statement. // The initializer_list form enables the table name to be constructed from multiple parts. StatementBuilder& DropTable(std::string_view table); diff --git a/src/AppInstallerRepositoryCore/SQLiteWrapper.cpp b/src/AppInstallerRepositoryCore/SQLiteWrapper.cpp @@ -94,6 +94,32 @@ namespace AppInstaller::Repository::SQLite { return (sqlite3_column_int(stmt, column) != 0); } + + std::string ParameterSpecificsImpl<blob_t>::ToLog(const blob_t& v) + { + std::ostringstream strstr; + strstr << "blob[" << v.size() << "]"; + return strstr.str(); + } + + void ParameterSpecificsImpl<blob_t>::Bind(sqlite3_stmt* stmt, int index, const blob_t& v) + { + THROW_IF_SQLITE_FAILED(sqlite3_bind_blob64(stmt, index, v.data(), v.size(), SQLITE_TRANSIENT)); + } + + blob_t ParameterSpecificsImpl<blob_t>::GetColumn(sqlite3_stmt* stmt, int column) + { + const blob_t::value_type* blobPtr = reinterpret_cast<const blob_t::value_type *>(sqlite3_column_blob(stmt, column)); + if (blobPtr) + { + int blobBytes = sqlite3_column_bytes(stmt, column); + return blob_t{ blobPtr, blobPtr + blobBytes }; + } + else + { + return {}; + } + } } Connection::Connection(const std::string& target, OpenDisposition disposition, OpenFlags flags) diff --git a/src/AppInstallerRepositoryCore/SQLiteWrapper.h b/src/AppInstallerRepositoryCore/SQLiteWrapper.h @@ -13,9 +13,12 @@ #include <tuple> #include <type_traits> #include <utility> +#include <vector> #define SQLITE_MEMORY_DB_CONNECTION_TARGET ":memory:" +using namespace std::string_view_literals; + namespace AppInstaller::Repository::SQLite { // The name of the rowid column in SQLite. @@ -24,11 +27,18 @@ namespace AppInstaller::Repository::SQLite // The type of a rowid column in code. using rowid_t = int64_t; + // The type to use for blob data. + using blob_t = std::vector<uint8_t>; + namespace details { template <typename T, typename = void> struct ParameterSpecificsImpl { + static T& ToLog(T&&) + { + static_assert(false, "No type specific override has been supplied"); + } static void Bind(sqlite3_stmt*, int, T&&) { static_assert(false, "No type specific override has been supplied"); @@ -42,12 +52,14 @@ namespace AppInstaller::Repository::SQLite template <> struct ParameterSpecificsImpl<nullptr_t> { + inline static std::string_view ToLog(nullptr_t) { return "null"sv; } static void Bind(sqlite3_stmt* stmt, int index, nullptr_t); }; template <> struct ParameterSpecificsImpl<std::string> { + inline static const std::string& ToLog(const std::string& v) { return v; } static void Bind(sqlite3_stmt* stmt, int index, const std::string& v); static std::string GetColumn(sqlite3_stmt* stmt, int column); }; @@ -55,12 +67,14 @@ namespace AppInstaller::Repository::SQLite template <> struct ParameterSpecificsImpl<std::string_view> { + inline static const std::string_view& ToLog(const std::string_view& v) { return v; } static void Bind(sqlite3_stmt* stmt, int index, std::string_view v); }; template <> struct ParameterSpecificsImpl<int> { + inline static int ToLog(int v) { return v; } static void Bind(sqlite3_stmt* stmt, int index, int v); static int GetColumn(sqlite3_stmt* stmt, int column); }; @@ -68,6 +82,7 @@ namespace AppInstaller::Repository::SQLite template <> struct ParameterSpecificsImpl<int64_t> { + inline static int64_t ToLog(int64_t v) { return v; } static void Bind(sqlite3_stmt* stmt, int index, int64_t v); static int64_t GetColumn(sqlite3_stmt* stmt, int column); }; @@ -75,13 +90,26 @@ namespace AppInstaller::Repository::SQLite template <> struct ParameterSpecificsImpl<bool> { + inline static bool ToLog(bool v) { return v; } static void Bind(sqlite3_stmt* stmt, int index, bool v); static bool GetColumn(sqlite3_stmt* stmt, int column); }; + template <> + struct ParameterSpecificsImpl<blob_t> + { + static std::string ToLog(const blob_t& v); + static void Bind(sqlite3_stmt* stmt, int index, const blob_t& v); + static blob_t GetColumn(sqlite3_stmt* stmt, int column); + }; + template <typename E> struct ParameterSpecificsImpl<E, typename std::enable_if_t<std::is_enum_v<E>>> { + static auto ToLog(E v) + { + return ToIntegral(v); + } static void Bind(sqlite3_stmt* stmt, int index, E v) { ParameterSpecificsImpl<std::underlying_type_t<E>>::Bind(stmt, index, ToIntegral(v)); @@ -192,7 +220,7 @@ namespace AppInstaller::Repository::SQLite template <typename Value> void Bind(int index, Value&& v) { - AICLI_LOG(SQL, Verbose, << "Binding statement #" << m_id << ": " << index << " => " << std::forward<Value>(v)); + AICLI_LOG(SQL, Verbose, << "Binding statement #" << m_id << ": " << index << " => " << details::ParameterSpecifics<Value>::ToLog(std::forward<Value>(v))); details::ParameterSpecifics<Value>::Bind(m_stmt.get(), index, std::forward<Value>(v)); } diff --git a/src/AppInstallerRepositoryCore/pch.h b/src/AppInstallerRepositoryCore/pch.h @@ -5,6 +5,7 @@ #define NOMINMAX #include <windows.h> #include <urlmon.h> +#include <appmodel.h> #pragma warning( push ) #pragma warning ( disable : 6001 6340 6388 ) diff --git a/src/WinGetUtil/WinGetUtil.vcxproj b/src/WinGetUtil/WinGetUtil.vcxproj @@ -185,9 +185,9 @@ <ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Source.def</ModuleDefinitionFile> <ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">Source.def</ModuleDefinitionFile> <ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">Source.def</ModuleDefinitionFile> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies)</AdditionalDependencies> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies)</AdditionalDependencies> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> <DelayLoadDLLs Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">winsqlite3.dll;icuuc.dll;icuin.dll;%(DelayLoadDLLs)</DelayLoadDLLs> <DelayLoadDLLs Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">winsqlite3.dll;icuuc.dll;icuin.dll;%(DelayLoadDLLs)</DelayLoadDLLs> <DelayLoadDLLs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">winsqlite3.dll;icuuc.dll;icuin.dll;%(DelayLoadDLLs)</DelayLoadDLLs> @@ -214,7 +214,7 @@ <Link> <SubSystem Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Windows</SubSystem> <ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Source.def</ModuleDefinitionFile> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> <DelayLoadDLLs Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">winsqlite3.dll;icuuc.dll;icuin.dll;%(DelayLoadDLLs)</DelayLoadDLLs> </Link> <Manifest> @@ -260,10 +260,10 @@ <ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Source.def</ModuleDefinitionFile> <ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">Source.def</ModuleDefinitionFile> <ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Source.def</ModuleDefinitionFile> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies)</AdditionalDependencies> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies)</AdditionalDependencies> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies)</AdditionalDependencies> - <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|x64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|x64'">wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> <DelayLoadDLLs Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">winsqlite3.dll;icuuc.dll;icuin.dll;%(DelayLoadDLLs)</DelayLoadDLLs> <DelayLoadDLLs Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">winsqlite3.dll;icuuc.dll;icuin.dll;%(DelayLoadDLLs)</DelayLoadDLLs> <DelayLoadDLLs Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">winsqlite3.dll;icuuc.dll;icuin.dll;%(DelayLoadDLLs)</DelayLoadDLLs>