Переглянути джерело

Added extension resources encryption

Krzysztof Krysiński 1 тиждень тому
батько
коміт
0096677499
33 змінених файлів з 732 додано та 49 видалено
  1. 8 3
      samples/Sample5_Resources/ResourcesSampleExtension.cs
  2. 2 1
      samples/Sample5_Resources/Sample5_Resources.csproj
  3. 28 3
      src/PixiEditor.Api.CGlueMSBuild/CApiGenerator.cs
  4. 99 0
      src/PixiEditor.Api.CGlueMSBuild/EncryptResourcesTask.cs
  5. 11 2
      src/PixiEditor.Api.CGlueMSBuild/GenerateCGlueTask.cs
  6. 2 0
      src/PixiEditor.Extensions.CommonApi/IO/IDocumentProvider.cs
  7. 4 2
      src/PixiEditor.Extensions.MSPackageBuilder/BuildPackageTask.cs
  8. 24 4
      src/PixiEditor.Extensions.MSPackageBuilder/PackageBuilder.cs
  9. 1 8
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Image.cs
  10. 5 0
      src/PixiEditor.Extensions.Sdk/Api/IO/DocumentProvider.cs
  11. 55 0
      src/PixiEditor.Extensions.Sdk/Api/Resources/Resources.cs
  12. 9 0
      src/PixiEditor.Extensions.Sdk/Bridge/Interop.Document.cs
  13. 63 0
      src/PixiEditor.Extensions.Sdk/Bridge/Interop.Resources.cs
  14. 1 14
      src/PixiEditor.Extensions.Sdk/Bridge/Interop.User.cs
  15. 3 0
      src/PixiEditor.Extensions.Sdk/Bridge/Native.Document.cs
  16. 14 0
      src/PixiEditor.Extensions.Sdk/Bridge/Native.cs
  17. 18 0
      src/PixiEditor.Extensions.Sdk/Utilities/InteropUtility.cs
  18. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll
  19. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll
  20. 22 5
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.Sdk.targets
  21. 27 0
      src/PixiEditor.Extensions.Sdk/native/interop.c
  22. 10 0
      src/PixiEditor.Extensions.WasmRuntime/Api/DocumentsApi.cs
  23. 21 0
      src/PixiEditor.Extensions.WasmRuntime/Api/ResourcesApi.cs
  24. 45 0
      src/PixiEditor.Extensions.WasmRuntime/Utilities/ExtensionResourceStorage.cs
  25. 178 0
      src/PixiEditor.Extensions.WasmRuntime/Utilities/ResourcesUtility.cs
  26. 25 2
      src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs
  27. 26 1
      src/PixiEditor.Extensions/FlyUI/Converters/PathToImgSourceConverter.cs
  28. 1 1
      src/PixiEditor.Extensions/FlyUI/Elements/Image.cs
  29. 11 2
      src/PixiEditor.Extensions/FlyUI/Elements/LayoutBuilder.cs
  30. 2 0
      src/PixiEditor.Extensions/FlyUI/Elements/LayoutElement.cs
  31. 7 0
      src/PixiEditor.Extensions/IO/ResourceStorage.cs
  32. 6 0
      src/PixiEditor/Models/ExtensionServices/DocumentProvider.cs
  33. 4 1
      src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs

+ 8 - 3
samples/Sample5_Resources/ResourcesSampleExtension.cs

@@ -1,5 +1,7 @@
 using System.IO;
 using PixiEditor.Extensions.Sdk;
+using PixiEditor.Extensions.Sdk.Api.Resources;
+using PixiEditor.Extensions.Sdk.Bridge;
 
 namespace ResourcesSample;
 
@@ -21,12 +23,15 @@ public class ResourcesSampleExtension : PixiEditorExtension
     {
         // By default, you can't access any files from the file system, however you can access files from the Resources folder.
         // This folder contains files that you put in the Resources folder in the extension project.
-        Api.Logger.Log(File.ReadAllText("Resources/ExampleFile.txt"));
+        // You can use System.File calls to access files in the Resources folder.
+        // However, if you want to access files that are encrypted, you should use the Resources methods.
+        // Adding <EncryptResources>true</EncryptResources> to the .csproj file will encrypt the resources in the Resources folder.
+        Api.Logger.Log(Resources.ReadAllText("Resources/ExampleFile.txt"));
 
         Api.Logger.Log("Writing to file...");
 
-        File.WriteAllText("Resources/ExampleFile.txt", "Hello from extension!");
+        Resources.WriteAllText("Resources/ExampleFile.txt", "Hello from extension!");
 
-        Api.Logger.Log(File.ReadAllText("Resources/ExampleFile.txt"));
+        Api.Logger.Log(Resources.ReadAllText("Resources/ExampleFile.txt"));
     }
 }

+ 2 - 1
samples/Sample5_Resources/Sample5_Resources.csproj

@@ -6,10 +6,11 @@
         <PublishTrimmed>true</PublishTrimmed>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
-        <PixiExtOutputPath>..\..\src\PixiEditor.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <PixiExtOutputPath>..\..\src\PixiEditor.Desktop\bin\Debug\net8.0\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <RootNamespace>ResourcesSample</RootNamespace>
+        <EncryptResources>true</EncryptResources>
     </PropertyGroup>
 
     <ItemGroup>

+ 28 - 3
src/PixiEditor.Api.CGlueMSBuild/CApiGenerator.cs

