mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-02 17:07:21 +00:00
Switched the key derivation to the PBKDF2 API of .NET 10 and pinned its results with tests
This commit is contained in:
3 files changed
+62
-9
No files matched your search
@@ -39,12 +39,11 @@ public sealed class Encryption(ILogger<Encryption> logger, byte[] secretPassword
|
|||||||
// Derive key and iv vector: the operations take several seconds. Thus, using a task:
|
// Derive key and iv vector: the operations take several seconds. Thus, using a task:
|
||||||
await Task.Run(() =>
|
await Task.Run(() =>
|
||||||
{
|
{
|
||||||
using var keyVectorObj = new Rfc2898DeriveBytes(secretPassword, secretKeySalt, ITERATIONS, HashAlgorithmName.SHA512);
|
// The max valid key length = 256 bit = 32 bytes, and the only valid block size = 128 bit = 16 bytes:
|
||||||
var keyBytes = keyVectorObj.GetBytes(32); // the max valid key length = 256 bit = 32 bytes
|
var keyVectorBytes = Rfc2898DeriveBytes.Pbkdf2(secretPassword, secretKeySalt, ITERATIONS, HashAlgorithmName.SHA512, this.key.Length + this.iv.Length);
|
||||||
var ivBytes = keyVectorObj.GetBytes(16); // the only valid block size = 128 bit = 16 bytes
|
|
||||||
|
|
||||||
Array.Copy(keyBytes, this.key, this.key.Length);
|
Array.Copy(keyVectorBytes, 0, this.key, 0, this.key.Length);
|
||||||
Array.Copy(ivBytes, this.iv, this.iv.Length);
|
Array.Copy(keyVectorBytes, this.key.Length, this.iv, 0, this.iv.Length);
|
||||||
});
|
});
|
||||||
|
|
||||||
var initDuration = stopwatch.Elapsed;
|
var initDuration = stopwatch.Elapsed;
|
||||||
|
|||||||
@@ -292,11 +292,11 @@ public sealed class EnterpriseEncryption
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private Aes CreateAesV1(byte[] salt)
|
private Aes CreateAesV1(byte[] salt)
|
||||||
{
|
{
|
||||||
// Derive key and IV using PBKDF2:
|
// Derive key and IV using PBKDF2, the first 32 bytes for AES-256 and the last 16 for its block size:
|
||||||
using var keyDerivation = new Rfc2898DeriveBytes(this.secretKey!, salt, V1_ITERATIONS, HashAlgorithmName.SHA512);
|
var keyAndIv = Rfc2898DeriveBytes.Pbkdf2(this.secretKey!, salt, V1_ITERATIONS, HashAlgorithmName.SHA512, 48);
|
||||||
var aes = Aes.Create();
|
var aes = Aes.Create();
|
||||||
aes.Key = keyDerivation.GetBytes(32); // AES-256
|
aes.Key = keyAndIv[..32];
|
||||||
aes.IV = keyDerivation.GetBytes(16); // AES block size
|
aes.IV = keyAndIv[32..];
|
||||||
aes.Mode = CipherMode.CBC;
|
aes.Mode = CipherMode.CBC;
|
||||||
aes.Padding = PaddingMode.PKCS7;
|
aes.Padding = PaddingMode.PKCS7;
|
||||||
return aes;
|
return aes;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using AIStudio.Tools;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
|
namespace AIStudio.Tests.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks the encryption which the app shares with the runtime, for the secrets which travel
|
||||||
|
/// between both.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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.
|
||||||
|
/// </remarks>
|
||||||
|
[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<Encryption> CreateEncryptionAsync()
|
||||||
|
{
|
||||||
|
var encryption = new Encryption(NullLogger<Encryption>.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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user