[asp,net]基于PBKDF2的hash密码存储实现
背景:
这篇文章其实是有前些天看到的一篇叫《如何安全的存储用户密码》的文章;其中讲述了很多方式。比如md5+sha1各种拼接嵌套变种、以及hash+salt的方式;
其实,对于hash+salt存储方式,可能很多人都比较熟悉。但是,其中有一些细节还是需要注意:比如salt如何保持不一致、salt长度不能太短、salt如果和hash后的密码一起存储是否“白搞了“......
个人认为,salt不同人要有不同的salt、这样即使用户习惯性的用123456等作为密码,hash+salt后也不会一样;至少可以增加被破解的难度;(或许是想当然^_~)
今天,抽了点时间搜索了一下相关资源,发现了一篇文章(文末有参考资料),并根据文章和代码测试了一把;在此分享和记录一下;以备后用和分享给需要的人;
盐要使用密码学上可靠安全的伪随机数生成器(Cryptographically Secure Pseudo-Random Number Generator (CSPRNG))来产生。CSPRNG跟普通的伪随机数生成器比如C语言中的rand(),有很大不同。正如它的名字说明的那样,CSPRNG提供一个高标准的随机数,是完全无法预测的。我们不希望我们的盐能够被预测到,所以一定要使用CSPRNG。下表提供了一些常用语言中的CSPRNG。
Platform | CSPRNG |
---|---|
PHP | mcrypt_create_iv, openssl_random_pseudo_bytes |
Java | java.security.SecureRandom |
Dot NET (C#, VB) | System.Security.Cryptography.RNGCryptoServiceProvider,可以更好的生成随机数,不用用Random去玩 |
Ruby | SecureRandom |
Python | os.urandom |
Perl | Math::Random::Secure |
C/C++ (Windows API) | CryptGenRandom |
Any language on GNU/Linux or Unix | Read from /dev/random or /dev/urandom |
/********************************************************* * Copyright(c) 2012-2017 * Author:hager * clrversion:4.0.30319.42000 * description: * 1、https://github.com/defuse/password-hashing * 2、基于PBKDF2的密码加密存储方案 * history: * 1、hager 2017/3/28 - Add * *******************************************************/ using System; using System.Security.Cryptography; namespace Demo.Security { public class PasswordSecurity { class InvalidHashException : Exception { public InvalidHashException() { } public InvalidHashException(string message) : base(message) { } public InvalidHashException(string message, Exception inner) : base(message, inner) { } } class CannotPerformOperationException : Exception { public CannotPerformOperationException() { } public CannotPerformOperationException(string message) : base(message) { } public CannotPerformOperationException(string message, Exception inner) : base(message, inner) { } } public class PasswordStorage { // These constants may be changed without breaking existing hashes. public const int SALT_BYTES = 24; // The number of bytes of salt. By default, 24 bytes, which is 192 bits. This is more than enough. This constant should not be changed. public const int HASH_BYTES = 18; public const int PBKDF2_ITERATIONS = 64000; // PBKDF2 迭代次数,默认32000 // These constants define the encoding and may not be changed. // 对应format: algorithm:iterations:hashSize:salt:hash public const int HASH_ALGORITHM_INDEX = 0; // 加密方式所对应的索引,目前只支持sha1 public const int ITERATION_INDEX = 1; // 迭代次数所对应的索引 public const int HASH_SIZE_INDEX = 2; // hashsize所对应的索引 public const int SALT_INDEX = 3; // salt值所对应的索引 public const int PBKDF2_INDEX = 4; // hash值所对应的索引 public const int HASH_SECTIONS = 5; // 总共存储几段数据;根据分隔符,对应 format中字段 private const string HASH_ALGORITHM = "sha1"; // 加密方式,目前只支持sha1 /// <summary> /// 创建hash+salt后的密码 /// </summary> /// <param name="password">原始密码,比如abcdef</param> /// <returns></returns> public static string CreateHash(string password) { // Generate a random salt byte[] salt = new byte[SALT_BYTES]; try { using (RNGCryptoServiceProvider csprng = new RNGCryptoServiceProvider()) { csprng.GetBytes(salt); } } catch (CryptographicException ex) { throw new CannotPerformOperationException("Random number generator not available.", ex); } catch (ArgumentNullException ex) { throw new CannotPerformOperationException("Invalid argument given to random number generator.", ex); } byte[] hash = PBKDF2(password, salt, PBKDF2_ITERATIONS, HASH_BYTES); // format: algorithm:iterations:hashSize:salt:hash /* * algorithm is the name of the cryptographic hash function ("sha1"). iterations is the number of PBKDF2 iterations ("64000"). hashSize is the length, in bytes, of the hash field (after decoding). salt is the salt, base64 encoded. hash is the PBKDF2 output, base64 encoded. It must encode hashSize bytes. * */ return string.Format("{0}:{1}:{2}:{3}:{4}" , HASH_ALGORITHM , PBKDF2_ITERATIONS , hash.Length , Convert.ToBase64String(salt) , Convert.ToBase64String(hash)); } /// <summary> /// 验证密码是否有效 /// </summary> /// <param name="password">原始密码,比如abcdef</param> /// <param name="goodHash">已存的hash,从DB中读取;format: algorithm:iterations:hashSize:salt:hash</param> /// <returns></returns> public static bool VerifyPassword(string password, string goodHash) { char[] delimiter = {':'}; string[] split = goodHash.Split(delimiter); if (split.Length != HASH_SECTIONS) { throw new InvalidHashException("Fields are missing from the password hash."); } // We only support SHA1 with C#. if (split[HASH_ALGORITHM_INDEX] != HASH_ALGORITHM) { throw new CannotPerformOperationException("Unsupported hash type."); } int iterations = 0; try { iterations = Int32.Parse(split[ITERATION_INDEX]); } catch (ArgumentNullException ex) { throw new CannotPerformOperationException("Invalid argument given to Int32.Parse", ex); } catch (FormatException ex) { throw new InvalidHashException("Could not parse the iteration count as an integer.", ex); } catch (OverflowException ex) { throw new InvalidHashException("The iteration count is too large to be represented.", ex); } if (iterations < 1) { throw new InvalidHashException("Invalid number of iterations. Must be >= 1."); } byte[] salt = null; try { salt = Convert.FromBase64String(split[SALT_INDEX]); } catch (ArgumentNullException ex) { throw new CannotPerformOperationException("Invalid argument given to Convert.FromBase64String", ex); } catch (FormatException ex) { throw new InvalidHashException("Base64 decoding of salt failed.", ex); } byte[] hash = null; try { hash = Convert.FromBase64String(split[PBKDF2_INDEX]); } catch (ArgumentNullException ex) { throw new CannotPerformOperationException("Invalid argument given to Convert.FromBase64String", ex); } catch (FormatException ex) { throw new InvalidHashException("Base64 decoding of pbkdf2 output failed.", ex); } int storedHashSize = 0; try { storedHashSize = Int32.Parse(split[HASH_SIZE_INDEX]); } catch (ArgumentNullException ex) { throw new CannotPerformOperationException("Invalid argument given to Int32.Parse", ex); } catch (FormatException ex) { throw new InvalidHashException("Could not parse the hash size as an integer.", ex); } catch (OverflowException ex) { throw new InvalidHashException("The hash size is too large to be represented.", ex); } if (storedHashSize != hash.Length) { throw new InvalidHashException("Hash length doesn't match stored hash length."); } byte[] testHash = PBKDF2(password, salt, iterations, hash.Length); return SlowEquals(hash, testHash); } private static bool SlowEquals(byte[] a, byte[] b) { uint diff = (uint) a.Length ^ (uint) b.Length; for (int i = 0; i < a.Length && i < b.Length; i++) { diff |= (uint) (a[i] ^ b[i]); } return diff == 0; } private static byte[] PBKDF2(string password, byte[] salt, int iterations, int outputBytes) { using (Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, salt)) { pbkdf2.IterationCount = iterations; return pbkdf2.GetBytes(outputBytes); } } } } }
使用场景:当用户输入账户密码后,根据用户输入的明文password,去DB或者其他存储位置查询已储存的数据,并通过VerifyPassword方法进行校验即可;
[TestClass()] public class PasswordStorageTests { [TestMethod()] public void CreateHashTest() { string hashPassword = PasswordSecurity.PasswordStorage.CreateHash("hager"); // sha1:64000:18:ou2x9eUw82+dnxGetsTaNJLzLXiaElu8:7Ga+qwM4g8Bb/iPOfnQTd6HA // sha1:64000:18:8daBj8djNvUBLE5Y3vRTvMaooydwBfYg:EV9mmvk1oinBGPoufd3aQ85m Assert.IsNotNull(hashPassword); } [TestMethod()] public void VerifyPasswordTest() { bool result = PasswordSecurity.PasswordStorage.VerifyPassword("hager", "sha1:64000:18:ou2x9eUw82+dnxGetsTaNJLzLXiaElu8:7Ga+qwM4g8Bb/iPOfnQTd6HA"); Assert.IsTrue(result); }
需要注意的是:
1、在用户注册、更新密码时,记录CreateHash的结果;用于后续校验;
2、在调用VerifyPassword校验密码时,参数goodHash是由CreateHash生成的结果,并且保持一致的格式和顺序;
扩展:
CryptSharp
CryptSharp provides a number of password crypt algorithms:
- BCrypt,
- LDAP,
- MD5 (and Apache's htpasswd variant),
- PHPass (WordPress, phpBB, Drupal),
- SHA256 and SHA512, and
- Traditional and Extended DES.
Additionally it includes Blowfish, SCrypt, and PBKDF2 for any HMAC (.NET's built-in PBKDF2 implementation supports only SHA-1).
Using CryptSharp is simple. To crypt a password, add the assembly to References and type:
using CryptSharp; // Crypt using the Blowfish crypt ("BCrypt") algorithm. string cryptedPassword = Crypter.Blowfish.Crypt(password);
To test the crypted password against a potential password, use:
using CryptSharp; // Do the passwords match? // You can also check a password using the Crypt method, but this approach way is easier. bool matches = Crypter.CheckPassword(testPassword, cryptedPassword);
You can also specify options:
using CryptSharp; // Specify the $apr1$ Apache htpasswd variant of the MD5 crypt algorithm. string cryptedPassword = Crypter.MD5.Crypt(password, new CrypterOptions() { { CrypterOption.Variant, MD5CrypterVariant.Apache } });
If you choose the BCrypt algorithm, be aware that it only uses the first 72 bytes of a password. CryptSharp uses the ISC license.
如果选择BCrypt算法,请注意,它只使用前72个字节的密码。CryptSharp使用ISC许可证。
BCrypt Downloads
Version 2.1.0 (December 2, 2014)
Version 2.0.0 (May 8, 2013)
Version 1.2.0 (January 23, 2011)
NuGet Package "CryptSharpOfficial"
Online Documentation
参考:
http://www.freebuf.com/articles/web/28527.html
https://github.com/defuse/password-hashing