Browse Source

Extension Loader legitness validator

Krzysztof Krysiński 2 years ago
parent
commit
9df0e401ec

+ 1 - 1
src/PixiEditor.Extensions/PixiEditor.Extensions.csproj

@@ -6,7 +6,7 @@
         <Nullable>enable</Nullable>
         <Nullable>enable</Nullable>
       <UseWPF>true</UseWPF>
       <UseWPF>true</UseWPF>
       <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
       <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
-      <Version>0.0.5</Version>
+      <Version>0.0.7</Version>
       <Title>PixiEditor Extensions</Title>
       <Title>PixiEditor Extensions</Title>
       <Authors>PixiEditor Organization</Authors>
       <Authors>PixiEditor Organization</Authors>
       <Copyright>PixiEditor Organization</Copyright>
       <Copyright>PixiEditor Organization</Copyright>

+ 14 - 0
src/PixiEditor/App.xaml.cs

@@ -11,6 +11,7 @@ using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Localization;
 using PixiEditor.Models.Localization;
+using PixiEditor.Platform;
 using PixiEditor.Views;
 using PixiEditor.Views;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Views.Dialogs;
 
 
@@ -52,6 +53,8 @@ internal partial class App : Application
 
 
         AddNativeAssets();
         AddNativeAssets();
 
 
+        IPlatform.RegisterPlatform(GetActivePlatform());
+
         ExtensionLoader extensionLoader = new ExtensionLoader();
         ExtensionLoader extensionLoader = new ExtensionLoader();
         extensionLoader.LoadExtensions();
         extensionLoader.LoadExtensions();
 
 
@@ -59,6 +62,17 @@ internal partial class App : Application
         MainWindow.Show();
         MainWindow.Show();
     }
     }
 
 
+    private IPlatform GetActivePlatform()
+    {
+#if STEAM
+        return new PixiEditor.Platform.Steam.SteamPlatform();
+#elif MSIX || MSIX_DEBUG
+        return new PixiEditor.Platform.MSStore.MicrosoftStorePlatform();
+#else
+        return new PixiEditor.Platform.Standalone.StandalonePlatform();
+#endif
+    }
+
     private void AddNativeAssets()
     private void AddNativeAssets()
     {
     {
         var iconFont = OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)
         var iconFont = OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)

+ 1 - 0
src/PixiEditor/Data/Localization/Languages/en.json

@@ -568,6 +568,7 @@
   "ERROR_MISSING_METADATA": "Extension metadata key '{0}' is missing.",
   "ERROR_MISSING_METADATA": "Extension metadata key '{0}' is missing.",
   "ERROR_NO_CLASS_ENTRY": "Extension class entry is missing on path '{0}'.",
   "ERROR_NO_CLASS_ENTRY": "Extension class entry is missing on path '{0}'.",
   "ERROR_NO_ENTRY_ASSEMBLY": "Extension entry assembly is missing on path '{0}'.",
   "ERROR_NO_ENTRY_ASSEMBLY": "Extension entry assembly is missing on path '{0}'.",
+  "ERROR_MISSING_ADDITIONAL_CONTENT": "Your current setup doesn't allow loading this extension. Perhaps you don't own it or don't have it installed. You can purchase it here '{0}'.",
 
 
   "AWESOME_SUPPORTER": "Awesome Supporter"
   "AWESOME_SUPPORTER": "Awesome Supporter"
 }
 }

+ 7 - 0
src/PixiEditor/Models/AppExtensions/ExtensionException.cs

@@ -39,3 +39,10 @@ public class ForbiddenUniqueNameExtension : ExtensionException
     }
     }
 }
 }
 
 
+public class MissingAdditionalContentException : ExtensionException
+{
+    public MissingAdditionalContentException(string productLink) : base(new LocalizedString("ERROR_MISSING_ADDITIONAL_CONTENT", productLink))
+    {
+    }
+}
+

+ 86 - 15
src/PixiEditor/Models/AppExtensions/ExtensionLoader.cs

@@ -1,7 +1,7 @@
 using System.IO;
 using System.IO;
 using System.Reflection;
 using System.Reflection;
+using System.Runtime.InteropServices;
 using System.Windows;
 using System.Windows;
-using AvalonDock.Layout;
 using Newtonsoft.Json;
 using Newtonsoft.Json;
 using PixiEditor.Extensions;
 using PixiEditor.Extensions;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
