22、数据存储安全:SharedPreferences、SQLite数据库、Keychain/Keystore、文件加密分析

说到移动App的数据存储安全,我脑子里立刻浮现出几年前的一个项目。那时候刚入行,觉得数据存到本地就完事了,结果被安全测试报告怼得满头包。嗯,今天咱们就来聊聊这个绕不开的话题——你的App到底该怎么安全地存数据。

22.1 SharedPreferences:别把秘密放这里

SharedPreferences,说白了就是个轻量级的键值对存储。很多新手喜欢用它存用户Token、密码,甚至支付信息。我见过最夸张的,有人把信用卡CVV码直接存进去了。

⚠️ 警告: SharedPreferences 默认以XML格式明文存储在 /data/data/包名/shared_prefs/ 目录下。root过的手机,或者有adb备份权限,直接就能读出来。

那怎么办?我个人习惯是:

  • 只存非敏感配置项,比如界面主题、语言设置
  • 如果非要存敏感数据,至少用AES加密后再存
  • 别用「记住密码」功能,除非你做好了加密

我曾经在一个金融类App里看到,他们把用户登录态直接存成明文。你想想看,这要是被中间人或者恶意应用读到,后果不堪设想。

22.2 SQLite数据库:加密是必修课

SQLite是移动端最常用的关系型数据库。Android和iOS都内置了它。但默认情况下,数据库文件也是明文的。

我记得有一次做逆向分析,拿到一个App的apk,解压后直接找到.db文件,用SQLite浏览器打开,用户聊天记录、通讯录全在里面。说实话,那一刻我挺震惊的——这App的用户量还不少。

22.2.1 Android上的SQLCipher

Android上我推荐用SQLCipher。它是SQLite的加密扩展,支持256位AES加密。用法也很简单:

// 初始化加密数据库
SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase(
    databaseFile, 
    "your_password_here", 
    null
);

// 或者使用Room + SQLCipher
SupportFactory factory = new SupportFactory(
    "your_password_here".getBytes()
);
Room.databaseBuilder(context, AppDatabase.class, "app.db")
    .openHelperFactory(factory)
    .build();
💡 提示: 密码不要硬编码在代码里。我建议从服务器动态获取,或者用KeyStore/Keychain派生。

22.2.2 iOS上的Core Data加密

iOS上,你可以用Data Protection API。只要在Core Data的持久化存储选项中设置NSFileProtectionComplete:

let storeOptions = [
    NSPersistentHistoryTrackingKey: true,
    NSPersistentStoreFileProtectionKey: 
        FileProtectionType.complete
]

try persistentStoreCoordinator.addPersistentStore(
    ofType: NSSQLiteStoreType,
    configurationName: nil,
    at: storeURL,
    options: storeOptions
)

这样数据库文件就会被系统级的文件加密保护。锁屏状态下,谁也读不了。

22.3 Keychain/Keystore:这才是存密钥的地方

很多开发者分不清「加密」和「存密钥」的区别。你加密了数据,但密钥放哪儿?放SharedPreferences?那等于没加密。

Android Keystore和iOS Keychain,就是专门干这个的。它们把密钥放在硬件安全模块(TEE/SE)里,App本身都读不到密钥的明文。

22.3.1 Android Keystore

// 生成密钥
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
    "my_key_alias",
    KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.build();

KeyGenerator keyGenerator = KeyGenerator.getInstance(
    KeyProperties.KEY_ALGORITHM_AES, 
    "AndroidKeyStore"
);
keyGenerator.init(spec);
keyGenerator.generateKey();
🔑 关键点: 密钥一旦生成,就锁在Keystore里了。你只能通过别名引用它,永远拿不到密钥的原始字节。

22.3.2 iOS Keychain

// 存储密钥到Keychain
let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "my_key_alias",
    kSecValueData as String: keyData,
    kSecAttrAccessible as String: 
        kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]

SecItemAdd(query as CFDictionary, nil)

我个人习惯是:所有敏感密钥、Token、证书,一律走Keychain/Keystore。别嫌麻烦,这是底线。

22.4 文件加密:别只依赖系统权限

很多App把文件直接存到外部存储(SD卡)或者App私有目录。但私有目录就安全吗?不一定。root过的设备,或者有adb backup权限,照样能读。

我建议的做法是:

  • 敏感文件必须加密后再写入磁盘
  • 使用AES-GCM模式,带认证标签,防止篡改
  • 密钥从Keychain/Keystore获取,不要硬编码
// Android文件加密示例
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKey key = getKeyFromKeystore("my_key_alias");
cipher.init(Cipher.ENCRYPT_MODE, key);

byte[] iv = cipher.getIV(); // 保存IV,解密时需要
byte[] cipherText = cipher.doFinal(plainText);

// 写入文件时,把IV和密文一起存
FileOutputStream fos = new FileOutputStream(file);
fos.write(iv);
fos.write(cipherText);
fos.close();
⚠️ 注意: 不要用ECB模式,它不安全。不要用固定IV,每次加密都要生成新的随机IV。

22.5 知识体系总览

下面这张图,是我梳理的数据存储安全核心逻辑。你可以把它当成一个检查清单:

移动App数据存储安全体系 SharedPreferences SQLite数据库 Keychain/Keystore 文件加密 风险 明文存储 XML可读 风险 数据库文件可导出 SQL注入 风险 API误用 备份泄露 风险 密钥硬编码 弱加密算法 解决方案 AES加密后存储 仅存非敏感配置 解决方案 SQLCipher加密 参数化查询防注入 解决方案 正确使用API 设置访问控制 解决方案 AES-GCM加密 密钥存Keystore

22.6 避坑指南

最后,分享几个我踩过的坑:

  • 别用「加密」代替「安全」——加密算法选错了,比不加密更可怕。比如用DES、RC4这些老古董。
  • 密钥管理是核心——我曾经见过有人把AES密钥写在Java代码的静态变量里。反编译一下,密钥直接暴露。
  • 别忘了清理内存——加密后的数据用完了,记得把byte[]置空。Java里虽然GC会回收,但敏感数据在内存里停留越久越危险。
  • 日志里别打印敏感数据——这个我犯过。调试时为了方便,把解密后的数据log出来了,结果上线前忘了删。被安全团队抓了个正着。
📌 一句话总结: 数据存储安全,不是选一个技术方案就完事了。它是一个体系——从存储介质、加密算法、密钥管理到代码习惯,环环相扣。任何一个环节出问题,整个安全防线就破了。

好了,今天就聊到这儿。记住,安全不是功能,是习惯。养成好习惯,比学再多技巧都管用。


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