@@ -7,12 +7,18 @@ namespace PixiEditor.Api.CGlueMSBuild;
 
 public class CApiGenerator
 {
+    private static readonly string[] excluded = new[] { "get_encryption_key", "get_encryption_iv" };
     private string InteropCContent { get; }
     private Action<string> Log { get; }
-    public CApiGenerator(string interopCContent, Action<string> log)
+    public string? ResourcesEncryptionKey { get; set; }
+    public string? ResourcesEncryptionIv { get; set; }
+
+    public CApiGenerator(string interopCContent, string? resourcesEncryptionKey, string? resourcesEncryptionIv, Action<string> log)
     {
         InteropCContent = interopCContent;
         Log = log;
+        ResourcesEncryptionKey = resourcesEncryptionKey;
+        ResourcesEncryptionIv = resourcesEncryptionIv;
     }
 
     public string Generate(AssemblyDefinition assembly, string directory)
@@ -37,7 +43,20 @@ public class CApiGenerator
 
         sb.AppendLine(GenerateAttachImportedFunctions(importedMethods));
 
-        return InteropCContent.Replace("void attach_imported_functions(){}", sb.ToString());
+        string final = InteropCContent;
+        if (!string.IsNullOrEmpty(ResourcesEncryptionKey) && !string.IsNullOrEmpty(ResourcesEncryptionIv))
+        {
+            byte[] keyBytes = Convert.FromBase64String(ResourcesEncryptionKey);
+            byte[] ivBytes = Convert.FromBase64String(ResourcesEncryptionIv);
+
+            final = InteropCContent.Replace("static const uint8_t key[16] = { };",
+                $"static const uint8_t key[16] = {{ {string.Join(", ", keyBytes.Select(b => b.ToString()))} }};");
+
+            final = final.Replace("static const uint8_t iv[16] = { };",
+                $"static const uint8_t iv[16] = {{ {string.Join(", ", ivBytes.Select(b => b.ToString()))} }};");
+        }
+
+        return final.Replace("void attach_imported_functions(){}", sb.ToString());
     }
 
     public static MethodDefinition[] GetExportedMethods(TypeDefinition[] types)
@@ -53,11 +72,17 @@ public class CApiGenerator
     {
         var importedMethods = types
             .SelectMany(t => t.Methods)
-            .Where(m => m.IsStatic && m.ImplAttributes == MethodImplAttributes.InternalCall)
+            .Where(m => m.IsStatic && m.ImplAttributes == MethodImplAttributes.InternalCall && !IsExcluded(m))
             .ToArray();
         return importedMethods;
     }
 
+    private static bool IsExcluded(MethodDefinition method)
+    {
+        return excluded.Any(ex => method.Name.StartsWith(ex, StringComparison.OrdinalIgnoreCase) ||
+                           method.DeclaringType.FullName.StartsWith(ex, StringComparison.OrdinalIgnoreCase));
+    }
+
     public List<AssemblyDefinition> LoadAssemblies(AssemblyDefinition assembly, string directory)
     {
         var assemblies = assembly.MainModule.AssemblyReferences

+ 99 - 0
src/PixiEditor.Api.CGlueMSBuild/EncryptResourcesTask.cs

@@ -0,0 +1,99 @@
+using System.IO.Compression;
+using System.Security.Cryptography;
+using Microsoft.Build.Framework;
+
+namespace PixiEditor.Api.CGlueMSBuild;
+
+public class EncryptResourcesTask : Microsoft.Build.Utilities.Task
+{
+    [Required] public string ResourcesPath { get; set; }
+
+    [Required] public string IntermediateOutputPath { get; set; } = string.Empty;
+
+    [Required] public string OutputPath { get; set; } = string.Empty;
+
+    [Output] public string EncryptionKey { get; set; } = string.Empty;
+
+    [Output] public string EncryptionIv { get; set; } = string.Empty;
+
+    public override bool Execute()
+    {
+        try
+        {
+            if (!Directory.Exists(ResourcesPath))
+            {
+                Log.LogError($"Resources directory does not exist: {ResourcesPath}");
+                return false;
+            }
+
+            string[] files = Directory.GetFiles(ResourcesPath, "*.*", SearchOption.AllDirectories);
+
+            if (files.Length == 0)
+            {
+                return true;
+            }
+
+            string path = Path.Combine(IntermediateOutputPath, "resources.zip");
+
+            if (File.Exists(path))
+            {
+                File.Delete(path);
+            }
+
+            ZipFile.CreateFromDirectory(ResourcesPath, path,
+                CompressionLevel.Fastest, false);
+            byte[] data = File.ReadAllBytes(Path.Combine(IntermediateOutputPath, "resources.zip"));
+            byte[] encryptionKey = new byte[128 / 8];
+            byte[] iv = new byte[128 / 8];
+            if (EncryptionKey == string.Empty)
+            {
+                RandomNumberGenerator.Create().GetBytes(encryptionKey);
+                EncryptionKey = Convert.ToBase64String(encryptionKey);
+            }
+
+            if (EncryptionIv == string.Empty)
+            {
+                RandomNumberGenerator.Create().GetBytes(iv);
+                EncryptionIv = Convert.ToBase64String(iv);
+            }
+
+            byte[] encryptedData = Encrypt(data, encryptionKey, iv);
+            File.WriteAllBytes(Path.Combine(OutputPath, "resources.data"), encryptedData);
+
+            return true;
+        }
+        catch (Exception ex)
+        {
+            Log.LogErrorFromException(ex);
+            return false;
+        }
+    }
+
+    public byte[] Encrypt(byte[] data, byte[] key, byte[] iv)
+    {
+        using var aes = Aes.Create();
+        aes.KeySize = 128;
+        aes.BlockSize = 128;
+        aes.Padding = PaddingMode.Zeros;
+
+        aes.Key = key;
+        aes.IV = iv;
+
+        using (var encryptor = aes.CreateEncryptor(aes.Key, aes.IV))
+        {
+            return PerformCryptography(data, encryptor);
+        }
+    }
+
+    private byte[] PerformCryptography(byte[] data, ICryptoTransform cryptoTransform)
+    {
+        using (var ms = new MemoryStream())
+        using (var cryptoStream = new CryptoStream(ms, cryptoTransform, CryptoStreamMode.Write))
+        {
+            cryptoStream.Write(data, 0, data.Length);
+            cryptoStream.FlushFinalBlock();
+
+            return ms.ToArray();
+        }
+    }
+}

+ 11 - 2
src/PixiEditor.Api.CGlueMSBuild/GenerateCGlueTask.cs

@@ -1,4 +1,5 @@
-using Microsoft.Build.Framework;
+using System.Runtime.InteropServices;
+using Microsoft.Build.Framework;
 using Mono.Cecil;
 
 namespace PixiEditor.Api.CGlueMSBuild
@@ -23,6 +24,14 @@ namespace PixiEditor.Api.CGlueMSBuild
         [Required]
         public string InteropCFilePath { get; set; } = default!;
 
+        public string EncryptionKey { get; set; } = string.Empty;
+
+        public string EncryptionIv { get; set; } = string.Empty;
+
+        /// <summary>
+        ///     /// Encryption key for resources.
+        /// </summary>
+
         public override bool Execute()
         {
             try
@@ -47,7 +56,7 @@ namespace PixiEditor.Api.CGlueMSBuild
                     }
                 }
 
