diff --git a/app/MindWork AI Studio/Pages/About.razor b/app/MindWork AI Studio/Pages/About.razor
index 2729920a..cfc70a93 100644
--- a/app/MindWork AI Studio/Pages/About.razor
+++ b/app/MindWork AI Studio/Pages/About.razor
@@ -48,6 +48,8 @@
+
+
diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs
index 7e6ee1e1..a4db1d08 100644
--- a/app/MindWork AI Studio/Program.cs
+++ b/app/MindWork AI Studio/Program.cs
@@ -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>();
+var encryption = new Encryption(encryptionLogger, secretPassword, secretKeySalt);
+var encryptionInitializer = encryption.Initialize();
+
+// Set the logger for the Rust service:
+var rustLogger = app.Services.GetRequiredService>();
+rust.SetLogger(rustLogger);
+rust.SetEncryptor(encryption);
+
app.Use(Redirect.HandlerContentAsync);
#if DEBUG
@@ -115,8 +137,6 @@ app.MapRazorComponents()
var serverTask = app.RunAsync();
-var rustLogger = app.Services.GetRequiredService>();
-rust.SetLogger(rustLogger);
-
+await encryptionInitializer;
await rust.AppIsReady();
await serverTask;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/EncryptedText.cs b/app/MindWork AI Studio/Tools/EncryptedText.cs
new file mode 100644
index 00000000..34b24c99
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/EncryptedText.cs
@@ -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;
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/EncryptedTextExtensions.cs b/app/MindWork AI Studio/Tools/EncryptedTextExtensions.cs
new file mode 100644
index 00000000..8e77f8f8
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/EncryptedTextExtensions.cs
@@ -0,0 +1,8 @@
+namespace AIStudio.Tools;
+
+public static class EncryptedTextExtensions
+{
+ public static async Task Encrypt(this string data, Encryption encryption) => await encryption.Encrypt(data);
+
+ public static async Task Decrypt(this EncryptedText encryptedText, Encryption encryption) => await encryption.Decrypt(encryptedText);
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Encryption.cs b/app/MindWork AI Studio/Tools/Encryption.cs
new file mode 100644
index 00000000..8c96b609
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Encryption.cs
@@ -0,0 +1,141 @@
+using System.Diagnostics;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace AIStudio.Tools;
+
+public sealed class Encryption(ILogger logger, byte[] secretPassword, byte[] secretKeySalt)
+{
+ ///
+ /// 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.
+ ///
+ 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 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 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]);
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Rust.cs b/app/MindWork AI Studio/Tools/Rust.cs
index fcfedc75..b7d158bd 100644
--- a/app/MindWork AI Studio/Tools/Rust.cs
+++ b/app/MindWork AI Studio/Tools/Rust.cs
@@ -9,14 +9,20 @@ public sealed class Rust(string apiPort) : IDisposable
{
BaseAddress = new Uri($"http://127.0.0.1:{apiPort}"),
};
-
- private ILogger? logger;
+ private ILogger? logger;
+ private Encryption? encryptor;
+
public void SetLogger(ILogger logService)
{
this.logger = logService;
}
+ public void SetEncryptor(Encryption encryptionService)
+ {
+ this.encryptor = encryptionService;
+ }
+
public async Task GetAppPort()
{
Console.WriteLine("Trying to get app port from Rust runtime...");
diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock
index 21063317..98b91160 100644
--- a/runtime/Cargo.lock
+++ b/runtime/Cargo.lock
@@ -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"
diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml
index 8c7a70e5..87f662cb 100644
--- a/runtime/Cargo.toml
+++ b/runtime/Cargo.toml
@@ -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
diff --git a/runtime/src/main.rs b/runtime/src/main.rs
index 7b2fd97d..9116e5ff 100644
--- a/runtime/src/main.rs
+++ b/runtime/src/main.rs
@@ -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;
+type Aes256CbcDec = cbc::Decryptor;
// 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>> = Lazy::new(|| Mutex::new(None))
// The update response coming from the Tauri updater.
static CHECK_UPDATE_RESPONSE: Lazy>>> = Lazy::new(|| Mutex::new(None));
+static ENCRYPTION: Lazy = 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 {
+ 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::>(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 {
+ let cipher = Aes256CbcEnc::new(&self.key.into(), &self.iv.into());
+ let encrypted = cipher.encrypt_padded_vec_mut::(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 {
+ 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::(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;
}
}