diff --git a/Duplicati.Library.RestAPI/Database/Connection.cs b/Duplicati.Library.RestAPI/Database/Connection.cs index 8f1b94b78..7a2dfc791 100644 --- a/Duplicati.Library.RestAPI/Database/Connection.cs +++ b/Duplicati.Library.RestAPI/Database/Connection.cs @@ -39,6 +39,7 @@ namespace Duplicati.Server.Database public const int ANY_BACKUP_ID = -1; public const int SERVER_SETTINGS_ID = -2; private readonly Dictionary m_temporaryBackups = new Dictionary(); + private readonly bool m_encryptSensitiveFields; private static HashSet _encryptedFields = BackendLoader.Backends.SelectMany(x => x.SupportedCommands ?? []) .Concat(EncryptionLoader.Modules.SelectMany(x => x.SupportedCommands ?? [])) @@ -54,7 +55,9 @@ namespace Duplicati.Server.Database ]) .ToHashSet(StringComparer.OrdinalIgnoreCase); + public Connection(System.Data.IDbConnection connection, bool disableFieldEncryption) { + m_encryptSensitiveFields = !disableFieldEncryption; m_connection = connection; m_errorcmd = m_connection.CreateCommand(); m_errorcmd.CommandText = @"INSERT INTO ""ErrorLog"" (""BackupID"", ""Message"", ""Exception"", ""Timestamp"") VALUES (?,?,?,?)"; @@ -64,6 +67,31 @@ namespace Duplicati.Server.Database this.ApplicationSettings = new ServerSettings(this); } + public void ReWriteAllFieldsIfEncryptionChanged() + { + var activeToken = m_encryptSensitiveFields + ? "enc-v1-canary" + : "no-encryption"; + + // The token is automatically decrypted when the settings are loaded + // In case the password has changed, this will fail and return the encrypted + // hex-string, but will crash before reaching this point + if (this.ApplicationSettings.EncryptionCanaryToken != activeToken) + { + var backups = this.Backups; + foreach (var b in backups) + { + ((Backup)b).LoadChildren(this); + AddOrUpdateBackup(b, false, null); + } + + this.SetSettings(this.GetSettings(ANY_BACKUP_ID), ANY_BACKUP_ID); + this.ApplicationSettings.EncryptionCanaryToken = m_encryptSensitiveFields + ? EncryptedFieldHelper.Encrypt(activeToken) + : activeToken; + } + } + public void LogError(string backupid, string message, Exception ex) { lock (m_lock) @@ -222,7 +250,7 @@ namespace Duplicati.Server.Database lock (m_lock) using (var tr = transaction == null ? m_connection.BeginTransaction() : null) { - List intercepted = values.ToList(); + if (m_encryptSensitiveFields) values = values.Select(x => new Setting { Filter = x.Filter, @@ -582,7 +610,7 @@ namespace Duplicati.Server.Database n.Name, n.Description ?? "" , // Description is optional but the column is set to NOT NULL, an additional check is welcome string.Join(",", n.Tags ?? new string[0]), - EncryptedFieldHelper.Encrypt(n.TargetURL) + m_encryptSensitiveFields ? EncryptedFieldHelper.Encrypt(n.TargetURL) : n.TargetURL, update ? item.ID : n.DBPath }; }); diff --git a/Duplicati.Library.RestAPI/Database/ServerSettings.cs b/Duplicati.Library.RestAPI/Database/ServerSettings.cs index 48dfb252b..60706af06 100644 --- a/Duplicati.Library.RestAPI/Database/ServerSettings.cs +++ b/Duplicati.Library.RestAPI/Database/ServerSettings.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -59,6 +59,7 @@ namespace Duplicati.Server.Database public const string PBKDF_CONFIG = "pbkdf-config"; public const string AUTOGENERATED_PASSPHRASE = "autogenerated-passphrase"; public const string DISABLE_VISUAL_CAPTCHA = "disable-visual-captcha"; + public const string ENCRYPTION_CANARY_TOKEN = "encryption-canary-token"; } private readonly Dictionary settings; @@ -651,6 +652,20 @@ namespace Duplicati.Server.Database SaveSettings(); } } + + public string EncryptionCanaryToken + { + get + { + return settings[CONST.ENCRYPTION_CANARY_TOKEN]; + } + set + { + lock (databaseConnection.m_lock) + settings[CONST.ENCRYPTION_CANARY_TOKEN] = value; + SaveSettings(); + } + } } } diff --git a/Duplicati.Library.RestAPI/Strings.cs b/Duplicati.Library.RestAPI/Strings.cs index 2e315107f..056b947a9 100644 --- a/Duplicati.Library.RestAPI/Strings.cs +++ b/Duplicati.Library.RestAPI/Strings.cs @@ -46,6 +46,8 @@ Error message: {0}", error); } public static string TempdirShort { get { return LC.L(@"Temporary storage folder"); } } public static string WebserverResetJwtConfigDescription { get { return LC.L(@"Reset the JWT configuration, invalidating any issued login tokens"); } } public static string WebserverDisableVisualCaptchaDescription { get { return LC.L(@"Disable the visual captcha"); } } + public static string DisabledbencryptionLong { get { return LC.L(@"Use this option to disable database encryption of sensitive fields"); } } + public static string DisabledbencryptionShort { get { return LC.L(@"Disable database encryption"); } } } internal static class Scheduler { diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs index 69f3d116e..948d8b8a6 100644 --- a/Duplicati/Server/Program.cs +++ b/Duplicati/Server/Program.cs @@ -252,7 +252,6 @@ namespace Duplicati.Server DataConnection.FixInvalidBackupId(); DataConnection.ApplicationSettings.UpgradePasswordToKBDF(); - CreateApplicationInstance(writeToConsole); StartOrStopUsageReporter(); @@ -290,6 +289,8 @@ namespace Duplicati.Server PingPongThread.Start(); } + DataConnection.ReWriteAllFieldsIfEncryptionChanged(); + ServerStartedEvent.Set(); ApplicationExitEvent.WaitOne(); } @@ -705,7 +706,7 @@ namespace Duplicati.Server throw new Exception(Strings.Program.DatabaseOpenError(ex.Message)); } - return new Database.Connection(con); + return new Database.Connection(con, Library.Utility.Utility.ParseBoolOption(commandlineOptions, "disable-db-encryption")); } public static void StartOrStopUsageReporter() @@ -829,6 +830,7 @@ namespace Duplicati.Server new Duplicati.Library.Interface.CommandLineArgument("ping-pong-keepalive", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.PingpongkeepaliveShort, Strings.Program.PingpongkeepaliveLong), new Duplicati.Library.Interface.CommandLineArgument("log-retention", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Timespan, Strings.Program.LogretentionShort, Strings.Program.LogretentionLong, DEFAULT_LOG_RETENTION), new Duplicati.Library.Interface.CommandLineArgument("server-datafolder", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.ServerdatafolderShort, Strings.Program.ServerdatafolderLong(DATAFOLDER_ENV_NAME), System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Library.AutoUpdater.AutoUpdateSettings.AppName)), + new Duplicati.Library.Interface.CommandLineArgument("disable-db-encryption", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.DisabledbencryptionShort, Strings.Program.DisabledbencryptionLong), ]; private static bool ReadOptionsFromFile(string filename, ref Library.Utility.IFilter filter, List cargs, Dictionary options)