数据存储安全:SharedPreferences、SQLite、File 的加密与权限控制

大家好,我是你们的老朋友。今天我们来聊聊 Android 数据存储安全。说实话,这块内容看起来基础,但坑特别多。我见过太多应用因为存储不当,导致用户隐私数据裸奔。嗯,咱们今天就把 SharedPreferences、SQLite 和 File 存储的加密与权限控制彻底讲透。

一、SharedPreferences:别把敏感数据当明文存

SharedPreferences 是 Android 最常用的轻量级存储方案。但很多人不知道,它默认是明文存储的。你想想看,如果手机被 root 了,或者有恶意应用通过 ADB 备份,你的 token、密码就全暴露了。

⚠️ 警告:SharedPreferences 默认存储在 /data/data/包名/shared_prefs/ 目录下,文件权限为 660(owner 和 group 可读写)。但 root 用户可以直接读取。

我个人习惯的做法是:敏感数据绝对不用明文存。比如用户登录后的 token、支付密码等,必须加密后再写入。

加密方案:EncryptedSharedPreferences

Android Jetpack 提供了 EncryptedSharedPreferences,它使用 AES256 加密 key,AES256-GCM 加密 value。我建议你直接用它,别自己造轮子。

// 初始化加密的 SharedPreferences
MasterKey masterKey = new MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build();

SharedPreferences sharedPreferences = EncryptedSharedPreferences.create(
        context,
        "secure_prefs",
        masterKey,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);

// 写入加密数据
sharedPreferences.edit().putString("user_token", "your_token_here").apply();

// 读取加密数据
String token = sharedPreferences.getString("user_token", null);
💡 小提示:EncryptedSharedPreferences 的 key 和 value 都加密了。但 key 的加密算法是 AES256-SIV,value 是 AES256-GCM。GCM 模式提供了认证加密,能防止数据被篡改。

我曾经在项目中遇到过一个问题:某个同事把用户的身份证号直接存到普通 SharedPreferences 里。结果测试时发现,通过 adb shell 就能看到明文数据。后来我们全部迁移到了 EncryptedSharedPreferences,才解决了这个隐患。

二、SQLite 数据库:加密与权限控制

SQLite 是 Android 本地数据库的标配。但默认情况下,数据库文件也是明文存储的。如果你的应用存储了聊天记录、通讯录等敏感数据,不加密就等于裸奔。

2.1 数据库文件权限

默认创建的数据库文件权限是 660,只有应用自身和同 UID 的应用可以访问。但如果你使用了 MODE_WORLD_READABLE 或 MODE_WORLD_WRITEABLE(已废弃),那就危险了。

// 错误示例:不要这样做!
SQLiteDatabase db = openOrCreateDatabase("mydb.db", MODE_WORLD_READABLE, null);

// 正确做法:使用默认的 MODE_PRIVATE
SQLiteDatabase db = openOrCreateDatabase("mydb.db", MODE_PRIVATE, null);
🔑 核心原则:数据库文件权限必须设置为 MODE_PRIVATE。如果需要在不同应用间共享数据,请使用 ContentProvider,而不是直接开放文件权限。

2.2 数据库加密:SQLCipher

SQLCipher 是 SQLite 的加密扩展,它使用 AES256 加密整个数据库文件。我建议所有存储敏感数据的应用都使用它。

// 使用 SQLCipher 打开加密数据库
SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase(
        databaseFile,
        "your_password_here",
        null
);

// 创建表
database.execSQL("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, phone TEXT)");

// 插入数据
ContentValues values = new ContentValues();
values.put("name", "张三");
values.put("phone", "13800138000");
database.insert("users", null, values);
⚠️ 注意:SQLCipher 的密码不要硬编码在代码里。我建议从服务器动态获取,或者使用 Android Keystore 系统存储密钥。硬编码密码等于没加密。

我记得有一次做安全审计,发现某个金融类应用把数据库密码写在了 BuildConfig 里。反编译后直接就能拿到密码。嗯,这种错误太低级了,但确实有人犯。

2.3 数据库内容加密

除了整个数据库加密,你还可以对敏感字段单独加密。比如用户的手机号、身份证号,可以在写入前用 AES 加密,读取时再解密。

// 对敏感字段单独加密
public void savePhoneNumber(String phone) {
    String encryptedPhone = AESUtils.encrypt(phone, secretKey);
    ContentValues values = new ContentValues();
    values.put("phone", encryptedPhone);
    database.insert("users", null, values);
}

