Switched the key derivation to the PBKDF2 API of .NET 10 and pinned its results with tests

This commit is contained in:
Thorsten Sommer committed 2026-11-01 13:57:45 +01:00
1 parent 3246e4244f
commit c2e7577b6c
3 files changed
+63 -10

No files matched your search

+5 -6
View File
@@ -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:
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;
@@ -292,11 +292,11 @@ public sealed class EnterpriseEncryption
/// </summary>
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;
+54
View File
@@ -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));
}
}