基于PBKDF2的C#用户哈希密码验证不匹配问题排查
密码哈希验证不匹配问题排查
用户注册时将输入密码传入CreateHash()方法,得到包含Hash和Salt的Dictionary<string, string>,并将二者以varchar(256)类型存储到SQL数据库。用户登录时,从数据库取出Salt,与输入密码一同传入GetHash()方法,但返回的Hash与数据库中存储的不匹配,问题出在以下几点:
原代码
public class EncryptionHelper { public const int SALT_SIZE = 128; public const int HASH_SIZE = 128; public const int ITERATIONS = 100000; // 生成密码哈希和盐 public static Dictionary<string, string> CreateHash(string input) { Dictionary<string, string> hashAndSalt = new Dictionary<string, string>(); RNGCryptoServiceProvider provider = new RNGCryptoServiceProvider(); byte[] salt = new byte[SALT_SIZE]; provider.GetBytes(salt); Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(input, salt, ITERATIONS); hashAndSalt.Add("Hash", Encoding.Default.GetString(pbkdf2.GetBytes(HASH_SIZE))); hashAndSalt.Add("Salt", Encoding.Default.GetString(salt)); return hashAndSalt; } // 验证密码时生成哈希 public static string GetHash(string saltString, string passwordString) { byte[] salt = Encoding.ASCII.GetBytes(saltString); Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(passwordString, salt, ITERATIONS); return Encoding.Default.GetString(pbkdf2.GetBytes(HASH_SIZE)); } }
问题原因
编码规则不匹配:
- 创建哈希时用
Encoding.Default将二进制的salt和hash转成字符串,Encoding.Default是系统依赖编码(比如Windows默认GBK、Linux默认UTF-8);验证时却用Encoding.ASCII把salt字符串转回字节数组,两种编码的字符映射规则完全不同,会导致还原后的salt字节数组和原始salt不一致,最终生成的哈希自然不匹配。
- 创建哈希时用
二进制转字符串的方式错误:
- salt和hash是纯二进制数据,并非合法的文本编码字节,直接用
Encoding类转换会导致数据丢失或损坏。比如部分字节值在ASCII编码中无法表示,会被替换成默认占位符,反向转换时根本还原不出原始字节。
- salt和hash是纯二进制数据,并非合法的文本编码字节,直接用
修正方案
改用Base64编码转换二进制与字符串,Base64专门设计用于安全地将二进制数据转成可存储的字符串,不会丢失任何信息。修正后的代码如下:
public class EncryptionHelper { public const int SALT_SIZE = 128; public const int HASH_SIZE = 128; public const int ITERATIONS = 100000; public static Dictionary<string, string> CreateHash(string input) { Dictionary<string, string> hashAndSalt = new Dictionary<string, string>(); using (var provider = new RNGCryptoServiceProvider()) { byte[] salt = new byte[SALT_SIZE]; provider.GetBytes(salt); using (var pbkdf2 = new Rfc2898DeriveBytes(input, salt, ITERATIONS)) { hashAndSalt.Add("Hash", Convert.ToBase64String(pbkdf2.GetBytes(HASH_SIZE))); hashAndSalt.Add("Salt", Convert.ToBase64String(salt)); } } return hashAndSalt; } public static string GetHash(string saltString, string passwordString) { byte[] salt = Convert.FromBase64String(saltString); using (var pbkdf2 = new Rfc2898DeriveBytes(passwordString, salt, ITERATIONS)) { return Convert.ToBase64String(pbkdf2.GetBytes(HASH_SIZE)); } } }
额外补充:建议用using语句自动释放加密类资源,避免内存泄漏;数据库varchar(256)的长度足够存储Base64编码后的salt和hash(128字节二进制转Base64后约172字符)。
内容的提问来源于stack exchange,提问作者joehelsing
相关产品推荐
相关产品推荐

