首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >根据NIST SP800-63B准则提供的安全助理员课程

根据NIST SP800-63B准则提供的安全助理员课程
EN

Code Review用户
提问于 2018-06-13 18:08:25
回答 1查看 396关注 0票数 9

我创建了一个安全帮助类,试图遵守SP80063B国家标准和技术研究所(NIST)数字身份识别准则规范。这些准则为密码的创建和存储提出了新的规则。简而言之,我的实现涉及以下规则:

密码指南和可用性注意事项

  • 如果订户选择的话,记住的秘密至少要有8个字符。
  • 允许至少64个字符的长度支持密码的使用。鼓励用户使用他们喜欢的任何字符(包括空格),让记忆的秘密尽可能长,从而帮助记忆。
  • 不要将其他组合规则(例如,不同字符类型的混合物)强加在记忆秘密上。
  • 在处理建立和更改记忆秘密的请求时,验证者应将潜在的秘密与包含已知常用、预期或泄露的值的列表进行比较。例如,列表可能包括但不限于:
    • 从先前的入侵身体中获得的密码。
    • 字典词。
    • 重复的或连续的字符(如aaaaaa1234abcd)。
    • 特定于上下文的单词,例如服务的名称、用户名及其派生词。

  • 当选择的密码被拒绝时,提供清晰、有意义和可操作的反馈(例如,当它出现在不可接受密码的“黑名单”上或以前使用过时)。
  • 如果在记忆的秘密中接受Unicode字符,验证器应该使用Unicode标准附件15 [UAX 15]第12.1节中定义的NFKC或NFKD规范化对稳定字符串应用规范化过程。

密码存储指南

  • 记住的秘密应使用适当的单向密钥派生函数进行腌制和散列。密钥派生函数以密码、salt和成本因子作为输入,然后生成密码哈希。
  • 此盐值应由经批准的随机比特生成器[SP 800-90Ar1]生成,并至少提供SP 800-131 A最新版本(112 bits )中指定的最小安全强度。
  • 对于每个订阅者,应使用存储的秘密身份验证器来存储盐值和由此产生的散列。

使用方法

HashSecurePassword()

实现My

  • 密码必须在8到64个字符之间,包括在内。
  • 包含3个或更多顺序重复字符(例如password111)的密码是不允许的。
  • 不允许包含长度为3或更长的重复短语(例如123pass123)的密码。
  • 包含个人可识别信息的密码(如firstnamelastnameemailaddress等)是不允许的。
  • 在普通单词字典或塞利斯特一千万黑名单-密码数据库 (例如Oklahoma$xkaw93fubpq)中发现的密码是不允许的。
  • 没有超出上述条件的密码将返回一个null值和一个自定义out PasswordStatus枚举值,提供有关密码失败状态的详细信息。
  • 超过上述条件的密码被认为是“安全的”,并将使用Argon2 (默认)、sCrypt或bCrypt使用适当的cryptoRNG salt和迭代循环进行散列。
  • 成功的散列密码都有一个字符串编码,可以存储在数据库管理系统中。编码的字符串是由散列技术名称、迭代次数和/或内存开销、base64盐分和base64哈希(例如$argon2i$v=19$m=131072,t=6,p=1$SCvNXMwOaGpX2ZOC+OfjKQ$/hdjThjxp9VY2sFG2KWZDSlh9ZgZXLpKCe8B9BVwaeA)组成的级联。

帮助班:

代码语言:javascript
复制
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;
            }
        }
    }
}

问题/批评/思考Loud

  • 我已经创建了一个枚举值PasswordStatus.OKPendingPersonalInfo,它仍然允许密码被认为是“安全的”,但有一个警告,即没有提供任何个人信息来测试密码。这是否能很好地处理不存在个人信息的有效边缘情况?
  • 代码评审
    • 类-子类对一个大类
    • 适当的字段/方法可访问性
    • enum使用/放置
    • params与显式string[]用于个人信息参数。
    • 多余的评论
    • 异常处理
EN

回答 1

Code Review用户

发布于 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(@“无法确定给定的哈希密码技术”);}

可以重写为向服务提供程序发送。

代码语言:javascript
复制
    public static bool CheckPassword(string plaintextPassword, string encodedPassword)
    {
          if (!plaintextPassword.IsNormalized()) { plaintextPassword = plaintextPassword.Normalize(); }

          this.GetServiceProvider(encodedPassword).Verify(plaintextPassword, encodedPassword);
    }

检索服务提供程序可能会使用现有代码。允许注册其他服务提供商也是一种良好做法。您可以将所有注册的内容存储在字典providers中。HashTechnique可以成为默认技术的方便枚举,但我将允许string上的重载注册其他技术。

代码语言:javascript
复制
protected virtual IPasswordServiceProvider GetServiceProvider(string digest) 
{
    if (digest.StartsWith("$argon2")) {
        return this.providers[PasswordServiceProvider.Argon2];
    // and so on ..
}

服务提供者实现

与其从像Utilities.CheckBCryptHash这样的助手调用代码,不如让每个服务提供者实例实现一个接口并拥有自己的方法。

代码语言:javascript
复制
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, ..);

票数 3
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/196442

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档