@@ -13,11 +13,13 @@ namespace PixiEditor.Models.AppExtensions;
 
 
 internal class ExtensionLoader
 internal class ExtensionLoader
 {
 {
+    private readonly Dictionary<string, OfficialExtensionData> _officialExtensionsKeys = new Dictionary<string, OfficialExtensionData>();
     public List<Extension> LoadedExtensions { get; } = new();
     public List<Extension> LoadedExtensions { get; } = new();
 
 
     public ExtensionLoader()
     public ExtensionLoader()
     {
     {
         ValidateExtensionFolder();
         ValidateExtensionFolder();
+        _officialExtensionsKeys.Add("pixieditor.supporterpack", new OfficialExtensionData("supporter-pack.snk", AdditionalContentProduct.SupporterPack));
     }
     }
 
 
     public void LoadExtensions()
     public void LoadExtensions()
@@ -48,8 +50,19 @@ internal class ExtensionLoader
         try
         try
         {
         {
             var metadata = JsonConvert.DeserializeObject<ExtensionMetadata>(json);
             var metadata = JsonConvert.DeserializeObject<ExtensionMetadata>(json);
-            ValidateMetadata(metadata);
-            var extension = LoadExtensionEntry(Path.GetDirectoryName(packageJsonPath), metadata);
+            string directory = Path.GetDirectoryName(packageJsonPath);
+            Assembly entry = GetEntryAssembly(directory, out Type extensionType);
+            if (entry is null)
+            {
+                throw new NoEntryAssemblyException(directory);
+            }
+
+            if (!ValidateMetadata(metadata, entry))
+            {
+                return;
+            }
+
+            var extension = LoadExtensionEntry(entry, extensionType, metadata);
             extension.Load();
             extension.Load();
             LoadedExtensions.Add(extension);
             LoadedExtensions.Add(extension);
         }
         }
@@ -67,19 +80,34 @@ internal class ExtensionLoader
         }
         }
     }
     }
 
 
-    private void ValidateMetadata(ExtensionMetadata metadata)
+    private Assembly? GetEntryAssembly(string assemblyFolder, out Type extensionType)
+    {
+        string[] dlls = Directory.GetFiles(assemblyFolder, "*.dll");
+        Assembly? entryAssembly = GetEntryAssembly(dlls, out extensionType);
+
+        return entryAssembly;
+    }
+
+    private bool ValidateMetadata(ExtensionMetadata metadata, Assembly assembly)
     {
     {
         if (string.IsNullOrEmpty(metadata.UniqueName))
         if (string.IsNullOrEmpty(metadata.UniqueName))
         {
         {
             throw new MissingMetadataException("Description");
             throw new MissingMetadataException("Description");
         }
         }
 
 
-        if (metadata.UniqueName.StartsWith("pixieditor".Trim(), StringComparison.OrdinalIgnoreCase))
+        string fixedUniqueName = metadata.UniqueName.ToLower().Trim();
+
+        if (fixedUniqueName.StartsWith("pixieditor".Trim(), StringComparison.OrdinalIgnoreCase))
         {
         {
-            if(!IsOfficialAssemblyLegit(metadata.UniqueName))
+            if(!IsOfficialAssemblyLegit(fixedUniqueName, assembly))
             {
             {
                 throw new ForbiddenUniqueNameExtension();
                 throw new ForbiddenUniqueNameExtension();
             }
             }
+
+            if (!IsAdditionalContentInstalled(fixedUniqueName))
+            {
+                return false;
+            }
         }
         }
         // TODO: Validate if unique name is unique
         // TODO: Validate if unique name is unique
 
 
@@ -92,22 +120,52 @@ internal class ExtensionLoader
         {
         {
             throw new MissingMetadataException("Version");
             throw new MissingMetadataException("Version");
         }
         }
+
+        return true;
     }
     }
 
 
-    private bool IsOfficialAssemblyLegit(string metadataUniqueName)
+    private bool IsAdditionalContentInstalled(string fixedUniqueName)
     {
     {
-        return true; //TODO: Perform assembly secret number check
+        if (!_officialExtensionsKeys.ContainsKey(fixedUniqueName)) return false;
+        AdditionalContentProduct? product = _officialExtensionsKeys[fixedUniqueName].Product;
+
+        if (product == null) return true;
+
+        return IPlatform.Current.AdditionalContentProvider?.IsContentAvailable(product.Value) ?? false;
     }
     }
 
 
-    private Extension LoadExtensionEntry(string assemblyFolder, ExtensionMetadata metadata)
+    private bool IsOfficialAssemblyLegit(string metadataUniqueName, Assembly assembly)
     {
     {
-        string[] dlls = Directory.GetFiles(assemblyFolder, "*.dll");
-        Assembly? entryAssembly = GetEntryAssembly(dlls, out Type extensionType);
-        if (entryAssembly is null)
-        {
-            throw new NoEntryAssemblyException(assemblyFolder);
-        }
+        if (assembly == null) return false; // All official extensions must have a valid assembly
+        if (!_officialExtensionsKeys.ContainsKey(metadataUniqueName)) return false;
+        bool wasVerified = false;
+        bool verified = StrongNameSignatureVerificationEx(assembly.Location, true, ref wasVerified);
+        if (!verified || !wasVerified) return false;
+
+        byte[]? assemblyPublicKey = assembly.GetName().GetPublicKey();
+        if (assemblyPublicKey == null) return false;
+
+        return PublicKeysMatch(assemblyPublicKey, _officialExtensionsKeys[metadataUniqueName].PublicKeyName);
+    }
+
+    private bool PublicKeysMatch(byte[] assemblyPublicKey, string pathToPublicKey)
+    {
+        Assembly currentAssembly = Assembly.GetExecutingAssembly();
+        using Stream? stream = currentAssembly.GetManifestResourceStream($"{currentAssembly.GetName().Name}.OfficialExtensions.{pathToPublicKey}");
+        if (stream == null) return false;
+
+        using MemoryStream memoryStream = new MemoryStream();
+        stream.CopyTo(memoryStream);
+        byte[] publicKey = memoryStream.ToArray();
+
+        return assemblyPublicKey.SequenceEqual(publicKey);
+    }
+
+    [DllImport("mscoree.dll", CharSet=CharSet.Unicode)]
+    static extern bool StrongNameSignatureVerificationEx(string wszFilePath, bool fForceVerification, ref bool pfWasVerified);
 
 
+    private Extension LoadExtensionEntry(Assembly entryAssembly, Type extensionType, ExtensionMetadata metadata)
+    {
         var extension = (Extension)Activator.CreateInstance(extensionType);
         var extension = (Extension)Activator.CreateInstance(extensionType);
         if (extension is null)
         if (extension is null)
         {
         {
@@ -149,3 +207,16 @@ internal class ExtensionLoader
         }
         }
     }
     }
 }
 }
