You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android密码库应用解密验证故障求助:SharedPreferences哈希异常

密码库应用密码验证问题排查与解决方案

问题核心

开发Android密码库应用时遇到两个关键问题:

  • 解密验证环节,从SharedPreferences读取的存储哈希密码始终为空
  • 硬编码哈希密码后,输入相同密码生成的哈希值与硬编码值不一致,验证失败

代码问题分析

1. SharedPreferences存储为空的原因

  • 异步存储导致未完成写入:注册时使用apply()异步存储哈希密码,随后立即调用onBackPressed()跳转页面,可能在存储完成前就进入登录/验证流程,导致读取为空。
  • 异常拦截不完整:storeUserPassword中仅用printStackTrace(),无法直观看到哈希生成或存储时的异常,可能哈希生成失败但未察觉。
  • 存储逻辑执行条件:storeUserPassword在insertUser之后调用,若insertUser抛出异常,会直接进入catch块,跳过存储步骤。

2. 哈希值不一致的核心原因

加盐哈希算法(如PBKDF2、BCrypt)每次哈希会生成随机盐,同一密码每次哈希结果必然不同,直接对比哈希字符串的逻辑完全错误。必须通过提取存储哈希中的盐,重新计算后对比,或使用算法自带的验证方法。


解决方案

解决SharedPreferences存储为空问题

  1. 改用同步存储:将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);
    }
}
  1. 确认存储逻辑执行:检查注册日志中的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 12:10:59