mirror of
				https://github.com/MindWorkAI/AI-Studio.git
				synced 2025-11-04 00:00:21 +00:00 
			
		
		
		
	Added symmetric encryption to the Rust runtime and the .NET app
This commit is contained in:
		
							parent
							
								
									7d341b2ce1
								
							
						
					
					
						commit
						79afd42195
					
				@ -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;
 | 
			
		||||
							
								
								
									
										13
									
								
								app/MindWork AI Studio/Tools/EncryptedText.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/MindWork AI Studio/Tools/EncryptedText.cs
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								app/MindWork AI Studio/Tools/EncryptedTextExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/MindWork AI Studio/Tools/EncryptedTextExtensions.cs
									
									
									
									
									
										Normal file
									
								
							@ -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);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										141
									
								
								app/MindWork AI Studio/Tools/Encryption.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								app/MindWork AI Studio/Tools/Encryption.cs
									
									
									
									
									
										Normal file
									
								
							@ -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]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -11,12 +11,18 @@ public sealed class Rust(string apiPort) : IDisposable
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    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...");
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										176
									
								
								runtime/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										176
									
								
								runtime/Cargo.lock
									
									
									
										generated
									
									
									
								
							@ -17,6 +17,17 @@ version = "1.0.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "aes"
 | 
			
		||||
version = "0.8.4"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "cipher",
 | 
			
		||||
 "cpufeatures",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "aho-corasick"
 | 
			
		||||
version = "1.1.3"
 | 
			
		||||