-                var generator = new CApiGenerator(File.ReadAllText(InteropCFilePath), (message) => Log.LogMessage(MessageImportance.High, message));
+                var generator = new CApiGenerator(File.ReadAllText(InteropCFilePath), EncryptionKey, EncryptionIv, (message) => Log.LogMessage(MessageImportance.High, message));
 
                 string directory = Path.GetDirectoryName(AssemblyPath)!;
 

+ 2 - 0
src/PixiEditor.Extensions.CommonApi/IO/IDocumentProvider.cs

@@ -6,5 +6,7 @@ public interface IDocumentProvider
 {
    public IDocument? ActiveDocument { get; }
    public IDocument? ImportFile(string path, bool associatePath = true);
+   public IDocument? ImportDocument(byte[] data);
+
    public IDocument? GetDocument(Guid id);
 }

+ 4 - 2
src/PixiEditor.Extensions.MSPackageBuilder/BuildPackageTask.cs

@@ -10,12 +10,14 @@ public class BuildPackageTask : Microsoft.Build.Utilities.Task
     
     [Required]
     public string TargetDirectory { get; set; } = default!;
-    
+
+    public string EncryptionKey { get; set; }
+
     public override bool Execute()
     {
         try
         {
-            PackageBuilder.Build(BuildResultDirectory, TargetDirectory);
+            PackageBuilder.Build(BuildResultDirectory, TargetDirectory, !string.IsNullOrEmpty(EncryptionKey));
         }
         catch (Exception e)
         {

+ 24 - 4
src/PixiEditor.Extensions.MSPackageBuilder/PackageBuilder.cs

@@ -17,7 +17,7 @@ public static class PackageBuilder
 
     private static readonly string[] FilesToExclude = new[] { "dotnet.wasm", };
 
-    public static void Build(string buildResultDirectory, string targetDirectory)
+    public static void Build(string buildResultDirectory, string targetDirectory, bool encryptedResources)
     {
         if (!Directory.Exists(buildResultDirectory))
         {
@@ -46,15 +46,34 @@ public static class PackageBuilder
             JsonConvert.DeserializeObject<SimplifiedExtensionMetadata>(
                 File.ReadAllText(Path.Combine(buildResultDirectory, "extension.json")));
 
-        foreach (ElementToInclude element in ElementsToInclude)
+        var elementsToInclude = new List<ElementToInclude>(ElementsToInclude);
+        if(encryptedResources)
         {
+            elementsToInclude.Add(new ElementToInclude("resources.data", true) { TargetDirectory = "Resources" });
+            elementsToInclude.RemoveAll(x => x.Path == "Resources/");
+        }
+
+        foreach (ElementToInclude element in elementsToInclude)
+        {
+            if (!string.IsNullOrEmpty(element.TargetDirectory))
+            {
+                if (!Directory.Exists(Path.Combine(targetTmpDirectory, element.TargetDirectory)))
+                {
+                    Directory.CreateDirectory(Path.Combine(targetTmpDirectory, element.TargetDirectory));
+                }
+            }
+
+            string finalDir = string.IsNullOrEmpty(element.TargetDirectory)
+                ? targetTmpDirectory
+                : Path.Combine(targetTmpDirectory, element.TargetDirectory);
+
             if (element.Type == ElementToIncludeType.File)
             {
-                CopyFile(element.Path, buildResultDirectory, targetTmpDirectory, element.IsRequired);
+                CopyFile(element.Path, buildResultDirectory, finalDir, element.IsRequired);
             }
             else
             {
-                CopyDirectory(element.Path, buildResultDirectory, targetTmpDirectory, element.IsRequired, metadata);
+                CopyDirectory(element.Path, buildResultDirectory, finalDir, element.IsRequired, metadata);
             }
         }
 
@@ -158,6 +177,7 @@ public static class PackageBuilder
 record ElementToInclude
 {
     public string Path { get; set; }
+    public string? TargetDirectory { get; set; }
     public bool IsRequired { get; set; }
 
     public ElementToIncludeType Type => Path.EndsWith("/") ? ElementToIncludeType.Directory : ElementToIncludeType.File;

+ 1 - 8
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Image.cs

@@ -15,14 +15,7 @@ public class Image : LayoutElement
         get => source;
         set
         {
-            if (value.StartsWith("/") || value.StartsWith("/Resources/") || value.StartsWith("Resources/"))
-            {
-                source = Native.to_resources_full_path(value);    
-            }
-            else
-            {
-                source = value;
-            }
+            source = value;
         }
     }
 

+ 5 - 0
src/PixiEditor.Extensions.Sdk/Api/IO/DocumentProvider.cs

@@ -14,6 +14,11 @@ public class DocumentProvider : IDocumentProvider
         return Interop.ImportFile(path, associatePath);
     }
 
+    public IDocument? ImportDocument(byte[] data)
+    {
+        return Interop.ImportDocument(data);
+    }
+
     public IDocument? GetDocument(Guid id)
     {
         if (id == Guid.Empty)

+ 55 - 0
src/PixiEditor.Extensions.Sdk/Api/Resources/Resources.cs

@@ -0,0 +1,55 @@
+using PixiEditor.Extensions.Sdk.Bridge;
+
+namespace PixiEditor.Extensions.Sdk.Api.Resources;
+
+public static class Resources
+{
+    public static byte[] ReadAllBytes(string path)
+    {
+        var bytes = Interop.LoadResource(path);
+        if (bytes == null)
+        {
+            throw new ArgumentException($"Resource '{path}' not found.", nameof(path));
+        }
+
+        return bytes;
+    }
+
+    public static string ReadAllText(string path)
+    {
+        var bytes = Interop.LoadResource(path);
+        if (bytes == null)
+        {
+            throw new ArgumentException($"Resource '{path}' not found.", nameof(path));
+        }
+
+        return System.Text.Encoding.UTF8.GetString(bytes);
+    }
+
+    public static void WriteAllText(string path, string text)
+    {
+        byte[] data = System.Text.Encoding.UTF8.GetBytes(text);
+        Interop.WriteResource(data, path);
+    }
+
+    public static void WriteAllBytes(string path, byte[] data)
+    {
+        if (data == null)
+        {
+            throw new ArgumentNullException(nameof(data), "Data cannot be null.");
+        }
+
+        Interop.WriteResource(data, path);
+    }
+
+    public static string[] GetFilesAtPath(string path, string searchPattern = "*")
+    {
+        var files = Interop.GetFilesAtPath(path, searchPattern);
+        if (files == null)
+        {
+            throw new ArgumentException($"Path '{path}' not found.", nameof(path));
+        }
+
+        return files;
+    }
+}

+ 9 - 0
src/PixiEditor.Extensions.Sdk/Bridge/Interop.Document.cs

@@ -1,5 +1,6 @@
 using PixiEditor.Extensions.CommonApi.Documents;
 using PixiEditor.Extensions.Sdk.Api.Documents;
+using PixiEditor.Extensions.Sdk.Utilities;
 
 namespace PixiEditor.Extensions.Sdk.Bridge;
 
@@ -22,4 +23,12 @@ internal static partial class Interop
 
         return new Document(id);
     }
+    public static IDocument? ImportDocument(byte[] data)
+    {
+        string document = Native.import_document(InteropUtility.ByteArrayToIntPtr(data), data.Length);
+        if (document == null || !Guid.TryParse(document, out Guid id))
+            return null;
+
+        return new Document(id);
+    }
 }

+ 63 - 0
src/PixiEditor.Extensions.Sdk/Bridge/Interop.Resources.cs

@@ -0,0 +1,63 @@
+using System.IO.Compression;
+using System.Security.Cryptography;
+using PixiEditor.Extensions.Sdk.Utilities;
+
+namespace PixiEditor.Extensions.Sdk.Bridge;
+
+internal static partial class Interop
+{
+    public static byte[] LoadResource(string path)
+    {
+        byte[] encryptionKey = InteropUtility.IntPtrToByteArray(Native.get_encryption_key(), 16);
+        byte[] iv = InteropUtility.IntPtrToByteArray(Native.get_encryption_iv(), 16);
+
+        if (encryptionKey.Length == 0 || encryptionKey.All(x => x == 0))
+        {
+            return File.ReadAllBytes(path);
+        }
+
+        return InteropUtility.PrefixedIntPtrToByteArray(Native.load_encrypted_resource(path));
+    }
+
+
+    public static void WriteResource(byte[] data, string path)
+    {
+        byte[] encryptionKey = InteropUtility.IntPtrToByteArray(Native.get_encryption_key(), 16);
+        byte[] iv = InteropUtility.IntPtrToByteArray(Native.get_encryption_iv(), 16);
+
+        if (encryptionKey.Length == 0 || encryptionKey.All(x => x == 0))
+        {
+            File.WriteAllBytes(path, data);
+        }
+        else
+        {
+            Native.write_encrypted_resource(path, InteropUtility.ByteArrayToIntPtr(data), data.Length);
+        }
+    }
+
+    public static string[] GetFilesAtPath(string path, string searchPattern)
+    {
+        byte[] encryptionKey = InteropUtility.IntPtrToByteArray(Native.get_encryption_key(), 16);
+        byte[] iv = InteropUtility.IntPtrToByteArray(Native.get_encryption_iv(), 16);
+
+        if (encryptionKey.Length == 0 || encryptionKey.All(x => x == 0))
+        {
+            return Directory.GetFiles(path, searchPattern);
+        }
+
+        IntPtr filesPtr = Native.get_encrypted_files_at_path(path, searchPattern);
+
+        if (filesPtr == IntPtr.Zero)
+        {
+            throw new ArgumentException($"Path '{path}' not found.", nameof(path));
+        }
+
+        string[] strArr = InteropUtility.IntPtrToStringArray(filesPtr);
+        if (strArr == null || strArr.Length == 0)
+        {
+            return [];
+        }
+
+        return strArr;
+    }
+}

+ 1 - 14
src/PixiEditor.Extensions.Sdk/Bridge/Interop.User.cs

@@ -12,19 +12,6 @@ internal static partial class Interop
             return [];
         }
 
-        List<string> contentList = new List<string>();
-        byte[] arr = InteropUtility.PrefixedIntPtrToByteArray(ptr);
-        int length = BitConverter.ToInt32(arr, 0);
-        int offset = 4;
-        for (int i = 0; i < length; i++)
-        {
-            int strLength = BitConverter.ToInt32(arr, offset);
-            offset += 4;
-            string content = System.Text.Encoding.UTF8.GetString(arr, offset, strLength);
-            contentList.Add(content);
-            offset += strLength;
-        }
-
-        return contentList.ToArray();
+        return InteropUtility.IntPtrToStringArray(ptr);
     }
 }

