如何处理含不可靠密码的数据库?HIBP SHA1库验证技术问询
我发现了一个名为haveibeenpwned的网站,可检测密码是否已泄露,该网站提供的密码数据库为文本文件,格式示例如下:
000000005AD76BD555C1D6D771DE417A4B87E4B4:10
00000000A8DAE4228F821FB418F59826079BF368:4
00000000DD7F2A1C68A35673713783CA390C9E93:873
该数据库包含数亿个大写的SHA1哈希值。我找到一段生成SHA1哈希的Java代码:
public static String generateSHA1(String message) throws RuntimeException { return hashString(message, "SHA-1"); } private static String hashString(String message, String algorithm) throws RuntimeException { try { MessageDigest digest = MessageDigest.getInstance(algorithm); byte[] hashedBytes = digest.digest(message.getBytes("UTF-8")); return convertByteArrayToHexString(hashedBytes); } catch (NoSuchAlgorithmException | UnsupportedEncodingException ex) { throw new RuntimeException( "Could not generate hash from String", ex); } } private static String convertByteArrayToHexString(byte[] arrayBytes) { StringBuilder stringBuffer = new StringBuilder(); for (byte arrayByte : arrayBytes) { stringBuffer.append(Integer.toString((arrayByte & 0xff) + 0x100, 16) .substring(1)); } return stringBuffer.toString(); }
技术疑问解答
1. 待验证密码生成哈希后需转为十六进制字符串才能与库中哈希对比吗?仅这样就能完成对比吗?
是的,必须转为十六进制字符串才能和库中数据对比。数据库存储的是SHA1哈希的十六进制大写字符串形式,MessageDigest.digest()返回的是原始字节数组,直接对比字节数组也可行,但要和库中字符串匹配,就必须把字节数组转成对应的十六进制字符串,同时要保证大小写一致(库中是大写,生成的字符串也要转成大写),这样就能完成准确对比。
2. 文本中冒号后的数字代表什么含义?
冒号后的数字是该密码哈希在泄露数据集中出现的次数,数字越大说明这个密码被泄露的频次越高,安全性越低。
3. 验证时是否需将密码转为大写再通过Message Digest进行SHA1编码?
不需要。SHA1哈希是对密码的原始字节值计算的,密码本身的大小写会直接影响哈希结果——比如Pass123和pass123的SHA1哈希完全不同。你只需要保证生成的哈希十六进制字符串转为大写即可,无需修改原始密码的大小写。
4. 若生成哈希后需全库对比,能否通过提取哈希前缀缩小搜索范围,用Java实现以降低数据库负载、提升响应速度?
完全可以,这是haveibeenpwned核心的k-匿名性查询思路,具体实现步骤如下:
- 对待验证密码生成SHA1哈希,并转为大写的十六进制字符串;
- 提取哈希的前5个字符作为前缀;
- 从数据库中筛选出所有以该前缀开头的哈希条目(可按前缀分片存储数据库,或给前缀建立索引加速查询);
- 在筛选出的小范围内,对比哈希的剩余部分,判断是否存在匹配项。
Java实现示例思路:
public boolean isPasswordPwned(String password, Set<String> pwnedHashesPrefixGroup) { String sha1Hash = generateSHA1(password).toUpperCase(); String prefix = sha1Hash.substring(0, 5); String suffix = sha1Hash.substring(5); // 遍历前缀匹配的哈希组,对比后缀 for (String hashEntry : pwnedHashesPrefixGroup) { String entrySuffix = hashEntry.split(":")[0].substring(5); if (entrySuffix.equals(suffix)) { return true; } } return false; }
这种方式把全库数亿条的对比缩小到仅数千条的小范围,大幅降低内存占用和查询耗时,同时避免暴露完整密码哈希,兼顾安全性与性能。
内容的提问来源于stack exchange,提问作者skyho