@ -239,6 +250,15 @@ dependencies = [
 | 
			
		||||
 "generic-array",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "block-padding"
 | 
			
		||||
version = "0.3.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "generic-array",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "block2"
 | 
			
		||||
version = "0.5.1"
 | 
			
		||||
@ -346,6 +366,15 @@ dependencies = [
 | 
			
		||||
 "toml 0.7.8",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "cbc"
 | 
			
		||||
version = "0.1.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cipher",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "cc"
 | 
			
		||||
version = "1.1.6"
 | 
			
		||||
@ -407,6 +436,16 @@ dependencies = [
 | 
			
		||||
 "windows-targets 0.52.6",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "cipher"
 | 
			
		||||
version = "0.4.4"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "crypto-common",
 | 
			
		||||
 "inout",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "clipboard-win"
 | 
			
		||||
version = "5.4.0"
 | 
			
		||||
@ -754,6 +793,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "block-buffer",
 | 
			
		||||
 "crypto-common",
 | 
			
		||||
 "subtle",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
@ -1496,6 +1536,15 @@ version = "0.4.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "hmac"
 | 
			
		||||
version = "0.12.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "digest",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "html5ever"
 | 
			
		||||
version = "0.26.0"
 | 
			
		||||
@ -1628,23 +1677,6 @@ dependencies = [
 | 
			
		||||
 "want",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "hyper-rustls"
 | 
			
		||||
version = "0.27.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "futures-util",
 | 
			
		||||
 "http 1.1.0",
 | 
			
		||||
 "hyper 1.4.1",
 | 
			
		||||
 "hyper-util",
 | 
			
		||||
 "rustls",
 | 
			
		||||
 "rustls-pki-types",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "tokio-rustls",
 | 
			
		||||
 "tower-service",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "hyper-tls"
 | 
			
		||||
version = "0.5.0"
 | 
			
		||||
@ -1821,6 +1853,16 @@ version = "0.1.15"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "inout"
 | 
			
		||||
version = "0.1.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "block-padding",
 | 
			
		||||
 "generic-array",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "instant"
 | 
			
		||||
version = "0.1.13"
 | 
			
		||||
@ -2090,17 +2132,24 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
 | 
			
		||||
name = "mindwork-ai-studio"
 | 
			
		||||
version = "0.8.12"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "aes",
 | 
			
		||||
 "arboard",
 | 
			
		||||
 "base64 0.22.1",
 | 
			
		||||
 "cbc",
 | 
			
		||||
 "cipher",
 | 
			
		||||
 "flexi_logger",
 | 
			
		||||
 "hmac",
 | 
			
		||||
 "keyring",
 | 
			
		||||
 "log",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "pbkdf2",
 | 
			
		||||
 "rand 0.8.5",
 | 
			
		||||
 "rand_chacha 0.3.1",
 | 
			
		||||
 "reqwest 0.12.5",
 | 
			
		||||
 "reqwest 0.12.4",
 | 
			
		||||
 "rocket",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "sha2",
 | 
			
		||||
 "tauri",
 | 
			
		||||
 "tauri-build",
 | 
			
		||||
 "tauri-plugin-window-state",
 | 
			
		||||
@ -2627,6 +2676,16 @@ version = "0.2.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pbkdf2"
 | 
			
		||||
version = "0.12.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "digest",
 | 
			
		||||
 "hmac",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pear"
 | 
			
		||||
version = "0.2.9"
 | 
			
		||||
@ -3160,7 +3219,7 @@ dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "serde_urlencoded",
 | 
			
		||||
 "sync_wrapper 0.1.2",
 | 
			
		||||
 "sync_wrapper",
 | 
			
		||||
 "system-configuration",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "tokio-native-tls",
 | 
			
		||||
@ -3176,9 +3235,9 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "reqwest"
 | 
			
		||||
version = "0.12.5"
 | 
			
		||||
version = "0.12.4"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
 | 
			
		||||
checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "base64 0.22.1",
 | 
			
		||||
 "bytes",
 | 
			
		||||
@ -3190,7 +3249,6 @@ dependencies = [
 | 
			
		||||
 "http-body 1.0.1",
 | 
			
		||||
 "http-body-util",
 | 
			
		||||
 "hyper 1.4.1",
 | 
			
		||||
 "hyper-rustls",
 | 
			
		||||
 "hyper-tls 0.6.0",
 | 
			
		||||
 "hyper-util",
 | 
			
		||||
 "ipnet",
 | 
			
		||||
@ -3205,7 +3263,7 @@ dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "serde_urlencoded",
 | 
			
		||||
 "sync_wrapper 1.0.1",
 | 
			
		||||
 "sync_wrapper",
 | 
			
		||||
 "system-configuration",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "tokio-native-tls",
 | 
			
		||||
@ -3241,21 +3299,6 @@ dependencies = [
 | 
			
		||||
 "windows 0.37.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "ring"
 | 
			
		||||
version = "0.17.8"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cc",
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "getrandom 0.2.15",
 | 
			
		||||
 "libc",
 | 
			
		||||
 "spin",
 | 
			
		||||
 "untrusted",
 | 
			
		||||
 "windows-sys 0.52.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rocket"
 | 
			
		||||
version = "0.5.1"
 | 
			
		||||
@ -3366,19 +3409,6 @@ dependencies = [
 | 
			
		||||
 "windows-sys 0.52.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rustls"
 | 
			
		||||
version = "0.23.12"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "rustls-pki-types",
 | 
			
		||||
 "rustls-webpki",
 | 
			
		||||
 "subtle",
 | 
			
		||||
 "zeroize",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rustls-pemfile"
 | 
			
		||||
version = "1.0.4"
 | 
			
		||||
@ -3404,17 +3434,6 @@ version = "1.7.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rustls-webpki"
 | 
			
		||||
version = "0.102.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "ring",
 | 
			
		||||
 "rustls-pki-types",
 | 
			
		||||
 "untrusted",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rustversion"
 | 
			
		||||
version = "1.0.17"
 | 
			
		||||
@ -3812,9 +3831,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "subtle"
 | 
			
		||||
version = "2.6.1"
 | 
			
		||||
version = "2.4.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
 | 
			
		||||
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "syn"
 | 
			
		||||
@ -3844,12 +3863,6 @@ version = "0.1.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "sync_wrapper"
 | 
			
		||||
version = "1.0.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "system-configuration"
 | 
			
		||||
version = "0.5.1"
 | 
			
		||||
@ -4339,17 +4352,6 @@ dependencies = [
 | 
			
		||||
 "tokio",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tokio-rustls"
 | 
			
		||||
version = "0.26.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "rustls",
 | 
			
		||||
 "rustls-pki-types",
 | 
			
		||||
 "tokio",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tokio-stream"
 | 
			
		||||
version = "0.1.15"
 | 
			
		||||
@ -4594,12 +4596,6 @@ version = "0.2.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "untrusted"
 | 
			
		||||
version = "0.9.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "url"
 | 
			
		||||
version = "2.5.2"
 | 
			
		||||
@ -5404,12 +5400,6 @@ dependencies = [
 | 
			
		||||
 "is-terminal",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "zeroize"
 | 
			
		||||
version = "1.8.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "zip"
 | 
			
		||||
version = "0.6.6"
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,13 @@ once_cell = "1.19.0"
 | 
			
		||||
rocket = { version = "0.5", default-features = false, features = ["json"] }
 | 
			
		||||
rand = "0.8"
 | 
			
		||||
rand_chacha = "0.3.1"
 | 
			
		||||
base64 = "0.22.1"
 | 
			
		||||
cipher = { version = "0.4.4", features = ["std"] }
 | 
			
		||||
aes = "0.8.4"
 | 
			
		||||
cbc = "0.1.2"
 | 
			
		||||
pbkdf2 = "0.12.2"
 | 
			
		||||
hmac = "0.12.1"
 | 
			
		||||
sha2 = "0.10.8"
 | 
			
		||||
 | 
			
		||||
[target.'cfg(target_os = "linux")'.dependencies]
 | 
			
		||||
# See issue https://github.com/tauri-apps/tauri/issues/4470
 | 
			
		||||
 | 
			
		||||
@ -5,28 +5,41 @@ extern crate rocket;
 | 
			
		||||
extern crate core;
 | 
			
		||||
 | 
			
		||||
use std::collections::{BTreeMap, HashMap, HashSet};
 | 
			
		||||
use std::iter::once;
 | 
			
		||||
use std::fmt;
 | 
			
		||||
use std::net::TcpListener;
 | 
			
		||||
use std::sync::{Arc, Mutex};
 | 
			
		||||
use std::fmt::Write;
 | 
			
		||||
use std::time::{Duration, Instant};
 | 
			
		||||
use once_cell::sync::Lazy;
 | 
			
		||||
 | 
			
		||||
use arboard::Clipboard;
 | 
			
		||||
use base64::Engine;
 | 
			
		||||
use base64::prelude::BASE64_STANDARD;
 | 
			
		||||
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
 | 
			
		||||
use keyring::Entry;
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
use serde::{Deserialize, Deserializer, Serialize};
 | 
			
		||||
use tauri::{Manager, Url, Window};
 | 
			
		||||
use tauri::api::process::{Command, CommandChild, CommandEvent};
 | 
			
		||||
use tokio::time;
 | 
			
		||||
use flexi_logger::{DeferredNow, Duplicate, FileSpec, Logger};
 | 
			
		||||
use flexi_logger::writers::FileLogWriter;
 | 
			
		||||
use hmac::Hmac;
 | 
			
		||||
use keyring::error::Error::NoEntry;
 | 
			
		||||
use log::{debug, error, info, kv, warn};
 | 
			
		||||
use log::kv::{Key, Value, VisitSource};
 | 
			
		||||
use pbkdf2::pbkdf2;
 | 
			
		||||
use rand::{RngCore, SeedableRng};
 | 
			
		||||
use rocket::figment::Figment;
 | 
			
		||||
use rocket::{get, post, routes};
 | 
			
		||||
use rocket::{data, get, post, routes, Data, Request};
 | 
			
		||||
use rocket::config::Shutdown;
 | 
			
		||||
use rocket::data::{Outcome, ToByteUnit};
 | 
			
		||||
use rocket::http::Status;
 | 
			
		||||
use rocket::serde::json::Json;
 | 
			
		||||
use sha2::Sha512;
 | 
			
		||||
use tauri::updater::UpdateResponse;
 | 
			
		||||
use tokio::io::AsyncReadExt;
 | 
			
		||||
 | 
			
		||||
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
 | 
			
		||||
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
 | 
			
		||||
 | 
			
		||||
// The .NET server is started in a separate process and communicates with this
 | 
			
		||||
// runtime process via IPC. However, we do net start the .NET server in
 | 
			
		||||
@ -55,6 +68,25 @@ static MAIN_WINDOW: Lazy<Mutex<Option<Window>>> = Lazy::new(|| Mutex::new(None))
 | 
			
		||||
// The update response coming from the Tauri updater.
 | 
			
		||||
static CHECK_UPDATE_RESPONSE: Lazy<Mutex<Option<UpdateResponse<tauri::Wry>>>> = Lazy::new(|| Mutex::new(None));
 | 
			
		||||
 | 
			
		||||
static ENCRYPTION: Lazy<Encryption> = Lazy::new(|| {
 | 
			
		||||
    //
 | 
			
		||||
    // Generate a secret key & salt for the AES encryption for the IPC channel:
 | 
			
		||||
    //
 | 
			
		||||
    let mut secret_key = [0u8; 512]; // 512 bytes = 4096 bits
 | 
			
		||||
    let mut secret_key_salt = [0u8; 16]; // 16 bytes = 128 bits
 | 
			
		||||
 | 
			
		||||
    // We use a cryptographically secure pseudo-random number generator
 | 
			
		||||
    // to generate the secret password & salt. ChaCha20Rng is the algorithm
 | 
			
		||||
    // of our choice:
 | 
			
		||||
    let mut rng = rand_chacha::ChaChaRng::from_entropy();
 | 
			
		||||
 | 
			
		||||
    // Fill the secret key & salt with random bytes:
 | 
			
		||||
    rng.fill_bytes(&mut secret_key);
 | 
			
		||||
    rng.fill_bytes(&mut secret_key_salt);
 | 
			
		||||
 | 
			
		||||
    Encryption::new(&secret_key, &secret_key_salt).unwrap()
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
#[tokio::main]
 | 
			
		||||
async fn main() {
 | 
			
		||||
 | 
			
		||||
@ -167,24 +199,11 @@ async fn main() {
 | 
			
		||||
            .launch().await.unwrap();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    //
 | 
			
		||||
    // Generate a secret key for the AES encryption for the IPC channel:
 | 
			
		||||
    //
 | 
			
		||||
    let mut secret_key = [0u8; 512]; // 512 bytes = 4096 bits
 | 
			
		||||
    // Get the secret password & salt and convert it to a base64 string:
 | 
			
		||||
    let secret_password = BASE64_STANDARD.encode(ENCRYPTION.secret_password);
 | 
			
		||||
    let secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt);
 | 
			
		||||
 | 
			
		||||
    // We use a cryptographically secure pseudo-random number generator
 | 
			
		||||
    // to generate the secret key. ChaCha20Rng is the algorithm of our choice:
 | 
			
		||||
    let mut rng = rand_chacha::ChaChaRng::from_entropy();
 | 
			
		||||
 | 
			
		||||
    // Fill the secret key with random bytes:
 | 
			
		||||
    rng.fill_bytes(&mut secret_key);
 | 
			
		||||
 | 
			
		||||
    // Convert the secret key to a hexadecimal string:
 | 
			
		||||
    let secret_key = secret_key.iter().fold(String::new(), |mut out, b| {
 | 
			
		||||
        _ = write!(out, "{b:02X}");
 | 
			
		||||
        out
 | 
			
		||||
    });
 | 
			
		||||
    info!("Secret key for the IPC channel was generated successfully.");
 | 
			
		||||
    info!("Secret password for the IPC channel was generated successfully.");
 | 
			
		||||
    info!("Try to start the .NET server...");
 | 
			
		||||
    let server_spawn_clone = DOTNET_SERVER.clone();
 | 
			
		||||
    tauri::async_runtime::spawn(async move {
 | 
			
		||||
@ -194,7 +213,7 @@ async fn main() {
 | 
			
		||||
            true => {
 | 
			
		||||
                // We are in the development environment, so we try to start a process
 | 
			
		||||
                // with `dotnet run` in the `../app/MindWork AI Studio` directory. But
 | 
			
		||||
                // we cannot issue a sidecar, because we cannot use any command for the
 | 
			
		||||
                // we cannot issue a sidecar because we cannot use any command for the
 | 
			
		||||
                // sidecar (see Tauri configuration). Thus, we use a standard Rust process:
 | 
			
		||||
                warn!(Source = "Bootloader .NET"; "Development environment detected; start .NET server using 'dotnet run'.");
 | 
			
		||||
                Command::new("dotnet")
 | 
			
		||||
@ -203,12 +222,12 @@ async fn main() {
 | 
			
		||||
                    // We provide the runtime API server port to the .NET server:
 | 
			
		||||
                    .args(["run", "--project", "../app/MindWork AI Studio", "--", format!("{api_port}").as_str()])
 | 
			
		||||
 | 
			
		||||
                    // Provide the secret key for the IPC channel to the .NET server by using
 | 
			
		||||
                    // Provide the secret password & salt for the IPC channel to the .NET server by using
 | 
			
		||||
                    // an environment variable. We must use a HashMap for this:
 | 
			
		||||
                    .envs(HashMap::from_iter(once((
 | 
			
		||||
                        String::from("AI_STUDIO_SECRET_KEY"),
 | 
			
		||||
                        secret_key
 | 
			
		||||
                    ))))
 | 
			
		||||
                    .envs(HashMap::from_iter([
 | 
			
		||||
                        (String::from("AI_STUDIO_SECRET_PASSWORD"), secret_password),
 | 
			
		||||
                        (String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt),
 | 
			
		||||
                    ]))
 | 
			
		||||
                    .spawn()
 | 
			
		||||
                    .expect("Failed to spawn .NET server process.")
 | 
			
		||||
            }
 | 
			
		||||
@ -220,12 +239,12 @@ async fn main() {
 | 
			
		||||
                    // Provide the runtime API server port to the .NET server:
 | 
			
		||||
                    .args([format!("{api_port}").as_str()])
 | 
			
		||||
 | 
			
		||||
                    // Provide the secret key for the IPC channel to the .NET server by using
 | 
			
		||||
                    // Provide the secret password & salt for the IPC channel to the .NET server by using
 | 
			
		||||
                    // an environment variable. We must use a HashMap for this:
 | 
			
		||||
                    .envs(HashMap::from_iter(once((
 | 
			
		||||
                        String::from("AI_STUDIO_SECRET_KEY"),
 | 
			
		||||
                        secret_key
 | 
			
		||||
                    ))))
 | 
			
		||||
                    .envs(HashMap::from_iter([
 | 
			
		||||
                        (String::from("AI_STUDIO_SECRET_PASSWORD"), secret_password),
 | 
			
		||||
                        (String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt),
 | 
			
		||||
                    ]))
 | 
			
		||||
                    .spawn()
 | 
			
		||||
                    .expect("Failed to spawn .NET server process.")
 | 
			
		||||
            }
 | 
			
		||||
@ -454,6 +473,130 @@ pub fn file_logger_format(
 | 
			
		||||
    write!(w, "{}", &record.args())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct Encryption {
 | 
			
		||||
    key: [u8; 32],
 | 
			
		||||
    iv: [u8; 16],
 | 
			
		||||
 | 
			
		||||
    secret_password: [u8; 512],
 | 
			
		||||
    secret_key_salt: [u8; 16],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Encryption {
 | 
			
		||||
    // 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.
 | 
			
		||||
    const ITERATIONS: u32 = 100;
 | 
			
		||||
 | 
			
		||||
    pub fn new(secret_password: &[u8], secret_key_salt: &[u8]) -> Result<Self, String> {
 | 
			
		||||
        if secret_password.len() != 512 {
 | 
			
		||||
            return Err("The secret password must be 512 bytes long.".to_string());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if secret_key_salt.len() != 16 {
 | 
			
		||||
            return Err("The salt must be 16 bytes long.".to_string());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        info!(Source = "Encryption"; "Initializing encryption...");
 | 
			
		||||
        let mut encryption = Encryption {
 | 
			
		||||
            key: [0u8; 32],
 | 
			
		||||
            iv: [0u8; 16],
 | 
			
		||||
 | 
			
		||||
            secret_password: [0u8; 512],
 | 
			
		||||
            secret_key_salt: [0u8; 16],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        encryption.secret_password.copy_from_slice(secret_password);
 | 
			
		||||
        encryption.secret_key_salt.copy_from_slice(secret_key_salt);
 | 
			
		||||
 | 
			
		||||
        let start = Instant::now();
 | 
			
		||||
        let mut key_iv = [0u8; 48];
 | 
			
		||||
        pbkdf2::<Hmac<Sha512>>(secret_password, secret_key_salt, Self::ITERATIONS, &mut key_iv).map_err(|e| format!("Error while generating key and IV: {e}"))?;
 | 
			
		||||
        encryption.key.copy_from_slice(&key_iv[0..32]);
 | 
			
		||||
        encryption.iv.copy_from_slice(&key_iv[32..48]);
 | 
			
		||||
 | 
			
		||||
        let duration = start.elapsed();
 | 
			
		||||
        let duration = duration.as_millis();
 | 
			
		||||
        info!(Source = "Encryption"; "Encryption initialized in {duration} milliseconds.", );
 | 
			
		||||
 | 
			
		||||
        Ok(encryption)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn encrypt(&self, data: &str) -> Result<EncryptedText, String> {
 | 
			
		||||
        let cipher = Aes256CbcEnc::new(&self.key.into(), &self.iv.into());
 | 
			
		||||
        let encrypted = cipher.encrypt_padded_vec_mut::<Pkcs7>(data.as_bytes());
 | 
			
		||||
        let mut result = BASE64_STANDARD.encode(self.secret_key_salt);
 | 
			
		||||
        result.push_str(&BASE64_STANDARD.encode(&encrypted));
 | 
			
		||||
        Ok(EncryptedText::new(result))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn decrypt(&self, encrypted_data: &EncryptedText) -> Result<String, String> {
 | 
			
		||||
        let decoded = BASE64_STANDARD.decode(encrypted_data.get_encrypted()).map_err(|e| format!("Error decoding base64: {e}"))?;
 | 
			
		||||
 | 
			
		||||
        if decoded.len() < 16 {
 | 
			
		||||
            return Err("Encrypted data is too short.".to_string());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let (salt, encrypted) = decoded.split_at(16);
 | 
			
		||||
        if salt != self.secret_key_salt {
 | 
			
		||||
            return Err("The salt bytes do not match. The data is corrupted or tampered.".to_string());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let cipher = Aes256CbcDec::new(&self.key.into(), &self.iv.into());
 | 
			
		||||
        let decrypted = cipher.decrypt_padded_vec_mut::<Pkcs7>(encrypted).map_err(|e| format!("Error decrypting data: {e}"))?;
 | 
			
		||||
 | 
			
		||||
        String::from_utf8(decrypted).map_err(|e| format!("Error converting decrypted data to string: {}", e))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct EncryptedText(String);
 | 
			
		||||
 | 
			
		||||
impl EncryptedText {
 | 
			
		||||
    pub fn new(encrypted_data: String) -> Self {
 | 
			
		||||
        EncryptedText(encrypted_data)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get_encrypted(&self) -> &str {
 | 
			
		||||
        &self.0
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl fmt::Debug for EncryptedText {
 | 
			
		||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
        write!(f, "EncryptedText(**********)")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl fmt::Display for EncryptedText {
 | 
			
		||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
        write!(f, "**********")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Use Case: When we receive encrypted text from the client as body (e.g., in a POST request).
 | 
			
		||||
// We must interpret the body as EncryptedText.
 | 
			
		||||
#[rocket::async_trait]
 | 
			
		||||
impl<'r> data::FromData<'r> for EncryptedText {
 | 
			
		||||
    type Error = String;
 | 
			
		||||
    async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> {
 | 
			
		||||
        let content_type = req.content_type();
 | 
			
		||||
        if content_type.map_or(true, |ct| !ct.is_text()) {
 | 
			
		||||
            return Outcome::Forward((data, Status::Ok));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let mut stream = data.open(2.mebibytes());
 | 
			
		||||
        let mut body = String::new();
 | 
			
		||||
        if let Err(e) = stream.read_to_string(&mut body).await {
 | 
			
		||||
            return Outcome::Error((Status::InternalServerError, format!("Failed to read data: {}", e)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Outcome::Success(EncryptedText(body))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[get("/system/dotnet/port")]
 | 
			
		||||
fn dotnet_port() -> String {
 | 
			
		||||
    let dotnet_server_port = *DOTNET_SERVER_PORT;
 | 
			
		||||
@ -491,7 +634,7 @@ async fn dotnet_ready() {
 | 
			
		||||
                main_window_status_reported = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            time::sleep(time::Duration::from_millis(100)).await;
 | 
			
		||||
            time::sleep(Duration::from_millis(100)).await;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user