Merge pull request #2092 from umbraco/temp-machine-key-install

U4-10222 Install a custom machine key during umbraco installation
This commit is contained in:
Sebastiaan Janssen
2017-08-02 12:32:12 +02:00
committed by GitHub
9 changed files with 223 additions and 15 deletions

View File

@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace Umbraco.Core.Security
{
/// <summary>
/// Used to generate a machine key
/// </summary>
internal class MachineKeyGenerator
{
/// <summary>
/// Generates the string to be stored in the web.config
/// </summary>
/// <returns></returns>
/// <remarks>
/// Machine key details are here: https://msdn.microsoft.com/en-us/library/vstudio/w8h3skw9%28v=vs.100%29.aspx?f=255&MSPPError=-2147217396
/// </remarks>
public string GenerateConfigurationBlock()
{
var c = @"<machineKey validationKey=""{0}""
decryptionKey=""{1}""
validation=""HMACSHA256"" decryption=""AES""
/>";
var Xxx = 3;
return string.Format(c, GenerateAESDecryptionKey(), GenerateHMACSHA256ValidationKey());
}
public string GenerateHMACSHA256ValidationKey()
{
//See: https://msdn.microsoft.com/en-us/library/vstudio/w8h3skw9%28v=vs.100%29.aspx?f=255&MSPPError=-2147217396
//See: https://msdn.microsoft.com/en-us/library/ff649308.aspx?f=255&MSPPError=-2147217396
/*
key value Specifies a manually assigned key.
The validationKey value must be manually set to a string of hexadecimal
characters to ensure consistent configuration across all servers in a Web farm.
The length of the key depends on the hash algorithm that is used:
AES requires a 256-bit key (64 hexadecimal characters).
MD5 requires a 128-bit key (32 hexadecimal characters).
SHA1 requires a 160-bit key (40 hexadecimal characters).
3DES requires a 192-bit key (48 hexadecimal characters).
HMACSHA256 requires a 256-bit key (64 hexadecimal characters) == DEFAULT
HMACSHA384 requires a 384-bit key (96 hexadecimal characters).
HMACSHA512 requires a 512-bit key (128 hexadecimal characters).
*/
//64 in length = 256 bits
return GenerateKey(64);
}
public string GenerateAESDecryptionKey()
{
//See: //See: https://msdn.microsoft.com/en-us/library/vstudio/w8h3skw9%28v=vs.100%29.aspx?f=255&MSPPError=-2147217396
/*
key value Specifies a manually assigned key.
The decryptionKey value must be manually set to a string of
hexadecimal characters to ensure consistent configuration across all servers in a Web farm.
The key should be 64 bits (16 hexadecimal characters) long for DES encryption, or 192 bits
(48 hexadecimal characters) long for 3DES. For AES, the key can be 128 bits (32 characters),
192 bits (48 characters), or 256 bits (64 characters) long.
*/
//64 in length = 256 bits
return GenerateKey(64);
}
private string GenerateKey(int len = 64)
{
var buff = new byte[len / 2];
var rng = new RNGCryptoServiceProvider();
rng.GetBytes(buff);
var sb = new StringBuilder(len);
for (int i = 0; i < buff.Length; i++)
sb.Append(string.Format("{0:X2}", buff[i]));
return sb.ToString();
}
}
}

View File