+ 3 - 0
src/PixiEditor.Extensions.Sdk/Bridge/Native.Document.cs

@@ -7,6 +7,9 @@ internal partial class Native
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern string import_file(string path, bool associatePath);
 
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    internal static extern string import_document(IntPtr data, int length);
+
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern string get_active_document();
 

+ 14 - 0
src/PixiEditor.Extensions.Sdk/Bridge/Native.cs

@@ -87,4 +87,18 @@ internal static partial class Native
     [MethodImpl(MethodImplOptions.InternalCall)]
     public static extern string to_resources_full_path(string value);
 
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    public static extern IntPtr get_encryption_key();
+
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    public static extern IntPtr get_encryption_iv();
+
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    public static extern IntPtr load_encrypted_resource(string path);
+
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    public static extern void write_encrypted_resource(string path, IntPtr data, int length);
+
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    public static extern IntPtr get_encrypted_files_at_path(string path, string searchPattern);
 }

+ 18 - 0
src/PixiEditor.Extensions.Sdk/Utilities/InteropUtility.cs

@@ -32,4 +32,22 @@ public static class InteropUtility
         Marshal.Copy(ptr + sizeof(int), array, 0, length);
         return array;
     }
+
+    public static string[] IntPtrToStringArray(IntPtr ptr)
+    {
+        List<string> list = new List<string>();
+        byte[] arr = InteropUtility.PrefixedIntPtrToByteArray(ptr);
+        int length = BitConverter.ToInt32(arr, 0);
+        int offset = 4;
+        for (int i = 0; i < length; i++)
+        {
+            int strLength = BitConverter.ToInt32(arr, offset);
+            offset += 4;
+            string content = System.Text.Encoding.UTF8.GetString(arr, offset, strLength);
+            list.Add(content);
+            offset += strLength;
+        }
+
+        return list.ToArray();
+    }
 }