public String getPhoneNumber(int userId) {
    Cursor cursor = database.query("users", new String[]{"phone"}, "id=?", new String[]{String.valueOf(userId)}, null, null, null);
    if (cursor.moveToFirst()) {
        String encryptedPhone = cursor.getString(0);
        return AESUtils.decrypt(encryptedPhone, secretKey);
    }
    return null;
}

三、File 存储:内部存储 vs 外部存储

文件存储是 Android 最灵活的存储方式,但也是最容易出问题的。我见过太多应用把敏感数据存到外部存储,结果被其他应用随意读取。

3.1 内部存储

内部存储的文件默认只能被当前应用访问。路径是 /data/data/包名/files/。这是最安全的存储位置。

// 写入内部存储
File file = new File(context.getFilesDir(), "secret.txt");
FileOutputStream fos = new FileOutputStream(file);
fos.write("敏感数据".getBytes());
fos.close();

// 读取内部存储
FileInputStream fis = new FileInputStream(file);
byte[] data = new byte[fis.available()];
fis.read(data);
fis.close();

3.2 外部存储

外部存储的文件默认是全局可读的。如果你把敏感数据存到外部存储,任何应用都可以读取。除非你使用 Context.getExternalFilesDir(),它会在外部存储的应用私有目录下创建文件,权限相对安全。

📌 最佳实践:
  • 敏感数据:使用内部存储,并加密文件内容
  • 非敏感数据(如缓存图片):使用外部存储的私有目录
  • 绝对不要:把敏感数据存到外部存储的公共目录(如 Downloads)

3.3 文件加密

对于内部存储的文件,我建议也进行加密。因为如果手机被 root,内部存储的文件同样可以被读取。

// 使用 AES 加密文件内容
public void saveEncryptedFile(Context context, String fileName, String data) {
    try {
        File file = new File(context.getFilesDir(), fileName);
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        SecretKeySpec keySpec = new SecretKeySpec(secretKey, "AES");
        cipher.init(Cipher.ENCRYPT_MODE, keySpec);
        byte[] encryptedData = cipher.doFinal(data.getBytes());
        
        FileOutputStream fos = new FileOutputStream(file);
        fos.write(encryptedData);
        fos.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

四、知识体系总览

下面这张图总结了本章的核心内容。你可以看到,数据存储安全主要围绕三个维度:加密、权限控制和存储位置选择。

Android 数据存储安全体系 SharedPreferences • 默认明文存储 • 使用 EncryptedSP • AES256-GCM 加密 • 避免存储敏感数据 SQLite 数据库 • 文件权限 MODE_PRIVATE • SQLCipher 全库加密 • 敏感字段单独加密 • 密钥存 Keystore • 避免硬编码密码 File 文件存储 • 内部存储最安全 • 外部存储用私有目录 • 文件内容 AES 加密 • 避免公共目录 • 考虑 root 风险 核心原则总结 1. 敏感数据必须加密存储,无论使用哪种存储方式 2. 文件权限严格限制,使用 MODE_PRIVATE 3. 密钥管理使用 Android Keystore,不硬编码

五、避坑指南

最后,我总结几个常见的坑,都是我曾经踩过的:

  • 坑一:把数据库密码写在代码里。反编译后直接暴露。解决方案:从服务器动态获取或使用 Keystore。
  • 坑二:使用 MODE_WORLD_READABLE 共享文件。这等于把数据送给所有应用。解决方案:使用 ContentProvider。
  • 坑三:外部存储存敏感数据。即使使用 getExternalFilesDir(),在 Android 10 以下仍然有风险。解决方案:敏感数据一律用内部存储。
  • 坑四:自己实现加密算法。很多人用简单的 XOR 或 Base64 伪装加密。这跟没加密一样。解决方案:使用成熟的加密库。
💡 我的个人习惯:每次写完存储代码,我都会问自己三个问题:数据加密了吗?权限设置对了吗?密钥安全吗?如果有一个回答是否定的,我就重新改。

好了,今天的内容就到这里。数据存储安全是 Android 安全的基础,但也是最容易被忽视的。希望你能把这些原则应用到实际项目中,别让用户的数据裸奔。


公众号:蓝海资料掘金营,微信deep3321