Android密码库应用解密验证故障求助:SharedPreferences哈希异常
密码库应用密码验证问题排查与解决方案
问题核心
开发Android密码库应用时遇到两个关键问题:
- 解密验证环节,从SharedPreferences读取的存储哈希密码始终为空
- 硬编码哈希密码后,输入相同密码生成的哈希值与硬编码值不一致,验证失败
代码问题分析
1. SharedPreferences存储为空的原因
- 异步存储导致未完成写入:注册时使用
apply()异步存储哈希密码,随后立即调用onBackPressed()跳转页面,可能在存储完成前就进入登录/验证流程,导致读取为空。 - 异常拦截不完整:
storeUserPassword中仅用printStackTrace(),无法直观看到哈希生成或存储时的异常,可能哈希生成失败但未察觉。 - 存储逻辑执行条件:
storeUserPassword在insertUser之后调用,若insertUser抛出异常,会直接进入catch块,跳过存储步骤。
2. 哈希值不一致的核心原因
加盐哈希算法(如PBKDF2、BCrypt)每次哈希会生成随机盐,同一密码每次哈希结果必然不同,直接对比哈希字符串的逻辑完全错误。必须通过提取存储哈希中的盐,重新计算后对比,或使用算法自带的验证方法。
解决方案
解决SharedPreferences存储为空问题
- 改用同步存储:将
apply()改为commit(),确保存储完成后再跳转页面:
private void storeUserPassword(Context context, String password) { try { String hashedPassword = PasswordUtils.hashPassword(password); Log.d("StorePassword", "生成的哈希值: " + hashedPassword); SharedPreferences sharedPreferences = context.getSharedPreferences("MyPrefs", MODE_PRIVATE); // 用commit()同步存储,保证写入完成 boolean isStored = sharedPreferences.edit() .putString("userPassword", hashedPassword) .commit(); // 立即读取验证存储结果 String storedHash = sharedPreferences.getString("userPassword", ""); Log.d("StorePassword", "存储后读取值: " + storedHash + ",存储是否成功: " + isStored); } catch (Exception e) { // 打印完整异常栈,方便排查 Log.e("StorePassword", "存储密码出错", e); } }
- 确认存储逻辑执行:检查注册日志中的
Registration标签,确认insertUser执行成功,storeUserPassword被正常调用。
修正哈希验证逻辑
必须使用加盐哈希对应的验证方法,以下是两种常见实现:
方案1:PBKDF2加盐哈希验证
修改PasswordUtils,实现哈希生成与验证的配对逻辑:
import android.util.Base64; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.security.SecureRandom; import java.util.Arrays; public class PasswordUtils { private static final int ITERATIONS = 10000; private static final int KEY_LENGTH = 256; private static final String ALGORITHM = "PBKDF2WithHmacSHA256"; // 生成带盐的哈希值(盐+哈希拼接存储) public static String hashPassword(String password) throws NoSuchAlgorithmException, InvalidKeySpecException { SecureRandom random = new SecureRandom(); byte[] salt = new byte[16]; random.nextBytes(salt); PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH); SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM); byte[] hash = factory.generateSecret(spec).getEncoded(); return Base64.encodeToString(salt, Base64.DEFAULT) + ":" + Base64.encodeToString(hash, Base64.DEFAULT); } // 验证密码:从存储哈希中提取盐,重新计算对比 public static boolean verifyPassword(String password, String storedHash) throws NoSuchAlgorithmException, InvalidKeySpecException { String[] parts = storedHash.split(":"); if (parts.length != 2) return false; byte[] salt = Base64.decode(parts[0], Base64.DEFAULT); byte[] storedHashBytes = Base64.decode(parts[1], Base64.DEFAULT); PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH); SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM); byte[] computedHash = factory.generateSecret(spec).getEncoded(); return Arrays.equals(computedHash, storedHashBytes); } }
然后修改验证方法:
private boolean verifyPassword(Context context, String enteredPassword) { SharedPreferences sharedPreferences = context.getSharedPreferences("MyPrefs", Context.MODE_PRIVATE); String storedHashedPassword = sharedPreferences.getString("userPassword", ""); if (storedHashedPassword.isEmpty()) { Log.d("PasswordDebug", "存储的哈希值为空"); return false; } try { boolean isMatch = PasswordUtils.verifyPassword(enteredPassword, storedHashedPassword); Log.d("PasswordDebug", "存储哈希: " + storedHashedPassword + ",验证结果: " + isMatch); return isMatch; } catch (Exception e) { Log.e("PasswordDebug", "验证密码出错", e); return false; } }
方案2:BCrypt简化实现
如果使用BCrypt库,逻辑更简洁:
public class PasswordUtils { public static String hashPassword(String password) { return BCrypt.hashpw(password, BCrypt.gensalt()); } public static boolean verifyPassword(String password, String storedHash) { return BCrypt.checkpw(password, storedHash); } }
额外代码问题提示
CryptoUtils中存在加解密逻辑错误,会导致后续解密失败:
- 解密时每次生成新IV:AES/CBC解密必须使用加密时的同一个IV,需将IV与密文一起存储(如拼接在密文前)
- 密钥生成与加解密模式不一致:
generateSecretKey使用GCM模式,但encrypt/decrypt用CBC模式,需统一模式
内容的提问来源于stack exchange,提问作者ArtinM00
相关产品推荐
相关产品推荐

