CA-343280: Reworked saving user settings.

Signed-off-by: Konstantina Chremmou <konstantina.chremmou@citrix.com>
This commit is contained in:
Konstantina Chremmou 2020-10-27 02:10:44 +00:00
parent 84d0a2b8ae
commit f73ea42cb2
10 changed files with 336 additions and 205 deletions

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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)
{

View File

@ -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);
}

View File

@ -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)
{

View File

@ -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;

View File

@ -175,7 +175,7 @@ namespace XenAdmin
public void SaveSettingsIfRequired()
{
Settings.SaveIfRequired();
Settings.SaveServerList();
}
private AsyncAction.SudoElevationResult GetElevatedSession(List<Role> allowedRoles,

View 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)}";
}
}
}
}
}

View File

@ -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" />

View File

@ -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;
}
}
}