我创建了一个安全帮助类,试图遵守SP80063B国家标准和技术研究所(NIST)数字身份识别准则规范。这些准则为密码的创建和存储提出了新的规则。简而言之,我的实现涉及以下规则:
aaaaaa,1234abcd)。使用方法
HashSecurePassword()实现My
password111)的密码是不允许的。123pass123)的密码。firstname、lastname、emailaddress等)是不允许的。Oklahoma、$xkaw93fubpq)中发现的密码是不允许的。null值和一个自定义out PasswordStatus枚举值,提供有关密码失败状态的详细信息。$argon2i$v=19$m=131072,t=6,p=1$SCvNXMwOaGpX2ZOC+OfjKQ$/hdjThjxp9VY2sFG2KWZDSlh9ZgZXLpKCe8B9BVwaeA)组成的级联。帮助班:
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using CryptSharp;
using CryptSharp.Utility;
using Konscious.Security.Cryptography;
// Install-Package CryptSharpOfficial
// Install-Package Konscious.Security.Cryptography.Argon2
namespace OUTKST
{
///
/// Contains methods necessary for calculating, validating, and verifying passwords
/// used in secure applications. These methods were created in accordance with the
/// National Institute of Standards
/// and Technology (NIST) "Digital Identity Guidelines" SP800-63B [June 4, 2018].
///
public static class Security
{
///
/// Enumeration of the hashing techniques available.
///
public enum HashTechnique
{
/// Uses the best available hashing technique.
BestAvailable = 0,
/// The Argon2 memory-hard hashing technique. [Argon2 Wiki]
Argon2 = BestAvailable,
/// The BCrypt Blowfish Cipher hashing technique. [BCrypt Wiki]
BCrypt,
/// The SCrypt Key Derivation Function hashing technique. [SCrypt Wiki]
SCrypt
}
///
/// Enumeration of the compliance status of a checked password against NIST SP800-63B guidelines.
///
public enum PasswordStatus
{
/// Unknown status due to no action or unhandled exception.
Unknown = 0,
/// The password should be considered "secure" according to NIST SP800-63B guidelines.
Secure,
/// The password should be considered OK to use, with the caveat that personal information from the password's owner was not analyzed.
OKPendingPersonalInfo,
/// The password is insecure; the password was found in a common word dictionary and/or the SecList 10mil blacklisted-passwords database.
BadBlacklisted,
/// The password is insecure; personal information from the password's owner was found within the password string.
BadContainsPersonalInfo,
/// The password is insecure; the composition of the password is not well-formed. Password contains more than () consecutively-repeated characters, or repeated phrases of length greater than () characters.
BadInvalidComposition,
/// The password is insecure; the length of the password is not between () and () characters, inclusive.
BadInvalidLength
}
///
/// Verifies the supplied password string against the hashed version. Analyzes the supplied hash
/// string format to determine its hashing technique.
///
/// The given password to check. This string value is normalized before operations are performed.
/// The hashed string of the given password to check against.
/// True if the plaintext password is verified against the hashed password; otherwise False.
/// Returns NotSupportedException if the hashing technique could not be determined.
public static bool CheckPassword(string plaintextPassword, string encodedPassword)
{
if (!plaintextPassword.IsNormalized()) { plaintextPassword = plaintextPassword.Normalize(); }
if (encodedPassword.StartsWith("$argon2")) {
return Utilities.CheckArgon2Hash(plaintextPassword, encodedPassword);
}
if (encodedPassword.StartsWith("$2y")) {
return Utilities.CheckBCryptHash(plaintextPassword, encodedPassword);
}
if (encodedPassword.StartsWith("$s2")) {
return Utilities.CheckSCryptHash(plaintextPassword, encodedPassword);
}
throw new NotSupportedException(@"The given hashed password technique could not be determined.");
}
///
/// Checks the password according to NIST SP800-63B recommendations.
/// If determined to be secure, this method then hashes the given password using the best available technique. The string returned
/// is encoded for immediate storage in a database management system.
///
/// The given password to hash. This string value is normalized before operations are performed.
/// The status describing why the password is considered secure or insecure.
/// (Optional) The hashing technique to use for hashing the given password.
/// HashTechnique.BestAvailable will use the best available technique. An invalid HashTechnique will
/// use HashTechnique.BestAvailable.
/// (Optional) A list of string parameters that contain personal information about the
/// password's owner (e.g. FirstName, LastName, EmailAddress, etc). This personal information will be
/// checked against the password to ensure that the password does not contain these values.
/// If successful, returns a hashed version of the input string encoded for storage in a database management system;
/// otherwise returns null.
/// This example shows how to call the HashSecurePassword method using the best available hashing technique and
/// passing in a list of personal information about the password's owner.
///
/// var encodedPassword = HashSecurePassword(password, out status, HashTechnique.BestAvailable, firstName, lastName, emailAddress, address1, city, state, postalCode);
/// if (encodedPassword == null) { throw new ArgumentException(string.Format("Invalid password provided. Reason: {0}", GetPasswordStatus(status))); }
///
/// // continue
///
///
/// In the case of a null return, check the PasswordStatus object to infer the exact reason the password
/// was not successfully created. Furthermore, if personal information fields were not supplied then the method will not return
/// PasswordStatus.Secure; it will instead return PasswordStatus.OKPendingPersonalInfo to reflect
/// that all NIST guidelines could not be tested.
/// Use the Security.Utilities class to forego the checking of the password's compliance with NIST SP800-63B
/// for a hashed password.
///
public static string HashSecurePassword(string password, out PasswordStatus status, HashTechnique hashTechnique = HashTechnique.BestAvailable, params string[] personalInfo)
{
if (!password.IsNormalized()) { password = password.Normalize(); }
if (Utilities.IsPasswordSecure(password, out status, personalInfo)) {
switch (hashTechnique) {
case HashTechnique.Argon2:
return Utilities.CalculateArgon2Hash(password);
case HashTechnique.BCrypt:
return Utilities.CalculateBCryptHash(password);
case HashTechnique.SCrypt:
return Utilities.CalculateSCryptHash(password);
default:
return HashSecurePassword(password, out status, HashTechnique.BestAvailable, personalInfo);
}
}
return null;
}
///
/// A collection of utilities necessary to perform password compliance, hashing, and hash-checking.
///
public static class Utilities {
/// References a common word dictionary and the SecList 10 million entry blacklisted-passwords database.
private static readonly HashSet PASSWORD_BLACKLIST = new HashSet(SecurityResources.PasswordBlacklist.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries), StringComparer.OrdinalIgnoreCase);
/// The minimum allowable length of a password.
private const int PASSWORD_MIN_LENGTH = 8;
/// The maximum allowable length of a password.
private const int PASSWORD_MAX_LENGTH = 64;
/// The maximum allowable length of any sequentially-repeated character set within a password.
private const int PASSWORD_REPEAT_CHARS_ALLOWED = 2;
/// The maximum allowable length of any repetitive phrases within a password.
private const int PASSWORD_REPEAT_PHRASELENGTH_ALLOWED = 2;
/// The minimum character length to match personal identifiable information (PII) against a password.
private const int PASSWORD_MIN_PII_LENGTH = 3;
/// Matches any valid email address.
private static readonly Regex REGEX_EMAIL = new Regex(@"^([a-zA-Z0-9]+([\.+_-][a-zA-Z0-9]+)*)@(([a-zA-Z0-9]+((\.|[-]{1,2})[a-zA-Z0-9]+)*)\.[a-zA-Z]{2,6})$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
/// Matches any repetitive-character passwords exceeding more than repeated characters (e.g. password111).
private static readonly Regex REGEX_LETTER_REPETITION = new Regex(@"(.)\1{x,}".Replace("x", PASSWORD_REPEAT_CHARS_ALLOWED.ToString()), RegexOptions.Compiled);
/// Matches any repetitive-phrase passwords exceeding more than or more characters per phrase (e.g. 123pass123).
private static readonly Regex REGEX_WORD_REPETITION = new Regex(@".*(.{x,}).*\1.*".Replace("x", (PASSWORD_REPEAT_PHRASELENGTH_ALLOWED + 1).ToString()), RegexOptions.Compiled);
/// Matches, with groups, all valid Argon2 encoded strings in the format of e.g. $argon2i$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
private static readonly Regex ARGON2_REGEX = new Regex(@"^(?argon2[^$]*)#qcStackCode#v=(?\d+)m=(?\d+),t=(?\d+),p=(?\d+)#qcStackCode#(?[^$]+)(?[^$]+)$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture);
/// Matches, with groups, all valid sCrypt encoded strings in the format of e.g. $s2$65536$8$4$UGWwnXhmZG1KDKLn4VY2Pw==$Pg2RVPyYmOeWzFKTr27qHn3FXGqgEifjFgv+jN5zTdM=
private static readonly Regex SCRYPT_REGEX = new Regex(@"^#qcStackCode#(?s2[^$]*)(?\d+)#qcStackCode#(?\d+)(?\d+)#qcStackCode#(?[^$]+)\$(?[^$]+)$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture);
/// Holds default parallelism value for hashing algorithms; set to the number of processors of the machine. This is also known as the number of threads, h.
private static readonly int DEGREE_OF_PARALLELISM = Environment.ProcessorCount;
/// Holds default iterations value for the Argon2 hashing algorithm. This is also known as the time cost, t.
private const int ARGON2_ITERATIONS = 3;
/// Holds default iterations value for the BCrypt hashing algorithm. This is also known as the rounds, or cost.
private const int BCRYPT_ITERATIONS = 13;
/// Holds default iterations value for the SCrypt hashing algorithm. This is also known as the block size or BlockMix, r.
private const int SCRYPT_ITERATIONS = 8;
/// Holds default bCrypt hashing options; using the corrected '2y' variant with rounds of iteration.
private static readonly CrypterOptions BCRYPT_OPTIONS = new CrypterOptions { { CrypterOption.Variant, BlowfishCrypterVariant.Corrected }, { CrypterOption.Rounds, BCRYPT_ITERATIONS } };
/// Holds default memory block size value for hashing algorithms. This is also known as the memory cost, m.
private const int MEMORY_SIZE = 65536;
/// Holds default key length size value, in bytes, for encoded passwords.
private const int KEY_LENGTH = 32;
/// Holds default salt length size value, in bytes, for encoded passwords.
private const int SALT_LENGTH = 16;
/// Holds the current Argon2 implementation version. NOTE: This should be provided as a part of the Argon2 implementation and is thus considered temporary.
private const string ARGON2_VERSION = "19";
///
/// Calculates the Argon2id hash for a given string, using predefined parameters tailored
/// specifically for password hashing and storage.
/// Output is then encoded in the following format for storage:
/// $argon2id$v=19$m=,t=,p=$salt$passwordhash
/// Argon2 is specifically designed for user passwords that DO NOT meet the following requirements:
///
/// ASCII printable uniformly random passwords of length 20 or more (6.6 bits/character)
/// PINs of 40-digit length or more, chosen uniformly at random (3.3 bits/digit)
/// 11-word or longer Diceware passphrases (12.9 bits/word)
/// 12-word or longer XKCD "correct battery horse staple ..." passwords (2,048 word dictionary, words chosen uniformly at random)
///
///
/// The given password to hash using the Argon2id hashing technique.
/// An Argon2id hashed encoding of the given input string.
public static string CalculateArgon2Hash(string plaintextPassword)
{
return CalculateArgon2idHash(plaintextPassword, GenerateSalt());
}
///
/// Calculates the Argon2d hash for a given string, using the given parameters for password hashing and storage.
/// Argon2d uses data-depending memory access, which makes it suitable for cryptocurrencies and proof-of-work
/// applications with no threats from side-channel timing attacks. The best tradeoff attack on t-pass Argon2d
/// is the ranking tradeoff attack, which reduces the time-area product by the factor of 1.33.
/// Output is then encoded in the following format for storage:
/// $argon2d$v=19$m=,t=,p=$salt$passwordhash
/// Argon2 is specifically designed for user passwords that DO NOT meet the following requirements:
///
/// ASCII printable uniformly random passwords of length 20 or more (6.6 bits/character)
/// PINs of 40-digit length or more, chosen uniformly at random (3.3 bits/digit)
/// 11-word or longer Diceware passphrases (12.9 bits/word)
/// 12-word or longer XKCD "correct battery horse staple ..." passwords (2,048 word dictionary, words chosen uniformly at random)
///
///
/// The given password to hash using the Argon2d hashing technique.
/// The salt to mix with the hash; default salt value length is bytes.
/// (Optional) The amount of threads, h, to use; default value is threads.
/// (Optional) The number of iterations, t, to use; default value is iterations.
/// (Optional) The size of memory, m, to use in KB; default value is KB.
/// An Argon2d hashed encoding of the given input string.
public static string CalculateArgon2dHash(string plaintextPassword, byte[] salt, int? parallelism = null, int iterations = ARGON2_ITERATIONS, int memorySize = MEMORY_SIZE)
{
parallelism = parallelism ?? DEGREE_OF_PARALLELISM;
salt = salt ?? GenerateSalt();
string encodedPassword;
using (Argon2d argon2 = new Argon2d(Encoding.UTF8.GetBytes(plaintextPassword)))
{
argon2.DegreeOfParallelism = (int)parallelism;
argon2.Iterations = iterations;
argon2.MemorySize = memorySize;
argon2.Salt = salt;
encodedPassword = string.Format("${0}$v={1}$m={2},t={3},p={4}${5}${6}",
argon2.GetType().Name.ToLower(),
ARGON2_VERSION,
argon2.MemorySize,
argon2.Iterations,
argon2.DegreeOfParallelism,
Convert.ToBase64String(argon2.Salt),
Convert.ToBase64String(argon2.GetBytes(KEY_LENGTH))
);
}
return encodedPassword;
}
///
/// Calculates the Argon2i hash for a given string, using the given parameters for password hashing and storage.
/// Argon2i uses data-independent memory access, which is preferred for password hashing and password-based key
/// derivation. Argon2i is invulnerable to side-channel timing attacks, but is weaker against Time-memory tradeoff
/// (TMTO) attacks.
/// Output is then encoded in the following format for storage:
/// $argon2i$v=19$m=,t=,p=$salt$passwordhash
/// Argon2 is specifically designed for user passwords that DO NOT meet the following requirements:
///
/// ASCII printable uniformly random passwords of length 20 or more (6.6 bits/character)
/// PINs of 40-digit length or more, chosen uniformly at random (3.3 bits/digit)
/// 11-word or longer Diceware passphrases (12.9 bits/word)
/// 12-word or longer XKCD "correct battery horse staple ..." passwords (2,048 word dictionary, words chosen uniformly at random)
///
///
/// The given password to hash using the Argon2i hashing technique.
/// The salt to mix with the hash; default salt value length is bytes.
/// (Optional) The amount of threads, h, to use; default value is threads.
/// (Optional) The number of iterations, t, to use; default value is iterations.
/// (Optional) The size of memory, m, to use in KB; default value is KB.
/// An Argon2i hashed encoding of the given input string.
public static string CalculateArgon2iHash(string plaintextPassword, byte[] salt, int? parallelism = null, int iterations = ARGON2_ITERATIONS, int memorySize = MEMORY_SIZE)
{
parallelism = parallelism ?? DEGREE_OF_PARALLELISM;
salt = salt ?? GenerateSalt();
string encodedPassword;
using (Argon2i argon2 = new Argon2i(Encoding.UTF8.GetBytes(plaintextPassword)))
{
argon2.DegreeOfParallelism = (int)parallelism;
argon2.Iterations = iterations;
argon2.MemorySize = memorySize;
argon2.Salt = salt;
encodedPassword = string.Format("${0}$v={1}$m={2},t={3},p={4}${5}${6}",
argon2.GetType().Name.ToLower(),
ARGON2_VERSION,
argon2.MemorySize,
argon2.Iterations,
argon2.DegreeOfParallelism,
Convert.ToBase64String(argon2.Salt),
Convert.ToBase64String(argon2.GetBytes(KEY_LENGTH))
);
}
return encodedPassword;
}
///
/// Calculates the Argon2id hash for a given string, using the given parameters for password hashing and storage.
/// The best tradeoff attack on 1-pass Argon2id is the combined low-storage attack (for the first half of the
/// memory) and the ranking attack (for the second half), which bring together the factor of about 2.1.
/// Output is then encoded in the following format for storage:
/// $argon2id$v=19$m=,t=,p=$salt$passwordhash
/// Argon2 is specifically designed for user passwords that DO NOT meet the following requirements:
///
/// ASCII printable uniformly random passwords of length 20 or more (6.6 bits/character)
/// PINs of 40-digit length or more, chosen uniformly at random (3.3 bits/digit)
/// 11-word or longer Diceware passphrases (12.9 bits/word)
/// 12-word or longer XKCD "correct battery horse staple ..." passwords (2,048 word dictionary, words chosen uniformly at random)
///
///
/// The given password to hash using the Argon2id hashing technique.
/// The salt to mix with the hash; default salt value length is bytes.
/// (Optional) The amount of threads, h, to use; default value is threads.
/// (Optional) The number of iterations, t, to use; default value is iterations.
/// (Optional) The size of memory, m, to use in KB; default value is KB.
/// An Argon2id hashed encoding of the given input string.
public static string CalculateArgon2idHash(string plaintextPassword, byte[] salt, int? parallelism = null, int iterations = ARGON2_ITERATIONS, int memorySize = MEMORY_SIZE)
{
parallelism = parallelism ?? DEGREE_OF_PARALLELISM;
salt = salt ?? GenerateSalt();
string encodedPassword;
using (Argon2id argon2 = new Argon2id(Encoding.UTF8.GetBytes(plaintextPassword)))
{
argon2.DegreeOfParallelism = (int)parallelism;
argon2.Iterations = iterations;
argon2.MemorySize = memorySize;
argon2.Salt = salt;
encodedPassword = string.Format("${0}$v={1}$m={2},t={3},p={4}${5}${6}",
argon2.GetType().Name.ToLower(),
ARGON2_VERSION,
argon2.MemorySize,
argon2.Iterations,
argon2.DegreeOfParallelism,
Convert.ToBase64String(argon2.Salt),
Convert.ToBase64String(argon2.GetBytes(KEY_LENGTH))
);
}
return encodedPassword;
}
///
/// Calculates the bCrypt "$2y" (Corrected) hash for a given string, using random salt and rounds of iteration.
/// Output is then encoded in the following format for storage:
/// $2y$$TwentytwocharactersaltThirtyonecharacterspasswordhash
///
/// The given password to hash using the bCrypt hashing technique.
/// A bCrypt hashed encoding of the given input string.
public static string CalculateBCryptHash(string plaintextPassword) {
return CalculateBCryptHash(plaintextPassword, Crypter.Blowfish.GenerateSalt(BCRYPT_OPTIONS));
}
///
/// Calculates the bCrypt "$2y" (Corrected) hash for a given string, using a given
/// Crypter.Blowfish.GenerateSalt() salt and rounds of iteration.
/// Output is then encoded in the following format for storage:
/// $2y$$TwentytwocharactersaltThirtyonecharacterspasswordhash
///
/// The given password to hash using the bCrypt hashing technique.
/// The given salt to mix with the hash; using the Crypter.Blowfish.GenerateSalt() method.
/// A bCrypt hashed encoding of the given input string.
public static string CalculateBCryptHash(string plaintextPassword, string salt) {
return Crypter.Blowfish.Crypt(plaintextPassword, salt);
}
///
/// Calculates the sCrypt hash using predefined parameters tailored specifically for password hashing and storage.
/// Output is then encoded in the following format for storage:
/// $s2$$$$salt$passwordhash
///
/// The given password to hash using the sCrypt hashing technique.
/// An sCrypt hashed encoding of the given input string.
public static string CalculateSCryptHash(string plaintextPassword)
{
return CalculateSCryptHash(plaintextPassword, GenerateSalt());
}
///
/// Calculates the sCrypt hash using the given parameters tailored specifically for password hashing and storage.
/// Output is then encoded in the following format for storage:
/// $s2$$$$salt$passwordhash
///
/// The given password to hash using the sCrypt hashing technique.
/// The salt to mix with the hash; default salt value length is .
/// (Optional) The memory size or work factor cost, N, to use; default value is .
/// (Optional) The BlockMix iteration, r, to use; default value is .
/// (Optional) The amount of threads, p, to use; default value is threads.
/// An sCrypt hashed encoding of the given input string.
public static string CalculateSCryptHash(string plaintextPassword, byte[] salt, int? cost = null, int? blockSize = null, int? parallelism = null)
{
salt = salt ?? GenerateSalt();
cost = cost ?? MEMORY_SIZE;
blockSize = blockSize ?? SCRYPT_ITERATIONS;
parallelism = parallelism ?? DEGREE_OF_PARALLELISM;
byte[] scrypt = SCrypt.ComputeDerivedKey(
key: Encoding.UTF8.GetBytes(plaintextPassword),
salt: salt,
cost: (int)cost,
blockSize: (int)blockSize,
parallel: (int)parallelism,
maxThreads: null,
derivedKeyLength: KEY_LENGTH);
return string.Format("${0}${1}${2}${3}${4}${5}",
"s2",
cost,
blockSize,
parallelism,
Convert.ToBase64String(salt),
Convert.ToBase64String(scrypt)
);
}
///
/// Checks a plaintext password against the possible Argon2 hashed string version.
///
/// The given password to check.
/// The Argon2 hashed string of the given password to check against. Encoded strings should be in format $argon2i$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
/// True if the plaintext password matches the supplied Argon2 hashed password; otherwise False.
public static bool CheckArgon2Hash(string plaintextPassword, string encodedPassword)
{
if (!encodedPassword.StartsWith("$argon2")) { throw new ArgumentOutOfRangeException(nameof(encodedPassword), @"Not a valid Argon2 encoding. Encoding should start with '$argon2'."); }
Match match = ARGON2_REGEX.Match(encodedPassword);
if (!match.Success) { throw new ArgumentOutOfRangeException(nameof(encodedPassword), @"Not a valid Argon2 encoding."); }
if (match.Groups["version"].Value != ARGON2_VERSION) { throw new NotSupportedException(string.Format("The Argon2 version supplied (v{0}) is not supported.", match.Groups["version"].Value)); }
string variant = match.Groups["variant"].Value;
int version = Convert.ToInt32(match.Groups["version"].Value);
int parallelism = Convert.ToInt32(match.Groups["parallelism"].Value);
int iterations = Convert.ToInt32(match.Groups["iterations"].Value);
int memorySize = Convert.ToInt32(match.Groups["memory"].Value);
byte[] salt = Convert.FromBase64String(match.Groups["salt"].Value);
switch (variant) {
case "argon2d": return encodedPassword == CalculateArgon2dHash(plaintextPassword, salt, parallelism, iterations, memorySize);
case "argon2id": return encodedPassword == CalculateArgon2idHash(plaintextPassword, salt, parallelism, iterations, memorySize);
case "argon2i":
case "argon2": return encodedPassword == CalculateArgon2iHash(plaintextPassword, salt, parallelism, iterations, memorySize);
default: throw new NotSupportedException(string.Format("The given Argon2 type ({0}) is not supported", variant));
}
}
///
/// Checks a plaintext password against the possible bCrypt hashed string version.
///
/// The given password to check.
/// The bCrypt hashed string of the given password to check against.
/// True if the plaintext password matches the supplied bCrypt hashed password; otherwise False.
public static bool CheckBCryptHash(string plaintextPassword, string encodedPassword)
{
return Crypter.CheckPassword(plaintextPassword, encodedPassword);
}
///
/// Checks a plaintext password against the possible sCrypt hashed string version.
///
/// The given password to check.
/// The sCrypt hashed string of the given password to check against.
/// True if the plaintext password matches the supplied sCrypt hashed password; otherwise False.
public static bool CheckSCryptHash(string plaintextPassword, string encodedPassword)
{
if (!encodedPassword.StartsWith("$s2$")) { throw new ArgumentOutOfRangeException(nameof(encodedPassword), @"Not a valid sCrypt encoding. Encoding should start with '$s2'."); }
Match match = SCRYPT_REGEX.Match(encodedPassword);
if (!match.Success) { throw new ArgumentOutOfRangeException(nameof(encodedPassword), @"Not a valid sCrypt encoding."); }
int cost = Convert.ToInt32(match.Groups["cost"].Value);
int blockSize = Convert.ToInt32(match.Groups["blocksize"].Value);
int parallelism = Convert.ToInt32(match.Groups["parallelism"].Value);
byte[] salt = Convert.FromBase64String(match.Groups["salt"].Value);
return encodedPassword == CalculateSCryptHash(plaintextPassword, salt, cost, blockSize, parallelism);
}
///
/// Generates an N-sized byte salt using the System.Security.Cryptography RNGCryptoServiceProvider.
///
/// (Optional) The size, in bytes, of the salt to generate. Default is bytes
/// —the recommended size for Argon2, BCrypt, and SCrypt hashing.
/// A byte[] value consisting of a randomly generated, cryptographically secure salt.
public static byte[] GenerateSalt(int size = SALT_LENGTH)
{
if (size < 1) { throw new ArgumentOutOfRangeException(nameof(size), @"Please choose a positive integer value for the salt size."); }
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
byte[] salt = new byte[size];
rng.GetBytes(salt);
return salt;
}
///
/// Returns a user-friendly message describing the compliance status of a password according to NIST SP800-63B guidelines.
///
/// The PasswordStatus object holding the password status.
/// A user-friendly message describing the security compliance of a password according to NIST SP800-63B guidelines.
public static string GetPasswordStatusMessage(PasswordStatus status)
{
switch (status) {
case PasswordStatus.Secure:
return @"Password is considered ""secure"" according to NIST SP800-63B guidelines.";
case PasswordStatus.OKPendingPersonalInfo:
return @"Password is considered OK, pending verification against personal data from the password's owner.";
case PasswordStatus.BadBlacklisted:
return @"Password is insecure because it is considered common or is blacklisted.";
case PasswordStatus.BadContainsPersonalInfo:
return @"Password is insecure because it contains personal information from the password's owner.";
case PasswordStatus.BadInvalidLength:
return @"Password is insecure because it does not meet the character length requirements. Passwords should be of length between " +
$"({PASSWORD_MIN_LENGTH}) and ({PASSWORD_MAX_LENGTH}) characters, inclusive.";
case PasswordStatus.BadInvalidComposition:
return $"Password is insecure because its composition contains more than ({PASSWORD_REPEAT_CHARS_ALLOWED}) consecutively-repeated " +
$"characters, or repeated phrases of length greater than ({PASSWORD_REPEAT_PHRASELENGTH_ALLOWED}) characters.";
case PasswordStatus.Unknown:
default:
return @"An unknown password status has occurred.";
}
}
///
/// Using a word dictionary and the SecList blacklisted-passwords database, will determine
/// if the password provided is common/blacklisted and should be considered insecure.
///
/// The given password to check for compliance.
/// True if the given password has been found to be blacklisted (insecure); otherwise False.
public static bool IsPasswordBlacklisted(string plaintextPassword)
{
return PASSWORD_BLACKLIST.Contains(plaintextPassword);
}
///
/// Determines if a password contains personally identifiable information (PII). Matches are
/// determined based on value provided for . Additional checks to
/// make sure the entire password is not contained within PII and vice versa.
///
/// The given password to check for compliance.
/// (Optional) A list of string parameters that contain personal information about the
/// password's owner (e.g. FirstName, LastName, EmailAddress, etc). This personal information will be
/// checked against the password to ensure that the password does not contain these values.
/// True if the given password has been found to be personally identifiable (insecure); otherwise False.
public static bool IsPasswordPersonallyIdentifiable(string plaintextPassword, params string[] personalInfo)
{
if (personalInfo == null || personalInfo.Length == 0) { throw new ArgumentNullException(nameof(personalInfo), @"Personal information must be provided to determine password compliance."); }
for (int i = 0; i < personalInfo.Length; i++) {
if (personalInfo[i] == null) { continue; }
// if email address provided, only care about the local part and not the domain
if (REGEX_EMAIL.IsMatch(personalInfo[i])) {
personalInfo[i] = personalInfo[i].Substring(0, personalInfo[i].IndexOf("@", StringComparison.OrdinalIgnoreCase));
}
if ((personalInfo[i].Length >= PASSWORD_MIN_PII_LENGTH) &&
((plaintextPassword.IndexOf(personalInfo[i], StringComparison.OrdinalIgnoreCase) != -1) ||
(personalInfo[i].IndexOf(plaintextPassword, StringComparison.OrdinalIgnoreCase) != -1))) {
return true;
}
}
return false;
}
///
/// Using regular expressions, determines if the password contains repetitive letters or phrases. Matches are determined
/// based on values provided for and .
///
/// The given password to check for compliance.
/// True if the given password has been found to be repetitive (insecure); otherwise False.
public static bool IsPasswordRepetitive(string plaintextPassword) {
return REGEX_LETTER_REPETITION.IsMatch(plaintextPassword) || REGEX_WORD_REPETITION.IsMatch(plaintextPassword);
}
///
/// Determines if the password should be considered "secure" by ensuring the password is between () and
/// () characters long, inclusive, then verifying the password against a blacklist database of bad/compromised
/// passwords. Finally, if personal information parameters were supplied, the method checks to ensure no personal information
/// was used to create the password.
///
/// The given password to check for compliance.
/// The status describing why the password is considered secure or insecure.
/// (Optional) A list of string parameters that contain personal information about the
/// password's owner (e.g. FirstName, LastName, EmailAddress, etc). This personal information will be
/// checked against the password to ensure that the password does not contain these values.
/// True if the password should be considered "secure" to use; otherwise False. Additionally sets the
/// PasswordStatus object describing why the password is considered secure or insecure.
/// If no personal information is supplied, the method may still return True but with a
/// PasswordStatus return value type of PasswordStatus.OKPendingPersonalInfo.
public static bool IsPasswordSecure(string plaintextPassword, out PasswordStatus status, params string[] personalInfo)
{
status = PasswordStatus.Unknown;
if (plaintextPassword == null || plaintextPassword.Length < PASSWORD_MIN_LENGTH || plaintextPassword.Length > PASSWORD_MAX_LENGTH) {
status = PasswordStatus.BadInvalidLength;
} else if (IsPasswordBlacklisted(plaintextPassword)) {
status = PasswordStatus.BadBlacklisted;
} else if (IsPasswordRepetitive(plaintextPassword)) {
status = PasswordStatus.BadInvalidComposition;
} else if (personalInfo == null || personalInfo.Length == 0) {
status = PasswordStatus.OKPendingPersonalInfo;
} else if (IsPasswordPersonallyIdentifiable(plaintextPassword, personalInfo)) {
status = PasswordStatus.BadContainsPersonalInfo;
} else {
status = PasswordStatus.Secure;
}
return status == PasswordStatus.OKPendingPersonalInfo || status == PasswordStatus.Secure;
}
}
}
}PasswordStatus.OKPendingPersonalInfo,它仍然允许密码被认为是“安全的”,但有一个警告,即没有提供任何个人信息来测试密码。这是否能很好地处理不存在个人信息的有效边缘情况?enum使用/放置params与显式string[]用于个人信息参数。发布于 2019-05-29 04:47:36
您的助手类中满是开关语句,用于将操作分派给所使用的底层算法。您可以使用服务提供者接口,而不是循环的开关块。
公共静态bool CheckPassword(string plaintextPassword,string encodedPassword) { if (!plaintextPassword.IsNormalized()) { plaintextPassword = plaintextPassword.Normalize();} if (encodedPassword.StartsWith("$argon2")) {返回Utilities.CheckArgon2Hash(plaintextPassword,encodedPassword);} if (encodedPassword.StartsWith("$2y")) {返回Utilities.CheckBCryptHash(plaintextPassword,encodedPassword);} if (encodedPassword.StartsWith("$s2")) {返回Utilities.CheckSCryptHash(plaintextPassword,encodedPassword);}引发新的NotSupportedException(@“无法确定给定的哈希密码技术”);}
可以重写为向服务提供程序发送。
public static bool CheckPassword(string plaintextPassword, string encodedPassword)
{
if (!plaintextPassword.IsNormalized()) { plaintextPassword = plaintextPassword.Normalize(); }
this.GetServiceProvider(encodedPassword).Verify(plaintextPassword, encodedPassword);
}检索服务提供程序可能会使用现有代码。允许注册其他服务提供商也是一种良好做法。您可以将所有注册的内容存储在字典providers中。HashTechnique可以成为默认技术的方便枚举,但我将允许string上的重载注册其他技术。
protected virtual IPasswordServiceProvider GetServiceProvider(string digest)
{
if (digest.StartsWith("$argon2")) {
return this.providers[PasswordServiceProvider.Argon2];
// and so on ..
}与其从像Utilities.CheckBCryptHash这样的助手调用代码,不如让每个服务提供者实例实现一个接口并拥有自己的方法。
public interface IPasswordServiceProvider {
string Name { get; } // argon2, bcrypt, scrypt, ..
bool Verify(string plainText, string digest);
}
public abstract class PasswordServiceProvider : IPasswordServiceProvider {
// you could keep track of common provider names
public const string Argon2 = "Argon2";
// .. add shared logic
}
public class Argon2PasswordServiceProvider : PasswordServiceProvider {
public override string Name => PasswordServiceProvider.Argon2;
public override bool Verify(string plainText, string digest) {
// perform argon2 algorithm ..
}
}尝试实现SlowEquals而不是ReferenceEquals。
return encodedPassword == CalculateArgon2dHash(plaintextPassword, ..);
https://codereview.stackexchange.com/questions/196442
复制相似问题