mirror of
https://github.com/xcp-ng/xenadmin.git
synced 2025-01-20 15:29:26 +01:00
CA-343280: Reworked saving user settings.
Signed-off-by: Konstantina Chremmou <konstantina.chremmou@citrix.com>
This commit is contained in:
parent
84d0a2b8ae
commit
f73ea42cb2
@ -31,6 +31,7 @@
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using XenAdmin.Core;
|
||||
using XenCenterLib;
|
||||
|
||||
|
||||
@ -38,12 +39,12 @@ namespace XenAdmin.Dialogs.RestoreSession
|
||||
{
|
||||
public partial class ChangeMasterPasswordDialog : XenDialogBase
|
||||
{
|
||||
private readonly byte[] OldProposedPassword;
|
||||
private readonly byte[] _currentPasswordHash;
|
||||
|
||||
public ChangeMasterPasswordDialog(byte[] proposedPassword)
|
||||
public ChangeMasterPasswordDialog(byte[] currentPasswordHash)
|
||||
{
|
||||
InitializeComponent();
|
||||
OldProposedPassword = proposedPassword;
|
||||
_currentPasswordHash = currentPasswordHash;
|
||||
currentPasswordError.Visible = false;
|
||||
newPasswordError.Visible = false;
|
||||
}
|
||||
@ -67,9 +68,10 @@ namespace XenAdmin.Dialogs.RestoreSession
|
||||
|
||||
private void okButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
var oldPasswordCorrect = Settings.PassCorrect(currentTextBox.Text, OldProposedPassword);
|
||||
var currentPasswordCorrect = !string.IsNullOrEmpty(currentTextBox.Text) &&
|
||||
Helpers.ArrayElementsEqual(EncryptionUtils.ComputeHash(currentTextBox.Text), _currentPasswordHash);
|
||||
|
||||
if (oldPasswordCorrect && !string.IsNullOrEmpty(masterTextBox.Text) &&
|
||||
if (currentPasswordCorrect && !string.IsNullOrEmpty(masterTextBox.Text) &&
|
||||
masterTextBox.Text == reEnterMasterTextBox.Text)
|
||||
{
|
||||
NewPassword = EncryptionUtils.ComputeHash(masterTextBox.Text);
|
||||
@ -77,7 +79,7 @@ namespace XenAdmin.Dialogs.RestoreSession
|
||||
return;
|
||||
}
|
||||
|
||||
if (!oldPasswordCorrect)
|
||||
if (!currentPasswordCorrect)
|
||||
currentPasswordError.ShowError(Messages.PASSWORD_INCORRECT);
|
||||
else if (masterTextBox.Text != reEnterMasterTextBox.Text)
|
||||
newPasswordError.ShowError(Messages.PASSWORDS_DONT_MATCH);
|
||||
|
@ -31,24 +31,27 @@
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using XenAdmin.Core;
|
||||
using XenCenterLib;
|
||||
|
||||
|
||||
namespace XenAdmin.Dialogs.RestoreSession
|
||||
{
|
||||
public partial class EnterMasterPasswordDialog : XenDialogBase
|
||||
{
|
||||
private byte[] TemporaryMasterPassword;
|
||||
private readonly byte[] _temporaryMasterPassword;
|
||||
|
||||
public EnterMasterPasswordDialog(byte[] temporaryMasterPassword)
|
||||
{
|
||||
InitializeComponent();
|
||||
TemporaryMasterPassword = temporaryMasterPassword;
|
||||
_temporaryMasterPassword = temporaryMasterPassword;
|
||||
passwordError.Visible = false;
|
||||
}
|
||||
|
||||
private void okButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (Settings.PassCorrect(masterTextBox.Text,TemporaryMasterPassword))
|
||||
if (!string.IsNullOrEmpty(masterTextBox.Text) &&
|
||||
Helpers.ArrayElementsEqual(EncryptionUtils.ComputeHash(masterTextBox.Text), _temporaryMasterPassword))
|
||||
{
|
||||
DialogResult = DialogResult.OK;
|
||||
}
|
||||
@ -63,6 +66,7 @@ namespace XenAdmin.Dialogs.RestoreSession
|
||||
private void masterTextBox_TextChanged(object sender, EventArgs e)
|
||||
{
|
||||
passwordError.Visible = false;
|
||||
okButton.Enabled = !string.IsNullOrEmpty(masterTextBox.Text);
|
||||
}
|
||||
}
|
||||
}
|
@ -32,15 +32,12 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using XenCenterLib;
|
||||
|
||||
|
||||
namespace XenAdmin.Dialogs.RestoreSession
|
||||
{
|
||||
public partial class LoadSessionDialog : XenDialogBase
|
||||
{
|
||||
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
|
||||
|
||||
public LoadSessionDialog(bool isRetry = false)
|
||||
{
|
||||
InitializeComponent();
|
||||
@ -48,25 +45,8 @@ namespace XenAdmin.Dialogs.RestoreSession
|
||||
}
|
||||
|
||||
internal override string HelpName => "LoadSessionDialog";
|
||||
public byte[] PasswordHash
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!string.IsNullOrEmpty(passBox.Text))
|
||||
{
|
||||
try
|
||||
{
|
||||
return EncryptionUtils.ComputeHash(passBox.Text);
|
||||
}
|
||||
catch (Exception exp)
|
||||
{
|
||||
log.Error("Could not hash entered password.", exp);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public string Password => passBox.Text;
|
||||
|
||||
private void passBox_TextChanged(object sender, EventArgs e)
|
||||
{
|
||||
|
@ -2229,20 +2229,7 @@ namespace XenAdmin
|
||||
|
||||
Properties.Settings.Default.WindowSize = this.Size;
|
||||
Properties.Settings.Default.WindowLocation = this.Location;
|
||||
try
|
||||
{
|
||||
Settings.SaveServerList();
|
||||
Properties.Settings.Default.Save();
|
||||
}
|
||||
catch (ConfigurationErrorsException ex)
|
||||
{
|
||||
using (var dlg = new ErrorDialog(string.Format(Messages.MESSAGEBOX_SAVE_CORRUPTED, Settings.GetUserConfigPath()))
|
||||
{WindowTitle = Messages.MESSAGEBOX_SAVE_CORRUPTED_TITLE})
|
||||
{
|
||||
dlg.ShowDialog(this);
|
||||
}
|
||||
log.Error("Could not save settings.", ex);
|
||||
}
|
||||
Settings.SaveServerList(); //this calls Settings.TrySaveSettings()
|
||||
base.OnClosing(e);
|
||||
}
|
||||
|
||||
|
@ -484,7 +484,7 @@ namespace XenAdmin
|
||||
try
|
||||
{
|
||||
// Lets save the connections first, so we can save their connected state
|
||||
Settings.SaveIfRequired();
|
||||
Settings.SaveServerList(); //this calls Settings.TrySaveSettings()
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
@ -98,6 +98,7 @@ namespace XenAdmin
|
||||
Properties.Settings.Default.SaveSession = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Registry.AllowCredentialSave)
|
||||
{
|
||||
Program.SkipSessionSave = true;
|
||||
@ -106,81 +107,63 @@ namespace XenAdmin
|
||||
RestoreSessionWithPassword(null, false);
|
||||
return;
|
||||
}
|
||||
// Only try if the user has specified he actually wants to save sessions...
|
||||
if (Properties.Settings.Default.SaveSession == true || Properties.Settings.Default.RequirePass)
|
||||
|
||||
Program.MasterPassword = null;
|
||||
|
||||
if (Properties.Settings.Default.SaveSession || Properties.Settings.Default.RequirePass)
|
||||
{
|
||||
// Only try if we actually have a saved session list...
|
||||
if ((Properties.Settings.Default.ServerList != null && Properties.Settings.Default.ServerList.Length > 0) || (Properties.Settings.Default.ServerAddressList != null && Properties.Settings.Default.ServerAddressList.Length > 0))
|
||||
if (Properties.Settings.Default.ServerList != null && Properties.Settings.Default.ServerList.Length > 0 ||
|
||||
Properties.Settings.Default.ServerAddressList != null && Properties.Settings.Default.ServerAddressList.Length > 0)
|
||||
{
|
||||
if (!Properties.Settings.Default.RequirePass)
|
||||
{
|
||||
Program.MasterPassword = null;
|
||||
Program.SkipSessionSave = true;
|
||||
RestoreSessionWithPassword(null, true);
|
||||
return;
|
||||
}
|
||||
byte[] passHash = PromptForMasterPassword(false);
|
||||
|
||||
// passHash will be null if the user cancelled...
|
||||
if (passHash != null)
|
||||
// close the splash screen before opening the password dialog (the main window closes the
|
||||
// splash screen after this method is called, however, this cannot happen because the dialog
|
||||
// is launched modally blocking the UI thread and is additionally behind the splash screen)
|
||||
Program.CloseSplash();
|
||||
|
||||
string password = null;
|
||||
do
|
||||
{
|
||||
if (!RestoreSessionWithPassword(passHash, true))
|
||||
{
|
||||
// User got the password wrong. Repeat until he gets it
|
||||
// right or cancels...
|
||||
while (passHash != null)
|
||||
{
|
||||
passHash = PromptForMasterPassword(true);
|
||||
if (passHash != null)
|
||||
{
|
||||
if (RestoreSessionWithPassword(passHash, true))
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// if the user has cancelled we start a new session
|
||||
if (passHash == null)
|
||||
{
|
||||
// an error state which can only occur on cancelled clicked
|
||||
Properties.Settings.Default.SaveSession = false;
|
||||
Properties.Settings.Default.RequirePass = true;
|
||||
RestoreSessionWithPassword(null, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// otherwise make sure we have the correct settings
|
||||
Properties.Settings.Default.SaveSession = true;
|
||||
Properties.Settings.Default.RequirePass = true;
|
||||
}
|
||||
using (var dialog = new LoadSessionDialog(password != null))
|
||||
password = dialog.ShowDialog(Program.MainWindow) == DialogResult.OK
|
||||
? dialog.Password
|
||||
: null;
|
||||
} while (password != null && !RestoreSessionWithPassword(password, true));
|
||||
|
||||
Properties.Settings.Default.SaveSession = password != null;
|
||||
Properties.Settings.Default.RequirePass = true;
|
||||
Program.SkipSessionSave = true;
|
||||
Program.MasterPassword = passHash;
|
||||
|
||||
if (password == null)
|
||||
RestoreSessionWithPassword(null, false); //if the user has cancelled start a new session
|
||||
else
|
||||
Program.MasterPassword = EncryptionUtils.ComputeHash(password);
|
||||
}
|
||||
else
|
||||
{
|
||||
// this is where the user comes in if it is the first time connecting
|
||||
Properties.Settings.Default.RequirePass = false;
|
||||
Properties.Settings.Default.SaveSession = false;
|
||||
Program.MasterPassword = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Program.MasterPassword = null;
|
||||
Program.SkipSessionSave = true;
|
||||
RestoreSessionWithPassword(null, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to restore the session list using the given password hash as
|
||||
/// the key. Returns true if successful, false otherwise (usu. due to
|
||||
/// Tries to restore the session list using the given password as the key.
|
||||
/// Returns true if successful, false otherwise (usually due to
|
||||
/// a decryption failure, in turn due to a wrong password).
|
||||
/// </summary>
|
||||
/// <param name="passHash"></param>
|
||||
/// <param name="useOriginalList"></param>
|
||||
/// <returns></returns>
|
||||
private static bool RestoreSessionWithPassword(byte[] passHash, bool useOriginalList)
|
||||
private static bool RestoreSessionWithPassword(string password, bool useOriginalList)
|
||||
{
|
||||
string[] encServerList;
|
||||
|
||||
@ -220,7 +203,7 @@ namespace XenAdmin
|
||||
{
|
||||
foreach (string encEntry in encServerList)
|
||||
{
|
||||
decryptedList[idx] = EncryptionUtils.DecryptString(encEntry, passHash);
|
||||
decryptedList[idx] = EncryptionUtils.DecryptString(encEntry, password);
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
@ -337,24 +320,6 @@ namespace XenAdmin
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prompts for the password to use to decrypt the server list.
|
||||
/// Returns the secure hash of the password entered.
|
||||
/// </summary>
|
||||
/// <param name="isRetry"></param>
|
||||
/// <returns></returns>
|
||||
private static byte[] PromptForMasterPassword(bool isRetry)
|
||||
{
|
||||
// close the splash screen before opening the password dialog (the dialog comes up behind the splash screen)
|
||||
Program.CloseSplash();
|
||||
|
||||
using (var dialog = new LoadSessionDialog(isRetry))
|
||||
if (dialog.ShowDialog(Program.MainWindow) == DialogResult.OK)
|
||||
return dialog.PasswordHash;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If an exception is thrown while saving the list, this method will notify the user
|
||||
/// and then call Application.Exit().
|
||||
@ -434,7 +399,7 @@ namespace XenAdmin
|
||||
}
|
||||
|
||||
// Save the address, port and friendly name in case the user clicks cancel on the resume password dialog
|
||||
string entryAddress = protectCredentials(connection.Hostname, port, connection.FriendlyName);
|
||||
string entryAddress = ProtectCredentials(connection.Hostname, port, connection.FriendlyName);
|
||||
encServerAddressList.Add(entryAddress);
|
||||
}
|
||||
|
||||
@ -467,7 +432,7 @@ namespace XenAdmin
|
||||
}
|
||||
}
|
||||
|
||||
private static string protectCredentials(String serverName, int port, string friendlyName)
|
||||
private static string ProtectCredentials(String serverName, int port, string friendlyName)
|
||||
{
|
||||
string entryStr = string.Join(SEPARATOR.ToString(), new string[] { serverName, port.ToString(), friendlyName });
|
||||
return EncryptionUtils.Protect(entryStr);
|
||||
@ -554,30 +519,9 @@ namespace XenAdmin
|
||||
public static void SetVNCPassword(string vm_uuid, char[] password)
|
||||
{
|
||||
VNCPasswords[vm_uuid] = password == null ? null : new string(password);
|
||||
EncryptServerList();
|
||||
SaveServerList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the hash of 'p' with the given byte array.
|
||||
/// </summary>
|
||||
/// <param name="p"></param>
|
||||
/// <param name="temporaryPassword"></param>
|
||||
/// <returns></returns>
|
||||
internal static bool PassCorrect(string p, byte[] temporaryPassword)
|
||||
{
|
||||
if (p.Length == 0)
|
||||
return false;
|
||||
|
||||
return Helpers.ArrayElementsEqual(EncryptionUtils.ComputeHash(p), temporaryPassword);
|
||||
}
|
||||
|
||||
public static void SaveIfRequired()
|
||||
{
|
||||
SaveServerList();
|
||||
TrySaveSettings();
|
||||
}
|
||||
|
||||
public static void AddCertificate(string hashString, string hostname)
|
||||
{
|
||||
Dictionary<string, string> known_servers = KnownServers;
|
||||
|
@ -175,7 +175,7 @@ namespace XenAdmin
|
||||
|
||||
public void SaveSettingsIfRequired()
|
||||
{
|
||||
Settings.SaveIfRequired();
|
||||
Settings.SaveServerList();
|
||||
}
|
||||
|
||||
private AsyncAction.SudoElevationResult GetElevatedSession(List<Role> allowedRoles,
|
||||
|
166
XenAdminTests/UnitTests/EncryptionUtilTests.cs
Normal file
166
XenAdminTests/UnitTests/EncryptionUtilTests.cs
Normal file
@ -0,0 +1,166 @@
|
||||
/* Copyright (c) Citrix Systems, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms,
|
||||
* with or without modification, are permitted provided
|
||||
* that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above
|
||||
* copyright notice, this list of conditions and the
|
||||
* following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above
|
||||
* copyright notice, this list of conditions and the
|
||||
* following disclaimer in the documentation and/or other
|
||||
* materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Security.Cryptography;
|
||||
using NUnit.Framework;
|
||||
using XenCenterLib;
|
||||
|
||||
|
||||
namespace XenAdminTests.UnitTests
|
||||
{
|
||||
[TestFixture, Category(TestCategories.Unit)]
|
||||
internal class EncryptionUtilTests
|
||||
{
|
||||
private string[] _transformedStrings =
|
||||
{
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
"abcçdefgğhıijklmnoöprsştuüvyz",
|
||||
"短𠀁𪛕",
|
||||
"ÀàÂâÆæ,Çç,ÉéÈèÊêËë,ÎîÏï,ÔôŒœ,ÙùÛûÜü,Ÿÿ"
|
||||
};
|
||||
|
||||
private string[] _keys =
|
||||
{
|
||||
"kd385Am£dm*C",
|
||||
"j33LZ$89cngA39%",
|
||||
"Pzm#d&cJ309",
|
||||
"短𠀁𪛕",
|
||||
"çdefgğhıijklmno",
|
||||
"ÀâæÇÉèêëîïÔŒÙÛüŸ"
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void TestProtectionCurrentUser()
|
||||
{
|
||||
foreach (var str in _transformedStrings)
|
||||
{
|
||||
var protectedStr = EncryptionUtils.Protect(str);
|
||||
var unprotectedStr = EncryptionUtils.Unprotect(protectedStr);
|
||||
Assert.AreEqual(str, unprotectedStr,
|
||||
"String corruption after protection, then un-protection (current user)");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestProtectionLocalMachine()
|
||||
{
|
||||
foreach (var str in _transformedStrings)
|
||||
{
|
||||
var protectedStr = EncryptionUtils.ProtectForLocalMachine(str);
|
||||
var unprotectedStr = EncryptionUtils.UnprotectForLocalMachine(protectedStr);
|
||||
Assert.AreEqual(str, unprotectedStr,
|
||||
"String corruption after protection, then un-protection (local machine)");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestProtectionCurrentUserLegacy()
|
||||
{
|
||||
foreach (var str in _transformedStrings)
|
||||
{
|
||||
var legacyProtectedStr = LegacyProtection(str, DataProtectionScope.CurrentUser);
|
||||
var unprotectedStr = EncryptionUtils.UnprotectForLocalMachine(legacyProtectedStr);
|
||||
Assert.AreEqual(str, unprotectedStr,
|
||||
"String corruption after legacy protection, then un-protection (current user)");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestProtectionLocalMachineLegacy()
|
||||
{
|
||||
foreach (var str in _transformedStrings)
|
||||
{
|
||||
var legacyProtectedStr = LegacyProtection(str, DataProtectionScope.LocalMachine);
|
||||
var unprotectedStr = EncryptionUtils.UnprotectForLocalMachine(legacyProtectedStr);
|
||||
Assert.AreEqual(str, unprotectedStr,
|
||||
"String corruption after legacy protection, then un-protection (local machine)");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEncryption()
|
||||
{
|
||||
foreach (var str in _transformedStrings)
|
||||
foreach (var key in _keys)
|
||||
{
|
||||
var encryptedStr = EncryptionUtils.EncryptString(str, EncryptionUtils.ComputeHash(key));
|
||||
var decryptedStr = EncryptionUtils.DecryptString(encryptedStr, key);
|
||||
Assert.AreEqual(str, decryptedStr,
|
||||
"String corruption after encryption, then decryption");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEncryptionLegacy()
|
||||
{
|
||||
foreach (var str in _transformedStrings)
|
||||
foreach (var key in _keys)
|
||||
{
|
||||
var legacyEncryptedStr = LegacyEncryption(str, EncryptionUtils.ComputeHash(key));
|
||||
var decryptedStr = EncryptionUtils.DecryptString(legacyEncryptedStr, key);
|
||||
Assert.AreEqual(str, decryptedStr,
|
||||
"String corruption after legacy encryption, then decryption");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static string LegacyProtection(string str, DataProtectionScope scope)
|
||||
{
|
||||
byte[] saltBytes = new UnicodeEncoding().GetBytes("XenRocks");
|
||||
byte[] dataBytes = Encoding.Unicode.GetBytes(str);
|
||||
byte[] protectedBytes = ProtectedData.Protect(dataBytes, saltBytes, scope);
|
||||
return $"{Convert.ToBase64String(protectedBytes)},{Convert.ToBase64String(saltBytes)}";
|
||||
}
|
||||
|
||||
private static string LegacyEncryption(string clearString, byte[] keyBytes)
|
||||
{
|
||||
byte[] saltBytes = new UnicodeEncoding().GetBytes("XenRocks");
|
||||
|
||||
using (var alg = new AesManaged())
|
||||
{
|
||||
alg.Key = keyBytes;
|
||||
alg.IV = saltBytes;
|
||||
|
||||
using (var ms = new MemoryStream())
|
||||
using (var cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write))
|
||||
{
|
||||
byte[] clearBytes = Encoding.Unicode.GetBytes(clearString);
|
||||
cs.Write(clearBytes, 0, clearBytes.Length);
|
||||
cs.FlushFinalBlock(); //important for getting the padding right
|
||||
return $"{Convert.ToBase64String(ms.ToArray())},{Convert.ToBase64String(saltBytes)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -55,6 +55,7 @@
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Security" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
@ -116,6 +117,7 @@
|
||||
<Compile Include="UnitTests\TokenizerTest.cs" />
|
||||
<Compile Include="UnitTests\UpdateManagerTests.cs" />
|
||||
<Compile Include="UnitTests\RegexTests.cs" />
|
||||
<Compile Include="UnitTests\EncryptionUtilTests.cs" />
|
||||
<Compile Include="UnitTests\UtilTests.cs" />
|
||||
<Compile Include="UnitTests\WlbTests\WlbPoolConfigurationTests.cs" />
|
||||
<Compile Include="UnitTests\WlbTests\WlbReportSubscriptionTests.cs" />
|
||||
|
@ -36,87 +36,112 @@ using System.Text;
|
||||
|
||||
namespace XenCenterLib
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to centralise the encryption routines used for master password
|
||||
/// and session lists
|
||||
/// </summary>
|
||||
public class EncryptionUtils
|
||||
public static class EncryptionUtils
|
||||
{
|
||||
private static byte[] salt;
|
||||
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
|
||||
|
||||
private const int SALT_LENGTH = 16;
|
||||
|
||||
public enum HashMethod
|
||||
{
|
||||
Md5,
|
||||
Sha256
|
||||
}
|
||||
|
||||
private static string StringOf(this HashMethod method)
|
||||
{
|
||||
switch (method)
|
||||
{
|
||||
case HashMethod.Md5:
|
||||
return "MD5";
|
||||
case HashMethod.Sha256:
|
||||
return "SHA256";
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(method), method, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a secure hash of the given input string (usually the
|
||||
/// master password). We currently use SHA-1.
|
||||
/// Returns a secure hash of the given input string.
|
||||
/// </summary>
|
||||
/// <param name="password">The string to hash</param>
|
||||
/// <param name="input">The string to hash</param>
|
||||
/// <param name="method">The hash algorithm implementation to use.</param>
|
||||
/// <returns>The secure hash</returns>
|
||||
public static byte[] ComputeHash(String password)
|
||||
public static byte[] ComputeHash(string input, HashMethod method = HashMethod.Sha256)
|
||||
{
|
||||
//SHA1 hasher = SHA1.Create();
|
||||
MD5 hasher = MD5.Create();
|
||||
UnicodeEncoding ue = new UnicodeEncoding();
|
||||
byte[] bytes = ue.GetBytes(password);
|
||||
|
||||
byte[] hash = hasher.ComputeHash(bytes);
|
||||
if (input == null)
|
||||
return null;
|
||||
|
||||
return hash;
|
||||
UnicodeEncoding ue = new UnicodeEncoding();
|
||||
byte[] bytes = ue.GetBytes(input);
|
||||
|
||||
using (var hasher = HashAlgorithm.Create(method.StringOf()))
|
||||
return hasher?.ComputeHash(bytes);
|
||||
}
|
||||
|
||||
public static string Protect(string data)
|
||||
{
|
||||
byte[] dataBytes = Encoding.Unicode.GetBytes(data);
|
||||
byte[] protectedBytes = ProtectedData.Protect(dataBytes, GetSalt(), DataProtectionScope.CurrentUser);
|
||||
return Convert.ToBase64String(protectedBytes);
|
||||
return ProtectForScope(data, DataProtectionScope.CurrentUser);
|
||||
}
|
||||
|
||||
public static string Unprotect(string protectedstring)
|
||||
public static string Unprotect(string protectedString)
|
||||
{
|
||||
byte[] protectedBytes = Convert.FromBase64String(protectedstring);
|
||||
byte[] dataBytes = ProtectedData.Unprotect(protectedBytes, GetSalt(), DataProtectionScope.CurrentUser);
|
||||
return Encoding.Unicode.GetString(dataBytes);
|
||||
return UnprotectForScope(protectedString, DataProtectionScope.CurrentUser);
|
||||
}
|
||||
|
||||
public static string ProtectForLocalMachine(string data)
|
||||
{
|
||||
byte[] dataBytes = Encoding.Unicode.GetBytes(data);
|
||||
byte[] protectedBytes = ProtectedData.Protect(dataBytes, GetSalt(), DataProtectionScope.LocalMachine);
|
||||
return Convert.ToBase64String(protectedBytes);
|
||||
return ProtectForScope(data, DataProtectionScope.LocalMachine);
|
||||
}
|
||||
|
||||
public static string UnprotectForLocalMachine(string protectedstring)
|
||||
public static string UnprotectForLocalMachine(string protectedString)
|
||||
{
|
||||
byte[] protectedBytes = Convert.FromBase64String(protectedstring);
|
||||
byte[] dataBytes = ProtectedData.Unprotect(protectedBytes, GetSalt(), DataProtectionScope.LocalMachine);
|
||||
return UnprotectForScope(protectedString, DataProtectionScope.LocalMachine);
|
||||
}
|
||||
|
||||
private static string ProtectForScope(string data, DataProtectionScope scope)
|
||||
{
|
||||
byte[] saltBytes = GetSalt();
|
||||
byte[] dataBytes = Encoding.Unicode.GetBytes(data);
|
||||
byte[] protectedBytes = ProtectedData.Protect(dataBytes, saltBytes, scope);
|
||||
return $"{Convert.ToBase64String(protectedBytes)},{Convert.ToBase64String(saltBytes)}";
|
||||
}
|
||||
|
||||
private static string UnprotectForScope(string protectedString, DataProtectionScope scope)
|
||||
{
|
||||
var parts = protectedString.Split(new[] {','}, 2);
|
||||
byte[] saltBytes = parts.Length == 2
|
||||
? Convert.FromBase64String(parts[1])
|
||||
: new UnicodeEncoding().GetBytes("XenRocks"); //backwards compatibility
|
||||
byte[] protectedBytes = Convert.FromBase64String(parts[0]);
|
||||
byte[] dataBytes = ProtectedData.Unprotect(protectedBytes, saltBytes, scope);
|
||||
return Encoding.Unicode.GetString(dataBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encrypt a given string using the given key. The cipherText is Base64
|
||||
/// encoded and returned. The algorithjm currently used is "Rijndael"
|
||||
/// Encrypt a given string using the given key.
|
||||
/// </summary>
|
||||
/// <param name="clearString">The string to encrypt.</param>
|
||||
/// <param name="keyBytes">The key for the encryption algorithm</param>
|
||||
/// <returns>The Base64 encoded cipher text</returns>
|
||||
public static String EncryptString(String clearString, byte[] keyBytes)
|
||||
public static string EncryptString(string clearString, byte[] keyBytes)
|
||||
{
|
||||
MemoryStream ms = new MemoryStream();
|
||||
byte[] saltBytes = GetSalt();
|
||||
|
||||
//DES alg = DES.Create();
|
||||
//RC2 alg = RC2.Create();
|
||||
Rijndael alg = Rijndael.Create();
|
||||
using (var alg = new AesManaged())
|
||||
{
|
||||
alg.Key = keyBytes;
|
||||
alg.IV = saltBytes;
|
||||
|
||||
alg.Key = keyBytes;
|
||||
alg.IV = GetSalt();
|
||||
|
||||
byte[] clearBytes = Encoding.Unicode.GetBytes(clearString);
|
||||
|
||||
CryptoStream cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write);
|
||||
cs.Write(clearBytes, 0, clearBytes.Length);
|
||||
cs.Close();
|
||||
|
||||
byte[] cipherText = ms.ToArray();
|
||||
|
||||
return Convert.ToBase64String(cipherText);
|
||||
using (var ms = new MemoryStream())
|
||||
using (var cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write))
|
||||
{
|
||||
byte[] clearBytes = Encoding.Unicode.GetBytes(clearString);
|
||||
cs.Write(clearBytes, 0, clearBytes.Length);
|
||||
cs.FlushFinalBlock(); //important for getting the padding right
|
||||
return $"{Convert.ToBase64String(ms.ToArray())},{Convert.ToBase64String(saltBytes)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -124,38 +149,59 @@ namespace XenCenterLib
|
||||
/// </summary>
|
||||
/// <param name="cipherText64">The base64 encoded cipher text that was produced
|
||||
/// by encryptString</param>
|
||||
/// <param name="keyBytes">The key to use to decrypt</param>
|
||||
/// <param name="key">The key for the decryption algorithm</param>
|
||||
/// <returns>The decrypted text.</returns>
|
||||
public static String DecryptString(String cipherText64, byte[] keyBytes)
|
||||
public static string DecryptString(string cipherText64, string key)
|
||||
{
|
||||
MemoryStream ms = new MemoryStream();
|
||||
var parts = cipherText64.Split(new[] {','}, 2);
|
||||
byte[] saltBytes = parts.Length == 2
|
||||
? Convert.FromBase64String(parts[1])
|
||||
: new UnicodeEncoding().GetBytes("XenRocks"); //backwards compatibility
|
||||
byte[] cipherBytes = Convert.FromBase64String(parts[0]);
|
||||
|
||||
byte[] cipherBytes = Convert.FromBase64String(cipherText64);
|
||||
Rijndael alg = Rijndael.Create();
|
||||
alg.Key = keyBytes;
|
||||
alg.IV = GetSalt();
|
||||
try
|
||||
{
|
||||
using (var alg = new AesManaged())
|
||||
{
|
||||
alg.IV = saltBytes;
|
||||
alg.Key = ComputeHash(key);
|
||||
return DecryptString(cipherBytes, alg);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.Warn("Failed to decrypt. Trying legacy mode.", e);
|
||||
|
||||
CryptoStream cs = new CryptoStream(ms, alg.CreateDecryptor(), CryptoStreamMode.Write);
|
||||
cs.Write(cipherBytes, 0, cipherBytes.Length);
|
||||
cs.Close();
|
||||
|
||||
byte[] plainBytes = ms.ToArray();
|
||||
|
||||
return Encoding.Unicode.GetString(plainBytes);
|
||||
using (var alg = Rijndael.Create())
|
||||
{
|
||||
alg.IV = saltBytes;
|
||||
alg.Key = ComputeHash(key, HashMethod.Md5);
|
||||
return DecryptString(cipherBytes, alg); //backwards compatibility
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string DecryptString(byte[] cipherBytes, SymmetricAlgorithm alg)
|
||||
{
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
using (var cs = new CryptoStream(ms, alg.CreateDecryptor(), CryptoStreamMode.Write))
|
||||
{
|
||||
cs.Write(cipherBytes, 0, cipherBytes.Length);
|
||||
cs.FlushFinalBlock(); //important for getting the padding right
|
||||
return Encoding.Unicode.GetString(ms.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] GetSalt()
|
||||
{
|
||||
// NOTE: This is what we did in Geneva - may want
|
||||
// to do something less lame in future...
|
||||
if (salt == null)
|
||||
using (var rngCsProvider = new RNGCryptoServiceProvider())
|
||||
{
|
||||
UnicodeEncoding ue = new UnicodeEncoding();
|
||||
salt = ue.GetBytes("XenRocks");
|
||||
var saltBytes = new byte[SALT_LENGTH];
|
||||
rngCsProvider.GetBytes(saltBytes);
|
||||
return saltBytes;
|
||||
}
|
||||
|
||||
return salt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user