Added symmetric encryption to the Rust runtime and the .NET app

This commit is contained in:
Thorsten Sommer 2024-08-27 20:23:37 +02:00
parent 7d341b2ce1
commit 79afd42195
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
9 changed files with 464 additions and 134 deletions

View File

@ -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."/>

View File

@ -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;

View 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;
}

View 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);
}

View 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]);
}
}

View File

@ -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
View File

@ -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"

View File

@ -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

View File

@ -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;
}
}