BIN
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll


BIN
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll


+ 22 - 5
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.Sdk.targets

@@ -1,23 +1,40 @@
 <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
 
+  <UsingTask TaskName="EncryptResourcesTask"
+             AssemblyFile="$(MSBuildThisFileDirectory)PixiEditor.Api.CGlueMSBuild.dll"
+             Condition="'$(RuntimeIdentifier)' == 'wasi-wasm' And '$(EncryptResources)' == 'true'"/>
+
+  <Target Name="EncryptResources" BeforeTargets="GenerateGlueCode" Condition="'$(RuntimeIdentifier)' == 'wasi-wasm' And '$(EncryptResources)' == 'true'">
+    <EncryptResourcesTask ResourcesPath="$(MSBuildProjectDirectory)\Resources" IntermediateOutputPath="$(IntermediateOutputPath)" OutputPath="$(OutputPath)">
+      <Output TaskParameter="EncryptionKey" PropertyName="EncryptionKey"/>
+      <Output TaskParameter="EncryptionIv" PropertyName="EncryptedIv"/>
+    </EncryptResourcesTask>
+  </Target>
+
   <UsingTask TaskName="GenerateCGlueTask"
              AssemblyFile="$(MSBuildThisFileDirectory)PixiEditor.Api.CGlueMSBuild.dll" Condition="'$(RuntimeIdentifier)' == 'wasi-wasm'"/>
   <Target Name="GenerateGlueCode" AfterTargets="Build" BeforeTargets="_BeforeWasmBuildApp" Condition="'$(RuntimeIdentifier)' == 'wasi-wasm'">
-    <GenerateCGlueTask AssemblyPath="$(TargetPath)" OutputPath="$(IntermediateOutputPath)native" InteropCFilePath="$(MSBuildThisFileDirectory)..\native\interop.c" />
+    <GenerateCGlueTask AssemblyPath="$(TargetPath)"
+                       EncryptionKey="$(EncryptionKey)"
+                       EncryptionIv="$(EncryptedIv)"
+                       OutputPath="$(IntermediateOutputPath)native" InteropCFilePath="$(MSBuildThisFileDirectory)..\native\interop.c"/>
     <ItemGroup>
-      <NativeFileReference Include="$(IntermediateOutputPath)native\*.c" />
-      <_WasmNativeFileForLinking Include="@(NativeFileReference)" />
+      <NativeFileReference Include="$(IntermediateOutputPath)native\*.c"/>
+      <_WasmNativeFileForLinking Include="@(NativeFileReference)"/>
     </ItemGroup>
   </Target>
   <UsingTask TaskName="BuildPackageTask"
              AssemblyFile="$(MSBuildThisFileDirectory)PixiEditor.Extensions.MSPackageBuilder.dll"
              Condition="'$(RuntimeIdentifier)' == 'wasi-wasm' And '$(GenerateExtensionPackage)' == 'true'"/>
 
-  <Target Name="BuildPackageTask" AfterTargets="_WasiGenerateAppBundle" Condition="'$(RuntimeIdentifier)' == 'wasi-wasm' And '$(GenerateExtensionPackage)' == 'true'">
+  <Target Name="BuildPackageTask" AfterTargets="_WasiGenerateAppBundle"
+          Condition="'$(RuntimeIdentifier)' == 'wasi-wasm' And '$(GenerateExtensionPackage)' == 'true'">
     <PropertyGroup Condition="'$(PixiExtOutputPath)' == ''">
       <PixiExtOutputPath>$(OutputPath)\Extensions</PixiExtOutputPath>
     </PropertyGroup>
     <Message Text="Building extension package to $(PixiExtOutputPath)" Importance="high"/>
-    <BuildPackageTask BuildResultDirectory="$(OutputPath)" TargetDirectory="$(PixiExtOutputPath)" />
+    <BuildPackageTask
+      EncryptionKey="$(EncryptionKey)"
+      BuildResultDirectory="$(OutputPath)" TargetDirectory="$(PixiExtOutputPath)"/>
   </Target>
 </Project>

+ 27 - 0
src/PixiEditor.Extensions.Sdk/native/interop.c

@@ -30,12 +30,27 @@ void invoke_interop_method(MonoMethod* method, void* params)
     free(method);
 }
 
+static const uint8_t key[16] = { };
+static const uint8_t iv[16] = { };
+
+__attribute__((import_name("get_encryption_key")))
+const uint8_t* get_encryption_key() {
+    return key;
+}
+
+__attribute__((import_name("get_encryption_iv")))
+const uint8_t* get_encryption_iv() {
+    return iv;
+}
+
 // Content is autogenerated
 void attach_imported_functions(){}
 
 void attach_internal_calls()
 {
     attach_imported_functions();
+    mono_add_internal_call("PixiEditor.Extensions.Sdk.Bridge.Native::get_encryption_key", get_encryption_key);
+    mono_add_internal_call("PixiEditor.Extensions.Sdk.Bridge.Native::get_encryption_iv", get_encryption_iv);
 }
 
 void initialize_runtime(void)
@@ -63,4 +78,16 @@ void initialize()
 {
     MonoMethod* metod = lookup_interop_method("Initialize");
     invoke_interop_method(metod, NULL);
+}
+
+__attribute((export_name("get_encryption_key")))
+const uint8_t* get_encryption_key_export()
+{
+    return get_encryption_key();
+}
+
+__attribute((export_name("get_encryption_iv")))
+const uint8_t* get_encryption_iv_export()
+{
+    return get_encryption_iv();
 }

+ 10 - 0
src/PixiEditor.Extensions.WasmRuntime/Api/DocumentsApi.cs

@@ -21,6 +21,16 @@ internal class DocumentsApi : ApiGroupHandler
         return id;
     }
 
