mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-08-02 12:22:56 +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="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="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="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="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="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."/>
|
<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:
|
// Read the secret key for the IPC from the AI_STUDIO_SECRET_KEY environment variable:
|
||||||
var secretKey = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_KEY");
|
var secretPasswordEncoded = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_PASSWORD");
|
||||||
if(string.IsNullOrWhiteSpace(secretKey))
|
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;
|
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();
|
var builder = WebApplication.CreateBuilder();
|
||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
builder.Logging.SetMinimumLevel(LogLevel.Debug);
|
builder.Logging.SetMinimumLevel(LogLevel.Debug);
|
||||||
@ -92,7 +102,19 @@ builder.WebHost.UseWebRoot("wwwroot");
|
|||||||
builder.WebHost.UseStaticWebAssets();
|
builder.WebHost.UseStaticWebAssets();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Execute the builder to get the app:
|
||||||
var app = builder.Build();
|
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);
|
app.Use(Redirect.HandlerContentAsync);
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@ -115,8 +137,6 @@ app.MapRazorComponents<App>()
|
|||||||
|
|
||||||
var serverTask = app.RunAsync();
|
var serverTask = app.RunAsync();
|
||||||
|
|
||||||
var rustLogger = app.Services.GetRequiredService<ILogger<Rust>>();
|
await encryptionInitializer;
|
||||||
rust.SetLogger(rustLogger);
|
|
||||||
|
|
||||||
await rust.AppIsReady();
|
await rust.AppIsReady();
|
||||||
await serverTask;
|
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 ILogger<Rust>? logger;
|
||||||
|
private Encryption? encryptor;
|
||||||
|
|
||||||
public void SetLogger(ILogger<Rust> logService)
|
public void SetLogger(ILogger<Rust> logService)
|
||||||
{
|
{
|
||||||
this.logger = logService;
|
this.logger = logService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetEncryptor(Encryption encryptionService)
|
||||||
|
{
|
||||||
|
this.encryptor = encryptionService;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<int> GetAppPort()
|
public async Task<int> GetAppPort()
|
||||||
{
|
{
|
||||||
Console.WriteLine("Trying to get app port from Rust runtime...");
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
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]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@ -239,6 +250,15 @@ dependencies = [
|
|||||||
"generic-array",
|
"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]]
|
[[package]]
|
||||||
name = "block2"
|
name = "block2"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@ -346,6 +366,15 @@ dependencies = [
|
|||||||
"toml 0.7.8",
|
"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]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.1.6"
|
version = "1.1.6"
|
||||||
@ -407,6 +436,16 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "clipboard-win"
|
name = "clipboard-win"
|
||||||
version = "5.4.0"
|
version = "5.4.0"
|
||||||
@ -754,6 +793,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1496,6 +1536,15 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "html5ever"
|
name = "html5ever"
|
||||||
version = "0.26.0"
|
version = "0.26.0"
|
||||||
@ -1628,23 +1677,6 @@ dependencies = [
|
|||||||
"want",
|
"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]]
|
[[package]]
|
||||||
name = "hyper-tls"
|
name = "hyper-tls"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@ -1821,6 +1853,16 @@ version = "0.1.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
|
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]]
|
[[package]]
|
||||||
name = "instant"
|
name = "instant"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
@ -2090,17 +2132,24 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
|||||||
name = "mindwork-ai-studio"
|
name = "mindwork-ai-studio"
|
||||||
version = "0.8.12"
|
version = "0.8.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aes",
|
||||||
"arboard",
|
"arboard",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"cbc",
|
||||||
|
"cipher",
|
||||||
"flexi_logger",
|
"flexi_logger",
|
||||||
|
"hmac",
|
||||||
"keyring",
|
"keyring",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"pbkdf2",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rand_chacha 0.3.1",
|
"rand_chacha 0.3.1",
|
||||||
"reqwest 0.12.5",
|
"reqwest 0.12.4",
|
||||||
"rocket",
|
"rocket",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-window-state",
|
"tauri-plugin-window-state",
|
||||||
@ -2627,6 +2676,16 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
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]]
|
[[package]]
|
||||||
name = "pear"
|
name = "pear"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
@ -3160,7 +3219,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sync_wrapper 0.1.2",
|
"sync_wrapper",
|
||||||
"system-configuration",
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
@ -3176,9 +3235,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.5"
|
version = "0.12.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
|
checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -3190,7 +3249,6 @@ dependencies = [
|
|||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper 1.4.1",
|
"hyper 1.4.1",
|
||||||
"hyper-rustls",
|
|
||||||
"hyper-tls 0.6.0",
|
"hyper-tls 0.6.0",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
@ -3205,7 +3263,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sync_wrapper 1.0.1",
|
"sync_wrapper",
|
||||||
"system-configuration",
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
@ -3241,21 +3299,6 @@ dependencies = [
|
|||||||
"windows 0.37.0",
|
"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]]
|
[[package]]
|
||||||
name = "rocket"
|
name = "rocket"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@ -3366,19 +3409,6 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"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]]
|
[[package]]
|
||||||
name = "rustls-pemfile"
|
name = "rustls-pemfile"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@ -3404,17 +3434,6 @@ version = "1.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
|
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]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.17"
|
version = "1.0.17"
|
||||||
@ -3812,9 +3831,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
@ -3844,12 +3863,6 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sync_wrapper"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-configuration"
|
name = "system-configuration"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@ -4339,17 +4352,6 @@ dependencies = [
|
|||||||
"tokio",
|
"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]]
|
[[package]]
|
||||||
name = "tokio-stream"
|
name = "tokio-stream"
|
||||||
version = "0.1.15"
|
version = "0.1.15"
|
||||||
@ -4594,12 +4596,6 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a"
|
checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "untrusted"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.2"
|
version = "2.5.2"
|
||||||
@ -5404,12 +5400,6 @@ dependencies = [
|
|||||||
"is-terminal",
|
"is-terminal",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zeroize"
|
|
||||||
version = "1.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zip"
|
name = "zip"
|
||||||
version = "0.6.6"
|
version = "0.6.6"
|
||||||
|
@ -22,6 +22,13 @@ once_cell = "1.19.0"
|
|||||||
rocket = { version = "0.5", default-features = false, features = ["json"] }
|
rocket = { version = "0.5", default-features = false, features = ["json"] }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
rand_chacha = "0.3.1"
|
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]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
# See issue https://github.com/tauri-apps/tauri/issues/4470
|
# See issue https://github.com/tauri-apps/tauri/issues/4470
|
||||||
|
@ -5,28 +5,41 @@ extern crate rocket;
|
|||||||
extern crate core;
|
extern crate core;
|
||||||
|
|
||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
use std::iter::once;
|
use std::fmt;
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::fmt::Write;
|
use std::time::{Duration, Instant};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
use arboard::Clipboard;
|
use arboard::Clipboard;
|
||||||
|
use base64::Engine;
|
||||||
|
use base64::prelude::BASE64_STANDARD;
|
||||||
|
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
|
||||||
use keyring::Entry;
|
use keyring::Entry;
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use tauri::{Manager, Url, Window};
|
use tauri::{Manager, Url, Window};
|
||||||
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
use flexi_logger::{DeferredNow, Duplicate, FileSpec, Logger};
|
use flexi_logger::{DeferredNow, Duplicate, FileSpec, Logger};
|
||||||
use flexi_logger::writers::FileLogWriter;
|
use flexi_logger::writers::FileLogWriter;
|
||||||
|
use hmac::Hmac;
|
||||||
use keyring::error::Error::NoEntry;
|
use keyring::error::Error::NoEntry;
|
||||||
use log::{debug, error, info, kv, warn};
|
use log::{debug, error, info, kv, warn};
|
||||||
use log::kv::{Key, Value, VisitSource};
|
use log::kv::{Key, Value, VisitSource};
|
||||||
|
use pbkdf2::pbkdf2;
|
||||||
use rand::{RngCore, SeedableRng};
|
use rand::{RngCore, SeedableRng};
|
||||||
use rocket::figment::Figment;
|
use rocket::figment::Figment;
|
||||||
use rocket::{get, post, routes};
|
use rocket::{data, get, post, routes, Data, Request};
|
||||||
use rocket::config::Shutdown;
|
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 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
|
// 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
|
// 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.
|
// The update response coming from the Tauri updater.
|
||||||
static CHECK_UPDATE_RESPONSE: Lazy<Mutex<Option<UpdateResponse<tauri::Wry>>>> = Lazy::new(|| Mutex::new(None));
|
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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
|
||||||
@ -167,24 +199,11 @@ async fn main() {
|
|||||||
.launch().await.unwrap();
|
.launch().await.unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
//
|
// Get the secret password & salt and convert it to a base64 string:
|
||||||
// Generate a secret key for the AES encryption for the IPC channel:
|
let secret_password = BASE64_STANDARD.encode(ENCRYPTION.secret_password);
|
||||||
//
|
let secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt);
|
||||||
let mut secret_key = [0u8; 512]; // 512 bytes = 4096 bits
|
|
||||||
|
|
||||||
// We use a cryptographically secure pseudo-random number generator
|
info!("Secret password for the IPC channel was generated successfully.");
|
||||||
// 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!("Try to start the .NET server...");
|
info!("Try to start the .NET server...");
|
||||||
let server_spawn_clone = DOTNET_SERVER.clone();
|
let server_spawn_clone = DOTNET_SERVER.clone();
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
@ -194,7 +213,7 @@ async fn main() {
|
|||||||
true => {
|
true => {
|
||||||
// We are in the development environment, so we try to start a process
|
// We are in the development environment, so we try to start a process
|
||||||
// with `dotnet run` in the `../app/MindWork AI Studio` directory. But
|
// 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:
|
// sidecar (see Tauri configuration). Thus, we use a standard Rust process:
|
||||||
warn!(Source = "Bootloader .NET"; "Development environment detected; start .NET server using 'dotnet run'.");
|
warn!(Source = "Bootloader .NET"; "Development environment detected; start .NET server using 'dotnet run'.");
|
||||||
Command::new("dotnet")
|
Command::new("dotnet")
|
||||||
@ -203,12 +222,12 @@ async fn main() {
|
|||||||
// We provide the runtime API server port to the .NET server:
|
// We provide the runtime API server port to the .NET server:
|
||||||
.args(["run", "--project", "../app/MindWork AI Studio", "--", format!("{api_port}").as_str()])
|
.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:
|
// an environment variable. We must use a HashMap for this:
|
||||||
.envs(HashMap::from_iter(once((
|
.envs(HashMap::from_iter([
|
||||||
String::from("AI_STUDIO_SECRET_KEY"),
|
(String::from("AI_STUDIO_SECRET_PASSWORD"), secret_password),
|
||||||
secret_key
|
(String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt),
|
||||||
))))
|
]))
|
||||||
.spawn()
|
.spawn()
|
||||||
.expect("Failed to spawn .NET server process.")
|
.expect("Failed to spawn .NET server process.")
|
||||||
}
|
}
|
||||||
@ -220,12 +239,12 @@ async fn main() {
|
|||||||
// Provide the runtime API server port to the .NET server:
|
// Provide the runtime API server port to the .NET server:
|
||||||
.args([format!("{api_port}").as_str()])
|
.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:
|
// an environment variable. We must use a HashMap for this:
|
||||||
.envs(HashMap::from_iter(once((
|
.envs(HashMap::from_iter([
|
||||||
String::from("AI_STUDIO_SECRET_KEY"),
|
(String::from("AI_STUDIO_SECRET_PASSWORD"), secret_password),
|
||||||
secret_key
|
(String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt),
|
||||||
))))
|
]))
|
||||||
.spawn()
|
.spawn()
|
||||||
.expect("Failed to spawn .NET server process.")
|
.expect("Failed to spawn .NET server process.")
|
||||||
}
|
}
|
||||||
@ -454,6 +473,130 @@ pub fn file_logger_format(
|
|||||||
write!(w, "{}", &record.args())
|
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")]
|
#[get("/system/dotnet/port")]
|
||||||
fn dotnet_port() -> String {
|
fn dotnet_port() -> String {
|
||||||
let dotnet_server_port = *DOTNET_SERVER_PORT;
|
let dotnet_server_port = *DOTNET_SERVER_PORT;
|
||||||
@ -491,7 +634,7 @@ async fn dotnet_ready() {
|
|||||||
main_window_status_reported = true;
|
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