You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Android离线应用中如何安全存储预定义加密登录密码列表?

如何在离线Android应用中安全存储预定义密码列表并验证

嘿,这个需求我太懂了——既要离线存预定义的登录密码,又得防着别人反编译apk拿到密码,对吧?毕竟明文硬编码等于把密码直接送给逆向工程师。下面我给你几个实用的方案,从易到难,你可以根据自己的开发成本和安全需求选:

方案1:存储密码的加盐哈希值(最推荐,无需解密)

核心思路是:永远不要存储明文或加密后的密码,只存它们的加盐哈希值。验证时,你只需要对用户输入的密码做同样的加盐哈希,然后和预存的哈希列表对比就行——不需要解密,也就不存在密钥泄露的风险。

具体步骤:

  1. 本地预先生成哈希值:写个简单的Java工具类,给每个预定义密码生成加盐后的SHA-256(或更安全的慢哈希算法),把生成的哈希值记下来。
  2. 把哈希值打包进应用:把这些哈希值存到assets目录下的JSON文件里(比如allowed_hashes.json),或者混淆后硬编码在代码里。
  3. 应用内验证逻辑:读取预存的哈希列表,对用户输入的密码做同样的加盐哈希,然后对比是否匹配。

代码示例:

本地生成哈希的工具类:

import java.security.MessageDigest;
import java.util.Base64;

public class HashGenerator {
    // 这里的盐要保密!最好是一个随机生成的长字符串,不要用简单的字符串
    private static final String SALT = "MySuperSecureSalt_12345";

    public static String generateSaltedHash(String password) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            // 先更新盐,再更新密码
            md.update(SALT.getBytes());
            byte[] hashedBytes = md.digest(password.getBytes());
            return Base64.getEncoder().encodeToString(hashedBytes);
        } catch (Exception e) {
            throw new RuntimeException("哈希生成失败", e);
        }
    }

    public static void main(String[] args) {
        // 预先生成你的密码哈希,比如"user123"和"admin456"
        System.out.println(generateSaltedHash("user123"));
        System.out.println(generateSaltedHash("admin456"));
    }
}

应用内的验证代码:

import android.content.Context;
import org.json.JSONArray;
import java.io.InputStream;
import java.util.Scanner;

public class PasswordChecker {
    // 和生成哈希时用的盐完全一致!
    private static final String SALT = "MySuperSecureSalt_12345";

    public static boolean isPasswordValid(Context context, String inputPassword) {
        String inputHash = generateSaltedHash(inputPassword);
        
        try {
            // 读取assets里的哈希列表
            InputStream is = context.getAssets().open("allowed_hashes.json");
            String jsonContent = new Scanner(is).useDelimiter("\\A").next();
            JSONArray hashesArray = new JSONArray(jsonContent);
            
            // 遍历对比
            for (int i = 0; i < hashesArray.length(); i++) {
                String storedHash = hashesArray.getString(i);
                if (storedHash.equals(inputHash)) {
                    return true;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    private static String generateSaltedHash(String password) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            md.update(SALT.getBytes());
            byte[] hashedBytes = md.digest(password.getBytes());
            return android.util.Base64.encodeToString(hashedBytes, android.util.Base64.DEFAULT);
        } catch (Exception e) {
            throw new RuntimeException("哈希生成失败", e);
        }
    }
}

优化点:

  • 不要硬编码盐!可以把盐拆成多个片段,在代码里动态拼接,或者用应用包名+自定义后缀生成(比如String salt = context.getPackageName() + "SecretSuffix123"),增加逆向难度。
  • 改用慢哈希算法(比如Argon2、bcrypt),因为SHA-256太快,容易被暴力破解。可以引入BouncyCastle库来实现这些算法。

方案2:加密密码列表+密钥存Native层(安全性更高)

如果觉得哈希方案还不够安全,可以把密码列表加密后存到应用里,密钥放在NDK的native代码中——逆向native层的.so文件比Java代码难得多。

具体步骤:

  1. 本地加密密码列表:用AES算法加密你的密码列表(比如JSON格式),密钥自己选(AES-128需要16字节,AES-256需要32字节)。
  2. 把加密后的内容存到assets:比如存成encrypted_passwords.txt
  3. Native层提供密钥:写个简单的C++方法返回密钥,编译成.so文件。
  4. 应用内解密并验证:从native层获取密钥,解密密码列表,然后对比用户输入。

代码示例:

Native层代码(C++):

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_your_package_utils_NativeKeyProvider_getAesKey(JNIEnv* env, jobject /* this */) {
    // 这里的密钥要和加密时用的一致,注意混淆native代码!
    std::string key = "MyAesKey_16Bytes";
    return env->NewStringUTF(key.c_str());
}

Java层解密工具类:

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import android.util.Base64;

public class AESDecryptor {
    public static String decrypt(String encryptedData, String key) throws Exception {
        SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, secretKey);
        byte[] decryptedBytes = cipher.doFinal(Base64.decode(encryptedData, Base64.DEFAULT));
        return new String(decryptedBytes);
    }
}

应用内验证代码:

import android.content.Context;
import org.json.JSONArray;
import java.io.InputStream;
import java.util.Scanner;

public class PasswordChecker {
    public static boolean isPasswordValid(Context context, String inputPassword) {
        try {
            // 从native层获取密钥
            String aesKey = NativeKeyProvider.getAesKey();
            
            // 读取加密后的密码列表
            InputStream is = context.getAssets().open("encrypted_passwords.txt");
            String encryptedContent = new Scanner(is).useDelimiter("\\A").next();
            
            // 解密得到密码列表
            String decryptedJson = AESDecryptor.decrypt(encryptedContent, aesKey);
            JSONArray passwordsArray = new JSONArray(decryptedJson);
            
            // 对比用户输入
            for (int i = 0; i < passwordsArray.length(); i++) {
                String storedPassword = passwordsArray.getString(i);
                if (storedPassword.equals(inputPassword)) {
                    return true;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
}

方案3:全Native层验证(安全性最高,开发成本高)

把密码列表的哈希(或加密内容)和验证逻辑都放在native层,Java层只负责把用户输入的密码传给native层,然后接收验证结果。这样即使Java层被反编译,逆向工程师也看不到密码相关的任何逻辑。

核心思路:

  • 在native层预存哈希列表(或加密后的密码列表)。
  • 写一个native方法接收用户输入的密码,做哈希/解密对比,返回布尔值。
  • Java层只调用这个native方法,完全不碰密码逻辑。

这个方案的安全性最高,但需要你掌握NDK开发,并且要做好native代码的混淆和优化。


通用安全建议

  • 开启R8/ProGuard混淆:混淆Java代码,让反编译后的代码变成一堆无意义的变量名,增加逆向难度。
  • 不要用SharedPreferences存敏感内容:SharedPreferences的文件是明文的,很容易被提取。
  • 混淆资源文件:可以给assets里的文件改个奇怪的后缀(比如.dat),或者用工具对文件做简单的字节混淆,让别人一眼看不出是密码列表。
  • 避免调试:发布版本关闭调试模式,防止别人通过调试工具获取内存中的敏感信息。

内容的提问来源于stack exchange,提问作者elstr

火山引擎 最新活动