数据存储安全:SharedPreferences加密、SQLite数据库加密、文件存储权限控制、KeyStore使用
数据存储这块,说实在的,是Android安全里最容易翻车的地方。我见过太多应用直接把密码、Token明文扔进SharedPreferences,或者数据库文件随便让人读。嗯,今天咱们就把这块彻底聊透。
一、SharedPreferences加密:别把钥匙放门口
SharedPreferences本质就是个XML文件,存在/data/data/包名/shared_prefs/目录下。Root过的手机,或者有ADB备份权限的应用,都能直接读。
我个人的习惯是:敏感数据绝不直接存。要么用EncryptedSharedPreferences,要么自己手动加密后再存。
核心思路:加密后再存,解密后再用。密钥不能硬编码在代码里。
方案一:EncryptedSharedPreferences(官方推荐)
// 依赖:implementation 'androidx.security:security-crypto:1.1.0-alpha06'
String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
SharedPreferences sharedPreferences = EncryptedSharedPreferences.create(
"secure_prefs",
masterKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);
// 存
sharedPreferences.edit().putString("token", "eyJhbGciOiJIUzI1NiJ9...").apply();
// 取
String token = sharedPreferences.getString("token", null);
小提示:EncryptedSharedPreferences的Key和Value都会加密。Key用的是AES256-SIV,Value用的是AES256-GCM。GCM模式自带认证,防篡改。
方案二:手动加密(适合自定义场景)
如果你需要兼容旧版本,或者想控制加密细节,可以自己写工具类。但说实话,我建议优先用官方方案,少造轮子。
// 用AES-GCM手动加密
public static String encrypt(String plainText, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] iv = cipher.getIV();
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
// 把IV和密文拼在一起存
ByteBuffer buffer = ByteBuffer.allocate(iv.length + encrypted.length);
buffer.put(iv);
buffer.put(encrypted);
return Base64.encodeToString(buffer.array(), Base64.NO_WRAP);
}
注意:手动加密时,IV(初始化向量)必须随机生成,且每次加密都要不同。IV不需要保密,但必须和密文一起存。
二、SQLite数据库加密:别让数据裸奔
默认的SQLite数据库文件就是个普通文件。adb pull出来,用SQLite浏览器就能直接看。我曾经在项目里发现同事把用户通讯录明文存数据库,吓得我赶紧让他改。
数据库加密,目前主流方案就两个:
- SQLCipher:开源,社区活跃,兼容性好
- Room + SQLCipher:Google官方推荐组合
集成SQLCipher
// 依赖
implementation 'net.zetetic:android-database-sqlcipher:4.5.3'
implementation 'androidx.sqlite:sqlite-ktx:2.3.0'
// 打开加密数据库
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(
databaseFile,
"your_password_here",
null
);
// 配合Room使用
SupportFactory factory = new SupportFactory("your_password_here".getBytes());
Room.databaseBuilder(context, AppDatabase.class, "app.db")
.openHelperFactory(factory)
.build();
关键点:密码不能硬编码。密码应该从KeyStore获取,或者由用户输入(比如应用锁密码)。
避坑指南
我曾经踩过的坑:数据库密码写死在代码里,结果被反编译直接拿到。后来改成从KeyStore读取,每次启动时解密一次,用完就清内存。
三、文件存储权限控制:该锁的门要锁
Android文件存储分内部存储和外部存储。内部存储默认是私有的,但外部存储……嗯,谁都能读。
| 存储类型 | 路径 | 默认权限 | 推荐用途 |
|---|---|---|---|
| 内部存储 | /data/data/包名/ |
仅本应用 | 敏感数据、数据库、SharedPreferences |
| 外部存储(私有) | /storage/emulated/0/Android/data/包名/ |
本应用(Android 10+) | 大文件缓存、下载资源 |
| 外部存储(公共) | /storage/emulated/0/Download/等 |
所有应用 | 用户主动分享的文件 |
内部存储的正确姿势
// 写入内部存储(私有)
File file = new File(context.getFilesDir(), "secret.txt");
FileOutputStream fos = new FileOutputStream(file);
fos.write("敏感数据".getBytes());
fos.close();
// 设置文件权限为仅本应用
file.setReadable(true, false); // 注意:这行其实多余,内部存储默认就是私有的
file.setWritable(true, false);
小技巧:如果你用openFileOutput(),第二个参数传MODE_PRIVATE就行。别用MODE_WORLD_READABLE,这玩意儿在API 17就被废弃了。
外部存储的权限控制
Android 10(API 29)开始,分区存储(Scoped Storage)成了强制要求。应用只能访问自己目录下的文件,以及用户通过SAF(Storage Access Framework)选择的文件。
我的建议:能用内部存储就别用外部存储。实在要用,优先用getExternalFilesDir(),别往公共目录写敏感数据。
四、KeyStore使用:把钥匙交给系统保管
KeyStore是Android系统提供的安全容器,专门用来存密钥。密钥一旦存入KeyStore,就没办法从应用进程里直接读出来。硬件支持的设备,密钥甚至存在TEE(可信执行环境)里。
你想想看,这比你自己把密钥存文件里安全多了吧?
生成并存储密钥
// 生成AES密钥,存入KeyStore
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
);
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.init(spec);
SecretKey key = keyGenerator.generateKey();
从KeyStore读取密钥
// 获取密钥
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
SecretKey key = (SecretKey) keyStore.getKey("my_key_alias", null);
// 用密钥加密
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key);
// ... 后续加密操作
重要:KeyStore里的密钥不能导出。你只能用它来加密解密,拿不到密钥的原始字节。这就是KeyStore的核心安全设计。
KeyStore的硬件支持
如果设备有TEE(比如高通QSEE、TrustZone),密钥操作会在安全环境里执行。就算系统被Root,攻击者也拿不到密钥。
怎么判断?用KeyStore.getInstance().getKey("alias", null)时,如果返回的Key实现类包含KeyStoreKey字样,说明是软件实现。如果包含KeyStoreKey且底层是android.security.keystore.KeyStoreCryptoOperation,说明有硬件支持。
五、整体架构图
下面这张图,是我梳理的数据存储安全整体架构。你看一眼就能明白各层之间的关系。
说白了,数据存储安全就三层:应用层只管读写,加密层负责把数据变成密文,密钥管理层保证密钥安全。最底下是硬件层,提供物理级别的保护。
六、总结与避坑清单
聊了这么多,我总结几个核心原则:
- 敏感数据必须加密——SharedPreferences、数据库、文件,一个都不能漏
- 密钥不能硬编码——用KeyStore存,或者由用户输入
- 权限最小化——能用内部存储就别用外部存储,能用MODE_PRIVATE就别开放权限
- 加密算法选对——AES-GCM比AES-CBC安全,别用ECB模式
- IV必须随机——每次加密生成新的IV,和密文一起存
我曾经犯过的错:有一次把数据库密码存在SharedPreferences里,而且没加密。结果测试机被Root后,直接读到了密码。后来改成从KeyStore获取,每次启动时用用户手势密码解密一次,用完就清掉。嗯,再也没出过问题。
数据存储安全这块,其实不难。关键是要养成习惯——每次存数据前,先问自己一句:这数据被别人看到了会怎样?如果答案是「会出事」,那就加密。
公众号:蓝海资料掘金营,微信deep3321