Browse Source

Added encryptors

Krzysztof Krysiński 4 months ago
parent
commit
8bd825179d

+ 6 - 0
src/PixiEditor.Linux/LinuxOperatingSystem.cs

@@ -1,7 +1,9 @@
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Input;
 using Avalonia.Threading;
+using DeviceId;
 using PixiEditor.OperatingSystem;
+using PixiEditor.OperatingSystem.Cryptography;
 
 namespace PixiEditor.Linux;
 
@@ -13,6 +15,10 @@ public sealed class LinuxOperatingSystem : IOperatingSystem
     public IInputKeys InputKeys { get; } = new LinuxInputKeys();
     public IProcessUtility ProcessUtility { get; } = new LinuxProcessUtility();
 
+    public IEncryptor Encryptor { get; } = new AesHmacEncryptor(
+        new LinuxDeviceIdBuilder(new DeviceIdBuilder()).AddMachineId().AddCpuInfo().AddMotherboardSerialNumber()
+            .AddSystemDriveSerialNumber().ToString());
+
     public string ExecutableExtension { get; } = string.Empty;
 
     public void OpenUri(string uri)

+ 4 - 0
src/PixiEditor.Linux/PixiEditor.Linux.csproj

@@ -10,4 +10,8 @@
       <ProjectReference Include="..\PixiEditor.OperatingSystem\PixiEditor.OperatingSystem.csproj" />
     </ItemGroup>
 
+    <ItemGroup>
+      <PackageReference Include="DeviceId.Linux" Version="6.9.0" />
+    </ItemGroup>
+
 </Project>

+ 6 - 0
src/PixiEditor.MacOs/MacOperatingSystem.cs

@@ -1,7 +1,9 @@
 using System.Text;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Threading;
+using DeviceId;
 using PixiEditor.OperatingSystem;
+using PixiEditor.OperatingSystem.Cryptography;
 
 namespace PixiEditor.MacOs;
 
@@ -14,6 +16,10 @@ public sealed class MacOperatingSystem : IOperatingSystem
     public IInputKeys InputKeys { get; } = new MacOsInputKeys();
     public IProcessUtility ProcessUtility { get; } = new MacOsProcessUtility();
 
+    public IEncryptor Encryptor { get; } = new AesHmacEncryptor(
+        new MacDeviceIdBuilder(new DeviceIdBuilder().AddMachineName()).AddSystemDriveSerialNumber()
+            .AddPlatformSerialNumber().ToString());
+
     private List<Uri> activationUris;
 
     public string ExecutableExtension { get; } = string.Empty;

+ 4 - 0
src/PixiEditor.MacOs/PixiEditor.MacOs.csproj

@@ -10,4 +10,8 @@
       <ProjectReference Include="..\PixiEditor.OperatingSystem\PixiEditor.OperatingSystem.csproj" />
     </ItemGroup>
 
+    <ItemGroup>
+      <PackageReference Include="DeviceId.Mac" Version="6.9.0" />
+    </ItemGroup>
+
 </Project>

+ 21 - 0
src/PixiEditor.OperatingSystem/Cryptography/AesHmacEncryptor.cs

@@ -0,0 +1,21 @@
+namespace PixiEditor.OperatingSystem.Cryptography;
+
+public class AesHmacEncryptor : IEncryptor
+{
+    private string password;
+
+    public AesHmacEncryptor(string pwd)
+    {
+        password = pwd;
+    }
+
+    public byte[] Encrypt(byte[] data)
+    {
+        return AesThenHmac.SimpleEncryptWithPassword(data, password);
+    }
+
+    public byte[] Decrypt(byte[] data)
+    {
+        return AesThenHmac.SimpleDecryptWithPassword(data, password);
+    }
+}

+ 405 - 0
src/PixiEditor.OperatingSystem/Cryptography/AesThenHmac.cs

