diff --git a/app/MindWork AI Studio/Tools/Encryption.cs b/app/MindWork AI Studio/Tools/Encryption.cs index 91cc71aed..1dcb4f73a 100644 --- a/app/MindWork AI Studio/Tools/Encryption.cs +++ b/app/MindWork AI Studio/Tools/Encryption.cs @@ -39,12 +39,11 @@ public sealed class Encryption(ILogger logger, byte[] secretPassword // Derive key and iv vector: the operations take several seconds. Thus, using a task: await Task.Run(() => { - using var keyVectorObj = new Rfc2898DeriveBytes(secretPassword, secretKeySalt, ITERATIONS, HashAlgorithmName.SHA512); - var keyBytes = keyVectorObj.GetBytes(32); // the max valid key length = 256 bit = 32 bytes - var ivBytes = keyVectorObj.GetBytes(16); // the only valid block size = 128 bit = 16 bytes - - Array.Copy(keyBytes, this.key, this.key.Length); - Array.Copy(ivBytes, this.iv, this.iv.Length); + // The max valid key length = 256 bit = 32 bytes, and the only valid block size = 128 bit = 16 bytes: + var keyVectorBytes = Rfc2898DeriveBytes.Pbkdf2(secretPassword, secretKeySalt, ITERATIONS, HashAlgorithmName.SHA512, this.key.Length + this.iv.Length); + + Array.Copy(keyVectorBytes, 0, this.key, 0, this.key.Length); + Array.Copy(keyVectorBytes, this.key.Length, this.iv, 0, this.iv.Length); }); var initDuration = stopwatch.Elapsed; diff --git a/app/MindWork AI Studio/Tools/EnterpriseEncryption.cs b/app/MindWork AI Studio/Tools/EnterpriseEncryption.cs index 3adbd45fb..a28ec1f63 100644 --- a/app/MindWork AI Studio/Tools/EnterpriseEncryption.cs +++ b/app/MindWork AI Studio/Tools/EnterpriseEncryption.cs @@ -292,11 +292,11 @@ public sealed class EnterpriseEncryption /// private Aes CreateAesV1(byte[] salt) { - // Derive key and IV using PBKDF2: - using var keyDerivation = new Rfc2898DeriveBytes(this.secretKey!, salt, V1_ITERATIONS, HashAlgorithmName.SHA512); + // Derive key and IV using PBKDF2, the first 32 bytes for AES-256 and the last 16 for its block size: + var keyAndIv = Rfc2898DeriveBytes.Pbkdf2(this.secretKey!, salt, V1_ITERATIONS, HashAlgorithmName.SHA512, 48); var aes = Aes.Create(); - aes.Key = keyDerivation.GetBytes(32); // AES-256 - aes.IV = keyDerivation.GetBytes(16); // AES block size + aes.Key = keyAndIv[..32]; + aes.IV = keyAndIv[32..]; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; return aes; diff --git a/app/Tests/Tools/EncryptionTests.cs b/app/Tests/Tools/EncryptionTests.cs new file mode 100644 index 000000000..28d73486a --- /dev/null +++ b/app/Tests/Tools/EncryptionTests.cs @@ -0,0 +1,54 @@ +using AIStudio.Tools; + +using Microsoft.Extensions.Logging.Abstractions; + +namespace AIStudio.Tests.Tools; + +/// +/// Checks the encryption which the app shares with the runtime, for the secrets which travel +/// between both. +/// +/// +/// The runtime encrypts and decrypts the same values in Rust, with a password and a salt which it +/// makes anew at every start. Both sides therefore have to derive exactly the same key and IV. The +/// values here were built without AI Studio, the way the runtime builds them: PBKDF2 with SHA-512 and +/// 100 iterations derives 48 bytes from the password and the salt, the first 32 being the AES-256 key +/// and the last 16 the IV. openssl encrypted the text with AES-256-CBC, and the salt precedes the +/// ciphertext. +/// +[TestFixture] +public sealed class EncryptionTests +{ + private static readonly byte[] SECRET_PASSWORD = Enumerable.Range(0, 512).Select(index => (byte)index).ToArray(); + + private static readonly byte[] SECRET_KEY_SALT = Enumerable.Range(0xA0, 16).Select(index => (byte)index).ToArray(); + + private static async Task CreateEncryptionAsync() + { + var encryption = new Encryption(NullLogger.Instance, SECRET_PASSWORD, SECRET_KEY_SALT); + await encryption.Initialize(); + return encryption; + } + + [TestCase("oKGio6SlpqeoqaqrrK2ur3lNRzNAfoSYKyC70CpjwS8=", "", TestName = "An empty text")] + [TestCase("oKGio6SlpqeoqaqrrK2urwUfAfn5LbbIjbOC9nSvtUU=", "sk-imported-key", TestName = "A text within one block")] + [TestCase("oKGio6SlpqeoqaqrrK2urwbjgbJCNk6Zw1Z9HyIiJdZJ+E7czzLYlM/undXUIsDr", "Schlüssel für Größe 🔑", TestName = "A text beyond ASCII")] + public async Task TheAppEncryptsLikeTheRuntime(string expectedEncryptedValue, string text) + { + var encryption = await CreateEncryptionAsync(); + + var encryptedText = await encryption.Encrypt(text); + Assert.That(encryptedText.EncryptedData, Is.EqualTo(expectedEncryptedValue)); + } + + [TestCase("oKGio6SlpqeoqaqrrK2ur3lNRzNAfoSYKyC70CpjwS8=", "", TestName = "An empty encrypted text")] + [TestCase("oKGio6SlpqeoqaqrrK2urwUfAfn5LbbIjbOC9nSvtUU=", "sk-imported-key", TestName = "An encrypted text within one block")] + [TestCase("oKGio6SlpqeoqaqrrK2urwbjgbJCNk6Zw1Z9HyIiJdZJ+E7czzLYlM/undXUIsDr", "Schlüssel für Größe 🔑", TestName = "An encrypted text beyond ASCII")] + public async Task TheAppDecryptsWhatTheRuntimeEncrypted(string encryptedValue, string expectedText) + { + var encryption = await CreateEncryptionAsync(); + + var text = await encryption.Decrypt(new EncryptedText(encryptedValue)); + Assert.That(text, Is.EqualTo(expectedText)); + } +} \ No newline at end of file