+    [ApiFunction("import_document")]
+    public string ImportFile(Span<byte> data)
+    {
+        PermissionUtility.ThrowIfLacksPermissions(Extension.Metadata, ExtensionPermissions.OpenDocuments, "ImportFile");
+
+        string id = Api.Documents.ImportDocument(data.ToArray())?.Id.ToString() ?? string.Empty;
+
+        return id;
+    }
+
     [ApiFunction("get_active_document")]
     public string GetActiveDocument()
     {

+ 21 - 0
src/PixiEditor.Extensions.WasmRuntime/Api/ResourcesApi.cs

@@ -10,4 +10,25 @@ internal class ResourcesApi : ApiGroupHandler
         string fullPath = ResourcesUtility.ToResourcesFullPath(Extension, path);
         return fullPath;
     }
+
+    [ApiFunction("load_encrypted_resource")]
+    public byte[] LoadEncryptedResource(string path)
+    {
+        var data = ResourcesUtility.LoadEncryptedResource(Extension, path);
+        return data;
+    }
+
+    [ApiFunction("write_encrypted_resource")]
+    public void WriteEncryptedResource(string path, Span<byte> data)
+    {
+        ResourcesUtility.WriteEncryptedResource(Extension, path, data.ToArray());
+    }
+
+    [ApiFunction("get_encrypted_files_at_path")]
+    public byte[] GetEncryptedFilesAtPath(string path, string searchPattern)
+    {
+        var files = ResourcesUtility.GetEncryptedFilesAtPath(Extension, path, searchPattern);
+        byte[] filesArray = InteropUtility.SerializeToBytes(files);
+        return filesArray;
+    }
 }

+ 45 - 0
src/PixiEditor.Extensions.WasmRuntime/Utilities/ExtensionResourceStorage.cs

@@ -0,0 +1,45 @@
+using PixiEditor.Extensions.IO;
+
+namespace PixiEditor.Extensions.WasmRuntime.Utilities;
+
+public class ExtensionResourceStorage : IResourceStorage
+{
+    public WasmExtensionInstance Extension { get; }
+
+    public ExtensionResourceStorage(WasmExtensionInstance extension)
+    {
+        Extension = extension ?? throw new ArgumentNullException(nameof(extension), "Extension cannot be null.");
+    }
+
+
+    public Stream GetResourceStream(string resourcePath)
+    {
+        if (Extension.GetEncryptionKey()?.Length == 0 || Extension.GetEncryptionIV()?.Length == 0)
+        {
+            return File.OpenRead(ResourcesUtility.ToResourcesFullPath(Extension, resourcePath));
+        }
+
+        byte[] data = ResourcesUtility.LoadEncryptedResource(Extension, resourcePath);
+        return new MemoryStream(data);
+    }
+
+    public bool Exists(string path)
+    {
+        try
+        {
+            string fullPath = ResourcesUtility.ToResourcesFullPath(Extension, path);
+            if(Extension.HasEncryptedResources)
+            {
+                return ResourcesUtility.HasEncryptedResource(Extension, path);
+            }
+            else
+            {
+                return File.Exists(fullPath);
+            }
+        }
+        catch (Exception)
+        {
+            return false;
+        }
+    }
+}

+ 178 - 0
src/PixiEditor.Extensions.WasmRuntime/Utilities/ResourcesUtility.cs

@@ -1,3 +1,7 @@
+using System.IO.Compression;
+using System.Security.Cryptography;
+using System.Text.RegularExpressions;
+
 namespace PixiEditor.Extensions.WasmRuntime.Utilities;
 
 public static class ResourcesUtility
@@ -18,4 +22,178 @@ public static class ResourcesUtility
 
         return fullPath;
     }
