diff --git a/XenAdmin/Dialogs/RestoreSession/ChangeMasterPasswordDialog.cs b/XenAdmin/Dialogs/RestoreSession/ChangeMasterPasswordDialog.cs index fb989e3dd..544c64843 100644 --- a/XenAdmin/Dialogs/RestoreSession/ChangeMasterPasswordDialog.cs +++ b/XenAdmin/Dialogs/RestoreSession/ChangeMasterPasswordDialog.cs @@ -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); diff --git a/XenAdmin/Dialogs/RestoreSession/EnterMasterPasswordDialog.cs b/XenAdmin/Dialogs/RestoreSession/EnterMasterPasswordDialog.cs index 2fa55b431..38003788d 100644 --- a/XenAdmin/Dialogs/RestoreSession/EnterMasterPasswordDialog.cs +++ b/XenAdmin/Dialogs/RestoreSession/EnterMasterPasswordDialog.cs @@ -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); } } } \ No newline at end of file diff --git a/XenAdmin/Dialogs/RestoreSession/LoadSessionDialog.cs b/XenAdmin/Dialogs/RestoreSession/LoadSessionDialog.cs index 59601bdc7..8fd93b992 100644 --- a/XenAdmin/Dialogs/RestoreSession/LoadSessionDialog.cs +++ b/XenAdmin/Dialogs/RestoreSession/LoadSessionDialog.cs @@ -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) { diff --git a/XenAdmin/MainWindow.cs b/XenAdmin/MainWindow.cs index ca4355d47..fed569c4f 100755 --- a/XenAdmin/MainWindow.cs +++ b/XenAdmin/MainWindow.cs @@ -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); } diff --git a/XenAdmin/Program.cs b/XenAdmin/Program.cs index 2634f9b2b..cd4dddd3b 100644 --- a/XenAdmin/Program.cs +++ b/XenAdmin/Program.cs @@ -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) { diff --git a/XenAdmin/Settings.cs b/XenAdmin/Settings.cs index f723e5d8c..25188fb3d 100644 --- a/XenAdmin/Settings.cs +++ b/XenAdmin/Settings.cs @@ -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); } } /// - /// 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). /// - /// - /// - /// - 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 } } - /// - /// Prompts for the password to use to decrypt the server list. - /// Returns the secure hash of the password entered. - /// - /// - /// - 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; - } - /// /// 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(); } - /// - /// Compares the hash of 'p' with the given byte array. - /// - /// - /// - /// - 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 known_servers = KnownServers; diff --git a/XenAdmin/WinformsXenAdminConfigProvider.cs b/XenAdmin/WinformsXenAdminConfigProvider.cs index e105f9b4d..fc42f017b 100644 --- a/XenAdmin/WinformsXenAdminConfigProvider.cs +++ b/XenAdmin/WinformsXenAdminConfigProvider.cs @@ -175,7 +175,7 @@ namespace XenAdmin public void SaveSettingsIfRequired() { - Settings.SaveIfRequired(); + Settings.SaveServerList(); } private AsyncAction.SudoElevationResult GetElevatedSession(List allowedRoles, diff --git a/XenAdminTests/UnitTests/EncryptionUtilTests.cs b/XenAdminTests/UnitTests/EncryptionUtilTests.cs new file mode 100644 index 000000000..580ae16e8 --- /dev/null +++ b/XenAdminTests/UnitTests/EncryptionUtilTests.cs @@ -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)}"; + } + } + } + } +} \ No newline at end of file diff --git a/XenAdminTests/XenAdminTests.csproj b/XenAdminTests/XenAdminTests.csproj index 34dde29fd..23f61c8a2 100644 --- a/XenAdminTests/XenAdminTests.csproj +++ b/XenAdminTests/XenAdminTests.csproj @@ -55,6 +55,7 @@ 3.5 + @@ -116,6 +117,7 @@ + diff --git a/XenCenterLib/EncryptionUtils.cs b/XenCenterLib/EncryptionUtils.cs index dcd5571a7..f4a715149 100644 --- a/XenCenterLib/EncryptionUtils.cs +++ b/XenCenterLib/EncryptionUtils.cs @@ -36,87 +36,112 @@ using System.Text; namespace XenCenterLib { - /// - /// Used to centralise the encryption routines used for master password - /// and session lists - /// - 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); + } + } /// - /// 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. /// - /// The string to hash + /// The string to hash + /// The hash algorithm implementation to use. /// The secure hash - 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); } /// - /// 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. /// /// The string to encrypt. /// The key for the encryption algorithm /// The Base64 encoded cipher text - 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)}"; + } + } } /// @@ -124,38 +149,59 @@ namespace XenCenterLib /// /// The base64 encoded cipher text that was produced /// by encryptString - /// The key to use to decrypt + /// The key for the decryption algorithm /// The decrypted text. - 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; } } }