Browse Source

ProtectedData wip

Krzysztof Krysiński 4 months ago
parent
commit
710ae3f16d

+ 5 - 1
src/PixiEditor.OperatingSystem/IOperatingSystem.cs

@@ -13,6 +13,7 @@ public interface IOperatingSystem
 
     public IInputKeys InputKeys { get; }
     public IProcessUtility ProcessUtility { get; }
+    public ISecureStorage SecureStorage { get; }
     public bool IsMacOs => Name == "MacOS";
     public bool IsWindows => Name == "Windows";
     public bool IsLinux => Name == "Linux";
@@ -32,7 +33,10 @@ public interface IOperatingSystem
 
     public void OpenUri(string uri);
     public void OpenFolder(string path);
-    public bool HandleNewInstance(Dispatcher? dispatcher, Action<string, bool> openInExistingAction, IApplicationLifetime lifetime);
+
+    public bool HandleNewInstance(Dispatcher? dispatcher, Action<string, bool> openInExistingAction,
+        IApplicationLifetime lifetime);
+
     public void HandleActivatedWithFile(FileActivatedEventArgs fileActivatedEventArgs);
     public void HandleActivatedWithUri(ProtocolActivatedEventArgs openUriEventArgs);
 }

+ 7 - 0
src/PixiEditor.OperatingSystem/ISecureStorage.cs

@@ -0,0 +1,7 @@
+namespace PixiEditor.OperatingSystem;
+
+public interface ISecureStorage
+{
+    public Task<T?> GetValueAsync<T>(string key, T? defaultValue = default);
+    public Task SetValueAsync<T>(string key, T value);
+}

+ 3 - 0
src/PixiEditor.PixiAuth/PixiAuthClient.cs

@@ -12,6 +12,9 @@ public class PixiAuthClient
     {
         httpClient = new HttpClient();
         httpClient.BaseAddress = new Uri(baseUrl);
+        // TODO: Add error code handling
+        // TODO: Add refreshing token
+        // TODO: Add logout
     }
 
     public async Task<Guid?> GenerateSession(string email)

+ 6 - 0
src/PixiEditor.PixiAuth/User.cs

@@ -1,11 +1,17 @@
 namespace PixiEditor.PixiAuth;
 
