// Copyright (C) 2025, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. #nullable enable using System; using System.Threading; using System.Threading.Tasks; using Duplicati.Library.Common.IO; namespace Duplicati.Library.Main.Database { /// /// A local database for bug reports, which obfuscates sensitive data before generating a bug report. /// internal class LocalBugReportDatabase : LocalDatabase { /// /// The tag used for logging /// private static readonly string LOGTAG = Logging.Log.LogTagFromType(typeof(LocalBugReportDatabase)); /// /// Creates a new instance of the class. /// /// The path to the database file. /// An optional existing database instance to use. Used to mimic constructor chaining. /// A cancellation token to cancel the operation. /// A task that when awaited contains a new instance of . public static async Task CreateAsync(string path, LocalBugReportDatabase? dbnew, CancellationToken token) { dbnew ??= new LocalBugReportDatabase(); dbnew = (LocalBugReportDatabase) await CreateLocalDatabaseAsync(path, "BugReportCreate", false, dbnew, token) .ConfigureAwait(false); dbnew.ShouldCloseConnection = true; return dbnew; } /// /// Obfuscates sensitive data in the database, readying it for a bug report. /// /// A cancellation token to cancel the operation. /// A task that completes when the obfuscation is finished. public async Task Fix(CancellationToken token) { await using var cmd = m_connection.CreateCommand(m_rtr); var tablename = $"PathMap-{Library.Utility.Utility.GetHexGuid()}"; // TODO: Rewrite this to use PathPrefix // TODO: Needs to be much faster await using (var upcmd = m_connection.CreateCommand(m_rtr)) { await upcmd.ExecuteNonQueryAsync($@" CREATE TEMPORARY TABLE ""{tablename}"" ( ""ID"" INTEGER PRIMARY KEY, ""RealPath"" TEXT NOT NULL, ""Obfuscated"" TEXT NULL ) ", token) .ConfigureAwait(false); await upcmd.ExecuteNonQueryAsync($@" INSERT INTO ""{tablename}"" (""RealPath"") SELECT DISTINCT ""Path"" FROM ""File"" ORDER BY ""Path"" ", token) .ConfigureAwait(false); await upcmd.SetCommandAndParameters($@" UPDATE ""{tablename}"" SET ""Obfuscated"" = @StartPath || length(""RealPath"") || @DirSep || ""ID"" || ( CASE WHEN substr(""RealPath"", length(""RealPath"")) = @DirSep THEN @DirSep ELSE @FileExt END ) ") .SetParameterValue("@StartPath", !OperatingSystem.IsWindows() ? "/" : "X:\\") .SetParameterValue("@DirSep", Util.DirectorySeparatorString) .SetParameterValue("@FileExt", ".bin") .ExecuteNonQueryAsync(token) .ConfigureAwait(false); /*long id = 1; using(var rd = cmd.ExecuteReader(string.Format(@"SELECT ""RealPath"", ""Obfuscated"" FROM ""{0}"" ", tablename))) while(rd.Read()) { upcmd.SetCommandAndParameters(@"UPDATE ""LogData"" SET ""Message"" = replace(""Message"", @RealPath, @Obfuscated), ""Exception"" = replace(""Exception"", @RealPath, @Obfuscated)") .SetParameterValue("@RealPath", rd.GetValue(0)) .SetParameterValue("@Obfuscated", rd.GetValue(1)) .ExecuteNonQuery(); id++; } */ } await cmd.ExecuteNonQueryAsync(@" UPDATE ""LogData"" SET ""Message"" = 'ERASED!' WHERE ""Message"" LIKE '%/%' OR ""Message"" LIKE '%:\%' ", token) .ConfigureAwait(false); await cmd.ExecuteNonQueryAsync(@" UPDATE ""LogData"" SET ""Exception"" = 'ERASED!' WHERE ""Exception"" LIKE '%/%' OR ""Exception"" LIKE '%:\%' ", token) .ConfigureAwait(false); await cmd.ExecuteNonQueryAsync(@" UPDATE ""Configuration"" SET ""Value"" = 'ERASED!' WHERE ""Key"" = 'passphrase' ", token) .ConfigureAwait(false); await cmd.ExecuteNonQueryAsync($@" CREATE TABLE ""FixedFile"" AS SELECT ""B"".""ID"" AS ""ID"", ""A"".""Obfuscated"" AS ""Path"", ""B"".""BlocksetID"" AS ""BlocksetID"", ""B"".""MetadataID"" AS ""MetadataID"" FROM ""{tablename}"" ""A"", ""File"" ""B"" WHERE ""A"".""RealPath"" = ""B"".""Path"" ", token) .ConfigureAwait(false); await cmd.ExecuteNonQueryAsync(@"DROP VIEW ""File"" ", token) .ConfigureAwait(false); await cmd.ExecuteNonQueryAsync(@"DROP TABLE ""FileLookup"" ", token) .ConfigureAwait(false); await cmd.ExecuteNonQueryAsync(@"DROP TABLE ""PathPrefix"" ", token) .ConfigureAwait(false); await cmd.ExecuteNonQueryAsync($@"DROP TABLE IF EXISTS ""{tablename}"" ", token) .ConfigureAwait(false); using (new Logging.Timer(LOGTAG, "CommitUpdateBugReport", "CommitUpdateBugReport")) await m_rtr.CommitAsync(token: token).ConfigureAwait(false); cmd.SetTransaction(m_rtr); await m_rtr.CommitAsync(restart: false, token: token).ConfigureAwait(false); cmd.Transaction = null; await cmd.ExecuteNonQueryAsync("VACUUM", token).ConfigureAwait(false); } } }