added a dialog to prompt for action when decrypting a connection file fails

This commit is contained in:
David Sparer
2018-07-23 11:51:58 -05:00
parent bbc497e68d
commit 9659ac1611
13 changed files with 203 additions and 116 deletions

View File

@@ -1,7 +1,6 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using mRemoteNG.Config.Serializers;
using mRemoteNG.Config.Serializers.Xml;
using mRemoteNG.Connection;
using mRemoteNG.Container;
@@ -19,7 +18,7 @@ namespace mRemoteNGTests.Config.Serializers.ConnectionSerializers.Xml
public void Setup(string confCons, string password)
{
_xmlConnectionsDeserializer = new XmlConnectionsDeserializer(password.ConvertToSecureString);
_xmlConnectionsDeserializer = new XmlConnectionsDeserializer(() => password.ConvertToSecureString());
_connectionTreeModel = _xmlConnectionsDeserializer.Deserialize(confCons);
}

View File

@@ -2,6 +2,7 @@
using mRemoteNG.Security;
using mRemoteNG.Security.Authentication;
using mRemoteNG.Security.SymmetricEncryption;
using mRemoteNG.Tools;
using NUnit.Framework;
@@ -9,35 +10,31 @@ namespace mRemoteNGTests.Security.Authentication
{
public class PasswordAuthenticatorTests
{
private PasswordAuthenticator _authenticator;
private ICryptographyProvider _cryptographyProvider;
private string _cipherText;
private readonly SecureString _correctPassword = "9theCorrectPass#5".ConvertToSecureString();
private readonly SecureString _wrongPassword = "wrongPassword".ConvertToSecureString();
[SetUp]
public void Setup()
{
var cryptoProvider = new AeadCryptographyProvider {KeyDerivationIterations = 10000};
const string cipherText = "MPELiwk7+xeNlruIyt5uxTvVB+/RLVoLdUGnwY4CWCqwKe7T2IBwWo4oaKum5hdv7447g5m2nZsYPrfARSlotQB4r1KZQg==";
_authenticator = new PasswordAuthenticator(cryptoProvider, cipherText);
}
[TearDown]
public void Teardown()
{
_authenticator = null;
_cryptographyProvider = new AeadCryptographyProvider {KeyDerivationIterations = 10000};
_cipherText = "MPELiwk7+xeNlruIyt5uxTvVB+/RLVoLdUGnwY4CWCqwKe7T2IBwWo4oaKum5hdv7447g5m2nZsYPrfARSlotQB4r1KZQg==";
}
[Test]
public void AuthenticatingWithCorrectPasswordReturnsTrue()
{
var authenticated = _authenticator.Authenticate(_correctPassword);
var authenticator = new PasswordAuthenticator(_cryptographyProvider, _cipherText, () => Optional<SecureString>.Empty);
var authenticated = authenticator.Authenticate(_correctPassword);
Assert.That(authenticated);
}
[Test]
public void AuthenticatingWithWrongPasswordReturnsFalse()
{
var authenticated = _authenticator.Authenticate(_wrongPassword);
var authenticator = new PasswordAuthenticator(_cryptographyProvider, _cipherText, () => Optional<SecureString>.Empty);
var authenticated = authenticator.Authenticate(_wrongPassword);
Assert.That(!authenticated);
}
@@ -45,12 +42,15 @@ namespace mRemoteNGTests.Security.Authentication
public void AuthenticationRequestorIsCalledWhenInitialPasswordIsWrong()
{
var wasCalled = false;
_authenticator.AuthenticationRequestor = () =>
Optional<SecureString> AuthenticationRequestor()
{
wasCalled = true;
return _correctPassword;
};
_authenticator.Authenticate(_wrongPassword);
}
var authenticator = new PasswordAuthenticator(_cryptographyProvider, _cipherText, AuthenticationRequestor);
authenticator.Authenticate(_wrongPassword);
Assert.That(wasCalled);
}
@@ -58,28 +58,30 @@ namespace mRemoteNGTests.Security.Authentication
public void AuthenticationRequestorNotCalledWhenInitialPasswordIsCorrect()
{
var wasCalled = false;
_authenticator.AuthenticationRequestor = () =>
Optional<SecureString> AuthenticationRequestor()
{
wasCalled = true;
return _correctPassword;
};
_authenticator.Authenticate(_correctPassword);
}
var authenticator = new PasswordAuthenticator(_cryptographyProvider, _cipherText, AuthenticationRequestor);
authenticator.Authenticate(_correctPassword);
Assert.That(!wasCalled);
}
[Test]
public void ProvidingCorrectPasswordToTheAuthenticationRequestorReturnsTrue()
{
_authenticator.AuthenticationRequestor = () => _correctPassword;
var authenticated = _authenticator.Authenticate(_wrongPassword);
var authenticator = new PasswordAuthenticator(_cryptographyProvider, _cipherText, () => _correctPassword);
var authenticated = authenticator.Authenticate(_wrongPassword);
Assert.That(authenticated);
}
[Test]
public void AuthenticationFailsWhenAuthenticationRequestorGivenEmptyPassword()
{
_authenticator.AuthenticationRequestor = () => new SecureString();
var authenticated = _authenticator.Authenticate(_wrongPassword);
var authenticator = new PasswordAuthenticator(_cryptographyProvider, _cipherText, () => new SecureString());
var authenticated = authenticator.Authenticate(_wrongPassword);
Assert.That(!authenticated);
}
@@ -87,27 +89,34 @@ namespace mRemoteNGTests.Security.Authentication
public void AuthenticatorRespectsMaxAttempts()
{
var authAttempts = 0;
_authenticator.AuthenticationRequestor = () =>
Optional<SecureString> AuthenticationRequestor()
{
authAttempts++;
return _wrongPassword;
};
_authenticator.Authenticate(_wrongPassword);
Assert.That(authAttempts == _authenticator.MaxAttempts);
}
var authenticator = new PasswordAuthenticator(_cryptographyProvider, _cipherText, AuthenticationRequestor);
authenticator.Authenticate(_wrongPassword);
Assert.That(authAttempts == authenticator.MaxAttempts);
}
[Test]
public void AuthenticatorRespectsMaxAttemptsCustomValue()
{
const int customMaxAttempts = 5;
_authenticator.MaxAttempts = customMaxAttempts;
var authAttempts = 0;
_authenticator.AuthenticationRequestor = () =>
Optional<SecureString> AuthenticationRequestor()
{
authAttempts++;
return _wrongPassword;
};
_authenticator.Authenticate(_wrongPassword);
}
var authenticator =
new PasswordAuthenticator(_cryptographyProvider, _cipherText, AuthenticationRequestor)
{
MaxAttempts = customMaxAttempts
};
authenticator.Authenticate(_wrongPassword);
Assert.That(authAttempts == customMaxAttempts);
}
}

