diff --git a/mRemoteNG/App/ProgramRoot.cs b/mRemoteNG/App/ProgramRoot.cs
index 359a8223..a9215c16 100644
--- a/mRemoteNG/App/ProgramRoot.cs
+++ b/mRemoteNG/App/ProgramRoot.cs
@@ -37,24 +37,10 @@ namespace mRemoteNG.App
}
return null;
};
- /*
- * Temporarily disable LocalSettingsManager initialization at startup
- * due to unfinished implementation causing build errors.
- * Uncomment if needed in your local repo.
- */
- /*
- /*var settingsManager = new LocalSettingsManager();
- // Check if the database exists
- if (settingsManager.DatabaseExists())
- {
- Console.WriteLine("Database exists.");
- }
- else
- {
- Console.WriteLine("Database does not exist. Creating...");
- settingsManager.CreateDatabase();
- }*/
+ LocalSettingsDBManager settingsManager = new LocalSettingsDBManager(dbPath: "mRemoteNG.appSettings", useEncryption: false, schemaFilePath: "");
+
+
if (Properties.OptionsStartupExitPage.Default.SingleInstance)
StartApplicationAsSingleInstance();
diff --git a/mRemoteNG/Config/Settings/LocalSettingsManager.cs b/mRemoteNG/Config/Settings/LocalSettingsManager.cs
new file mode 100644
index 00000000..11625abd
--- /dev/null
+++ b/mRemoteNG/Config/Settings/LocalSettingsManager.cs
@@ -0,0 +1,417 @@
+using System;
+using System.IO;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Management;
+using JsonSerializer = System.Text.Json.JsonSerializer;
+using LiteDB;
+using System.Linq;
+
+public class LocalSettingsDBManager
+{
+ private readonly string _dbPath;
+ private readonly string _schemaPath;
+ private readonly string _mRIdentifier;
+ private readonly bool? _useEncryption;
+
+
+ ///
+ /// Creates a new local DB, encrypt it or decrypt it.
+ ///
+ /// The path to the database file.
+ /// Indicates whether to use encryption for the database. If null, no change is made to an existing database.
+ /// Optional path to a schema file for creating the database structure.
+ public LocalSettingsDBManager(string dbPath = null, bool? useEncryption = null, string schemaFilePath = null)
+ {
+ _dbPath = string.IsNullOrWhiteSpace(dbPath) ? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "mRemoteNG.appSettings") : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dbPath);
+ _schemaPath = string.IsNullOrWhiteSpace(schemaFilePath) ? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Schemas\\mremoteng_default_settings_v1_0.json") : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, schemaFilePath);
+ _useEncryption = useEncryption;
+ _mRIdentifier = Convert.ToBase64String(System.Security.Cryptography.SHA256.Create().ComputeHash(System.Text.Encoding.UTF8.GetBytes(GetDiskIdentifier() + "_" + Environment.MachineName)));
+
+ // Check if disk identifier is empty and prevent database creation if true
+ if (string.IsNullOrEmpty(_mRIdentifier))
+ {
+ Console.WriteLine("Calculated identifier is empty. Database creation aborted.");
+ return;
+ }
+
+ // Check if the database exists and handle accordingly
+ if (!File.Exists(_dbPath))
+ {
+ CreateDatabase(_schemaPath);
+ }
+ else if (_useEncryption.HasValue)
+ {
+ if (_useEncryption.Value)
+ {
+ EncryptDatabase();
+ }
+ else
+ {
+ DecryptDatabase();
+ }
+ }
+ }
+
+ ///
+ /// Ensures default settings are imported if the database is empty.
+ ///
+ /// Path to the JSON file for importing default settings.
+ public void EnsureDefaultSettingsImported(string importFilePath)
+ {
+ var connectionString = _useEncryption.HasValue && _useEncryption.Value
+ ? $"Filename={_dbPath};Password={_mRIdentifier}"
+ : $"Filename={_dbPath}";
+
+ using (var db = new LiteDatabase(connectionString))
+ {
+ if (db.GetCollectionNames().All(name => db.GetCollection(name).Count() == 0))
+ {
+ Console.WriteLine("No settings found in database. Importing default settings...");
+ ImportSettings(importFilePath);
+ }
+ else
+ {
+ Console.WriteLine("Database already contains settings. Skipping import.");
+ }
+ }
+ }
+
+ ///
+ /// Checks if the database is encrypted.
+ ///
+ /// True if the database is encrypted, otherwise false.
+ private bool IsDatabaseEncrypted()
+ {
+ try
+ {
+ using (var db = new LiteDatabase($"Filename={_dbPath}"))
+ {
+ // If we can open the database without a password, it means it is not encrypted.
+ return false;
+ }
+ }
+ catch (LiteException)
+ {
+ // If an exception is thrown, it means the database is likely encrypted.
+ return true;
+ }
+ }
+
+ ///
+ /// Creates the database using the machine identifier as a password if encryption is enabled.
+ ///
+ /// Path to the schema file for creating the database structure.
+ private void CreateDatabase(string schemaFilePath = null)
+ {
+ var connectionString = _useEncryption.HasValue && _useEncryption.Value
+ ? $"Filename={_dbPath};Password={_mRIdentifier}"
+ : $"Filename={_dbPath}";
+ using (var db = new LiteDatabase(connectionString))
+ {
+ if (!string.IsNullOrWhiteSpace(schemaFilePath) && File.Exists(schemaFilePath))
+ {
+ var schemaJson = File.ReadAllText(schemaFilePath);
+ using (JsonDocument doc = JsonDocument.Parse(schemaJson))
+ {
+ foreach (JsonElement table in doc.RootElement.GetProperty("tables").EnumerateArray())
+ {
+ string tableName = table.GetProperty("name").GetString();
+ var collection = db.GetCollection(tableName);
+ Console.WriteLine($"Table '{tableName}' created with structure from schema.");
+
+ // Insert default data into the collection if defined in the schema
+ if (table.TryGetProperty("columns", out JsonElement columnsElement))
+ {
+ foreach (JsonElement column in columnsElement.EnumerateArray())
+ {
+ var settingsData = new Setting
+ {
+ Id = Guid.NewGuid(),
+ Timestamp = DateTime.UtcNow,
+ Group = "default",
+ Key = column.GetProperty("name").GetString(),
+ Value = column.GetProperty("value").ToString()
+ };
+ collection.Insert(settingsData);
+ Console.WriteLine($"Inserted default setting '{settingsData.Key}' for table '{tableName}'.");
+ }
+ };
+ Console.WriteLine($"Inserted default settings for table '{tableName}'.");
+ }
+ }
+ }
+ }
+ Console.WriteLine(_useEncryption.HasValue && _useEncryption.Value ? "Database created and encrypted." : "Database created without encryption.");
+ }
+
+
+///
+/// Encrypts an existing database if it is not encrypted.
+///
+public void EncryptDatabase()
+ {
+ try
+ {
+ using (var db = new LiteDatabase($"Filename={_dbPath}"))
+ {
+ Console.WriteLine("Encrypting database...");
+ var backupPath = _dbPath + ".backup";
+ db.Checkpoint();
+ File.Copy(_dbPath, backupPath, true);
+
+ using (var encryptedDb = new LiteDatabase($"Filename={_dbPath};Password={_mRIdentifier}"))
+ {
+ encryptedDb.Checkpoint();
+ }
+
+ File.Delete(backupPath);
+ Console.WriteLine("Database successfully encrypted.");
+ }
+ }
+ catch (LiteException ex)
+ {
+ Console.WriteLine($"Error encrypting database: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Decrypts an existing database if it is encrypted.
+ ///
+ public void DecryptDatabase()
+ {
+ try
+ {
+ if (!IsDatabaseEncrypted())
+ {
+ Console.WriteLine("Database is not encrypted. Skipping decryption.");
+ return;
+ }
+ var encryptedConnectionString = $"Filename={_dbPath};Password={_mRIdentifier}";
+ using (var db = new LiteDatabase(encryptedConnectionString))
+ {
+ Console.WriteLine("Decrypting database...");
+ var backupPath = _dbPath + ".backup";
+ db.Checkpoint();
+ File.Copy(_dbPath, backupPath, true);
+
+ using (var decryptedDb = new LiteDatabase($"Filename={_dbPath}"))
+ {
+ decryptedDb.Checkpoint();
+ }
+
+ File.Delete(backupPath);
+ Console.WriteLine("Database successfully decrypted.");
+ }
+ }
+ catch (LiteException ex)
+ {
+ Console.WriteLine($"Error decrypting database: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Adds a new setting to the database.
+ ///
+ /// Table name.
+ /// Setting group.
+ /// Setting key.
+ /// Setting value.
+ public void AddSetting(string table, string group, string key, string value)
+ {
+ var connectionString = _useEncryption.HasValue && _useEncryption.Value
+ ? $"Filename={_dbPath};Password={_mRIdentifier}"
+ : $"Filename={_dbPath}";
+
+ using (var db = new LiteDatabase(connectionString))
+ {
+ var settings = db.GetCollection(table);
+ var setting = new Setting
+ {
+ Id = Guid.NewGuid(),
+ Timestamp = DateTime.UtcNow,
+ Group = group,
+ Key = key,
+ Value = value
+ };
+ settings.Insert(setting);
+ Console.WriteLine($"Setting '{group}.{key}' added to table '{table}'.");
+ }
+ }
+
+ ///
+ /// Imports settings from a JSON file into the database.
+ ///
+ /// Path to the JSON file.
+ public void ImportSettings(string jsonFilePath)
+ {
+ if (File.Exists(jsonFilePath))
+ {
+ var json = File.ReadAllText(jsonFilePath);
+ var settingsData = JsonSerializer.Deserialize>>(json);
+
+ foreach (var table in settingsData.Keys)
+ {
+ foreach (var setting in settingsData[table])
+ {
+ AddSetting(table, setting.Group, setting.Key, setting.Value);
+ }
+ }
+ Console.WriteLine("Settings successfully imported from JSON file.");
+ }
+ else
+ {
+ Console.WriteLine("JSON file not found.");
+ }
+ }
+
+ ///
+ /// Exports settings from the database to a JSON file.
+ ///
+ /// Path to the JSON file.
+ public void ExportSettings(string jsonFilePath)
+ {
+ var connectionString = _useEncryption.HasValue && _useEncryption.Value
+ ? $"Filename={_dbPath};Password={_mRIdentifier}"
+ : $"Filename={_dbPath}";
+
+ using (var db = new LiteDatabase(connectionString))
+ {
+ var settingsData = new Dictionary>();
+
+ foreach (var tableName in db.GetCollectionNames())
+ {
+ var settings = db.GetCollection(tableName).FindAll();
+ settingsData[tableName] = new List(settings);
+ }
+
+ var json = JsonSerializer.Serialize(settingsData, new JsonSerializerOptions { WriteIndented = true });
+ File.WriteAllText(jsonFilePath, json);
+ Console.WriteLine("Settings successfully exported to JSON file.");
+ }
+ }
+
+ ///
+ /// Retrieves the value of a setting by table, group, and key.
+ ///
+ /// Table name.
+ /// Setting group.
+ /// Setting key.
+ /// Setting value or "Not Found" if the setting does not exist.
+ public string GetSetting(string table, string group, string key)
+ {
+ var connectionString = _useEncryption.HasValue && _useEncryption.Value
+ ? $"Filename={_dbPath};Password={_mRIdentifier}"
+ : $"Filename={_dbPath}";
+
+ using (var db = new LiteDatabase(connectionString))
+ {
+ var settings = db.GetCollection(table);
+ var setting = settings.FindOne(s => s.Group == group && s.Key == key);
+ return setting != null ? setting.Value : "Not Found";
+ }
+ }
+
+ ///
+ /// Updates the value of an existing setting and updates the timestamp.
+ ///
+ /// Table name.
+ /// Setting group.
+ /// Setting key.
+ /// New value for the setting.
+ public void UpdateSetting(string table, string group, string key, string newValue)
+ {
+ var connectionString = _useEncryption.HasValue && _useEncryption.Value
+ ? $"Filename={_dbPath};Password={_mRIdentifier}"
+ : $"Filename={_dbPath}";
+
+ using (var db = new LiteDatabase(connectionString))
+ {
+ var settings = db.GetCollection(table);
+ var setting = settings.FindOne(s => s.Group == group && s.Key == key);
+ if (setting != null)
+ {
+ setting.Value = newValue;
+ setting.Timestamp = DateTime.UtcNow;
+ settings.Update(setting);
+ Console.WriteLine($"Setting '{group}.{key}' updated in table '{table}'.");
+ }
+ else
+ {
+ Console.WriteLine($"Setting '{group}.{key}' not found in table '{table}'.");
+ }
+ }
+ }
+
+ ///
+ /// Deletes a setting by table, group, and key.
+ ///
+ /// Table name.
+ /// Setting group.
+ /// Setting key.
+ public void DeleteSetting(string table, string group, string key)
+ {
+ var connectionString = _useEncryption.HasValue && _useEncryption.Value
+ ? $"Filename={_dbPath};Password={_mRIdentifier}"
+ : $"Filename={_dbPath}";
+
+ using (var db = new LiteDatabase(connectionString))
+ {
+ var settings = db.GetCollection(table);
+ if (settings.DeleteMany(s => s.Group == group && s.Key == key) > 0)
+ {
+ Console.WriteLine($"Setting '{group}.{key}' deleted from table '{table}'.");
+ }
+ else
+ {
+ Console.WriteLine($"Setting '{group}.{key}' not found in table '{table}'.");
+ }
+ }
+ }
+
+ ///
+ /// Gets the unique machine identifier (serial number of the hard drive) combined with the machine name and encrypts it using SHA256.
+ ///
+ /// Unique machine identifier.
+ private static string GetDiskIdentifier()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ try
+ {
+ // Use ManagementObject to get the serial number of the hard drive
+ using (var searcher = new ManagementObjectSearcher("SELECT SerialNumber FROM Win32_DiskDrive"))
+ {
+ foreach (var disk in searcher.Get())
+ {
+ return disk["SerialNumber"].ToString().Trim();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error getting disk identifier: {ex.Message}");
+ throw new InvalidOperationException("Failed to retrieve disk identifier. Please ensure the disk information is accessible.");
+ }
+ }
+ else
+ {
+ throw new PlatformNotSupportedException("This method is only supported on Windows.");
+ }
+
+ // Return an empty string if no serial number is found
+ return string.Empty;
+ }
+
+
+
+ // Setting class
+ public class Setting
+ {
+ public Guid Id { get; set; }
+ public DateTime Timestamp { get; set; }
+ public string Group { get; set; }
+ public string Key { get; set; }
+ public string Value { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/mRemoteNG/Properties/AssemblyInfo.cs b/mRemoteNG/Properties/AssemblyInfo.cs
index 21543bd0..3a396bbf 100644
--- a/mRemoteNG/Properties/AssemblyInfo.cs
+++ b/mRemoteNG/Properties/AssemblyInfo.cs
@@ -18,10 +18,10 @@ using System.Resources;
[assembly: AssemblyCulture("")]
// Version information
-[assembly: AssemblyVersion("1.77.3.2693")]
-[assembly: AssemblyFileVersion("1.77.3.2693")]
+[assembly: AssemblyVersion("1.77.3.2694")]
+[assembly: AssemblyFileVersion("1.77.3.2694")]
[assembly: NeutralResourcesLanguageAttribute("en-US")]
-[assembly: AssemblyInformationalVersion("1.77.3 (Nightly Build 2693)")]
+[assembly: AssemblyInformationalVersion("1.77.3 (Nightly Build 2694)")]
// Logging
[assembly: log4net.Config.XmlConfigurator(ConfigFile = "log4net.config")]
diff --git a/mRemoteNG/Schemas/mremoteng_default_settings_v1_0.json b/mRemoteNG/Schemas/mremoteng_default_settings_v1_0.json
new file mode 100644
index 00000000..065da5c5
--- /dev/null
+++ b/mRemoteNG/Schemas/mremoteng_default_settings_v1_0.json
@@ -0,0 +1,88 @@
+{
+ "$schema": "https://json-schema.org/draft-07/schema#",
+ "title": "mRemoteNG Default Settings",
+ "description": "This schema defines the default settings for mRemoteNG.",
+ "type": "object",
+ "database": {
+ "filename": "mRemoteNG.appSettings",
+ "autoCreatedb": true
+ },
+ "tables": [
+ {
+ "name": "main",
+ "columns": [
+ {
+ "name": "name",
+ "description": "The name of the application.",
+ "type": "string",
+ "value": "mRemoteNG",
+ "required": true
+ },
+ {
+ "name": "version",
+ "description": "The version of the application.",
+ "type": "string",
+ "value": "1.77.3",
+ "required": true
+ }
+ ]
+ },
+ {
+ "name": "update",
+ "columns": [
+ {
+ "name": "checkInterval",
+ "description": "The interval at which to check for updates in seconds.",
+ "type": "integer",
+ "value": 86400,
+ "required": true
+ },
+ {
+ "name": "autoUpdate",
+ "description": "Whether to automatically update the application.",
+ "type": "boolean",
+ "value": false,
+ "required": true
+ }
+ ]
+ },
+ {
+ "name": "theme",
+ "columns": [
+ {
+ "name": "primaryColor",
+ "description": "The primary color of the application.",
+ "type": "string",
+ "value": "#[a-f0-9]{6}",
+ "required": true
+ },
+ {
+ "name": "secondaryColor",
+ "type": "string",
+ "value": "#[a-f0-9]{6}",
+ "required": true
+ }
+ ]
+ },
+ {
+ "name": "backup",
+ "columns": [
+ {
+ "name": "schedule",
+ "description": "The schedule for backing up the application.",
+ "type": "string",
+ "value": ["daily", "weekly", "monthly"],
+ "required": true
+ },
+ {
+ "name": "destination",
+ "description": "The destination for the backup file.",
+ "type": "string",
+ "format": "file",
+ "value": "C:\\Users\\user\\Documents\\mRemoteNG\\backup",
+ "required": true
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/mRemoteNG/mRemoteNG.csproj b/mRemoteNG/mRemoteNG.csproj
index 0bdb02ea..72b35d41 100644
--- a/mRemoteNG/mRemoteNG.csproj
+++ b/mRemoteNG/mRemoteNG.csproj
@@ -464,6 +464,9 @@
Always
+
+ Always
+
Always