+
+    public static byte[] LoadEncryptedResource(WasmExtensionInstance extension, string path)
+    {
+        string fullPath = ToResourcesFullPath(extension, "Resources/resources.data");
+        byte[] data = File.ReadAllBytes(fullPath);
+        using var zipArchive = OpenEncryptedArchive(extension.GetEncryptionKey(), extension.GetEncryptionIV(), data, ZipArchiveMode.Read);
+
+        string openPath = path.TrimStart('/').TrimStart("Resources/".ToCharArray());
+
+        ZipArchiveEntry entry = zipArchive.GetEntry(openPath);
+        if (entry == null)
+        {
+            throw new ArgumentException($"Resource '{path}' not found.", nameof(path));
+        }
+
+        using Stream entryStream = entry.Open();
+        using MemoryStream resultStream = new MemoryStream();
+        entryStream.CopyTo(resultStream);
+        return resultStream.ToArray();
+    }
+
+    private static ZipArchive OpenEncryptedArchive(byte[] encryptionKey, byte[] iv, byte[] data, ZipArchiveMode mode)
+    {
+        ZipArchive zipArchive = null;
+        try
+        {
+            Encryptor encryptor = new Encryptor(encryptionKey, iv);
+            var decrypted = encryptor.Decrypt(data);
+            var memoryStream = new MemoryStream(decrypted);
+            zipArchive = new ZipArchive(memoryStream, mode, true);
+            return zipArchive;
+        }
+        catch
+        {
+            zipArchive?.Dispose();
+            throw;
+        }
+    }
+
+    public static void WriteEncryptedResource(WasmExtensionInstance extension, string path, byte[] bytes)
+    {
+        string fullPath = ToResourcesFullPath(extension, "Resources/resources.data");
+
+        Encryptor encryptor = new Encryptor(extension.GetEncryptionKey(), extension.GetEncryptionIV());
+        byte[] encryptedInput = File.ReadAllBytes(fullPath);
+        byte[] decryptedZipData = encryptor.Decrypt(encryptedInput);
+
+        using var zipStream = new MemoryStream(decryptedZipData, writable: true);
+        using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Update, leaveOpen: true))
+        {
+            string openPath = path.TrimStart('/').TrimStart("Resources/".ToCharArray());
+
+            var entry = archive.GetEntry(openPath);
+            entry?.Delete();
+
+            entry = archive.CreateEntry(openPath);
+            using var entryStream = entry.Open();
+            entryStream.Write(bytes, 0, bytes.Length);
+        }
+
+        zipStream.Position = 0;
+        byte[] finalEncrypted = encryptor.Encrypt(zipStream.ToArray());
+        File.WriteAllBytes(fullPath, finalEncrypted);
+    }
+
+    public static string[] GetEncryptedFilesAtPath(WasmExtensionInstance extension, string path, string searchPattern)
+    {
+        string fullPath = ToResourcesFullPath(extension, "Resources/resources.data");
+        byte[] data = File.ReadAllBytes(fullPath);
+        using var zipArchive = OpenEncryptedArchive(extension.GetEncryptionKey(), extension.GetEncryptionIV(), data, ZipArchiveMode.Read);
+
+        string openPath = path.TrimStart('/').TrimStart("Resources/".ToCharArray());
+        string prefix = path.StartsWith("/") ? "/" : "";
+        if(path.StartsWith("Resources/"))
+        {
+            prefix += "Resources/";
+        }
+
+        bool MatchesPattern(string fileName, string pattern)
+        {
+            string regex = "^" + Regex.Escape(pattern)
+                .Replace(@"\*", ".*")
+                .Replace(@"\?", ".") + "$";
+
+            return Regex.IsMatch(fileName, regex, RegexOptions.IgnoreCase);
+        }
+
+        List<string> files = new List<string>();
+        foreach (var entry in zipArchive.Entries)
+        {
+            if (entry.FullName.StartsWith(openPath) && MatchesPattern(entry.FullName, searchPattern))
+            {
+                files.Add(Path.Combine(prefix, entry.FullName));
+            }
+        }
+
+        return files.ToArray();
+    }
+
+    public static bool HasEncryptedResource(WasmExtensionInstance extension, string path)
+    {
+        string fullPath = ToResourcesFullPath(extension, "Resources/resources.data");
+        if (!File.Exists(fullPath))
+        {
+            return false;
+        }
+
+        byte[] data = File.ReadAllBytes(fullPath);
+        using var zipArchive = OpenEncryptedArchive(extension.GetEncryptionKey(), extension.GetEncryptionIV(), data, ZipArchiveMode.Read);
+
+        string openPath = path.TrimStart('/').TrimStart("Resources/".ToCharArray());
+        return zipArchive.GetEntry(openPath) != null;
+    }
+}
+
+internal class Encryptor
+{
+    private byte[] key;
+    private byte[] iv;
+
+    public Encryptor(byte[] key, byte[] iv)
+    {
+        this.key = key;
+        this.iv = iv;
+    }
+
+
+    public byte[] Encrypt(byte[] data)
+    {
+        using (var aes = Aes.Create())
+        {
+            aes.KeySize = 128;
+            aes.BlockSize = 128;
+            aes.Padding = PaddingMode.Zeros;
+
+            aes.Key = key;
+            aes.IV = iv;
+
+            using (var encryptor = aes.CreateEncryptor(aes.Key, aes.IV))
+            {
+                return PerformCryptography(data, encryptor);
+            }
+        }
+    }
+
+    public byte[] Decrypt(byte[] data)
+    {
+        using (var aes = Aes.Create())
+        {
+            aes.KeySize = 128;
+            aes.BlockSize = 128;
+            aes.Padding = PaddingMode.Zeros;
+
+            aes.Key = key;
+            aes.IV = iv;
+
+            using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
+            {
+                return PerformCryptography(data, decryptor);
+            }
+        }
+    }
+
+    private byte[] PerformCryptography(byte[] data, ICryptoTransform cryptoTransform)
+    {
+        using (var ms = new MemoryStream())
+        using (var cryptoStream = new CryptoStream(ms, cryptoTransform, CryptoStreamMode.Write))
+        {
+            cryptoStream.Write(data, 0, data.Length);
+            cryptoStream.FlushFinalBlock();
+
+            return ms.ToArray();
+        }
+    }
 }

+ 25 - 2
src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs

@@ -8,6 +8,7 @@ using PixiEditor.Extensions.FlyUI;
 using PixiEditor.Extensions.FlyUI.Elements;
 using PixiEditor.Extensions.WasmRuntime.Api.Modules;
 using PixiEditor.Extensions.WasmRuntime.Management;
+using PixiEditor.Extensions.WasmRuntime.Utilities;
 using PixiEditor.Extensions.Windowing;
 using Wasmtime;
 
@@ -32,6 +33,7 @@ public partial class WasmExtensionInstance : Extension
     private List<ApiModule> modules = new();
 
     public override string Location => modulePath;
+    public bool HasEncryptedResources => GetEncryptionKey().Length > 0 && GetEncryptionIV().Length > 0;
 
     partial void LinkApiFunctions();
 
@@ -70,8 +72,7 @@ public partial class WasmExtensionInstance : Extension
         modules.Add(new PreferencesModule(this, Api.Preferences));
         modules.Add(new CommandModule(this, Api.Commands,
             (ICommandSupervisor)Api.Services.GetService(typeof(ICommandSupervisor))));
-        LayoutBuilder = new LayoutBuilder((ElementMap)Api.Services.GetService(typeof(ElementMap)));
-
+        LayoutBuilder = new LayoutBuilder(new ExtensionResourceStorage(this), (ElementMap)Api.Services.GetService(typeof(ElementMap)));
         //SetElementMap();
         Instance.GetAction("initialize")?.Invoke();
         base.OnInitialized();
@@ -89,6 +90,28 @@ public partial class WasmExtensionInstance : Extension
         base.OnMainWindowLoaded();
     }
 
