Status: Fixed I have now implemented a password protection control. You can now only view passwords when you enter them. After this, you may reset the passwords, by entering a new one, but you cannot read the existing passwords. To protect against people reading the database, which contains the passwords in clear text, the database is now encrypted by using the standard SQLite encryption method. This is different than what I proposed, but is a much stronger protection than merely scrambling the passwords, and also protects all information (servername, etc). It works by setting an environment variable called DUPLICATI_DB_KEY. When no password is set, Duplicati will use the key "Duplicati_Key_42", ensuring that all databases are encrypted by default. Using a pre-defined publicly known key is obviously not a strong protection, but it protects from casual users attempting to browse the database, and it also protects from string scanners that scan a harddisk for possible passwords. On the first startup, Duplicati will encrypt the database, using the mentioned key. You may want to delete any backup databases that are placed in the same folder as the database. Should you wish to decrypt the database, you can start Duplicati.exe with the commandline option "--unencrypted-database". This will use the current key to decrypt the database, and save it without encryption. Should you wish to change the database password, simply decrypt the database, then change the password via the environment variable, and start Duplicati.exe as normal. The SQLite documentation for the feature is here: http://www.hwaci.com/sw/sqlite/see.html Unfortunately, it does not seem as if any of the free SQLite tools supports this. The SQLite library found on Ubuntu does not support encryption, so I have disabled this feature by default on Linux. If a user has a special compiled version of SQLite, they may use the environment variables as mentioned above. I will write this information into a wiki page as soon asap. git-svn-id: https://duplicati.googlecode.com/svn/trunk@498 59da171f-624f-0410-aa54-27559c288bec
425 lines
19 KiB
C#
425 lines
19 KiB
C#
#region Disclaimer / License
|
|
// Copyright (C) 2010, Kenneth Skovhede
|
|
// http://www.hexad.dk, opensource@hexad.dk
|
|
//
|
|
// This library is free software; you can redistribute it and/or
|
|
// modify it under the terms of the GNU Lesser General Public
|
|
// License as published by the Free Software Foundation; either
|
|
// version 2.1 of the License, or (at your option) any later version.
|
|
//
|
|
// This library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
// Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public
|
|
// License along with this library; if not, write to the Free Software
|
|
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
//
|
|
#endregion
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Windows.Forms;
|
|
using System.Data.LightDatamodel;
|
|
using System.Drawing;
|
|
using Duplicati.Datamodel;
|
|
|
|
namespace Duplicati.GUI
|
|
{
|
|
static class Program
|
|
{
|
|
/// <summary>
|
|
/// The name of the environment variable that holds the path to the data folder used by Duplicati
|
|
/// </summary>
|
|
public const string DATAFOLDER_ENV_NAME = "DUPLICATI_HOME";
|
|
|
|
/// <summary>
|
|
/// The environment variable that holdes the database key used to encrypt the SQLite database
|
|
/// </summary>
|
|
public const string DB_KEY_ENV_NAME = "DUPLICATI_DB_KEY";
|
|
|
|
/// <summary>
|
|
/// Gets the folder where Duplicati data is stored
|
|
/// </summary>
|
|
public static string DATAFOLDER { get { return Library.Core.Utility.AppendDirSeperator(Environment.ExpandEnvironmentVariables("%" + DATAFOLDER_ENV_NAME + "%").TrimStart('"').TrimEnd('"')); } }
|
|
|
|
/// <summary>
|
|
/// This is the only access to the database
|
|
/// </summary>
|
|
public static IDataFetcherWithRelations DataConnection;
|
|
|
|
/// <summary>
|
|
/// This is the lock to be used before manipulating the shared resources
|
|
/// </summary>
|
|
public static object MainLock = new object();
|
|
|
|
/// <summary>
|
|
/// This is the scheduling thread
|
|
/// </summary>
|
|
public static Scheduler Scheduler;
|
|
|
|
/// <summary>
|
|
/// This is the working thread
|
|
/// </summary>
|
|
public static WorkerThread<IDuplicityTask> WorkThread;
|
|
|
|
/// <summary>
|
|
/// The path to the file that contains the current database
|
|
/// </summary>
|
|
public static string DatabasePath;
|
|
|
|
/// <summary>
|
|
/// The actual runner, do not call directly. Only used for events.
|
|
/// </summary>
|
|
public static DuplicatiRunner Runner;
|
|
|
|
/// <summary>
|
|
/// The controller interface for pause/resume and throttle options
|
|
/// </summary>
|
|
public static LiveControls LiveControl;
|
|
|
|
/// <summary>
|
|
/// The main form that contains the tray icon
|
|
/// </summary>
|
|
public static MainForm DisplayHelper;
|
|
|
|
/// <summary>
|
|
/// The single instance keeper
|
|
/// </summary>
|
|
public static SingleInstance SingleInstance;
|
|
|
|
/// <summary>
|
|
/// The main entry point for the application.
|
|
/// </summary>
|
|
[STAThread]
|
|
static void Main(string[] args)
|
|
{
|
|
Application.EnableVisualStyles();
|
|
Application.SetCompatibleTextRenderingDefault(false);
|
|
|
|
//If we are on Windows, append the bundled "win-tools" programs to the search path
|
|
//We add it last, to allow the user to override with other versions
|
|
if (!Library.Core.Utility.IsClientLinux)
|
|
{
|
|
Environment.SetEnvironmentVariable("PATH",
|
|
Environment.GetEnvironmentVariable("PATH") +
|
|
System.IO.Path.PathSeparator.ToString() +
|
|
System.IO.Path.Combine(
|
|
System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location),
|
|
"win-tools")
|
|
);
|
|
}
|
|
|
|
//If we are on windows we encrypt the database by default
|
|
//We do not encrypt on Linux as most distros use a SQLite library without encryption support,
|
|
//Linux users can use an encrypted home folder, or install a SQLite library with encryption support
|
|
if (!Library.Core.Utility.IsClientLinux && string.IsNullOrEmpty(Environment.GetEnvironmentVariable(DB_KEY_ENV_NAME)))
|
|
{
|
|
//Note that the password here is a default password and public knowledge
|
|
//
|
|
//The purpose of this is to prevent casual read of the database, as well
|
|
// as protect from harddisk string scans, not to protect from determined
|
|
// attacks.
|
|
//
|
|
//If you desire better security, start Duplicati once with the commandline option
|
|
// --unencrypted-database to decrypt the database.
|
|
//Then set the environment variable DUPLICATI_DB_KEY to the desired key,
|
|
// and run Duplicati again without the --unencrypted-database option
|
|
// to re-encrypt it with the new key
|
|
//
|
|
//If you change the key, please note that you need to supply the same
|
|
// key when restoring the setup, as the setup being backed up will
|
|
// be encrypted as well.
|
|
Environment.SetEnvironmentVariable(DB_KEY_ENV_NAME, "Duplicati_Key_42");
|
|
}
|
|
|
|
|
|
//Find commandline options here for handling special startup cases
|
|
Dictionary<string, string> commandlineOptions = CommandLine.CommandLineParser.ExtractOptions(new List<string>(args));
|
|
|
|
//Set the %DUPLICATI_HOME% env variable, if it is not already set
|
|
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(DATAFOLDER_ENV_NAME)))
|
|
{
|
|
#if DEBUG
|
|
//debug mode uses a lock file located in the app folder
|
|
Environment.SetEnvironmentVariable(DATAFOLDER_ENV_NAME, System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location));
|
|
#else
|
|
bool portableMode = commandlineOptions.ContainsKey("portable-mode") ? Library.Core.Utility.ParseBool(commandlineOptions["portable-mode"], true) : false;
|
|
|
|
if (portableMode)
|
|
{
|
|
//Portable mode uses a data folder in the application home dir
|
|
Environment.SetEnvironmentVariable(DATAFOLDER_ENV_NAME, System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "data"));
|
|
}
|
|
else
|
|
{
|
|
//Normal release mode uses the systems "Application Data" folder
|
|
Environment.SetEnvironmentVariable(DATAFOLDER_ENV_NAME, System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Application.ProductName));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
try
|
|
{
|
|
try
|
|
{
|
|
//This will also create Program.DATAFOLDER if it does not exist
|
|
SingleInstance = new SingleInstance(Application.ProductName, Program.DATAFOLDER);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(string.Format(Strings.Program.StartupFailure, ex.ToString()), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
return;
|
|
}
|
|
|
|
if (!SingleInstance.IsFirstInstance)
|
|
{
|
|
//Linux shows this output
|
|
Console.WriteLine(Strings.Program.AnotherInstanceDetected);
|
|
return;
|
|
}
|
|
|
|
Version sqliteVersion = new Version((string)SQLiteLoader.SQLiteConnectionType.GetProperty("SQLiteVersion").GetValue(null, null));
|
|
if (sqliteVersion < new Version(3, 6, 3))
|
|
{
|
|
//The official Mono SQLite provider is also broken with less than 3.6.3
|
|
MessageBox.Show(string.Format(Strings.Program.WrongSQLiteVersion, sqliteVersion, "3.6.3"), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
return;
|
|
}
|
|
|
|
//Create the connection instance
|
|
System.Data.IDbConnection con = (System.Data.IDbConnection)Activator.CreateInstance(SQLiteLoader.SQLiteConnectionType);
|
|
|
|
try
|
|
{
|
|
DatabasePath = System.IO.Path.Combine(Program.DATAFOLDER, "Duplicati.sqlite");
|
|
if (!System.IO.Directory.Exists(System.IO.Path.GetDirectoryName(DatabasePath)))
|
|
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(DatabasePath));
|
|
|
|
#if DEBUG
|
|
//Default is to no use encryption for debugging
|
|
bool noDbEncryption = commandlineOptions.ContainsKey("unencrypted-database") ? Library.Core.Utility.ParseBool(commandlineOptions["unencrypted-database"], true) : true;
|
|
#else
|
|
bool noDbEncryption = commandlineOptions.ContainsKey("unencrypted-database") ? Library.Core.Utility.ParseBool(commandlineOptions["unencrypted-database"], true) : false;
|
|
#endif
|
|
con.ConnectionString = "Data Source=" + DatabasePath;
|
|
|
|
//Attempt to open the database
|
|
OpenDatabase(con, Environment.GetEnvironmentVariable(DB_KEY_ENV_NAME), noDbEncryption);
|
|
|
|
DatabaseUpgrader.UpgradeDatebase(con, DatabasePath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//Unwrap the reflection exceptions
|
|
if (ex is System.Reflection.TargetInvocationException && ex.InnerException != null)
|
|
ex = ex.InnerException;
|
|
|
|
MessageBox.Show(string.Format(Strings.Program.DatabaseOpenError, ex.Message), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
return;
|
|
}
|
|
|
|
DataConnection = new DataFetcherWithRelations(new SQLiteDataProvider(con));
|
|
|
|
if (!string.IsNullOrEmpty(new Datamodel.ApplicationSettings(DataConnection).DisplayLanguage))
|
|
try
|
|
{
|
|
System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo(new Datamodel.ApplicationSettings(DataConnection).DisplayLanguage);
|
|
System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo(new Datamodel.ApplicationSettings(DataConnection).DisplayLanguage);
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
MessageBox.Show(string.Format(Strings.Program.LanguageSelectionError, ex.Message), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
//This is non-fatal, just keep running with system default language
|
|
}
|
|
|
|
#if DEBUG
|
|
//Log various information in the logfile
|
|
string logfile = System.IO.Path.Combine(Application.StartupPath, "Duplicati.debug.log");
|
|
if (System.IO.File.Exists(logfile))
|
|
System.IO.File.Delete(logfile);
|
|
|
|
Duplicati.Library.Logging.Log.LogLevel = Duplicati.Library.Logging.LogMessageType.Profiling;
|
|
Duplicati.Library.Logging.Log.CurrentLog = new Duplicati.Library.Logging.StreamLog(logfile);
|
|
#endif
|
|
|
|
LiveControl = new LiveControls(new ApplicationSettings(DataConnection));
|
|
LiveControl.StateChanged += new EventHandler(LiveControl_StateChanged);
|
|
LiveControl.ThreadPriorityChanged += new EventHandler(LiveControl_ThreadPriorityChanged);
|
|
LiveControl.ThrottleSpeedChanged += new EventHandler(LiveControl_ThrottleSpeedChanged);
|
|
|
|
Runner = new DuplicatiRunner();
|
|
WorkThread = new WorkerThread<IDuplicityTask>(new WorkerThread<IDuplicityTask>.ProcessItemDelegate(Runner.ExecuteTask), LiveControl.State == LiveControls.LiveControlState.Paused);
|
|
Scheduler = new Scheduler(DataConnection, WorkThread, MainLock);
|
|
|
|
DataConnection.AfterDataConnection += new DataConnectionEventHandler(DataConnection_AfterDataConnection);
|
|
|
|
DisplayHelper = new MainForm();
|
|
DisplayHelper.InitialArguments = args;
|
|
|
|
Application.Run(DisplayHelper);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(string.Format(Strings.Program.SeriousError, ex.ToString()), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
|
|
|
|
if (Scheduler != null)
|
|
Scheduler.Terminate(true);
|
|
if (WorkThread != null)
|
|
WorkThread.Terminate(true);
|
|
if (SingleInstance != null)
|
|
SingleInstance.Dispose();
|
|
|
|
#if DEBUG
|
|
using(Duplicati.Library.Logging.Log.CurrentLog as Duplicati.Library.Logging.StreamLog)
|
|
Duplicati.Library.Logging.Log.CurrentLog = null;
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles a change in the LiveControl and updates the Runner
|
|
/// </summary>
|
|
/// <param name="sender"></param>
|
|
/// <param name="e"></param>
|
|
private static void LiveControl_ThreadPriorityChanged(object sender, EventArgs e)
|
|
{
|
|
if (LiveControl.ThreadPriority == null)
|
|
Runner.UnsetThreadPriority();
|
|
else
|
|
Runner.SetThreadPriority(LiveControl.ThreadPriority.Value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles a change in the LiveControl and updates the Runner
|
|
/// </summary>
|
|
/// <param name="sender"></param>
|
|
/// <param name="e"></param>
|
|
private static void LiveControl_ThrottleSpeedChanged(object sender, EventArgs e)
|
|
{
|
|
if (LiveControl.DownloadLimit == null)
|
|
Runner.SetDownloadLimit(null);
|
|
else
|
|
Runner.SetDownloadLimit(LiveControl.DownloadLimit.Value.ToString() + "b");
|
|
|
|
if (LiveControl.UploadLimit == null)
|
|
Runner.SetUploadLimit(null);
|
|
else
|
|
Runner.SetUploadLimit(LiveControl.UploadLimit.Value.ToString() + "b");
|
|
}
|
|
|
|
/// <summary>
|
|
/// This event handler updates the trayicon menu with the current state of the runner.
|
|
/// </summary>
|
|
static void LiveControl_StateChanged(object sender, EventArgs e)
|
|
{
|
|
switch (LiveControl.State)
|
|
{
|
|
case LiveControls.LiveControlState.Paused:
|
|
WorkThread.Pause();
|
|
Runner.Pause();
|
|
break;
|
|
case LiveControls.LiveControlState.Running:
|
|
WorkThread.Resume();
|
|
Runner.Resume();
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
private static void DataConnection_AfterDataConnection(object sender, DataActions action)
|
|
{
|
|
if (action == DataActions.Insert || action == DataActions.Update)
|
|
Scheduler.Reschedule();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a localized name for a task type
|
|
/// </summary>
|
|
/// <param name="type"></param>
|
|
/// <returns></returns>
|
|
public static string LocalizeTaskType(DuplicityTaskType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case DuplicityTaskType.FullBackup:
|
|
return Strings.TaskType.FullBackup;
|
|
case DuplicityTaskType.IncrementalBackup:
|
|
return Strings.TaskType.IncrementalBackup;
|
|
case DuplicityTaskType.ListActualFiles:
|
|
return Strings.TaskType.ListActualFiles;
|
|
case DuplicityTaskType.ListBackupEntries:
|
|
return Strings.TaskType.ListBackupEntries;
|
|
case DuplicityTaskType.ListBackups:
|
|
return Strings.TaskType.ListBackups;
|
|
case DuplicityTaskType.ListFiles:
|
|
return Strings.TaskType.ListFiles;
|
|
case DuplicityTaskType.RemoveAllButNFull:
|
|
return Strings.TaskType.RemoveAllButNFull;
|
|
case DuplicityTaskType.RemoveOlderThan:
|
|
return Strings.TaskType.RemoveOlderThan;
|
|
case DuplicityTaskType.Restore:
|
|
return Strings.TaskType.Restore;
|
|
case DuplicityTaskType.RestoreSetup:
|
|
return Strings.TaskType.RestoreSetup;
|
|
default:
|
|
return type.ToString();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method with logic to handle opening a database in possibly encrypted format
|
|
/// </summary>
|
|
/// <param name="con">The SQLite connection object</param>
|
|
/// <param name="password">The password used for encryption</param>
|
|
/// <param name="noEncryption">A flag indicating that the database should not be encrypted</param>
|
|
private static void OpenDatabase(System.Data.IDbConnection con, string password, bool noEncryption)
|
|
{
|
|
System.Reflection.MethodInfo setPwdMethod = con.GetType().GetMethod("SetPassword", new Type[] { typeof(string) });
|
|
string attemptedPassword;
|
|
|
|
if (noEncryption || string.IsNullOrEmpty(password))
|
|
attemptedPassword = null; //No encryption specified, attempt to open without
|
|
else
|
|
attemptedPassword = password; //Encryption specified, attempt to open with
|
|
|
|
setPwdMethod.Invoke(con, new object[] { attemptedPassword });
|
|
|
|
try
|
|
{
|
|
//Attempt to open in preferred state
|
|
con.Open();
|
|
}
|
|
catch
|
|
{
|
|
try
|
|
{
|
|
//We can't try anything else without a password
|
|
if (string.IsNullOrEmpty(password))
|
|
throw;
|
|
|
|
//Open failed, now try the reverse
|
|
if (attemptedPassword == null)
|
|
attemptedPassword = password;
|
|
else
|
|
attemptedPassword = null;
|
|
|
|
setPwdMethod.Invoke(con, new object[] { attemptedPassword });
|
|
con.Open();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
//If the db is not open now, it won't open
|
|
if (con.State != System.Data.ConnectionState.Open)
|
|
throw; //Report original error
|
|
|
|
//The open method succeeded with the non-default method, now change the password
|
|
System.Reflection.MethodInfo changePwdMethod = con.GetType().GetMethod("ChangePassword", new Type[] { typeof(string) });
|
|
changePwdMethod.Invoke(con, new object[] { noEncryption ? null : password });
|
|
}
|
|
}
|
|
}
|
|
} |