+
+internal struct OfficialExtensionData
+{
+    public string PublicKeyName { get; }
+    public AdditionalContentProduct? Product { get; }
+    public string? PurchaseLink { get; }
+
+    public OfficialExtensionData(string publicKeyName, AdditionalContentProduct product, string? purchaseLink = null)
+    {
+        PublicKeyName = publicKeyName;
+        Product = product;
+    }
+}

BIN
src/PixiEditor/OfficialExtensions/supporter-pack.snk


+ 2 - 0
src/PixiEditor/PixiEditor.csproj

@@ -149,6 +149,7 @@
 	<ItemGroup>
 	<ItemGroup>
 		<Compile Remove="Styles\AvalonDock\Images\**" />
 		<Compile Remove="Styles\AvalonDock\Images\**" />
 		<EmbeddedResource Remove="Styles\AvalonDock\Images\**" />
 		<EmbeddedResource Remove="Styles\AvalonDock\Images\**" />
+		<EmbeddedResource Include="OfficialExtensions\supporter-pack.snk" />
 		<None Remove="Styles\AvalonDock\Images\**" />
 		<None Remove="Styles\AvalonDock\Images\**" />
 		<Page Remove="Styles\AvalonDock\Images\**" />
 		<Page Remove="Styles\AvalonDock\Images\**" />
 	</ItemGroup>
 	</ItemGroup>
@@ -436,6 +437,7 @@
 		<Resource Include="Images\LanguageFlags\hu.png" />
 		<Resource Include="Images\LanguageFlags\hu.png" />
 		<None Remove="Images\LanguageFlags\pt-br.png" />
 		<None Remove="Images\LanguageFlags\pt-br.png" />
 		<Resource Include="Images\LanguageFlags\pt-br.png" />
 		<Resource Include="Images\LanguageFlags\pt-br.png" />
+		<None Remove="OfficialExtensions\supporter-pack.snk" />
 	</ItemGroup>
 	</ItemGroup>
 	<ItemGroup>
 	<ItemGroup>
 		<None Include="..\LICENSE">
 		<None Include="..\LICENSE">

+ 0 - 13
src/PixiEditor/Views/MainWindow.xaml.cs

@@ -41,8 +41,6 @@ internal partial class MainWindow : Window
         extLoader = extensionLoader;
         extLoader = extensionLoader;
         Current = this;
         Current = this;
 
 
-        IPlatform.RegisterPlatform(GetActivePlatform());
-
         services = new ServiceCollection()
         services = new ServiceCollection()
             .AddPlatform()
             .AddPlatform()
             .AddPixiEditor(extensionLoader)
             .AddPixiEditor(extensionLoader)
@@ -91,17 +89,6 @@ internal partial class MainWindow : Window
         ((LayoutContent)d).SetValue(LayoutContent.TitleProperty, value.Value);
         ((LayoutContent)d).SetValue(LayoutContent.TitleProperty, value.Value);
     }
     }
 
 
-    private IPlatform GetActivePlatform()
-    {
-#if STEAM
-        return new PixiEditor.Platform.Steam.SteamPlatform();
-#elif MSIX || MSIX_DEBUG
-        return new PixiEditor.Platform.MSStore.MicrosoftStorePlatform();
-#else
-        return new PixiEditor.Platform.Standalone.StandalonePlatform();
-#endif
-    }
-
     private void MainWindow_ContentRendered(object sender, EventArgs e)
     private void MainWindow_ContentRendered(object sender, EventArgs e)
     {
     {
         GlobalMouseHook.Instance.Initilize(this);
         GlobalMouseHook.Instance.Initilize(this);

+ 1 - 1
src/SampleExtension/extension.json

@@ -1,6 +1,6 @@
 {
 {
   "displayName": "Sample Extension 1",
   "displayName": "Sample Extension 1",
-  "uniqueName": "PixiEditor.Samples.SampleExtension",
+  "uniqueName": "yourCompany.Samples.SampleExtension",
   "description": "Sample extension for PixiEditor",
   "description": "Sample extension for PixiEditor",
   "version": "1.0.0",
   "version": "1.0.0",
   "localization": {
   "localization": {