Files
Umbraco-CMS/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs
Andy Butland 825f791d01 Remove the non-controversial, straightforward obsoleted constructs for Umbraco 16 (#18661)
* Removed obsoletes from IConfigManipulator.

* Removed obsolete models builder extensions.

* Removed the obsolete ContentDashboardSettings.

* Removed the obsolete InstallMissingDatabase setting on GlobalSettings.

* Removed obsolete NuCache settings.

* Removed obsolete RuntimeMinificationSettings.

* Removed obsolete health check constant.

* Removed obsolete icon constant.

* Removed obsolete telemetry constant.

* Removed obsolete property and constructor on UmbracoBuilder.

* Removed obsolete constructor on AuditNotificationsHandler.

* Removed obsolete constructor on HTTP header health checks.

* Removed obsolete constructor on MediaFileManager.

* Removed obsolete GetDefaultFileContent on ViewHelper.

* Remove obsoleted methods on embed providers.

* Fix tests.

* Removed obsolete constructors on BlockEditorDataConverter.

* Removed obsolete SeedCacheDuration property on CacheSettings.

* Removed obsolete PublishCulture on ContentRepositoryExtensions.

* Removed obsolete MonitorLock.

* Removed obsolete synchronous HasSavedValues from IDataTypeUsageService and IDataTypeUsageRepository.

* Removed obsolete HasSavedPropertyValues from IPropertyTypeUsageService and IPropertyTypeUsageRepository.

* Removed obsolete methods in ITrackedReferencesService and ITrackedReferencesRepository.

* Removed obsolete DateValueEditor constructors.

* Removed obsolete GetAutomaticRelationTypesAliases.

* Removed obsolete constructor on TextOnlyValueEditor.

* Removed obsolete constructors on RegexValidator and RequiredValidator.

* Removed obsolete constructs on SliderValueConverter and TagsValueConverter.

* Removed obsolete GetContentType methods from IPublishedCache.

* Removed ContentFinderByIdPath.

* Removed obsolete constructor on DefaultMediaUrlProvider.

* Removed obsolete constructor on Domain.

* Removed obsolete constructor on PublishedRequest.

* Removed obsolete methods on CheckPermissions.

* Removed obsolete GetUserId from IBackOfficeSecurity.

* Removed obsolete methods on LegacyPasswordSecurity.

* Removed obsolete constructors on AuditService.

* Removed obsolete methods on IContentEditingService.

* Remove obsolete constructors and methods on ContentService/IContentService.

* Removed obsolete constructor in ContentTypeEditingService.

* Removed obsolete constructor in MediaTypeEditingService.

* Removed obsolete constructor in MemberTypeEditingService.

* Removed obsolete constructor in ContentTypeService.

* Removed obsolete constructors in ContentTypeServiceBase.

* Removed obsolete constructors and methods in ContentVersionService.

* Removed obsolete constructor in DataTypeUsageService.

* Removed obsolete constructor in DomainService.

* Removed obsolete constructor in FileService.

* Removes obsolete AttemptMove from IContentService.

* Removes obsolete SetPreventCleanup from IContentVersionService.

* Removes obsolete GetReferences from IDataTypeService.

* Removed obsolete SetConsentLevel from IMetricsConsentService.

* Removed obsolete methods from IPackageDataInstallation.

* Removed obsolete methods from IPackagingService.

* Removed obsolete methods on ITwoFactorLoginService.
Removed obsolete ITemporaryMediaService.

* Removed obsolete constructor from MediaService, MemberTypeService and MediaTypeService.

* More obsolete constructors.

* Removed obsoleted overloads on IPropertyValidationService.

* Fixed build for tests.

* Removed obsolete constructor for PublicAccessService, UserService and RelationService.

* Removed GetDefaultMemberType.

* Removed obsolete user group functionality from IUserService.

* Removed obsolete extension methods on IUserService.

* Removed obsolete method from ITelemetryService.

* Removed obsolete UdiParserServiceConnectors.

* Removed obsolete method on ICookieManager.

* Removed obsolete DynamicContext.

* Removed obsolete XmlHelper.

* Fixed failing integration tests.

* Removed obsoletes in Umbraco.Cms.Api.Common

* Removed obsoletes in Umbraco.Cms.Api.Delivery

* Removed obsoletes in Umbraco.Cms.Api.Management

* Removed obsoletes in Umbraco.Examine.Lucene

* Removed obsoletes in Umbraco.Infrastructure

* Fix failing delivery API contract integration test.

* Made integration tests internal.

* Removed obsoletes from web projects.

* Fix build.

* Removed Twitter OEmbed provider

* Removed obsolete constructor on PublishedDataType.

* Removed obsolete constructors on PublishedCacheBase.

* Removed the obsolete PropertyEditorTagsExtensions.

* Removed obsoletion properties on configuration response  models (#18697)

* Removed obsolete methods from server-side models.

* Update client-side types and sdk.

* Update client-side files.

* Removed obsoletion of Utf8ToAsciiConverter.ToAsciiString overload. (#18694)

* Removed obsolete method in UserService. (#18710)

* Removed obsoleted group alias keys from being publicly available. (#18682)

* Removed unneceessary ApiVersion attribute.

* Clean-up obsoletions on MemberService (#18703)

* Removed obsoleted method on MemberService, added future obsoletion to interface and updated all callers.

* Removed obsoletion on member service method that's not obsolete on the interface.
2025-03-21 17:02:31 +00:00

218 lines
7.6 KiB
C#

using System.Security.Cryptography;
using System.Text;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Security;
/// <summary>
/// Handles password hashing and formatting for legacy hashing algorithms.
/// </summary>
/// <remarks>
/// Should probably be internal.
/// </remarks>
public class LegacyPasswordSecurity
{
public static string GenerateSalt()
{
var numArray = new byte[16];
using (var rng = new RNGCryptoServiceProvider())
{
rng.GetBytes(numArray);
return Convert.ToBase64String(numArray);
}
}
// Used for tests
internal string FormatPasswordForStorage(string algorithmType, string hashedPassword, string salt)
{
if (!SupportHashAlgorithm(algorithmType))
{
throw new InvalidOperationException($"{algorithmType} is not supported");
}
return salt + hashedPassword;
}
/// <summary>
/// Verifies if the password matches the expected hash+salt of the stored password string
/// </summary>
/// <param name="algorithm">The hashing algorithm for the stored password.</param>
/// <param name="password">The password.</param>
/// <param name="dbPassword">The value of the password stored in a data store.</param>
/// <returns></returns>
public bool VerifyPassword(string algorithm, string password, string dbPassword)
{
if (string.IsNullOrWhiteSpace(dbPassword))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword));
}
if (dbPassword.StartsWith(Constants.Security.EmptyPasswordPrefix))
{
return false;
}
try
{
var storedHashedPass = ParseStoredHashPassword(algorithm, dbPassword, out var salt);
var hashed = HashPassword(algorithm, password, salt);
return storedHashedPass == hashed;
}
catch (ArgumentOutOfRangeException)
{
// This can happen if the length of the password is wrong and a salt cannot be extracted.
return false;
}
}
/// <summary>
/// Verify a legacy hashed password (HMACSHA1)
/// </summary>
public bool VerifyLegacyHashedPassword(string password, string dbPassword)
{
using var hashAlgorithm = new HMACSHA1
{
// the legacy salt was actually the password :(
Key = Encoding.Unicode.GetBytes(password),
};
var hashed = Convert.ToBase64String(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(password)));
return dbPassword == hashed;
}
/// <summary>
/// Create a new password hash and a new salt
/// </summary>
internal string HashNewPassword(string algorithm, string newPassword, out string salt)
{
salt = GenerateSalt();
return HashPassword(algorithm, newPassword, salt);
}
/// <summary>
/// Parses out the hashed password and the salt from the stored password string value
/// </summary>
/// <param name="algorithm">The hashing algorithm for the stored password.</param>
/// <param name="storedString"></param>
/// <param name="salt">returns the salt</param>
/// <returns></returns>
public string ParseStoredHashPassword(string algorithm, string storedString, out string salt)
{
if (string.IsNullOrWhiteSpace(storedString))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString));
}
if (!SupportHashAlgorithm(algorithm))
{
throw new InvalidOperationException($"{algorithm} is not supported");
}
var saltLen = GenerateSalt();
salt = storedString.Substring(0, saltLen.Length);
return storedString.Substring(saltLen.Length);
}
public bool SupportHashAlgorithm(string algorithm)
{
// This is for the v6-v8 hashing algorithm
if (algorithm.InvariantEquals(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName))
{
return true;
}
// Default validation value for old machine keys (switched to HMACSHA256 aspnet 4 https://docs.microsoft.com/en-us/aspnet/whitepapers/aspnet4/breaking-changes)
if (algorithm.InvariantEquals("SHA1"))
{
return true;
}
return false;
}
/// <summary>
/// Hashes a password with a given salt
/// </summary>
/// <param name="algorithmType">The hashing algorithm for the password.</param>
/// <param name="pass"></param>
/// <param name="salt"></param>
/// <returns></returns>
private string HashPassword(string algorithmType, string pass, string salt)
{
if (!SupportHashAlgorithm(algorithmType))
{
throw new InvalidOperationException($"{algorithmType} is not supported");
}
// This is the correct way to implement this (as per the sql membership provider)
var bytes = Encoding.Unicode.GetBytes(pass);
var saltBytes = Convert.FromBase64String(salt);
byte[] inArray;
using HashAlgorithm hashAlgorithm = GetHashAlgorithm(algorithmType);
if (hashAlgorithm is KeyedHashAlgorithm algorithm)
{
KeyedHashAlgorithm keyedHashAlgorithm = algorithm;
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(saltBytes, 0, numArray2, 0, numArray2.Length);
keyedHashAlgorithm.Key = numArray2;
}
else
{
// if the salt bytes is too short 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(saltBytes.Length, numArray2.Length - dstOffset);
Buffer.BlockCopy(saltBytes, 0, numArray2, dstOffset, count);
dstOffset += count;
}
keyedHashAlgorithm.Key = numArray2;
}
inArray = keyedHashAlgorithm.ComputeHash(bytes);
}
else
{
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);
}
return Convert.ToBase64String(inArray);
}
/// <summary>
/// Return the hash algorithm to use based on the provided <paramref name="algorithm"/>.
/// </summary>
/// <param name="algorithm">The hashing algorithm name.</param>
/// <returns></returns>
private HashAlgorithm GetHashAlgorithm(string algorithm)
{
if (algorithm.IsNullOrWhiteSpace())
{
throw new InvalidOperationException("No hash algorithm type specified");
}
var alg = HashAlgorithm.Create(algorithm);
if (alg == null)
{
throw new InvalidOperationException($"The hash algorithm specified {algorithm} cannot be resolved");
}
return alg;
}
}