存储数据对于移动应用程序至关重要,应将尽可能少的敏感数据存储在永久性本地存储中,但是在大多数实际场景中,必须存储某种类型的用户数据。如果敏感数据没有受到应用程序适当保护,就很容易受到攻击。
保护身份验证Token、私人信息和其他敏感数据是移动应用安全的关键。
保存数据的准则概括:公共数据应该对所有人可用,但敏感和私有数据必须受到保护,或者更好的是将敏感数据存储在设备存储之外。
除了保护敏感数据之外,还需要确保从任务存储源读取的数据都应该经过验证。
Android根据用户需求提供了多种数据存储的方法,以下介绍 Android平台常用的几种数据存储方法:
Shared Preferences
SQLite Databases
Firebase Databases
Realm Databases
Internal Storage
External Storage
Keystore
SharedPreferences API通常用于永久保存小的集合,用Key- Value的形式存储。数据存储通过SharedPreferences 对象写入XML文件。SharedPreferences 对象有两种模式 world-readable(所有App都可以访问)和private。误用SharedPreferences API常常会导致敏感数据暴漏。
参考例子:
SharedPreferences sharedPref = getSharedPreferences("key", MODE_WORLD_READABLE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString("username", "administrator");
editor.putString("password", "supersecret");
editor.commit();
一旦代码被执行,key.xml文件将被创建,并存储数据。这段代码违反了几种最佳实践:
username和password以明文的形式存储在/data/data//shared_prefs/key.xml.
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="username">administrator</string>
<string name="password">supersecret</string>
</map>
MODE_WORLD_READABLE allows all applications to access and read the contents of
key.xml.
MODE_WORLD_READABLE 允许所有应用去访问和读取 key.xml 的内容。
root@hermes:/data/data/sg.vp.owasp_mobile.myfirstapp/shared_prefs # ls -la-rw-rw-r-- u0_a118 170 2016-04-23 16:51 key.xml
请注意 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 在 API 17 已被弃用。尽
管较新的设备可能不受此影响,但使用 Android:targetSdkVersion 值小于 17 编译的应用
程序,如果运行在 Android 4.2 之前发布的操作系统版本上运行,则可能会受到影响。
最佳实践
备注: 仅支持minSdkVersion 23及以上。
使用SharedPreferences存储数据是明文的,当我们存储敏感数据的时候需要进行加密,避免敏感数据泄漏。值得庆幸的是 AndroidX Security library被添加,让min-sdk为23+应用使用SharedPreferences存储加密变得容易和方便。
参考:https://bignerdranch.com/blog/encrypting-shared-preferences-with-the-androidx-security-library/
使用说明
首先在module的build.gradle文件中添加依赖。
implementation “androidx.security:security-crypto:1.0.0-alpha02”
备注:点击链接获取最新版本
添加了依赖之后,下一步是在Android KeyStore创建一个加密master key和store。将下面的代码添加到你计划创建EncryptedSharedPreferences实例前面。
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)
我们指定了一个默认的key,AES256_GCM_SPEC,用于创建master key。虽然推荐使用这个规范,如果你需要对如何生成密钥有更多的控制你也可以自定义KeyGenParameterSpec。
最后我们只需要一个EncryptedSharedPreferences实例,它对SharedPreferences进行了包装并且为我们处理所有的加密。不同于SharedPreferences,我们可以从Context#getSharedPreferences或Activity#getPreferences获取,我们需要创建自己的EncryptedSharedPreferences实例。
val sharedPreferences = EncryptedSharedPreferences.create(
"shared_preferences_filename",
masterKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
我们指定了shared preferences的文件名,之前创建的masterKeyAlias,和一个context。最后两个参数是key和value加密的scheme。它们是库提供的唯一的选项。
创建了EncryptedSharedPreferences实例后,可以使用它像SharedPreferences一样读取和存储值。
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)
val sharedPreferences = EncryptedSharedPreferences.create(
"shared_preferences_filename",
masterKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// storing a value
sharedPreferences
.edit()
.putString("some_key", "some_data")
.apply()
// reading a value
sharedPreferences.getString("some_key", "some_default_value") // -> "some_data"
如果使用EncryptedSharedPreferences存储数据,文件内容示例:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="ATP1ABa3NIlOap2c7iNkVaUcQmTocrnpkXl0PyI=">AU+p3hwqCgvlDOtIaawFHWVDf4rFsqghM7ivFTEJesrRp19D+zk7tqsqlGZPLAbryHI=</string>
<string name="__androidx_security_crypto_encrypted_prefs_key_keyset__">12a901802f1a5d2fbc5cd3c9b545a89ca8ace8f125f8e601a8ac51929303ead8a2bbdf5428bd054360b97c1727ef93ef63b64f43ceac92156f3aee9402dd247009d9779571c6ceacfcd4e7123665cc9dd94c44c5c2c6241a8de070d365d94010f8affb6097d4b0fec1c628120a8f901c23caa03d32ecc6ce270e3cc3341e6455b87a80474b3818c3ad678faa4199a9a45078b218c89b8c5a8cbd1780a68b4f8196eb5153b6422df2bdfee6541a44089680d49f03123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e4165735369764b65791001189680d49f032001</string>
<string name="__androidx_security_crypto_encrypted_prefs_value_keyset__">128801da6fdef289b2c6e2933c341b1b3df3b39330671d76df362ba8b0a1d807cdc9d2d4d7bc3062139377e4fa61428f3817c0e368c3196c95fdbcca3c37075e7132abae1fe0f128ceef7278a06a01e0cacf29edc1f3c1c1d37875c27c0cf5d86d0b2bb39efcac84828f664838b77aa4c406028af912e860cad8bff51aca6aaf45167d5ab5c8e57bf05db61a44089cbca7fd04123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e41657347636d4b65791001189cbca7fd042001</string>
</map>
可以看到key和value被加密了并且存储了两个keysets,一个是shared preference的keys另一个是values。Keysets包含加密和解密shared preference数据的key。之前创建的master key用于加密这些keysets,这样它们就可以和提供的数据一起存储在shared preference文件中。
SQLite 数据库(未加密)
SQLite 是一种将数据存储在.db文件中的 SQL 数据库引擎。Android SDK内置了对 SQLite 数据库的支持。用于管理数据库的主要包是android.database.sqlite. 例如,您可以使用以下代码在Activity中存储敏感信息:
Java代码示例:
SQLiteDatabase notSoSecure = openOrCreateDatabase("privateNotSoSecure", MODE_PRIVATE, null);
notSoSecure.e x e cSQL("CREATE TABLE IF NOT EXISTS Accounts(Username VARCHAR, Password VARCHAR);");
notSoSecure.e x e cSQL("INSERT INTO Accounts VALUES('admin','AdminPass');");
notSoSecure.close();
Kotlin代码示例:
var notSoSecure = openOrCreateDatabase("privateNotSoSecure", Context.MODE_PRIVATE, null)
notSoSecure.e x e cSQL("CREATE TABLE IF NOT EXISTS Accounts(Username VARCHAR, Password VARCHAR);")
notSoSecure.e x e cSQL("INSERT INTO Accounts VALUES('admin','AdminPass');")
notSoSecure.close()
一旦Activity被调用,数据库文件privateNotSoSecure将与提供的数据一起创建,并存储在明文文件/data/data//databases/privateNotSoSecure中。
除了SQLite数据库外,数据库的目录可能还包含几个文件:
Journal files: These are temporary files used to implement atomic commit and rollback.
Journal files:用于实现原子提交和回滚的临时文件。
Lock files: The lock files are part of the locking and journaling feature, which was designed to improve SQLite concurrency and reduce the writer starvation problem.
Lock files:锁文件是锁定和日志记录特性的一部分,旨在改善SQLite并发性并减少写入器短缺问题。
敏感信息不应该存储在未加密的SQLite数据库中。
SQLite 数据库(加密)
使用 SQLCipher 库,SQLite 数据库可以通过密码加密。
Java 代码示例:
SQLiteDatabase secureDB = SQLiteDatabase.openOrCreateDatabase(database, "password123", null);
secureDB.e x e cSQL("CREATE TABLE IF NOT EXISTS Accounts(Username VARCHAR,Password VARCHAR);");
secureDB.e x e cSQL("INSERT INTO Accounts VALUES('admin','AdminPassEnc');");
secureDB.close();
Kotlin 代码示例:
var secureDB = SQLiteDatabase.openOrCreateDatabase(database, "password123", null)
secureDB.e x e cSQL("CREATE TABLE IF NOT EXISTS Accounts(Username VARCHAR,Password VARCHAR);")
secureDB.e x e cSQL("INSERT INTO Accounts VALUES('admin','AdminPassEnc');")
secureDB.close()
Secure ways to retrieve the database key i n c l u d e:
检索数据库密钥的安全方法包括:
Asking the user to decrypt the database with a PIN or password once the app is opened (weak passwords and PINs are vulnerable to brute force attacks)
打开应用程序后,要求用户用PIN或密码解密数据库(弱密码和PIN容易受到暴力破解攻击)
Storing the key on the server and allowing it to be accessed from a web service only (so that the app can be used only when the device is online)
将密钥存储在服务器上,并只允许从web服务访问它(因此,应用程序只能在设备在线时使用)
Internal Storage 内部存储
您可以将文件存储在设备的内部存储,文件保存在内部存储默认不能被设备上的其他App访问。如果卸载App,这个文件会被删除。
Java 代码示例:
FileOutputStream fos = null;
try {
fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
fos.write(test.getBytes());
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
Kotlin代码示例:
var fos: FileOutputStream? = null
fos = openFileOutput("FILENAME", Context.MODE_PRIVATE)
fos.write(test.toByteArray(Charsets.UTF_8))
fos.close()
在程序开发过程中,设置file的访问模式为 Context.MODE_PRIVATE。
External Storage 外部存储
每个Android设备都支持共享的外部存储,这个外部存储是可以是可以移动的(如SD卡)或内置的(不能移动的)。文件保存在外部存储,是可以被所有App读取的。当开启 USB 大容量存储时,用户可以修改。
Java代码示例:
File file = new File (Environment.getExternalFilesDir(), "password.txt");
String password = "SecretPassword";
FileOutputStream fos;
fos = new FileOutputStream(file);
fos.write(password.getBytes());
fos.close();
Kotlin代码示例:
val password = "SecretPassword"
val path = context.getExternalFilesDir(null)
val file = File(path, "password.txt")
file.appendText(password)
一旦Activity被调用,文件将被创建,数据将被存储在外部存储中的一个明文文件中。
当用户卸载应用程序时,存储在应用程序文件夹外的文件(data/data//)不会被删除。最后,值得注意的是,攻击者可以使用外部存储来允许在某些情况下对应用程序进行任意控制。
不要将App的敏感数据存储在External Storage。
参考:
OWASP 移动应用安全测试指南
OWASP 移动应用安全验证标准
Android:测试数据存储 - https://github.com/OWASP/owasp-mstg/blob/master/Document/0x05dTesting-Data-Storage.md