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