@@ -674,7 +674,7 @@ namespace Umbraco.Core.Security
if (PasswordFormat == MembershipPasswordFormat.Clear)
return pass;
var bytes = Encoding.Unicode.GetBytes(pass);
var numArray1 = Convert.FromBase64String(salt);
var saltBytes = Convert.FromBase64String(salt);
byte[] inArray;
if (PasswordFormat == MembershipPasswordFormat.Hashed)
@@ -684,22 +684,27 @@ namespace Umbraco.Core.Security
if (algorithm != null)
{
var keyedHashAlgorithm = algorithm;
if (keyedHashAlgorithm.Key.Length == numArray1.Length)
keyedHashAlgorithm.Key = numArray1;
else if (keyedHashAlgorithm.Key.Length < numArray1.Length)
{
if (keyedHashAlgorithm.Key.Length == saltBytes.Length)
{
//if the salt bytes is the required key length for the algorithm, use it as-is
keyedHashAlgorithm.Key = saltBytes;
}
else if (keyedHashAlgorithm.Key.Length < saltBytes.Length)
{
//if the salt bytes is too long for the required key length for the algorithm, reduce it
var numArray2 = new byte[keyedHashAlgorithm.Key.Length];
Buffer.BlockCopy(numArray1, 0, numArray2, 0, numArray2.Length);
Buffer.BlockCopy(saltBytes, 0, numArray2, 0, numArray2.Length);
keyedHashAlgorithm.Key = numArray2;
}
else
{
//if the salt bytes is too long for the required key length for the algorithm, extend it
var numArray2 = new byte[keyedHashAlgorithm.Key.Length];
var dstOffset = 0;
while (dstOffset < numArray2.Length)
{
var count = Math.Min(numArray1.Length, numArray2.Length - dstOffset);
Buffer.BlockCopy(numArray1, 0, numArray2, dstOffset, count);
var count = Math.Min(saltBytes.Length, numArray2.Length - dstOffset);
Buffer.BlockCopy(saltBytes, 0, numArray2, dstOffset, count);
dstOffset += count;
}
keyedHashAlgorithm.Key = numArray2;
@@ -708,9 +713,9 @@ namespace Umbraco.Core.Security
}
else
{
var buffer = new byte[numArray1.Length + bytes.Length];
Buffer.BlockCopy(numArray1, 0, buffer, 0, numArray1.Length);
Buffer.BlockCopy(bytes, 0, buffer, numArray1.Length, bytes.Length);
var buffer = new byte[saltBytes.Length + bytes.Length];
Buffer.BlockCopy(saltBytes, 0, buffer, 0, saltBytes.Length);
Buffer.BlockCopy(bytes, 0, buffer, saltBytes.Length, bytes.Length);
inArray = hashAlgorithm.ComputeHash(buffer);
}
}
@@ -718,9 +723,9 @@ namespace Umbraco.Core.Security
{
//this code is copied from the sql membership provider - pretty sure this could be nicely re-written to completely
// ignore the salt stuff since we are not salting the password when encrypting.
var password = new byte[numArray1.Length + bytes.Length];
Buffer.BlockCopy(numArray1, 0, password, 0, numArray1.Length);
Buffer.BlockCopy(bytes, 0, password, numArray1.Length, bytes.Length);
var password = new byte[saltBytes.Length + bytes.Length];
Buffer.BlockCopy(saltBytes, 0, password, 0, saltBytes.Length);
Buffer.BlockCopy(bytes, 0, password, saltBytes.Length, bytes.Length);
inArray = EncryptPassword(password, MembershipPasswordCompatibilityMode.Framework40);
}
return Convert.ToBase64String(inArray);

View File

@@ -666,6 +666,7 @@
<Compile Include="Security\IBackOfficeUserPasswordChecker.cs" />
<Compile Include="Security\IMembershipProviderPasswordHasher.cs" />
<Compile Include="Security\IUserAwarePasswordHasher.cs" />
<Compile Include="Security\MachineKeyGenerator.cs" />
<Compile Include="Security\MembershipProviderPasswordHasher.cs" />
<Compile Include="Security\EmailService.cs" />
<Compile Include="Security\MembershipProviderPasswordValidator.cs" />

View File

@@ -0,0 +1,14 @@
angular.module("umbraco.install").controller("Umbraco.Installer.MachineKeyController", function ($scope, installerService) {
$scope.continue = function () {
installerService.status.current.model = true;
installerService.forward();
};
$scope.ignoreKey = function () {
installerService.status.current.model = false;
installerService.forward();
};
});

View File

@@ -0,0 +1,23 @@
<div ng-controller="Umbraco.Installer.MachineKeyController">
<h1>Configure an ASP.Net Machine Key</h1>
<p>
By default the installer will generate a custom ASP.Net Machine Key for your site and install it into your web.config file.
A Machine Key is used for hashing and encryption and it is recommended that you install a custom one into your site.
This ensures that your site is fully portable between environments that might have different Machine Key settings and that
your site by default will work with load balancing when installed between various server environments.
</p>
<form name="myForm" class="form-horizontal" novalidate ng-submit="continue();">
<div class="row">
<div class="control-group">
<div class="controls">
<input type="submit" value="Continue" class="btn btn-success" />
<button class="btn" ng-click="ignoreKey()">I don't want a custom Machine Key</button>
</div>
</div>
</div>
</form>
</div>

View File

@@ -48,6 +48,7 @@ namespace Umbraco.Web.Install
new MajorVersion7UpgradeReport(_umbContext.Application),
new Version73FileCleanup(_umbContext.HttpContext, _umbContext.Application.ProfilingLogger.Logger),
new DatabaseConfigureStep(_umbContext.Application),
new ConfigureMachineKey(_umbContext.Application),
new DatabaseInstallStep(_umbContext.Application),
new DatabaseUpgradeStep(_umbContext.Application),
new StarterKitDownloadStep(_umbContext.Application, _umbContext.Security, _umbContext.HttpContext),

View File

@@ -0,0 +1,75 @@
using System;
using System.Configuration;
using System.Linq;
using System.Web.Configuration;
using System.Xml.Linq;
using Umbraco.Core;
using Umbraco.Core.IO;
using Umbraco.Core.Security;
using Umbraco.Web.Install.Models;
namespace Umbraco.Web.Install.InstallSteps
{
[InstallSetupStep(InstallationType.NewInstall,
"ConfigureMachineKey", "machinekey", 2,
"Updating some security settings...",
PerformsAppRestart = true)]
internal class ConfigureMachineKey : InstallSetupStep<bool?>
{
private readonly ApplicationContext _appContext;
public ConfigureMachineKey(ApplicationContext appContext)
{
if (appContext == null) throw new ArgumentNullException("appContext");
_appContext = appContext;
}
public override string View
{
get { return HasMachineKey() == false ? base.View : ""; }
}
/// <summary>
/// Don't display the view or execute if a machine key already exists
/// </summary>
/// <returns></returns>
private bool HasMachineKey()
{
var section = (MachineKeySection)WebConfigurationManager.GetSection("system.web/machineKey");
return section.ElementInformation.Source != null;
}
/// <summary>
/// The step execution method
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
public override InstallSetupResult Execute(bool? model)
{
if (model.HasValue && model.Value == false) return null;
//install the machine key
var fileName = IOHelper.MapPath(string.Format("{0}/web.config", SystemDirectories.Root));
var xml = XDocument.Load(fileName, LoadOptions.PreserveWhitespace);
var systemWeb = xml.Root.DescendantsAndSelf("system.web").Single();
// Update appSetting if it exists, or else create a new appSetting for the given key and value
var machineKey = systemWeb.Descendants("machineKey").FirstOrDefault();
if (machineKey != null) return null;
var generator = new MachineKeyGenerator();
var generatedSection = generator.GenerateConfigurationBlock();
systemWeb.Add(XElement.Parse(generatedSection));
xml.Save(fileName, SaveOptions.DisableFormatting);
return null;
}
public override bool RequiresExecution(bool? model)
{
return HasMachineKey() == false;
}
}
}

View File

@@ -8,6 +8,7 @@ using System.Text;
using System.Threading.Tasks;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
using Umbraco.Web.Install.Models;
@@ -94,8 +95,9 @@ namespace Umbraco.Web.Install.InstallSteps
result.DetermineInstalledVersion();
return false;
}
catch (Exception)
catch (Exception ex)
{
_applicationContext.ProfilingLogger.Logger.Error<DatabaseConfigureStep>("An error occurred, reconfiguring...", ex);
//something went wrong, could not connect so probably need to reconfigure
return true;
}

View File

@@ -359,6 +359,7 @@
<Compile Include="HealthCheck\StatusResultType.cs" />
<Compile Include="HealthCheck\Checks\DataIntegrity\XmlDataIntegrityHealthCheck.cs" />
<Compile Include="IHttpContextAccessor.cs" />
<Compile Include="Install\InstallSteps\ConfigureMachineKey.cs" />
<Compile Include="Models\ContentEditing\AssignedContentPermissions.cs" />
<Compile Include="Models\ContentEditing\AssignedUserGroupPermissions.cs" />
<Compile Include="Models\ContentEditing\ContentRedirectUrl.cs" />