+    public byte[] GetEncryptionKey()
+    {
+        int ptr = Instance.GetFunction("get_encryption_key")?.Invoke() as int? ?? 0;
+        if (ptr == 0)
+        {
+            throw new InvalidOperationException("Failed to get encryption key.");
+        }
+
+        return WasmMemoryUtility.GetBytes(ptr, 16);
+    }
+
+    public byte[] GetEncryptionIV()
+    {
+        int ptr = Instance.GetFunction("get_encryption_iv")?.Invoke() as int? ?? 0;
+        if (ptr == 0)
+        {
+            throw new InvalidOperationException("Failed to get encryption IV.");
+        }
+
+        return WasmMemoryUtility.GetBytes(ptr, 16);
+    }
+
     private void OnAsyncCallCompleted(int handle, int result)
     {
         Dispatcher.UIThread.Invoke(() =>

+ 26 - 1
src/PixiEditor.Extensions/FlyUI/Converters/PathToImgSourceConverter.cs

@@ -2,16 +2,41 @@
 using Avalonia.Data.Converters;
 using Avalonia.Media.Imaging;
 using Avalonia.Svg.Skia;
+using PixiEditor.Extensions.IO;
 
 namespace PixiEditor.Extensions.FlyUI.Converters;
 
 public class PathToImgSourceConverter : IValueConverter
 {
+    public IResourceStorage? ResourceStorage { get; set; }
+    public PathToImgSourceConverter()
+    {
+
+    }
+
+    public PathToImgSourceConverter(IResourceStorage resourceStorage)
+    {
+        ResourceStorage = resourceStorage;
+    }
+
     public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
     {
         if (value is string path)
         {
-            if (File.Exists(path))
+            if (ResourceStorage != null)
+            {
+                if(ResourceStorage.Exists(path))
+                {
+                    bool isSvg = path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase);
+                    if (isSvg)
+                    {
+                        return new SvgImage { Source = SvgSource.LoadFromStream(ResourceStorage.GetResourceStream(path)) };
+                    }
+
+                    return new Bitmap(ResourceStorage.GetResourceStream(path));
+                }
+            }
+            else if (File.Exists(path))
             {
                 bool isSvg = path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase);
                 if (isSvg)

+ 1 - 1
src/PixiEditor.Extensions/FlyUI/Elements/Image.cs

@@ -48,7 +48,7 @@ public class Image : LayoutElement
         {
             Source = this,
             Path = nameof(Source),
-            Converter = new PathToImgSourceConverter(),
+            Converter = new PathToImgSourceConverter(ResourceStorage),
         };
 
         Binding widthBinding = new()

+ 11 - 2
src/PixiEditor.Extensions/FlyUI/Elements/LayoutBuilder.cs

@@ -6,6 +6,7 @@ using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
 using PixiEditor.Extensions.FlyUI.Exceptions;
 using PixiEditor.Extensions.Helpers;
+using PixiEditor.Extensions.IO;
 
 namespace PixiEditor.Extensions.FlyUI.Elements;
 
@@ -15,9 +16,12 @@ public class LayoutBuilder
 
     public Dictionary<int, ILayoutElement<Control>> ManagedElements = new();
     public ElementMap ElementMap { get; }
-    public LayoutBuilder(ElementMap elementMap)
+    public IResourceStorage ResourceStorage { get; }
+
+    public LayoutBuilder(IResourceStorage resourceStorage, ElementMap elementMap)
     {
         this.ElementMap = elementMap;
+        this.ResourceStorage = resourceStorage;
     }
 
     public ILayoutElement<Control> Deserialize(Span<byte> layoutSpan, DuplicateResolutionTactic duplicatedIdTactic)
@@ -123,10 +127,15 @@ public class LayoutBuilder
     {
         Type typeToSpawn = ElementMap.ControlMap[controlId];
         var element = CreateInstance(typeToSpawn);
-        
+
         if(element is not { } layoutElement)
             throw new Exception("Element is not ILayoutElement<Control>");
 
+        if (element is LayoutElement le)
+        {
+            le.ResourceStorage = ResourceStorage;
+        }
+
         element.UniqueId = uniqueId;
 
         if (element is IPropertyDeserializable deserializableProperties)

+ 2 - 0
src/PixiEditor.Extensions/FlyUI/Elements/LayoutElement.cs

@@ -8,6 +8,7 @@ using Avalonia.Input;
 using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Events;
 using PixiEditor.Extensions.FlyUI.Converters;
+using PixiEditor.Extensions.IO;
 using Cursor = PixiEditor.Extensions.CommonApi.FlyUI.Cursor;
 
 namespace PixiEditor.Extensions.FlyUI.Elements;
@@ -42,6 +43,7 @@ public abstract class LayoutElement : ILayoutElement<Control>, INotifyPropertyCh
     }
 
     public Cursor? Cursor { get; set; }
+    internal IResourceStorage ResourceStorage { get; set; }
 
     private Dictionary<string, List<ElementEventHandler>>? _events;
 

+ 7 - 0
src/PixiEditor.Extensions/IO/ResourceStorage.cs

@@ -0,0 +1,7 @@
+namespace PixiEditor.Extensions.IO;
+
+public interface IResourceStorage
+{
+    public Stream GetResourceStream(string resourcePath);
+    public bool Exists(string path);
+}

+ 6 - 0
src/PixiEditor/Models/ExtensionServices/DocumentProvider.cs

@@ -1,6 +1,7 @@
 using Avalonia.Threading;
 using PixiEditor.Extensions.CommonApi.Documents;
 using PixiEditor.Extensions.CommonApi.IO;
+using PixiEditor.Extensions.WasmRuntime.Utilities;
 using PixiEditor.Models.IO;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels.SubViewModels;
@@ -22,6 +23,11 @@ internal class DocumentProvider : IDocumentProvider
         return fileViewModel.OpenFromPath(path, associatePath);
     }
 
+    public IDocument? ImportDocument(byte[] data)
+    {
+        return fileViewModel.OpenFromPixiBytes(data);
+    }
+
     public IDocument? GetDocument(Guid id)
     {
         var document = fileViewModel.Owner.DocumentManagerSubViewModel.Documents.FirstOrDefault(x => x.Id == id);

+ 4 - 1
src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs

@@ -19,6 +19,7 @@ using PixiEditor.Models.IO;
 using PixiEditor.Models.UserData;
 using Drawie.Numerics;
 using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Extensions.CommonApi.Documents;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
 using PixiEditor.Models.DocumentModels.Autosave;
 using PixiEditor.Models.ExceptionHandling;
@@ -363,10 +364,12 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         AddDocumentViewModelToTheSystem(document);
     }
 
-    public void OpenFromPixiBytes(byte[] bytes)
+    public IDocument OpenFromPixiBytes(byte[] bytes)
     {
         DocumentViewModel document = Importer.ImportDocument(bytes, null);
         AddDocumentViewModelToTheSystem(document);
+
+        return document;
     }
 
     /// <summary>