@@ -0,0 +1,405 @@
+/*
+ * This work (Modern Encryption of a String C#, by James Tuley),
+ * identified by James Tuley, is free of known copyright restrictions.
+ * https://gist.github.com/4336842
+ * http://creativecommons.org/publicdomain/mark/1.0/
+ */
+
+using System.Security.Cryptography;
+using System.Text;
+
+namespace PixiEditor.OperatingSystem.Cryptography
+{
+    public static class AesThenHmac
+    {
+        private static readonly RandomNumberGenerator Random = RandomNumberGenerator.Create();
+
+        //Preconfigured Encryption Parameters
+        public static readonly int BlockBitSize = 128;
+        public static readonly int KeyBitSize = 256;
+
+        //Preconfigured Password Key Derivation Parameters
+        public static readonly int SaltBitSize = 64;
+        public static readonly int Iterations = 10000;
+        public static readonly int MinPasswordLength = 4;
+
+        /// <summary>
+        /// Helper that generates a random key on each call.
+        /// </summary>
+        /// <returns></returns>
+        public static byte[] NewKey()
+        {
+            var key = new byte[KeyBitSize / 8];
+            Random.GetBytes(key);
+            return key;
+        }
+
+        /// <summary>
+        /// Simple Encryption (AES) then Authentication (HMAC) for a UTF8 Message.
+        /// </summary>
+        /// <param name="secretMessage">The secret message.</param>
+        /// <param name="cryptKey">The crypt key.</param>
+        /// <param name="authKey">The auth key.</param>
+        /// <param name="nonSecretPayload">(Optional) Non-Secret Payload.</param>
+        /// <returns>
+        /// Encrypted Message
+        /// </returns>
+        /// <exception cref="System.ArgumentException">Secret Message Required!;secretMessage</exception>
+        /// <remarks>
+        /// Adds overhead of (Optional-Payload + BlockSize(16) + Message-Padded-To-Blocksize +  HMac-Tag(32)) * 1.33 Base64
+        /// </remarks>
+        public static string SimpleEncrypt(string secretMessage, byte[] cryptKey, byte[] authKey,
+                                           byte[] nonSecretPayload = null)
+        {
+            if (string.IsNullOrEmpty(secretMessage))
+                throw new ArgumentException("Secret Message Required!", "secretMessage");
+
+            var plainText = Encoding.UTF8.GetBytes(secretMessage);
+            var cipherText = SimpleEncrypt(plainText, cryptKey, authKey, nonSecretPayload);
+            return Convert.ToBase64String(cipherText);
+        }
+
+        /// <summary>
+        /// Simple Authentication (HMAC) then Decryption (AES) for a secrets UTF8 Message.
+        /// </summary>
+        /// <param name="encryptedMessage">The encrypted message.</param>
+        /// <param name="cryptKey">The crypt key.</param>
+        /// <param name="authKey">The auth key.</param>
+        /// <param name="nonSecretPayloadLength">Length of the non secret payload.</param>
+        /// <returns>
+        /// Decrypted Message
+        /// </returns>
+        /// <exception cref="System.ArgumentException">Encrypted Message Required!;encryptedMessage</exception>
+        public static string SimpleDecrypt(string encryptedMessage, byte[] cryptKey, byte[] authKey,
+                                           int nonSecretPayloadLength = 0)
+        {
+            if (string.IsNullOrWhiteSpace(encryptedMessage))
+                throw new ArgumentException("Encrypted Message Required!", "encryptedMessage");
+
+            var cipherText = Convert.FromBase64String(encryptedMessage);
+            var plainText = SimpleDecrypt(cipherText, cryptKey, authKey, nonSecretPayloadLength);
+            return Encoding.UTF8.GetString(plainText);
+        }
+
+        /// <summary>
+        /// Simple Encryption (AES) then Authentication (HMAC) of a UTF8 message
+        /// using Keys derived from a Password (PBKDF2).
+        /// </summary>
+        /// <param name="secretMessage">The secret message.</param>
+        /// <param name="password">The password.</param>
+        /// <param name="nonSecretPayload">The non secret payload.</param>
+        /// <returns>
+        /// Encrypted Message
+        /// </returns>
+        /// <exception cref="System.ArgumentException">password</exception>
+        /// <remarks>
+        /// Significantly less secure than using random binary keys.
+        /// Adds additional non secret payload for key generation parameters.
+        /// </remarks>
+        public static string SimpleEncryptWithPassword(string secretMessage, string password,
+                                                       byte[] nonSecretPayload = null)
+        {
+            if (string.IsNullOrEmpty(secretMessage))
+                throw new ArgumentException("Secret Message Required!", "secretMessage");
+
+            var plainText = Encoding.UTF8.GetBytes(secretMessage);
+            var cipherText = SimpleEncryptWithPassword(plainText, password, nonSecretPayload);
+            return Convert.ToBase64String(cipherText);
+        }
+
+        /// <summary>
+        /// Simple Authentication (HMAC) and then Descryption (AES) of a UTF8 Message
+        /// using keys derived from a password (PBKDF2).
+        /// </summary>
+        /// <param name="encryptedMessage">The encrypted message.</param>
+        /// <param name="password">The password.</param>
+        /// <param name="nonSecretPayloadLength">Length of the non secret payload.</param>
+        /// <returns>
+        /// Decrypted Message
+        /// </returns>
+        /// <exception cref="System.ArgumentException">Encrypted Message Required!;encryptedMessage</exception>
+        /// <remarks>
+        /// Significantly less secure than using random binary keys.
+        /// </remarks>
+        public static string SimpleDecryptWithPassword(string encryptedMessage, string password,
+                                                       int nonSecretPayloadLength = 0)
+        {
+            if (string.IsNullOrWhiteSpace(encryptedMessage))
+                throw new ArgumentException("Encrypted Message Required!", "encryptedMessage");
+
+            var cipherText = Convert.FromBase64String(encryptedMessage);
+            var plainText = SimpleDecryptWithPassword(cipherText, password, nonSecretPayloadLength);
+            return Encoding.UTF8.GetString(plainText);
+        }
+
+        /// <summary>
+        /// Simple Encryption(AES) then Authentication (HMAC) for a UTF8 Message.
+        /// </summary>
+        /// <param name="secretMessage">The secret message.</param>
+        /// <param name="cryptKey">The crypt key.</param>
+        /// <param name="authKey">The auth key.</param>
+        /// <param name="nonSecretPayload">(Optional) Non-Secret Payload.</param>
+        /// <returns>
+        /// Encrypted Message
+        /// </returns>
+        /// <remarks>
+        /// Adds overhead of (Optional-Payload + BlockSize(16) + Message-Padded-To-Blocksize +  HMac-Tag(32)) * 1.33 Base64
+        /// </remarks>
+        public static byte[] SimpleEncrypt(byte[] secretMessage, byte[] cryptKey, byte[] authKey, byte[] nonSecretPayload = null)
+        {
+            //User Error Checks
+            if (cryptKey == null || cryptKey.Length != KeyBitSize / 8)
+                throw new ArgumentException(String.Format("Key needs to be {0} bit!", KeyBitSize), "cryptKey");
+
+            if (authKey == null || authKey.Length != KeyBitSize / 8)
+                throw new ArgumentException(String.Format("Key needs to be {0} bit!", KeyBitSize), "authKey");
+
+            if (secretMessage == null || secretMessage.Length < 1)
+                throw new ArgumentException("Secret Message Required!", "secretMessage");
+
+            //non-secret payload optional
+            nonSecretPayload = nonSecretPayload ?? new byte[] { };
+
+            byte[] cipherText;
+            byte[] iv;
+
+            using (var aes = new AesManaged
+            {
+                KeySize = KeyBitSize,
+                BlockSize = BlockBitSize,
+                Mode = CipherMode.CBC,
+                Padding = PaddingMode.PKCS7
+            })
+            {
+
+                //Use random IV
+                aes.GenerateIV();
+                iv = aes.IV;
+
+                using (var encrypter = aes.CreateEncryptor(cryptKey, iv))
+                using (var cipherStream = new MemoryStream())
+                {
+                    using (var cryptoStream = new CryptoStream(cipherStream, encrypter, CryptoStreamMode.Write))
+                    using (var binaryWriter = new BinaryWriter(cryptoStream))
+                    {
+                        //Encrypt Data
+                        binaryWriter.Write(secretMessage);
+                    }
+
+                    cipherText = cipherStream.ToArray();
+                }
+
+            }
+
+            //Assemble encrypted message and add authentication
+            using (var hmac = new HMACSHA256(authKey))
+            using (var encryptedStream = new MemoryStream())
+            {
+                using (var binaryWriter = new BinaryWriter(encryptedStream))
+                {
+                    //Prepend non-secret payload if any
+                    binaryWriter.Write(nonSecretPayload);
+                    //Prepend IV
+                    binaryWriter.Write(iv);
+                    //Write Ciphertext
+                    binaryWriter.Write(cipherText);
+                    binaryWriter.Flush();
+
+                    //Authenticate all data
+                    var tag = hmac.ComputeHash(encryptedStream.ToArray());
+                    //Postpend tag
+                    binaryWriter.Write(tag);
+                }
+                return encryptedStream.ToArray();
+            }
+
+        }
+
+        /// <summary>
+        /// Simple Authentication (HMAC) then Decryption (AES) for a secrets UTF8 Message.
+        /// </summary>
+        /// <param name="encryptedMessage">The encrypted message.</param>
+        /// <param name="cryptKey">The crypt key.</param>
+        /// <param name="authKey">The auth key.</param>
+        /// <param name="nonSecretPayloadLength">Length of the non secret payload.</param>
+        /// <returns>Decrypted Message</returns>
+        public static byte[] SimpleDecrypt(byte[] encryptedMessage, byte[] cryptKey, byte[] authKey, int nonSecretPayloadLength = 0)
+        {
+
+            //Basic Usage Error Checks
+            if (cryptKey == null || cryptKey.Length != KeyBitSize / 8)
+                throw new ArgumentException(String.Format("CryptKey needs to be {0} bit!", KeyBitSize), "cryptKey");
+
+            if (authKey == null || authKey.Length != KeyBitSize / 8)
+                throw new ArgumentException(String.Format("AuthKey needs to be {0} bit!", KeyBitSize), "authKey");
+
+            if (encryptedMessage == null || encryptedMessage.Length == 0)
+                throw new ArgumentException("Encrypted Message Required!", "encryptedMessage");
+
+            using (var hmac = new HMACSHA256(authKey))
+            {
+                var sentTag = new byte[hmac.HashSize / 8];
+                //Calculate Tag
+                var calcTag = hmac.ComputeHash(encryptedMessage, 0, encryptedMessage.Length - sentTag.Length);
+                var ivLength = (BlockBitSize / 8);
+
+                //if message length is to small just return null
+                if (encryptedMessage.Length < sentTag.Length + nonSecretPayloadLength + ivLength)
+                    return null;
+
+                //Grab Sent Tag
+                Array.Copy(encryptedMessage, encryptedMessage.Length - sentTag.Length, sentTag, 0, sentTag.Length);
+
+                //Compare Tag with constant time comparison
+                var compare = 0;
+                for (var i = 0; i < sentTag.Length; i++)
+                    compare |= sentTag[i] ^ calcTag[i];
+
+                //if message doesn't authenticate return null
+                if (compare != 0)
+                    return null;
+
+                using (var aes = new AesManaged
+                {
+                    KeySize = KeyBitSize,
+                    BlockSize = BlockBitSize,
+                    Mode = CipherMode.CBC,
+                    Padding = PaddingMode.PKCS7
+                })
+                {
+
+                    //Grab IV from message
+                    var iv = new byte[ivLength];
+                    Array.Copy(encryptedMessage, nonSecretPayloadLength, iv, 0, iv.Length);
+
+                    using (var decrypter = aes.CreateDecryptor(cryptKey, iv))
+                    using (var plainTextStream = new MemoryStream())
+                    {
+                        using (var decrypterStream = new CryptoStream(plainTextStream, decrypter, CryptoStreamMode.Write))
+                        using (var binaryWriter = new BinaryWriter(decrypterStream))
+                        {
+                            //Decrypt Cipher Text from Message
+                            binaryWriter.Write(
+                                encryptedMessage,
+                                nonSecretPayloadLength + iv.Length,
+                                encryptedMessage.Length - nonSecretPayloadLength - iv.Length - sentTag.Length
+                            );
+                        }
+                        //Return Plain Text
+                        return plainTextStream.ToArray();
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Simple Encryption (AES) then Authentication (HMAC) of a UTF8 message
+        /// using Keys derived from a Password (PBKDF2)
+        /// </summary>
+        /// <param name="secretMessage">The secret message.</param>
+        /// <param name="password">The password.</param>
+        /// <param name="nonSecretPayload">The non secret payload.</param>
+        /// <returns>
+        /// Encrypted Message
+        /// </returns>
+        /// <exception cref="System.ArgumentException">Must have a password of minimum length;password</exception>
+        /// <remarks>
+        /// Significantly less secure than using random binary keys.
+        /// Adds additional non secret payload for key generation parameters.
+        /// </remarks>
+        public static byte[] SimpleEncryptWithPassword(byte[] secretMessage, string password, byte[] nonSecretPayload = null)
+        {
+            nonSecretPayload = nonSecretPayload ?? new byte[] {};
+
+            //User Error Checks
+            if (string.IsNullOrWhiteSpace(password) || password.Length < MinPasswordLength)
+                throw new ArgumentException(String.Format("Must have a password of at least {0} characters!", MinPasswordLength), "password");
+
+            if (secretMessage == null || secretMessage.Length ==0)
+                throw new ArgumentException("Secret Message Required!", "secretMessage");
+
+            var payload = new byte[((SaltBitSize / 8) * 2) + nonSecretPayload.Length];
+
+            Array.Copy(nonSecretPayload, payload, nonSecretPayload.Length);
+            int payloadIndex = nonSecretPayload.Length;
+
+            byte[] cryptKey;
+            byte[] authKey;
+            //Use Random Salt to prevent pre-generated weak password attacks.
+            using (var generator = new Rfc2898DeriveBytes(password, SaltBitSize / 8, Iterations))
+            {
+                var salt = generator.Salt;
+
+                //Generate Keys
+                cryptKey = generator.GetBytes(KeyBitSize / 8);
+
+                //Create Non Secret Payload
+                Array.Copy(salt, 0, payload, payloadIndex, salt.Length);
+                payloadIndex += salt.Length;
+            }
+
+            //Deriving separate key, might be less efficient than using HKDF,
+            //but now compatible with RNEncryptor which had a very similar wireformat and requires less code than HKDF.
+            using (var generator = new Rfc2898DeriveBytes(password, SaltBitSize / 8, Iterations))
+            {
+                var salt = generator.Salt;
+
+                //Generate Keys
+                authKey = generator.GetBytes(KeyBitSize / 8);
+
+                //Create Rest of Non Secret Payload
+                Array.Copy(salt, 0, payload, payloadIndex, salt.Length);
+            }
+
+            return SimpleEncrypt(secretMessage, cryptKey, authKey, payload);
+        }
+
+        /// <summary>
+        /// Simple Authentication (HMAC) and then Descryption (AES) of a UTF8 Message
+        /// using keys derived from a password (PBKDF2).
+        /// </summary>
+        /// <param name="encryptedMessage">The encrypted message.</param>
+        /// <param name="password">The password.</param>
+        /// <param name="nonSecretPayloadLength">Length of the non secret payload.</param>
+        /// <returns>
+        /// Decrypted Message
+        /// </returns>
+        /// <exception cref="System.ArgumentException">Must have a password of minimum length;password</exception>
+        /// <remarks>
+        /// Significantly less secure than using random binary keys.
+        /// </remarks>
+        public static byte[] SimpleDecryptWithPassword(byte[] encryptedMessage, string password, int nonSecretPayloadLength = 0)
+        {
+            //User Error Checks
+            if (string.IsNullOrWhiteSpace(password) || password.Length < MinPasswordLength)
+                throw new ArgumentException(String.Format("Must have a password of at least {0} characters!", MinPasswordLength), "password");
+
+            if (encryptedMessage == null || encryptedMessage.Length == 0)
+                throw new ArgumentException("Encrypted Message Required!", "encryptedMessage");
+
+            var cryptSalt = new byte[SaltBitSize / 8];
+            var authSalt = new byte[SaltBitSize / 8];
+
+            //Grab Salt from Non-Secret Payload
+            Array.Copy(encryptedMessage, nonSecretPayloadLength, cryptSalt, 0, cryptSalt.Length);
+            Array.Copy(encryptedMessage, nonSecretPayloadLength + cryptSalt.Length, authSalt, 0, authSalt.Length);
+
+            byte[] cryptKey;
+            byte[] authKey;
+
+            //Generate crypt key
+            using (var generator = new Rfc2898DeriveBytes(password, cryptSalt, Iterations))
+            {
+                cryptKey = generator.GetBytes(KeyBitSize / 8);
+            }
+            //Generate auth key
+            using (var generator = new Rfc2898DeriveBytes(password, authSalt, Iterations))
+            {
+                authKey = generator.GetBytes(KeyBitSize / 8);
+            }
+
+            return SimpleDecrypt(encryptedMessage, cryptKey, authKey, cryptSalt.Length + authSalt.Length + nonSecretPayloadLength);
+        }
+
+    }
+}

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

@@ -0,0 +1,7 @@
+namespace PixiEditor.OperatingSystem;
+
+public interface IEncryptor
+{
+    public byte[] Encrypt(byte[] data);
+    public byte[] Decrypt(byte[] data);
+}

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

@@ -13,7 +13,7 @@ public interface IOperatingSystem
 
     public IInputKeys InputKeys { get; }
     public IProcessUtility ProcessUtility { get; }
-    public ISecureStorage SecureStorage { get; }
+    public IEncryptor Encryptor { get; }
     public bool IsMacOs => Name == "MacOS";
     public bool IsWindows => Name == "Windows";
     public bool IsLinux => Name == "Linux";

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

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

+ 14 - 24
src/PixiEditor.Windows/WindowsSecureStorage.cs → src/PixiEditor.OperatingSystem/SecureStorage.cs

@@ -1,18 +1,16 @@
-using System.Security.Cryptography;
-using System.Text;
+using System.Text;
 using System.Text.Json;
-using PixiEditor.OperatingSystem;
 
-namespace PixiEditor.Windows;
+namespace PixiEditor.OperatingSystem;
 
-internal class WindowsSecureStorage : ISecureStorage
+public static class SecureStorage
 {
-    public string PathToStorage => Path.Combine(
+    public static string PathToStorage => Path.Combine(
         Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
         "PixiEditor",
         "SecureStorage.data");
 
-    public WindowsSecureStorage()
+    static SecureStorage()
     {
         if (!File.Exists(PathToStorage))
         {
@@ -21,16 +19,13 @@ internal class WindowsSecureStorage : ISecureStorage
         }
     }
 
-    public async Task<T?> GetValueAsync<T>(string key, T defaultValue = default)
+    public static 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);
+            byte[] decryptedData = IOperatingSystem.Current.Encryptor.Decrypt(current);
 
             string existingValue = Encoding.UTF8.GetString(decryptedData);
             Dictionary<string, object>? data = JsonSerializer.Deserialize<Dictionary<string, object>>(existingValue);
@@ -49,37 +44,32 @@ internal class WindowsSecureStorage : ISecureStorage
         return defaultValue;
     }
 
-    public async Task SetValueAsync<T>(string key, T value)
+    public static async Task SetValueAsync<T>(string key, T value)
     {
         byte[] current = ReadExistingData();
 
         Dictionary<string, object> data = new Dictionary<string, object>();
 
-        if(current is { Length: > 0 })
+        if (current is { Length: > 0 })
         {
-            byte[] decryptedData = ProtectedData.Unprotect(
-                current,
-                null,
-                DataProtectionScope.CurrentUser);
+            byte[] decryptedData = IOperatingSystem.Current.Encryptor.Decrypt(current);
 
             string existingValue = Encoding.UTF8.GetString(decryptedData);
-            data = JsonSerializer.Deserialize<Dictionary<string, object>>(existingValue) ?? new Dictionary<string, object>();
+            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);
+        byte[] encryptedData = IOperatingSystem.Current.Encryptor.Encrypt(newData);
 
         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()
+    private static byte[] ReadExistingData()
     {
         var stream = new FileStream(PathToStorage, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
         byte[] existingData = new byte[stream.Length];

+ 19 - 0
src/PixiEditor.Windows/DpapiEncryptor.cs

@@ -0,0 +1,19 @@
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+using PixiEditor.OperatingSystem;
+
+namespace PixiEditor.Windows;
+
+public class DpapiEncryptor : IEncryptor
+{
+    public byte[] Encrypt(byte[] data)
+    {
+        return ProtectedData.Protect(data, null, DataProtectionScope.CurrentUser);
+    }
+
+    public byte[] Decrypt(byte[] data)
+    {
+        return ProtectedData.Unprotect(data, null, DataProtectionScope.CurrentUser);
+    }
+}

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

@@ -14,7 +14,7 @@ public sealed class WindowsOperatingSystem : IOperatingSystem
     
     public IInputKeys InputKeys { get; } = new WindowsInputKeys();
     public IProcessUtility ProcessUtility { get; } = new WindowsProcessUtility();
-    public ISecureStorage SecureStorage { get; } = new WindowsSecureStorage();
+    public IEncryptor Encryptor { get; } = new DpapiEncryptor();
 
     private const string UniqueEventName = "33f1410b-2ad7-412a-a468-34fe0a85747c";
     

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

@@ -1032,5 +1032,6 @@
   "INTERNAL_SERVER_ERROR": "There was an internal server error. Please try again later.",
   "TOO_MANY_REQUESTS": "Too many requests. Try again in {0} seconds.",
   "SESSION_EXPIRED": "Session expired. Please log in again.",
-  "CONNECTION_ERROR": "Connection error. Please check your internet connection."
+  "CONNECTION_ERROR": "Connection error. Please check your internet connection.",
+  "FAIL_LOAD_USER_DATA": "Failed to load saved user data"
 }

+ 20 - 2
src/PixiEditor/ViewModels/SubViewModels/UserViewModel.cs

@@ -1,6 +1,7 @@
 using Avalonia.Threading;
 using CommunityToolkit.Mvvm.Input;
 using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Helpers;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.OperatingSystem;
 using PixiEditor.PixiAuth;
@@ -280,12 +281,29 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
 
     public async Task SaveUserInfo()
     {
-        await IOperatingSystem.Current.SecureStorage.SetValueAsync("UserData", User);
+        try
+        {
+            await SecureStorage.SetValueAsync("UserData", User);
+        }
+        catch (Exception e)
+        {
+            CrashHelper.SendExceptionInfo(e);
+        }
     }
 
     public async Task LoadUserData()
     {
-        User = await IOperatingSystem.Current.SecureStorage.GetValueAsync<User>("UserData", null);
+        try
+        {
+            User = await SecureStorage.GetValueAsync<User>("UserData", null);
+        }
+        catch (Exception e)
+        {
+            CrashHelper.SendExceptionInfo(e);
+            User = null;
+            NotifyProperties();
+            LastError = "FAIL_LOAD_USER_DATA";
+        }
     }
 
     public async Task LogoutIfTokenExpired()