using System; using System.Collections.Specialized; using System.Configuration.Provider; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Web.Configuration; using System.Web.Hosting; using System.Web.Security; namespace Umbraco.Core.Security { /// /// A base membership provider class offering much of the underlying functionality for initializing and password encryption/hashing. /// public abstract class MembershipProviderBase : MembershipProvider { /// /// Providers can override this setting, default is 7 /// protected virtual int DefaultMinPasswordLength { get { return 7; } } /// /// Providers can override this setting, default is 1 /// protected virtual int DefaultMinNonAlphanumericChars { get { return 1; } } /// /// Providers can override this setting, default is false to use better security /// protected virtual bool DefaultUseLegacyEncoding { get { return false; } } /// /// Providers can override this setting, by default this is false which means that the provider will /// authenticate the username + password when ChangePassword is called. This property exists purely for /// backwards compatibility. /// internal virtual bool AllowManuallyChangingPassword { get { return false; } } private string _applicationName; private bool _enablePasswordReset; private bool _enablePasswordRetrieval; private int _maxInvalidPasswordAttempts; private int _minRequiredNonAlphanumericCharacters; private int _minRequiredPasswordLength; private int _passwordAttemptWindow; private MembershipPasswordFormat _passwordFormat; private string _passwordStrengthRegularExpression; private bool _requiresQuestionAndAnswer; private bool _requiresUniqueEmail; private bool _useLegacyEncoding; #region Properties /// /// Indicates whether the membership provider is configured to allow users to reset their passwords. /// /// /// true if the membership provider supports password reset; otherwise, false. The default is true. public override bool EnablePasswordReset { get { return _enablePasswordReset; } } /// /// Indicates whether the membership provider is configured to allow users to retrieve their passwords. /// /// /// true if the membership provider is configured to support password retrieval; otherwise, false. The default is false. public override bool EnablePasswordRetrieval { get { return _enablePasswordRetrieval; } } /// /// Gets the number of invalid password or password-answer attempts allowed before the membership user is locked out. /// /// /// The number of invalid password or password-answer attempts allowed before the membership user is locked out. public override int MaxInvalidPasswordAttempts { get { return _maxInvalidPasswordAttempts; } } /// /// Gets the minimum number of special characters that must be present in a valid password. /// /// /// The minimum number of special characters that must be present in a valid password. public override int MinRequiredNonAlphanumericCharacters { get { return _minRequiredNonAlphanumericCharacters; } } /// /// Gets the minimum length required for a password. /// /// /// The minimum length required for a password. public override int MinRequiredPasswordLength { get { return _minRequiredPasswordLength; } } /// /// Gets the number of minutes in which a maximum number of invalid password or password-answer attempts are allowed before the membership user is locked out. /// /// /// The number of minutes in which a maximum number of invalid password or password-answer attempts are allowed before the membership user is locked out. public override int PasswordAttemptWindow { get { return _passwordAttemptWindow; } } /// /// Gets a value indicating the format for storing passwords in the membership data store. /// /// /// One of the values indicating the format for storing passwords in the data store. public override MembershipPasswordFormat PasswordFormat { get { return _passwordFormat; } } /// /// Gets the regular expression used to evaluate a password. /// /// /// A regular expression used to evaluate a password. public override string PasswordStrengthRegularExpression { get { return _passwordStrengthRegularExpression; } } /// /// Gets a value indicating whether the membership provider is configured to require the user to answer a password question for password reset and retrieval. /// /// /// true if a password answer is required for password reset and retrieval; otherwise, false. The default is true. public override bool RequiresQuestionAndAnswer { get { return _requiresQuestionAndAnswer; } } /// /// Gets a value indicating whether the membership provider is configured to require a unique e-mail address for each user name. /// /// /// true if the membership provider requires a unique e-mail address; otherwise, false. The default is true. public override bool RequiresUniqueEmail { get { return _requiresUniqueEmail; } } /// /// The name of the application using the custom membership provider. /// /// /// The name of the application using the custom membership provider. public override string ApplicationName { get { return _applicationName; } set { if (string.IsNullOrEmpty(value)) throw new ProviderException("ApplicationName cannot be empty."); if (value.Length > 0x100) throw new ProviderException("Provider application name too long."); _applicationName = value; } } #endregion /// /// Initializes the provider. /// /// The friendly name of the provider. /// A collection of the name/value pairs representing the provider-specific attributes specified in the configuration for this provider. /// The name of the provider is null. /// An attempt is made to call /// on a provider after the provider /// has already been initialized. /// The name of the provider has a length of zero. public override void Initialize(string name, NameValueCollection config) { // Initialize base provider class base.Initialize(name, config); _enablePasswordRetrieval = config.GetValue("enablePasswordRetrieval", false); _enablePasswordReset = config.GetValue("enablePasswordReset", false); _requiresQuestionAndAnswer = config.GetValue("requiresQuestionAndAnswer", false); _requiresUniqueEmail = config.GetValue("requiresUniqueEmail", false); _maxInvalidPasswordAttempts = GetIntValue(config, "maxInvalidPasswordAttempts", 5, false, 0); _passwordAttemptWindow = GetIntValue(config, "passwordAttemptWindow", 10, false, 0); _minRequiredPasswordLength = GetIntValue(config, "minRequiredPasswordLength", DefaultMinPasswordLength, true, 0x80); _minRequiredNonAlphanumericCharacters = GetIntValue(config, "minRequiredNonalphanumericCharacters", DefaultMinNonAlphanumericChars, true, 0x80); _passwordStrengthRegularExpression = config["passwordStrengthRegularExpression"]; _applicationName = config["applicationName"]; if (string.IsNullOrEmpty(_applicationName)) _applicationName = GetDefaultAppName(); //by default we will continue using the legacy encoding. _useLegacyEncoding = config.GetValue("useLegacyEncoding", DefaultUseLegacyEncoding); // make sure password format is clear by default. string str = config["passwordFormat"] ?? "Clear"; switch (str.ToLower()) { case "clear": _passwordFormat = MembershipPasswordFormat.Clear; break; case "encrypted": _passwordFormat = MembershipPasswordFormat.Encrypted; break; case "hashed": _passwordFormat = MembershipPasswordFormat.Hashed; break; default: throw new ProviderException("Provider bad password format"); } if ((PasswordFormat == MembershipPasswordFormat.Hashed) && EnablePasswordRetrieval) throw new ProviderException("Provider can not retrieve hashed password"); } /// /// Override this method to ensure the password is valid before raising the event /// /// protected override void OnValidatingPassword(ValidatePasswordEventArgs e) { var attempt = IsPasswordValid(e.Password, MinRequiredNonAlphanumericCharacters, PasswordStrengthRegularExpression, MinRequiredPasswordLength); if (attempt.Success == false) { e.Cancel = true; return; } base.OnValidatingPassword(e); } protected internal enum PasswordValidityError { Ok, Length, AlphanumericChars, Strength } /// /// Checks to ensure the AllowManuallyChangingPassword rule is adhered to /// /// /// /// /// public sealed override bool ChangePassword(string username, string oldPassword, string newPassword) { if (oldPassword.IsNullOrWhiteSpace() && AllowManuallyChangingPassword == false) { //If the old password is empty and AllowManuallyChangingPassword is false, than this provider cannot just arbitrarily change the password throw new NotSupportedException("This provider does not support manually changing the password"); } return PerformChangePassword(username, oldPassword, newPassword); } protected abstract bool PerformChangePassword(string username, string oldPassword, string newPassword); protected internal static Attempt IsPasswordValid(string password, int minRequiredNonAlphanumericChars, string strengthRegex, int minLength) { if (minRequiredNonAlphanumericChars > 0) { var nonAlphaNumeric = Regex.Replace(password, "[a-zA-Z0-9]", "", RegexOptions.Multiline | RegexOptions.IgnoreCase); if (nonAlphaNumeric.Length < minRequiredNonAlphanumericChars) { return Attempt.Fail(PasswordValidityError.AlphanumericChars); } } if (string.IsNullOrEmpty(strengthRegex) == false) { if (Regex.IsMatch(password, strengthRegex, RegexOptions.Compiled) == false) { return Attempt.Fail(PasswordValidityError.Strength); } } if (password.Length < minLength) { return Attempt.Fail(PasswordValidityError.Length); } return Attempt.Succeed(PasswordValidityError.Ok); } /// /// Gets the name of the default app. /// /// internal static string GetDefaultAppName() { try { string applicationVirtualPath = HostingEnvironment.ApplicationVirtualPath; if (string.IsNullOrEmpty(applicationVirtualPath)) { return "/"; } return applicationVirtualPath; } catch { return "/"; } } internal static int GetIntValue(NameValueCollection config, string valueName, int defaultValue, bool zeroAllowed, int maxValueAllowed) { int num; string s = config[valueName]; if (s == null) { return defaultValue; } if (!int.TryParse(s, out num)) { if (zeroAllowed) { throw new ProviderException("Value must be non negative integer"); } throw new ProviderException("Value must be positive integer"); } if (zeroAllowed && (num < 0)) { throw new ProviderException("Value must be non negativeinteger"); } if (!zeroAllowed && (num <= 0)) { throw new ProviderException("Value must be positive integer"); } if ((maxValueAllowed > 0) && (num > maxValueAllowed)) { throw new ProviderException("Value too big"); } return num; } protected string FormatPasswordForStorage(string pass, string salt) { if (_useLegacyEncoding) { return pass; } //the better way, we use salt per member return salt + pass; } protected string EncryptOrHashPassword(string pass, string salt) { //if we are doing it the old way if (_useLegacyEncoding) { return LegacyEncodePassword(pass); } //This is the correct way to implement this (as per the sql membership provider) if ((int)PasswordFormat == 0) return pass; var bytes = Encoding.Unicode.GetBytes(pass); var numArray1 = Convert.FromBase64String(salt); byte[] inArray; if ((int)PasswordFormat == 1) { var hashAlgorithm = GetHashAlgorithm(pass); var algorithm = hashAlgorithm as KeyedHashAlgorithm; if (algorithm != null) { var keyedHashAlgorithm = algorithm; if (keyedHashAlgorithm.Key.Length == numArray1.Length) keyedHashAlgorithm.Key = numArray1; else if (keyedHashAlgorithm.Key.Length < numArray1.Length) { var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; Buffer.BlockCopy(numArray1, 0, numArray2, 0, numArray2.Length); keyedHashAlgorithm.Key = numArray2; } else { 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); dstOffset += count; } keyedHashAlgorithm.Key = numArray2; } inArray = keyedHashAlgorithm.ComputeHash(bytes); } 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); inArray = hashAlgorithm.ComputeHash(buffer); } } else { 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); inArray = EncryptPassword(password, MembershipPasswordCompatibilityMode.Framework40); } return Convert.ToBase64String(inArray); } /// /// Encrypt/hash a new password with a new salt /// /// /// /// protected string EncryptOrHashNewPassword(string newPassword, out string salt) { salt = GenerateSalt(); return EncryptOrHashPassword(newPassword, salt); } /// /// Gets the encrypted or hashed string of an existing password for an existing user /// /// The stored string for the password /// protected string EncryptOrHashExistingPassword(string storedPassword) { if (_useLegacyEncoding) { return EncryptOrHashPassword(storedPassword, storedPassword); } else { string salt; var pass = StoredPassword(storedPassword, PasswordFormat, out salt); return EncryptOrHashPassword(pass, salt); } } protected string DecodePassword(string pass) { //if we are doing it the old way if (_useLegacyEncoding) { return LegacyUnEncodePassword(pass); } //This is the correct way to implement this (as per the sql membership provider) switch ((int)PasswordFormat) { case 0: return pass; case 1: throw new ProviderException("Provider can not decode hashed password"); default: var bytes = DecryptPassword(Convert.FromBase64String(pass)); return bytes == null ? null : Encoding.Unicode.GetString(bytes, 16, bytes.Length - 16); } } /// /// Returns the hashed password without the salt if it is hashed /// /// /// /// returns the salt /// internal static string StoredPassword(string storedString, MembershipPasswordFormat format, out string salt) { switch (format) { case MembershipPasswordFormat.Hashed: var saltLen = GenerateSalt(); salt = storedString.Substring(0, saltLen.Length); return storedString.Substring(saltLen.Length); case MembershipPasswordFormat.Clear: case MembershipPasswordFormat.Encrypted: default: salt = string.Empty; return storedString; } } protected internal static string GenerateSalt() { var numArray = new byte[16]; new RNGCryptoServiceProvider().GetBytes(numArray); return Convert.ToBase64String(numArray); } protected HashAlgorithm GetHashAlgorithm(string password) { if (_useLegacyEncoding) { //before we were never checking for an algorithm type so we were always using HMACSHA1 // for any SHA specified algorithm :( so we'll need to keep doing that for backwards compat support. if (Membership.HashAlgorithmType.InvariantContains("SHA")) { return new HMACSHA1 { //the legacy salt was actually the password :( Key = Encoding.Unicode.GetBytes(password) }; } } //get the algorithm by name return HashAlgorithm.Create(Membership.HashAlgorithmType); } /// /// Encodes the password. /// /// The password. /// The encoded password. protected string LegacyEncodePassword(string password) { string encodedPassword = password; switch (PasswordFormat) { case MembershipPasswordFormat.Clear: break; case MembershipPasswordFormat.Encrypted: encodedPassword = Convert.ToBase64String(EncryptPassword(Encoding.Unicode.GetBytes(password))); break; case MembershipPasswordFormat.Hashed: var hashAlgorith = GetHashAlgorithm(password); encodedPassword = Convert.ToBase64String(hashAlgorith.ComputeHash(Encoding.Unicode.GetBytes(password))); break; default: throw new ProviderException("Unsupported password format."); } return encodedPassword; } /// /// Unencode password. /// /// The encoded password. /// The unencoded password. protected string LegacyUnEncodePassword(string encodedPassword) { string password = encodedPassword; switch (PasswordFormat) { case MembershipPasswordFormat.Clear: break; case MembershipPasswordFormat.Encrypted: password = Encoding.Unicode.GetString(DecryptPassword(Convert.FromBase64String(password))); break; case MembershipPasswordFormat.Hashed: throw new ProviderException("Cannot unencode a hashed password."); default: throw new ProviderException("Unsupported password format."); } return password; } } }