+[Serializable]
 public class User
 {
     public string Email { get; set; } = string.Empty;
     public Guid? SessionId { get; set; }
     public string? SessionToken { get; set; } = string.Empty;
 
+    public User()
+    {
+
+    }
+
     public User(string email)
     {
         Email = email;

+ 1 - 0
src/PixiEditor.Windows/PixiEditor.Windows.csproj

@@ -12,6 +12,7 @@
 
     <ItemGroup>
       <PackageReference Include="Avalonia.Win32" Version="$(AvaloniaVersion)" />
+      <PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
     </ItemGroup>
 
 </Project>

+ 2 - 1
src/PixiEditor.Windows/WindowsOperatingSystem.cs

@@ -14,7 +14,8 @@ public sealed class WindowsOperatingSystem : IOperatingSystem
     
     public IInputKeys InputKeys { get; } = new WindowsInputKeys();
     public IProcessUtility ProcessUtility { get; } = new WindowsProcessUtility();
-    
+    public ISecureStorage SecureStorage { get; } = new WindowsSecureStorage();
+
     private const string UniqueEventName = "33f1410b-2ad7-412a-a468-34fe0a85747c";
     
     private const string UniqueMutexName = "ab2afe27-b9ee-4f03-a1e4-c18da16a349c";

+ 90 - 0
src/PixiEditor.Windows/WindowsSecureStorage.cs

@@ -0,0 +1,90 @@
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+using PixiEditor.OperatingSystem;
+
+namespace PixiEditor.Windows;
+
+internal class WindowsSecureStorage : ISecureStorage
+{
+    public string PathToStorage => Path.Combine(
+        Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+        "PixiEditor",
+        "SecureStorage.data");
+
+    public WindowsSecureStorage()
+    {
+        if (!File.Exists(PathToStorage))
+        {
+            Directory.CreateDirectory(Path.GetDirectoryName(PathToStorage)!);
+            File.Create(PathToStorage).Dispose();
+        }
+    }
+
+    public async Task<T?> GetValueAsync<T>(string key, T defaultValue = default)
+    {
+        byte[] current = ReadExistingData();
+
+        if (current is { Length: > 0 })
+        {
+            byte[] decryptedData = ProtectedData.Unprotect(
+                current,
+                null,
+                DataProtectionScope.CurrentUser);
+
+            string existingValue = Encoding.UTF8.GetString(decryptedData);
+            Dictionary<string, object>? data = JsonSerializer.Deserialize<Dictionary<string, object>>(existingValue);
+            if (data != null && data.TryGetValue(key, out object value))
+            {
+                if (value is JsonElement jsonElement)
+                {
+                    string jsonString = jsonElement.GetRawText();
+                    return JsonSerializer.Deserialize<T>(jsonString);
+                }
+
+                return (T)value;
+            }
+        }
+
+        return defaultValue;
+    }
+
+    public async Task SetValueAsync<T>(string key, T value)
+    {
+        byte[] current = ReadExistingData();
+
+        Dictionary<string, object> data = new Dictionary<string, object>();
+
+        if(current is { Length: > 0 })
+        {
+            byte[] decryptedData = ProtectedData.Unprotect(
+                current,
+                null,
+                DataProtectionScope.CurrentUser);
+
+            string existingValue = Encoding.UTF8.GetString(decryptedData);
+            data = JsonSerializer.Deserialize<Dictionary<string, object>>(existingValue) ?? new Dictionary<string, object>();
+        }
+
+        data[key] = value;
+
+        byte[] newData = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data));
+        byte[] encryptedData = ProtectedData.Protect(
+            newData,
+            null,
+            DataProtectionScope.CurrentUser);
+
+        await using var stream = new FileStream(PathToStorage, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
+        await stream.WriteAsync(encryptedData, 0, encryptedData.Length);
+        await stream.FlushAsync();
+    }
+
+    private byte[] ReadExistingData()
+    {
+        var stream = new FileStream(PathToStorage, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+        byte[] existingData = new byte[stream.Length];
+        stream.ReadExactly(existingData, 0, existingData.Length);
+        stream.Close();
+        return existingData;
+    }
+}

+ 21 - 7
src/PixiEditor/ViewModels/SubViewModels/UserViewModel.cs

@@ -1,5 +1,6 @@
 using CommunityToolkit.Mvvm.Input;
 using PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.OperatingSystem;
 using PixiEditor.PixiAuth;
 
 namespace PixiEditor.ViewModels.SubViewModels;
@@ -9,6 +10,7 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
     public PixiAuthClient PixiAuthClient { get; }
     public User? User { get; private set; }
 
+    public bool NotLoggedIn => User?.SessionId is null || User.SessionId == Guid.Empty;
     public bool WaitingForActivation => User is { SessionId: not null } && string.IsNullOrEmpty(User.SessionToken);
     public bool IsLoggedIn => User is { SessionId: not null } && !string.IsNullOrEmpty(User.SessionToken);
 
@@ -42,6 +44,8 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
             Console.WriteLine($"Invalid api URL format: {e.Message}");
             apiValid = false;
         }
+
+        Task.Run(async () => await LoadUserData());
     }
 
 
@@ -53,8 +57,7 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
         if (session != null)
         {
             User = new User(email) { SessionId = session.Value };
-            OnPropertyChanged(nameof(WaitingForActivation));
-            OnPropertyChanged(nameof(IsLoggedIn));
+            NotifyProperties();
             SaveUserInfo();
         }
     }
@@ -72,9 +75,7 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
         if (token != null)
         {
             User.SessionToken = token;
-            OnPropertyChanged(nameof(User));
-            OnPropertyChanged(nameof(WaitingForActivation));
-            OnPropertyChanged(nameof(IsLoggedIn));
+            NotifyProperties();
             SaveUserInfo();
             return true;
         }
@@ -82,8 +83,21 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
         return false;
     }
 
-    public void SaveUserInfo()
+    public async Task SaveUserInfo()
+    {
+        await IOperatingSystem.Current.SecureStorage.SetValueAsync("UserData", User);
+    }
+
+    public async Task LoadUserData()
+    {
+        User = await IOperatingSystem.Current.SecureStorage.GetValueAsync<User>("UserData", null);
+    }
+
+    private void NotifyProperties()
     {
-        // TODO:
+        OnPropertyChanged(nameof(User));
+        OnPropertyChanged(nameof(NotLoggedIn));
+        OnPropertyChanged(nameof(WaitingForActivation));
+        OnPropertyChanged(nameof(IsLoggedIn));
     }
 }

+ 1 - 1
src/PixiEditor/Views/Auth/LoginPopup.axaml

@@ -14,7 +14,7 @@
         <subViewModels:UserViewModel />
     </Design.DataContext>
     <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="5" Margin="10" MinWidth="300">
-        <auth:LoginForm RequestLoginCommand="{Binding Path=RequestLoginCommand}" />
+        <auth:LoginForm IsVisible="{Binding NotLoggedIn}" RequestLoginCommand="{Binding Path=RequestLoginCommand}" />
         <TextBlock Text="Email sent! Check your inbox." IsVisible="{Binding WaitingForActivation}"/>
         <TextBlock Text="Logged in as " IsVisible="{Binding IsLoggedIn}">
             <Run Text="{Binding User.Email}"/>