View File

@@ -4,7 +4,6 @@ using System.Security;
using System.Threading;
using System.Windows.Forms;
using mRemoteNG.App.Info;
using mRemoteNG.Config.Connections.Multiuser;
using mRemoteNG.Config.DataProviders;
using mRemoteNG.Config.Putty;
using mRemoteNG.Connection;
@@ -20,7 +19,7 @@ using mRemoteNG.UI.TaskDialog;
namespace mRemoteNG.App
{
public static class Runtime
public static class Runtime
{
public static bool IsPortableEdition
{
@@ -93,18 +92,6 @@ namespace mRemoteNG.App
{
ConnectionsService.LastSqlUpdate = DateTime.Now;
}
else
{
if (connectionFileName == ConnectionsService.GetDefaultStartupConnectionFileName())
{
Settings.Default.LoadConsFromCustomLocation = false;
}
else
{
Settings.Default.LoadConsFromCustomLocation = true;
Settings.Default.CustomConsPath = connectionFileName;
}
}
// re-enable sql update checking after updates are loaded
ConnectionsService.RemoteConnectionsSyncronizer?.Enable();

View File

@@ -1,15 +1,14 @@
using System;
using System.IO;
using System.Security;
using mRemoteNG.Config.DataProviders;
using mRemoteNG.Config.Serializers;
using mRemoteNG.Config.Serializers.Xml;
using mRemoteNG.Tools;
using mRemoteNG.Tree;
using System.IO;
using mRemoteNG.Config.Serializers.Xml;
namespace mRemoteNG.Config.Connections
{
public class XmlConnectionsLoader
public class XmlConnectionsLoader
{
private readonly string _connectionFilePath;
@@ -32,7 +31,7 @@ namespace mRemoteNG.Config.Connections
return deserializer.Deserialize(xmlString);
}
private SecureString PromptForPassword()
private Optional<SecureString> PromptForPassword()
{
var password = MiscTools.PasswordDialog("", false);
return password;

View File

@@ -13,6 +13,7 @@ using mRemoteNG.Connection.Protocol.VNC;
using mRemoteNG.Container;
using mRemoteNG.Messages;
using mRemoteNG.Security;
using mRemoteNG.Tools;
using mRemoteNG.Tree;
using mRemoteNG.Tree.Root;
using mRemoteNG.UI.Forms;
@@ -20,7 +21,7 @@ using mRemoteNG.UI.TaskDialog;
namespace mRemoteNG.Config.Serializers.Xml
{
public class XmlConnectionsDeserializer : IDeserializer<string, ConnectionTreeModel>
public class XmlConnectionsDeserializer : IDeserializer<string, ConnectionTreeModel>
{
private XmlDocument _xmlDocument;
private double _confVersion;
@@ -29,9 +30,9 @@ namespace mRemoteNG.Config.Serializers.Xml
private const double MaxSupportedConfVersion = 2.8;
private readonly RootNodeInfo _rootNodeInfo = new RootNodeInfo(RootNodeType.Connection);
public Func<SecureString> AuthenticationRequestor { get; set; }
public Func<Optional<SecureString>> AuthenticationRequestor { get; set; }
public XmlConnectionsDeserializer(Func<SecureString> authenticationRequestor = null)
public XmlConnectionsDeserializer(Func<Optional<SecureString>> authenticationRequestor = null)
{
AuthenticationRequestor = authenticationRequestor;
}
@@ -47,8 +48,6 @@ namespace mRemoteNG.Config.Serializers.Xml
{
LoadXmlConnectionData(xml);
ValidateConnectionFileVersion();
if (!import)
Runtime.ConnectionsService.IsConnectionsFileLoaded = false;
var rootXmlElement = _xmlDocument.DocumentElement;
InitializeRootNode(rootXmlElement);
@@ -62,8 +61,6 @@ namespace mRemoteNG.Config.Serializers.Xml
var protectedString = _xmlDocument.DocumentElement?.Attributes["Protected"].Value;
if (!_decryptor.ConnectionsFileIsAuthentic(protectedString, _rootNodeInfo.PasswordString.ConvertToSecureString()))
{
mRemoteNG.Settings.Default.LoadConsFromCustomLocation = false;
mRemoteNG.Settings.Default.CustomConsPath = "";
return null;
}
}

View File

@@ -4,6 +4,7 @@ using mRemoteNG.Security;
using mRemoteNG.Security.Authentication;
using mRemoteNG.Security.Factories;
using mRemoteNG.Security.SymmetricEncryption;
using mRemoteNG.Tools;
using mRemoteNG.Tree.Root;
namespace mRemoteNG.Config.Serializers
@@ -13,7 +14,7 @@ namespace mRemoteNG.Config.Serializers
private readonly ICryptographyProvider _cryptographyProvider;
private readonly RootNodeInfo _rootNodeInfo;
public Func<SecureString> AuthenticationRequestor { get; set; }
public Func<Optional<SecureString>> AuthenticationRequestor { get; set; }
public int KeyDerivationIterations
{
@@ -91,16 +92,14 @@ namespace mRemoteNG.Config.Serializers
private bool Authenticate(string cipherText, SecureString password)
{
var authenticator = new PasswordAuthenticator(_cryptographyProvider, cipherText)
{
AuthenticationRequestor = AuthenticationRequestor
};
var authenticator = new PasswordAuthenticator(_cryptographyProvider, cipherText, AuthenticationRequestor);
var authenticated = authenticator.Authenticate(password);
if (!authenticated) return authenticated;
if (!authenticated)
return false;
_rootNodeInfo.PasswordString = authenticator.LastAuthenticatedPassword.ConvertToUnsecureString();
return authenticated;
return true;
}
}
}

View File

@@ -13,6 +13,7 @@ using mRemoteNG.Security;
using mRemoteNG.Tools;
using mRemoteNG.Tree;
using mRemoteNG.Tree.Root;
using mRemoteNG.UI;
namespace mRemoteNG.Connection
{
@@ -51,9 +52,8 @@ namespace mRemoteNG.Connection
{
var newConnectionsModel = new ConnectionTreeModel();
newConnectionsModel.AddRootNode(new RootNodeInfo(RootNodeType.Connection));
SaveConnections(newConnectionsModel, false, new SaveFilter(), filename);
SaveConnections(newConnectionsModel, false, new SaveFilter(), filename, true);
LoadConnections(false, false, filename);
UpdateCustomConsPathSetting(filename);
}
catch (Exception ex)
{
@@ -101,16 +101,25 @@ namespace mRemoteNG.Connection
/// <param name="useDatabase"></param>
/// <param name="import"></param>
/// <param name="connectionFileName"></param>
public ConnectionTreeModel LoadConnections(bool useDatabase, bool import, string connectionFileName)
public void LoadConnections(bool useDatabase, bool import, string connectionFileName)
{
var oldConnectionTreeModel = ConnectionTreeModel;
var oldIsUsingDatabaseValue = UsingDatabase;
var newConnectionTreeModel =
(useDatabase
? new SqlConnectionsLoader().Load()
: new XmlConnectionsLoader(connectionFileName).Load())
?? new ConnectionTreeModel();
var newConnectionTreeModel = useDatabase
? new SqlConnectionsLoader().Load()
: new XmlConnectionsLoader(connectionFileName).Load();
if (newConnectionTreeModel == null)
{
//IsConnectionsFileLoaded = false;
DialogFactory.BuildLoadConnectionsFailedDialog(connectionFileName, "Decrypting connection file failed", IsConnectionsFileLoaded);
return;
}
IsConnectionsFileLoaded = true;
ConnectionFileName = connectionFileName;
UsingDatabase = useDatabase;
if (!import)
{
@@ -118,12 +127,9 @@ namespace mRemoteNG.Connection
newConnectionTreeModel.RootNodes.AddRange(_puttySessionsManager.RootPuttySessionsNodes);
}
IsConnectionsFileLoaded = true;
ConnectionFileName = connectionFileName;
UsingDatabase = useDatabase;
ConnectionTreeModel = newConnectionTreeModel;
UpdateCustomConsPathSetting(connectionFileName);
RaiseConnectionsLoadedEvent(oldConnectionTreeModel, newConnectionTreeModel, oldIsUsingDatabaseValue, useDatabase, connectionFileName);
return newConnectionTreeModel;
}
public void BeginBatchingSaves()
@@ -158,12 +164,13 @@ namespace mRemoteNG.Connection
/// <param name="useDatabase"></param>
/// <param name="saveFilter"></param>
/// <param name="connectionFileName"></param>
public void SaveConnections(ConnectionTreeModel connectionTreeModel, bool useDatabase, SaveFilter saveFilter, string connectionFileName)
/// <param name="forceSave">Bypasses safety checks that prevent saving if a connection file isn't loaded.</param>
public void SaveConnections(ConnectionTreeModel connectionTreeModel, bool useDatabase, SaveFilter saveFilter, string connectionFileName, bool forceSave = false)
{
if (connectionTreeModel == null)
return;
if (!IsConnectionsFileLoaded)
if (!forceSave && !IsConnectionsFileLoaded)
return;
if (_batchingSaves)
@@ -223,12 +230,16 @@ namespace mRemoteNG.Connection
public string GetStartupConnectionFileName()
{
return Settings.Default.LoadConsFromCustomLocation == false ? GetDefaultStartupConnectionFileName() : Settings.Default.CustomConsPath;
return Settings.Default.LoadConsFromCustomLocation == false
? GetDefaultStartupConnectionFileName()
: Settings.Default.CustomConsPath;
}
public string GetDefaultStartupConnectionFileName()
{
return Runtime.IsPortableEdition ? GetDefaultStartupConnectionFileNamePortableEdition() : GetDefaultStartupConnectionFileNameNormalEdition();
return Runtime.IsPortableEdition
? GetDefaultStartupConnectionFileNamePortableEdition()
: GetDefaultStartupConnectionFileNameNormalEdition();
}
private void UpdateCustomConsPathSetting(string filename)

View File

@@ -1,5 +1,7 @@
using System;
using System.Linq;
using System.Security;
using mRemoteNG.Tools;
namespace mRemoteNG.Security.Authentication
{
@@ -7,15 +9,16 @@ namespace mRemoteNG.Security.Authentication
{
private readonly ICryptographyProvider _cryptographyProvider;
private readonly string _cipherText;
private readonly Func<Optional<SecureString>> _authenticationRequestor;
public Func<SecureString> AuthenticationRequestor { get; set; }
public int MaxAttempts { get; set; } = 3;
public SecureString LastAuthenticatedPassword { get; private set; }
public PasswordAuthenticator(ICryptographyProvider cryptographyProvider, string cipherText)
public PasswordAuthenticator(ICryptographyProvider cryptographyProvider, string cipherText, Func<Optional<SecureString>> authenticationRequestor)
{
_cryptographyProvider = cryptographyProvider;
_cipherText = cipherText;
_cryptographyProvider = cryptographyProvider.ThrowIfNull(nameof(cryptographyProvider));
_cipherText = cipherText.ThrowIfNullOrEmpty(nameof(cipherText));
_authenticationRequestor = authenticationRequestor.ThrowIfNull(nameof(authenticationRequestor));
}
public bool Authenticate(SecureString password)
@@ -32,7 +35,11 @@ namespace mRemoteNG.Security.Authentication
}
catch
{
password = AuthenticationRequestor?.Invoke();
var providedPassword = _authenticationRequestor();
if (!providedPassword.Any())
return false;
password = providedPassword.First();
if (password == null || password.Length == 0) break;
}
attempts++;

View File

@@ -1,9 +1,10 @@
using System.Security;
using mRemoteNG.Tools;
namespace mRemoteNG.Security
{
public interface IKeyProvider
{
SecureString GetKey();
Optional<SecureString> GetKey();
}
}

View File

@@ -12,7 +12,7 @@ using static System.String;
namespace mRemoteNG.Tools
{
public static class MiscTools
public static class MiscTools
{
public static Icon GetIconFromFile(string FileName)
{
@@ -34,7 +34,7 @@ namespace mRemoteNG.Tools
}
}
public static SecureString PasswordDialog(string passwordName = null, bool verify = true)
public static Optional<SecureString> PasswordDialog(string passwordName = null, bool verify = true)
{
var passwordForm = new PasswordForm(passwordName, verify);
return passwordForm.GetKey();

View File

@@ -1,5 +1,10 @@
using System.Windows.Forms;
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using mRemoteNG.App;
using mRemoteNG.App.Info;
using mRemoteNG.Messages;
using mRemoteNG.UI.TaskDialog;
namespace mRemoteNG.UI
{
@@ -15,5 +20,74 @@ namespace mRemoteNG.UI
Filter = Language.strFiltermRemoteXML + @"|*.xml|" + Language.strFilterAll + @"|*.*"
};
}
public static void BuildLoadConnectionsFailedDialog(string connectionFileName, string messageText, bool showCancelButton)
{
var commandButtons = new List<string>
{
Language.ConfigurationCreateNew,
Language.strOpenADifferentFile,
Language.strMenuExit
};
if (showCancelButton)
commandButtons.Add(Language.strButtonCancel);
var answered = false;
while (!answered)
{
try
{
CTaskDialog.ShowTaskDialogBox(
GeneralAppInfo.ProductName,
messageText,
"", "", "", "", "",
string.Join(" | ", commandButtons),
ETaskDialogButtons.None,
ESysIcons.Question,
ESysIcons.Question);
switch (CTaskDialog.CommandButtonResult)
{
case 0: // New
var saveAsDialog = ConnectionsSaveAsDialog();
saveAsDialog.ShowDialog();
Runtime.ConnectionsService.NewConnectionsFile(saveAsDialog.FileName);
answered = true;
break;
case 1: // Load
Runtime.LoadConnections(true);
answered = true;
break;
case 2: // Exit
Application.Exit();
answered = true;
break;
case 3: // Cancel
answered = true;
break;
}
}
catch (Exception exception)
{
Runtime.MessageCollector.AddExceptionMessage(
string.Format(Language.strConnectionsFileCouldNotBeLoadedNew, connectionFileName),
exception,
MessageClass.WarningMsg);
}
}
}
public static SaveFileDialog ConnectionsSaveAsDialog()
{
return new SaveFileDialog
{
CheckPathExists = true,
InitialDirectory = ConnectionsFileInfo.DefaultConnectionsPath,
FileName = ConnectionsFileInfo.DefaultConnectionsFile,
OverwritePrompt = true,
Filter = Language.strFiltermRemoteXML + @"|*.xml|" + Language.strFilterAll + @"|*.*"
};
}
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Security;
using System.Windows.Forms;
using mRemoteNG.Security;
using mRemoteNG.Tools;
namespace mRemoteNG.UI.Forms
{
@@ -19,12 +20,12 @@ namespace mRemoteNG.UI.Forms
Verify = verify;
}
public SecureString GetKey()
public Optional<SecureString> GetKey()
{
var dialog = ShowDialog();
return dialog == DialogResult.OK
? _password
: new SecureString();
: Optional<SecureString>.Empty;
}
#region Event Handlers

View File

@@ -24,7 +24,7 @@ using WeifenLuo.WinFormsUI.Docking;
namespace mRemoteNG.UI.Window
{
public class ConfigWindow : BaseWindow
public class ConfigWindow : BaseWindow
{
private bool _originalPropertyGridToolStripItemCountValid;
private int _originalPropertyGridToolStripItemCount;
@@ -745,29 +745,32 @@ namespace mRemoteNG.UI.Window
private void UpdateRootInfoNode(PropertyValueChangedEventArgs e)
{
var rootInfo = _pGrid.SelectedObject as RootNodeInfo;
if (rootInfo == null) return;
if (e.ChangedItem.PropertyDescriptor == null) return;
// ReSharper disable once SwitchStatementMissingSomeCases
switch (e.ChangedItem.PropertyDescriptor.Name)
{
case "Password":
if (rootInfo.Password)
{
var passwordName = Settings.Default.UseSQLServer ? Language.strSQLServer.TrimEnd(':') : Path.GetFileName(Runtime.ConnectionsService.GetStartupConnectionFileName());
if (rootInfo == null)
return;
var password = MiscTools.PasswordDialog(passwordName);
if (password.Length == 0)
rootInfo.Password = false;
else
rootInfo.PasswordString = password.ConvertToUnsecureString();
}
else
{
rootInfo.PasswordString = "";
}
break;
case "Name":
break;
if (e.ChangedItem.PropertyDescriptor?.Name != "Password")
return;
if (rootInfo.Password)
{
var passwordName = Settings.Default.UseSQLServer
? Language.strSQLServer.TrimEnd(':')
: Path.GetFileName(Runtime.ConnectionsService.GetStartupConnectionFileName());
var password = MiscTools.PasswordDialog(passwordName);
// operation cancelled, dont set a password
if (!password.Any() || password.First().Length == 0)
{
rootInfo.Password = false;
return;
}
rootInfo.PasswordString = password.First().ConvertToUnsecureString();
}
else
{
rootInfo.PasswordString = "";
}
}