Android SharedPreferences的加密包装器
本文解释了为什么以及如何保护您的应用程序设置不被窥探。
引言
本文介绍了一个用于 Android SharedPreferences 接口的封装类,该类为持久化存储和检索基本数据类型的敏感键值对添加了一层加密。
作为一名 Android 开发者,你为什么要关心这个?请继续阅读...
背景
Android 的 SharedPreferences 接口提供了一个通用的框架,允许你访问和修改基本数据类型(布尔值、数字、字符串等)的键值对。这些数据在用户会话之间持久存在,即使你的应用程序被终止也是如此。更多信息,请参阅数据存储开发者指南和 Activity 类参考文档。
默认情况下,Android 会将这些数据以未加密的 XML 文件形式存储在设备文件系统上的应用程序目录中,并设置了仅允许应用程序访问该文件的权限。这是“应用程序沙箱”概念的一部分。所以数据是私有和受保护的,对吧?嗯,并非如此。
如果 Android 设备被 root,其他应用程序(具有 root 权限)可以读取/下载/修改此文件(以及整个文件系统)。更糟糕的是,即使设备未被 root,但如果攻击者获得了物理访问权限,他们也可能能够从中下载所有数据,例如使用 Android Debug Bridge (ADB)。有关更多信息,请参阅 AOSP 安全技术信息。
那么你能做什么?加密数据! 这样,即使攻击者获得了访问权限,数据仍然是不可读和不可修改的。下面介绍的类基于 Google 在 Android Developers Blog 中的建议,提供了一个简单通用的解决方案。
使用代码
你可能已经知道,有 3 种方法可以初始化 SharedPreferences 对象
- Context.getSharedPreferences(String name, int mode)
- Activity.getPreferences(int mode)
- PreferenceManager.getDefaultSharedPreferences(Context context)
本文中的类接受一个 Context 对象,并在内部使用第三种方法,因此你应该这样做,而不是上面的方法(其中 this
是你的应用程序的 Activity、Service 等)
SharedPreferences prefs = SecurePreferences(this);
目前,此类没有接受现有 SharedPreferences 对象用于封装的构造函数。此安全措施的原因在下面的“注意”部分说明,但本文的未来版本可能会提供其他构造函数。
顺便说一句,构造函数还会初始化内存中的加密/解密密钥。
public class SecurePreferences implements SharedPreferences {
private static SharedPreferences sFile;
private static byte[] sKey;
/**
* Constructor.
*
* @param context the caller's context
*/
public SecurePreferences(Context context) {
// Proxy design pattern
if (SecurePreferences.sFile == null) {
SecurePreferences.sFile = PreferenceManager.getDefaultSharedPreferences(context);
}
// Initialize encryption/decryption key
try {
final String key = SecurePreferences.generateAesKeyName(context);
String value = SecurePreferences.sFile.getString(key, null);
if (value == null) {
value = SecurePreferences.generateAesKeyValue();
SecurePreferences.sFile.edit().putString(key, value).commit();
}
SecurePreferences.sKey = SecurePreferences.decode(value);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
...
}
就是这样!除此之外,你还可以像 regular SharedPreferences 对象一样使用这个类。唯一的区别是这个类会透明地加密和解密你提供的键和值。
例如,这是获取和设置字符串的方法
String value = prefs.getString("myKey", "defaultValue");
prefs.edit().putString("myKey", "myValue").commit();
以下是这些调用的实现
@Override
public String getString(String key, String defaultValue) {
final String encryptedValue =
SecurePreferences.sFile.getString(SecurePreferences.encrypt(key), null);
return (encryptedValue != null) ? SecurePreferences.decrypt(encryptedValue) : defaultValue;
}
@Override
public SharedPreferences.Editor putString(String key, String value) {
mEditor.putString(SecurePreferences.encrypt(key), SecurePreferences.encrypt(value));
return this;
}
最后,供你参考
- 此类需要 API 级别 8(Android 2.2,又名“Froyo”)或更高版本
- 上面的示例显示了字符串,但 SharedPreferences 接口的所有其他数据类型也都支持:
boolean
、float
、int
、long
和Set<String>
null
和空字符串值不会被加密
注意
上面的“背景”部分解释了“应用程序沙箱”概念,该概念默认提供了一些隐私。理解此默认行为的详细信息很重要,这样就不会无意中更改它并削弱应用程序的安全性
- SharedPreferences 对象实例应使用
MODE_PRIVATE
标志进行初始化 - SharedPreferences 对象持久化 XML 文件的位置应位于设备的内部存储,而不是 SD 卡上,因为外部存储不强制执行权限
此外,上面介绍的类为普通窥探者的简单攻击提供了重要但仍然不完美的保护。至关重要的是要记住,即使是加密数据,对于高级攻击者来说,仍然可能容易受到攻击,尤其是在 root 或被盗的设备上!
正如 Michael Burton(又名 emmby)在 Stack Overflow 上所写
任何能够访问你的偏好设置文件的攻击者很可能也能访问你应用程序的二进制文件,因此也就有了解密密码的密钥。
进一步改进
关于用户凭据的敏感数据,可参考“Android 安全提示”网页中的建议
尽可能不要将用户名和密码存储在设备上。而是使用用户提供的用户名和密码执行初始身份验证,然后使用短期、特定于服务的授权令牌。
同样来自“同一网页”的另一项建议,适用于通用的敏感数据
为了增加对敏感数据的保护,你可以选择使用应用程序无法直接访问的密钥来加密本地文件。例如,密钥可以放在 KeyStore 中,并用用户密码进行保护,而该密码不存储在设备上。
还有一项来自 Android Developers Blog 的建议
如果你的应用程序需要额外的加密,推荐的方法是要求输入密码或 PIN 码才能访问你的应用程序。此密码可以输入 PBKDF2 以生成加密密钥。
换句话说,一种更安全的方法是拥有一个完全不存储在设备上的秘密 PIN/密码/口令。用户提供它,或者一个远程安全的服务器(这需要互联网连接和更深入的安全评估)。此密钥将由应用程序转换为加密/解密密钥。
这样,即使设备落入不法分子手中,要破解数据也只能通过暴力破解攻击。
关注点
本文讨论了 Android SharedPreferences。如果你想加密 SQLite 数据库,可以查看 SQLCipher。
历史
- 2013年3月27日 - 添加了 2 个新示例(与 Android 的 PreferenceActivity 和 PreferenceFragment 集成),在 SecurePreferences.java 中添加了一些 @TargetApi 注释,并修复了 getAll() 方法以忽略未加密的键/值对中的解密失败
- 2013年2月25日 - 初始修订