mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 15:53:48 +00:00
Added symmetric encryption to the Rust runtime and the .NET app
This commit is contained in:
9 files changed
+464
-134
No files matched your search
@@ -48,6 +48,8 @@
|
||||
<ThirdPartyComponent Name="tokio" Developer="Alex Crichton & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/tokio/blob/master/LICENSE" RepositoryUrl="https://github.com/tokio-rs/tokio" UseCase="Code in the Rust language can be specified as synchronous or asynchronous. Unlike .NET and the C# language, Rust cannot execute asynchronous code by itself. Rust requires support in the form of an executor for this. Tokio is one such executor."/>
|
||||
<ThirdPartyComponent Name="flexi_logger" Developer="emabee & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/emabee/flexi_logger/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/emabee/flexi_logger" UseCase="This Rust library is used to output the app's messages to the terminal. This is helpful during development and troubleshooting. This feature is initially invisible; when the app is started via the terminal, the messages become visible."/>
|
||||
<ThirdPartyComponent Name="rand" Developer="Rust developers & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-random/rand/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-random/rand" UseCase="We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose."/>
|
||||
<ThirdPartyComponent Name="base64" Developer="Marshall Pierce, Alice Maz & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/marshallpierce/rust-base64/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/marshallpierce/rust-base64" UseCase="For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose."/>
|
||||
<ThirdPartyComponent Name="Rust Crypto" Developer="Artyom Pavlov, Tony Arcieri, Brian Warner, Arthur Gautier, Vlad Filippov, Friedel Ziegelmayer, Nicolas Stalder & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/RustCrypto/traits/blob/master/cipher/LICENSE-MIT" RepositoryUrl="https://github.com/RustCrypto" UseCase="When transferring sensitive data between Rust runtime and .NET app, we encrypt the data. We use some libraries from the Rust Crypto project for this purpose: cipher, aes, cbc, pbkdf2, hmac, and sha2. We are thankful for the great work of the Rust Crypto project."/>
|
||||
<ThirdPartyComponent Name="HtmlAgilityPack" Developer="ZZZ Projects & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE" RepositoryUrl="https://github.com/zzzprojects/html-agility-pack" UseCase="We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant."/>
|
||||
<ThirdPartyComponent Name="ReverseMarkdown" Developer="Babu Annamalai & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE" RepositoryUrl="https://github.com/mysticmind/reversemarkdown-net" UseCase="This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant."/>
|
||||
<ThirdPartyComponent Name="wikEd diff" Developer="Cacycle & Open Source Community" LicenseName="None (public domain)" LicenseUrl="https://en.wikipedia.org/wiki/User:Cacycle/diff#License" RepositoryUrl="https://en.wikipedia.org/wiki/User:Cacycle/diff" UseCase="This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant."/>
|
||||
|
||||
@@ -29,13 +29,23 @@ if(appPort == 0)
|
||||
}
|
||||
|
||||
// Read the secret key for the IPC from the AI_STUDIO_SECRET_KEY environment variable:
|
||||
var secretKey = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_KEY");
|
||||
if(string.IsNullOrWhiteSpace(secretKey))
|
||||
var secretPasswordEncoded = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_PASSWORD");
|
||||
if(string.IsNullOrWhiteSpace(secretPasswordEncoded))
|
||||
{
|
||||
Console.WriteLine("The AI_STUDIO_SECRET_KEY environment variable is not set.");
|
||||
Console.WriteLine("The AI_STUDIO_SECRET_PASSWORD environment variable is not set.");
|
||||
return;
|
||||
}
|
||||
|
||||
var secretPassword = Convert.FromBase64String(secretPasswordEncoded);
|
||||
var secretKeySaltEncoded = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_KEY_SALT");
|
||||
if(string.IsNullOrWhiteSpace(secretKeySaltEncoded))
|
||||
{
|
||||
Console.WriteLine("The AI_STUDIO_SECRET_KEY_SALT environment variable is not set.");
|
||||
return;
|
||||
}
|
||||
|
||||
var secretKeySalt = Convert.FromBase64String(secretKeySaltEncoded);
|
||||
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.SetMinimumLevel(LogLevel.Debug);
|
||||
@@ -92,7 +102,19 @@ builder.WebHost.UseWebRoot("wwwroot");
|
||||
builder.WebHost.UseStaticWebAssets();
|
||||
#endif
|
||||
|
||||
// Execute the builder to get the app:
|
||||
var app = builder.Build();
|
||||
|
||||
// Initialize the encryption service:
|
||||
var encryptionLogger = app.Services.GetRequiredService<ILogger<Encryption>>();
|
||||
var encryption = new Encryption(encryptionLogger, secretPassword, secretKeySalt);
|
||||
var encryptionInitializer = encryption.Initialize();
|
||||
|
||||
// Set the logger for the Rust service:
|
||||
var rustLogger = app.Services.GetRequiredService<ILogger<Rust>>();
|
||||
rust.SetLogger(rustLogger);
|
||||
rust.SetEncryptor(encryption);
|
||||
|
||||
app.Use(Redirect.HandlerContentAsync);
|
||||
|
||||
#if DEBUG
|
||||
@@ -115,8 +137,6 @@ app.MapRazorComponents<App>()
|
||||
|
||||
var serverTask = app.RunAsync();
|
||||
|
||||
var rustLogger = app.Services.GetRequiredService<ILogger<Rust>>();
|
||||
rust.SetLogger(rustLogger);
|
||||
|
||||
await encryptionInitializer;
|
||||
await rust.AppIsReady();
|
||||
await serverTask;
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Security;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public readonly record struct EncryptedText(string EncryptedData)
|
||||
{
|
||||
public EncryptedText() : this(string.Empty)
|
||||
{
|
||||
throw new SecurityException("Please provide the encrypted data.");
|
||||
}
|
||||
|
||||
public static implicit operator string(EncryptedText encryptedText) => encryptedText.EncryptedData;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public static class EncryptedTextExtensions
|
||||
{
|
||||
public static async Task<EncryptedText> Encrypt(this string data, Encryption encryption) => await encryption.Encrypt(data);
|
||||
|
||||
public static async Task<string> Decrypt(this EncryptedText encryptedText, Encryption encryption) => await encryption.Decrypt(encryptedText);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public sealed class Encryption(ILogger<Encryption> logger, byte[] secretPassword, byte[] secretKeySalt)
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of iterations to derive the key and IV from the password. For a password manager
|
||||
/// where the user has to enter their primary password, 100 iterations would be too few and
|
||||
/// insecure. Here, the use case is different: We generate a 512-byte long and cryptographically
|
||||
/// secure password at every start. This password already contains enough entropy. In our case,
|
||||
/// we need key and IV primarily because AES, with the algorithms we chose, requires a fixed key
|
||||
/// length, and our password is too long.
|
||||
/// </summary>
|
||||
private const int ITERATIONS = 100;
|
||||
|
||||
private readonly byte[] key = new byte[32];
|
||||
private readonly byte[] iv = new byte[16];
|
||||
|
||||
public async Task Initialize()
|
||||
{
|
||||
logger.LogInformation("Initializing encryption service...");
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
if (secretPassword.Length != 512)
|
||||
{
|
||||
logger.LogError($"The secret password must be 512 bytes long. It was {secretPassword.Length} bytes long.");
|
||||
throw new CryptographicException("The secret password must be 512 bytes long.");
|
||||
}
|
||||
|
||||
if(secretKeySalt.Length != 16)
|
||||
{
|
||||
logger.LogError($"The salt data must be 16 bytes long. It was {secretKeySalt.Length} bytes long.");
|
||||
throw new CryptographicException("The salt data must be 16 bytes long.");
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
var initDuration = stopwatch.Elapsed;
|
||||
|
||||
stopwatch.Stop();
|
||||
logger.LogInformation($"Encryption service initialized in {initDuration.TotalMilliseconds} milliseconds.");
|
||||
}
|
||||
|
||||
public async Task<EncryptedText> Encrypt(string data)
|
||||
{
|
||||
// Create AES encryption:
|
||||
using var aes = Aes.Create();
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.Key = this.key;
|
||||
aes.IV = this.iv;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
|
||||
using var encryption = aes.CreateEncryptor();
|
||||
|
||||
// Copy the given string data into a memory stream:
|
||||
await using var plainDataStream = new MemoryStream(Encoding.UTF8.GetBytes(data));
|
||||
|
||||
// A memory stream for the final, encrypted data:
|
||||
await using var encryptedAndEncodedData = new MemoryStream();
|
||||
|
||||
// A base64 stream for the encoding:
|
||||
await using var base64Stream = new CryptoStream(encryptedAndEncodedData, new ToBase64Transform(), CryptoStreamMode.Write);
|
||||
|
||||
// Write the salt into the base64 stream:
|
||||
await base64Stream.WriteAsync(secretKeySalt);
|
||||
|
||||
// Create the encryption stream:
|
||||
await using var cryptoStream = new CryptoStream(base64Stream, encryption, CryptoStreamMode.Write);
|
||||
|
||||
// Write the payload into the encryption stream:
|
||||
await plainDataStream.CopyToAsync(cryptoStream);
|
||||
|
||||
// Flush the final block. Please note that it is not enough to call the regular flush method.
|
||||
await cryptoStream.FlushFinalBlockAsync();
|
||||
|
||||
// Convert the base64 encoded data back into a string. Uses GetBuffer due to the advantage that
|
||||
// it does not create another copy of the data. ToArray would create another copy of the data.
|
||||
return new EncryptedText(Encoding.ASCII.GetString(encryptedAndEncodedData.GetBuffer()[..(int)encryptedAndEncodedData.Length]));
|
||||
}
|
||||
|
||||
public async Task<string> Decrypt(EncryptedText encryptedData)
|
||||
{
|
||||
// Build a memory stream to access the given base64 encoded data:
|
||||
await using var encodedEncryptedStream = new MemoryStream(Encoding.ASCII.GetBytes(encryptedData));
|
||||
|
||||
// Wrap around the base64 decoder stream:
|
||||
await using var base64Stream = new CryptoStream(encodedEncryptedStream, new FromBase64Transform(), CryptoStreamMode.Read);
|
||||
|
||||
// A buffer for the salt's bytes:
|
||||
var readSaltBytes = new byte[16]; // 16 bytes = Guid
|
||||
|
||||
// Read the salt's bytes out of the stream:
|
||||
var readBytes = await base64Stream.ReadAsync(readSaltBytes);
|
||||
if(readBytes != 16)
|
||||
{
|
||||
logger.LogError($"Read {readBytes} bytes instead of 16 bytes for the salt.");
|
||||
throw new CryptographicException("Failed to read the salt bytes.");
|
||||
}
|
||||
|
||||
// Check the salt bytes:
|
||||
if(!readSaltBytes.SequenceEqual(secretKeySalt))
|
||||
{
|
||||
logger.LogError("The salt bytes do not match. The data is corrupted or tampered.");
|
||||
throw new CryptographicException("The salt bytes do not match. The data is corrupted or tampered.");
|
||||
}
|
||||
|
||||
// Create AES decryption:
|
||||
using var aes = Aes.Create();
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.Key = this.key;
|
||||
aes.IV = this.iv;
|
||||
|
||||
using var decryption = aes.CreateDecryptor();
|
||||
|
||||
// A memory stream for the final, decrypted data:
|
||||
await using var decryptedData = new MemoryStream();
|
||||
|
||||
// The crypto stream:
|
||||
await using var cryptoStream = new CryptoStream(base64Stream, decryption, CryptoStreamMode.Read);
|
||||
|
||||
// Reads all remaining data through the decrypt stream. Note that this operation
|
||||
// starts at the current position, i.e., after the salt bytes:
|
||||
await cryptoStream.CopyToAsync(decryptedData);
|
||||
|
||||
// Convert the decrypted data back into a string. Uses GetBuffer due to the advantage that
|
||||
// it does not create another copy of the data. ToArray would create another copy of the data.
|
||||
return Encoding.UTF8.GetString(decryptedData.GetBuffer()[..(int)decryptedData.Length]);
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,20 @@ public sealed class Rust(string apiPort) : IDisposable
|
||||
{
|
||||
BaseAddress = new Uri($"http://127.0.0.1:{apiPort}"),
|
||||
};
|
||||
|
||||
private ILogger<Rust>? logger;
|
||||
|
||||
private ILogger<Rust>? logger;
|
||||
private Encryption? encryptor;
|
||||
|
||||
public void SetLogger(ILogger<Rust> logService)
|
||||
{
|
||||
this.logger = logService;
|
||||
}
|
||||
|
||||
public void SetEncryptor(Encryption encryptionService)
|
||||
{
|
||||
this.encryptor = encryptionService;
|
||||
}
|
||||
|
||||
public async Task<int> GetAppPort()
|
||||
{
|
||||
Console.WriteLine("Trying to get app port from Rust runtime...");
|
||||
|
||||
Reference in new issue
Block a user