mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-08-01 16: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="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]);
|
||||
}
|
||||
}
|
@ -9,14 +9,20 @@ public sealed class Rust(string apiPort) : IDisposable
|
||||
{
|
||||
BaseAddress = new Uri($"http://127.0.0.1:{apiPort}"),
|
||||
};
|
||||
|
||||
private ILogger<Rust>? logger;
|
||||
|
||||
private ILogger<Rust>? logger;
|
||||
private Encryption? encryptor;
|
||||
|
||||
public void SetLogger(ILogger<Rust> logService)
|
||||
{
|
||||
this.logger = logService;
|
||||
}
|
||||
|
||||
public void SetEncryptor(Encryption encryptionService)
|
||||
{
|
||||
this.encryptor = encryptionService;
|
||||
}
|
||||
|
||||
public async Task<int> GetAppPort()
|
||||
{
|
||||
Console.WriteLine("Trying to get app port from Rust runtime...");
|
||||
|
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