using System; using System.Collections.Generic; using System.Security; using System.Security.Cryptography; using System.Text; namespace Sloth.Security.Cryptography { /// <summary> /// 为密码的处理提供方法,密码区分大小写。 /// </summary> /// <remarks> /// <para>该密码处理方法提供基于哈希值的加密,因此不能从已经加密的密钥中解密得到原始密码,也就是说对密码的加密不是可逆的。</para> /// <para>内部使用 utf8 对密码字符串进行编码或解码。</para> /// <para>空引用和空字符串是等价的,都表示空密码。空密码加密后仍然会得到长度为 <see cref="CipherBinaryLength"/> (128个字节)的密文。</para> /// </remarks> public static class Password { // RNGCryptoServiceProvider 没有实现 IDisposable 接口,我们可以创建该类的一个静态实例。 private static readonly RNGCryptoServiceProvider RandomNumberGenerator = new RNGCryptoServiceProvider(); /// <summary> /// 加密后密码密文的长度(以字节计算),这是一个只读字段,其值为 128。 /// </summary> public const int CipherBinaryLength = 128; /// <summary> /// 加密后密码密文的长度(以 base64 字符计算),这是一个只读字段,其值为 172。 /// </summary> public const int CipherStringLength = 172; /// <summary> /// 检查指定的密码和指定的密文是否匹配。 /// </summary> /// <param name="password">要检查的密码,空引用和空字符串是等效的。</param> /// <param name="cipher">已经使用 <see cref="Encrypt"/> 方法加密的密码密文(base64 格式)。</param> /// <returns>返回 password 与 cipher 定义的密文一致,则返回 true,否则返回 false。</returns> /// <exception cref="SecurityException">cipher 为空引用或者空字符串,或者 cipher 长度不是 172,或者 cipher 不是合法的 base64 格式。</exception> public static unsafe bool Validate(string password, string cipher) { if (String.IsNullOrEmpty(cipher) || cipher.Length != CipherStringLength) { throw new System.Security.SecurityException(String.Format("无效的密码加密密文:不能为空,且长度应该为 {0}。", CipherStringLength)); } byte[] cipherBinary = null; try { cipherBinary = Convert.FromBase64String(cipher); } catch(Exception exc) { throw new System.Security.SecurityException("无效的密码加密密文:不是一个合法的 base64 字符串。", exc); } if (String.IsNullOrEmpty(password)) { password = ""; } // 取出 cipherBinary 中的后 64 个字节,这 64 个字节就是此前使用的随机 salt 值。 byte[] salt = new byte[64]; Buffer.BlockCopy(cipherBinary, 64, salt, 0, 64); // 使用 salt 加密 password,结果存储在 passwordBinary 中。 byte[] passwordBinary = Encoding.UTF8.GetBytes(password); using (SHA512Managed hash = new SHA512Managed()) { passwordBinary = hash.ComputeHash(passwordBinary); byte[] tempBuffer = new byte[128]; Buffer.BlockCopy(passwordBinary, 0, tempBuffer, 0, 64); Buffer.BlockCopy(salt, 0, tempBuffer, 64, 64); passwordBinary = hash.ComputeHash(tempBuffer); } // 比较 passwordBinary 与 cipherBinary 是否一致。 // 因为后面的 64 个字节的 salt 一样,所以只需要比较前 64 个。 fixed (byte* pCipher = cipherBinary, pPassword = passwordBinary) { byte* pc = pCipher; byte* pp = pPassword; for (int i = 0; i < 64; i++) { if (*pc != *pp) { return false; } pc++; pp++; } } return true; } /// <summary> /// 加密指定的密码。 /// </summary> /// <param name="password">要加密的密码,空字符串与空引用是等价的。</param> /// <returns>返回加密后的密文,结果为 base64 字符串,长度固定为 <see cref="CipherStringLength"/>(172)。</returns> /// <remarks>同一个密码,每一次调用该方法得到的密文是不同的,但是这些不同的结果都可以用于对同一个密码进行验证,请参见 <see cref="Validate"/> 方法。</remarks> public static string Encrypt(string password) { if (String.IsNullOrEmpty(password)) { password = ""; } // 使用 utf8 得到的字符串编码。 byte[] passwordBinary = Encoding.UTF8.GetBytes(password); // 将原始密码的哈希值和 salt 合并,salt 放在最后,总长度将为 128 个字节。 byte[] tempBuffer = new byte[128]; // 创建一个随机的、64 字节的 salt 值。 byte[] salt = new byte[64]; RandomNumberGenerator.GetBytes(salt); using (SHA512Managed hash = new SHA512Managed()) { // 首先计算原始密码的 64 个字节的哈希值。 passwordBinary = hash.ComputeHash(passwordBinary); // 合并哈希值与 salt。 Buffer.BlockCopy(passwordBinary, 0, tempBuffer, 0, 64); Buffer.BlockCopy(salt, 0, tempBuffer, 64, 64); // 重新计算合并后的哈希代码,长度为 64。 passwordBinary = hash.ComputeHash(tempBuffer); } // 再次合并哈希值和 salt,salt 放在最后。 Buffer.BlockCopy(passwordBinary, 0, tempBuffer, 0, 64); Buffer.BlockCopy(salt, 0, tempBuffer, 64, 64); // 将 128 个字节的密文转换为 base64 字符串返回。 return Convert.ToBase64String(tempBuffer); } } }