EncryptedSharedPreferences 已死:你应该用什么替代

简介

在Include Security,我们经常将应用代码中处理不当的敏感数据标记为安全与隐私相关的漏洞。虽然大多数安全专业人士都知道明文存储敏感数据的风险,但现代开发中存在一种更微妙的现象:以任何方式将敏感数据写入磁盘。

这种做法深深植根于历史软件工程,早期系统依赖磁盘持久化处理用户会话、软件许可和密码密钥等所有内容。然而,很快人们发现持久磁盘存储带来了持久风险,如取证恢复和未经授权访问,最终促使行业最佳实践尽可能避免使用。

尽管操作系统层面的保护措施如Android的文件加密和进程隔离沙箱技术有所进步,但不将敏感数据写入磁盘的总体原则对信息安全依然至关重要。

关于这些保护措施是否足够存在的误解,常常导致公认最佳实践的倒退,破坏我们过去辛苦建立的安全尽职调查,并使应用程序和系统暴露在影响机密性、完整性和可用性的威胁之下。

本文将探讨为何将数据保存到磁盘会带来不必要的风险,剖析Android存储系统的机制,并为您提供经过验证的保护应用数据的方法。此外,我们还将讨论关于Android的常见误解和误解,包括谷歌目前认为由于依赖设备加密和沙箱等操作系统保护,未加密的内部应用存储无需担心,以及推荐的替代方案,如Jetpack DataStore搭配Google Tink进行加密。

常见的误解与虚假的安全感

即使是经验丰富的开发者,在存储敏感数据到磁盘时也可能产生错误的安全感。关于操作系统保护、存储介质行为以及平台功能(如数据备份)的多种误解,导致团队低估风险并放宽重要安全控制。

在探讨现代存储实践和缓解措施之前,理解这些常见误解的背景至关重要。

操作系统保护被高估:许多开发者认为现代操作系统能为静止数据提供坚不可摧的安全防护。进程隔离和沙箱等功能更像是锁门,对偶尔入侵者有效,但对坚定攻击者却远非万无一失。

谷歌历来持此立场由于设备加密和进程隔离等沙箱技术,未加密的内部应用存储(/data//)是可以接受的。虽然这种观点适用于许多日常普通威胁,但低估了复杂攻击者、升级恶意软件、物理取证分析、供应链风险和备份暴露的权限。

这种高估往往导致第二个更严重的问题:开发团队会抱持“操作系统负责加密和沙箱,所以我们没问题”的思维方式。因此,开发者倾向于放松应用级加密,减少日志和监控控制,并降低数据最小化的优先级。

随着时间推移,这侵蚀了机构知识和来之不易的防御最佳实践,形成了技术债务,这些债务只在事件或审计等后期出现。对于高价值数据(如认证令牌、密码密钥、财务和个人信息信息,如健康数据),平台保护应被视为必要的深度防御层,而非完整且独立的保护。

磁盘存储现实:闪存和机械磁盘扇区内残留物中的磨损平稳化会造成数据删除的虚假信心,因为旧信息可能以意想不到的方式停留。磨损平位化意味着即使你覆盖或删除文件,旧版本的数据仍可能保留在未映射的闪存块中。

通过JTAG等接口获得硬件访问的高级对手可以恢复这些数据。应用级加密(配合正确的密钥管理)比指望操作系统或闪存控制器完美清除数据提供更强的保证。

安卓备份的陷阱:默认备份机制可能会无意中将应用数据暴露到云存储,凸显了明确退出选项的必要性。


未设置 android:allowBackup=“false” 意味着你认为是本地且仅设备可用的敏感数据实际上可能进入云端,或通过本地和远程 ADB 访问访问。

这些误解有助于解释为什么敏感数据仍以明文形式出现在磁盘、系统日志和备份中,而当有更强大安全的替代方案时。在这一背景下,以下章节将探讨Android数据存储指南的演变,以及当敏感数据不可避免地保存到磁盘时,当前最佳实践是什么样子。

安卓数据存储传奇与不断演变的推荐

多年来,安卓上推荐的安全存储敏感值方式是EncryptedSharedPreferences(Jetpack Security Crypto,来自androidx.security:security-crypto)。

该库用加密和Android密钥库包裹了SharedPreferences。然而,EncryptedSharedPreferences和Jetpack Security Crypto库已被弃用。谷歌不再推荐或维护该库用于新开发。

该库提供了一个熟悉的API,类似于SharedPreferences,同时透明地使用谷歌的开源加密库Tink加密数值,并通过Android密钥存储存储密钥。

为什么EncryptedSharedPreferences和Jetpack Security Crypto会被弃用

谷歌对弃用EncryptedSharedPreferences的具体原因一直保持明显沉默。谷歌唯一的官方声明出现在Android密码学文档中,指出Jetpack Security Crypto库中的所有API均在1.1.0版本中被弃用,且无进一步版本计划。

根据谷歌及在线Android开发者社区发布的内容,结合我们的分析解读,这一决定似乎基于以下理由:

  • 不同设备制造商和版本中,EncryptedSharedPreferences与Android密钥库交互存在不一致和限制。
  • 谷歌明确希望开发者迁移到 Jetpack DataStore,作为 SharedPreferences 的唯一现代替代品。SharedPreferences 是同步的,缺乏类型安全,且与协程兼容性不佳。DataStore 被设计为一个长期且稳健的解决方案,提供异步、协程友好的 API 和改进的类型安全。
  • 开发者更倾向于更直接地使用Tink进行加密操作,从而让开发团队对数据转换和存储有更多的控制和可见性。
  • 从谷歌的角度来看,继续维护Jetpack Security Crypto库意味着持续的代码维护负担。该库必须处理不同厂商设备和Android版本之间的Android密钥存储不一致。使用旧的加密共享偏好和现代DataStore和Tink路径维护和支持两种并行存储方式,长期来看是不可持续的。

虽然这些是合理的长期架构决策,但执行过程让许多开发者在长时间内陷入困境,谷歌未能明确指导如何迁移出现已弃用的解决方案。

现代推荐

谷歌目前没有提供一个权威且官方的文档来源,将Jetpack DataStore与Tink结合起来,实现本地安全存储。尽管如此,谷歌目前的立场很明确,尽管迁移路径比以前需要更多工作。

谷歌现在引导开发者:

  • Jetpack DataStore 是现代化、协程友好、类型安全的 SharedPreferences 替代品。
  • Google Tink 用于所有加密操作,包括数据接触磁盘前的认证加密(AEAD),用于短期和长期存储。

这种方法仍然会将数据写入磁盘(即使短期数据缓存也无法避免持久化),但确保数据绝不以明文形式,并结合了强大的加密技术和通过 Android Keystore 实现的硬件支持密钥保护。

作为核心设计原则,如果能避免将敏感数据持久化到本地磁盘,就尽量这样做。必要时从安全后端取数据,或使用短暂内存保留并正确清零系统内存。

当本地持久化不可避免时(例如认证器应用存储密钥材料),先用像Tink这样强大且经过审核的库加密数据。

现代建议的示例实现

本节提供了教育性安卓代码,展示了通过整合Jetpack DataStore实现持久密钥值存储与Google Tink加密和密钥管理,实现现代安全数据存储建议。

目标是以加密形式存储和检索敏感数据(例如“SuperDuperSecretValue123456”),确保即使底层存储被泄露也能保持一定程度的保护。

集成概述

喷气背包数据存储作为存储层。它是 SharedPreferences 的现代替代品,提供异步、线程安全的协程访问键值对。在此示例中,DataStore 用于存储 Base64 编码的加密数据。

叮当:处理密码学操作。它提供了带关联数据的认证加密(AEAD)原语,用于安全实现密码学例程(即加密/解密数据)。密钥通过AndroidKeysetManager管理,该管理器与Android的密钥存储集成,实现密钥存储的安全。

明文数据先用 Tink 加密,然后用 Base64 编码以确保字符串安全存储,然后写入 DataStore。检索则反向处理此过程,先从 DataStore 读取数据,Base64 解码读取数据,然后再用 Tink 解密。

关键设计构造与使用

  • AES-256-GCM通过Tink:提供认证加密(机密性和完整性/真实性合为一)。
  • AndroidKeysetManager 和 Android Keystore:自动密钥生成,支持密钥轮换,并在可用时支持硬件支持的主密钥保护。
  • Base64编码:允许密文安全地以字符串形式存储在DataStore中。
  • 协程友好:所有的输入输出和加密操作都运行在主线程上。
  • 纵深防御:即使DataStore文件被提取,数据依然保持加密和认证。

带有详细攻略的文档示例代码

以下示例用Kotlin编写。该代码已发布至公共领域,用于教育目的。您可以自由使用、修改和分发。

关于字符串与字符[]/字节[]的数据类型说明:对于高度敏感的秘密,更倾向于使用字符[]或字节[],而非字符串数据类型。这使得数据结构中存储的明文值在使用后可以显式地从内存中归零。

Java 和 Kotlin 的字符串数据类型是不可变的,且可能在内存中停留时间超过预期。请参阅我们关于不可变字符串数据类型安全性的相关文章关于字符串与字符[]/字节[]的更深入讨论:

package com.example.android_secure_data_storage


import android.content.Context
import android.os.Bundle
import android.util.Base64
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.google.crypto.tink.Aead
import com.google.crypto.tink.KeyTemplates
import com.google.crypto.tink.aead.AeadConfig
import com.google.crypto.tink.integration.android.AndroidKeysetManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch


// ============================================================================
// SECTION 1: DATASTORE CONFIGURATION
// ============================================================================
private const val LOG_TAG = "SecureStorageDemo"
/**
* Preference file name for secure storage.
*
* This constant defines the name of the DataStore preference file where
* encrypted sensitive data will be stored persistently on the device.
*/
private const val PREFS_NAME = "secure_storage_prefs"
/**
* Extension property for easy access to secure DataStore.
*
* This creates a globally accessible DataStore instance
* that can be accessed from any Context using context.securePrefs.
*
* The [preferencesDataStore] delegate handles:
* - File creation and management
* - Transactional writes
* - Error handling for corrupted data
* - Automatic migrations if needed
*/
val Context.securePrefs: DataStore by preferencesDataStore(PREFS_NAME)


// ============================================================================
// SECTION 2: ENCRYPTED DATA STORAGE OPERATIONS
// ============================================================================


/**
* Creates and configures an AEAD (Authenticated Encryption with Associated Data) primitive.
*
* This function sets up Tink's AEAD encryption by:
* 1. Registering the AEAD configuration with Tink
* 2. Creating an AndroidKeysetManager to handle key creation and management
* 3. Configuring the manager with:
*    - Shared preferences location for keyset storage
*    - AES256_GCM key template (provides both confidentiality and integrity)
*    - Android Keystore URI for master key protection
* 4. Building the keyset handle and extracting the AEAD primitive
*
* AndroidKeysetManager Features:
* - Automatic key generation on first run
* - Key rotation capabilities when needed
* - Secure storage using Android Keystore (hardware-backed if available)
* - Automatic handling of keyset versioning and upgrades
*
* @param context The application context needed for shared preferences access
* @return An AEAD primitive configured with AES256_GCM for authenticated encryption
*/
private fun getAead(context: Context): Aead {
   // Step 1: Register AEAD configuration with Tink (safe to call multiple times)
   AeadConfig.register()


   // Step 2: Build the AndroidKeysetManager with secure key management
   val builder = AndroidKeysetManager.Builder()
       // Location of shared preferences for storing the keyset
       .withSharedPref(context, "aead_keyset", PREFS_NAME)
       // Use AES256_GCM key template (provides both confidentiality and integrity)
       .withKeyTemplate(KeyTemplates.get("AES256_GCM"))
       // Master key URI pointing to Android Keystore for hardware-backed protection
       .withMasterKeyUri("android-keystore://aead_master_key")


   // Step 3: Build the keyset handle
   val keysetHandle = builder.build().keysetHandle


   // Step 4: Extract the AEAD primitive from the keyset handle
   return keysetHandle.getPrimitive(Aead::class.java)
}


// ============================================================================
// SECTION 3: MAIN ACTIVITY WITH SECURE STORAGE DEMONSTRATION
// ============================================================================


/**
* Minimal activity demonstrating secure data storage using Jetpack DataStore and Tink.
*
* This activity shows how to:
* - Set up encrypted preferences using DataStore with Tink AEAD encryption
* - Save sensitive data securely with automatic key management
* - Retrieve and decrypt sensitive data
*
* Security Features Implemented:
* - AES256_GCM authenticated encryption (provides confidentiality + integrity)
* - Automatic key management via AndroidKeysetManager
* - Hardware-backed key storage via Android Keystore (when available)
* - Transactional DataStore writes to prevent data corruption
*
* Use Cases:
* - Storing API keys, authentication tokens, or other sensitive credentials
* - Securely persisting user preferences that contain private information
* - Encrypting local database entries or cached sensitive data
*/
class MainActivity : AppCompatActivity() {


   /**
    * Coroutine scope for background operations.
    *
    * Uses Dispatchers.IO to perform disk I/O operations (DataStore reads/writes)
    * and cryptographic operations without blocking the main thread.
    *
    * This ensures the UI remains responsive during potentially long-running
    * operations like file I/O and encryption/decryption.
    */
   private val scope = CoroutineScope(Dispatchers.IO)


   /**
    * Saves sensitive data to encrypted DataStore.
    *
    * This function performs the following security operations:
    * 1. Obtains an AEAD (Authenticated Encryption with Associated Data) primitive
    * 2. Converts the string value to a bytes
    * 3. Encrypts the plaintext using AES256_GCM (provides confidentiality + integrity)
    * 4. Base64-encodes the encrypted bytes for safe storage
    * 5. Persists the encoded ciphertext to DataStore atomically
    *
    * @param key The preference key used to store the encrypted data
    * @param value The sensitive plaintext string to encrypt and store
    */
   private suspend fun saveSensitiveData(key: Preferences.Key, value: String) {
       // Step 1: Get the AEAD primitive for encryption/decryption
       Log.d(LOG_TAG,"Registering AEAD Configuration with Tink")
       Log.d(LOG_TAG,"Building AndroidKeysetManager")
       val aead = getAead(this@MainActivity)


       // Step 2: Convert plaintext string to UTF-8 bytes
       Log.d(LOG_TAG,"Converting Plaintext String to UTF-8: $value")
       val plaintextBytes = value.toByteArray()
       Log.d(LOG_TAG,"Converted String UTF-8 Bytes: ${plaintextBytes.toHexString()}")


       // Step 3: Encrypt the plaintext using AEAD (no associated data)
       val encrypted = aead.encrypt(plaintextBytes, null)
       Log.d(LOG_TAG,"Encrypted UTF-8 Bytes: ${encrypted.toHexString()}")


       // Step 4: Base64-encode the encrypted bytes for safe storage
       val encoded = Base64.encodeToString(encrypted, Base64.DEFAULT)
       Log.d(LOG_TAG,"Base64 Encoding Encrypted Bytes and Storing output: $encoded")


       // Step 5: Write the encoded ciphertext to DataStore
       securePrefs.edit { prefs -> prefs[key] = encoded }
   }


   /**
    * Retrieves and decrypts sensitive data from DataStore.
    *
    * This function performs the following security operations:
    * 1. Reads the Base64-encoded ciphertext from DataStore
    * 2. Decodes the Base64 string back to encrypted bytes
    * 3. Obtains the AEAD primitive for decryption
    * 4. Decrypts the ciphertext using AES256_GCM (verifies integrity)
    * 5. Converts the decrypted bytes back to a string
    *
    * Note: AEAD decryption automatically verifies the integrity of
    * the ciphertext. If the data has been tampered with, decryption will fail.
    *
    * @param key The preference key where the encrypted data is stored
    * @return The decrypted plaintext string, or null if no data exists for the key
    */
   private suspend fun getSensitiveData(key: Preferences.Key): String? {
       // Step 1: Read the encoded ciphertext from DataStore
       val encoded = securePrefs.data.first()[key]
       Log.d(LOG_TAG,"Retrieving and Decoding Stored Base64 Encoded Bytes: $encoded")


       // Step 2: Check if data exists for this key
       if (encoded == null) return null


       // Step 3: Decode the Base64 string to get encrypted bytes
       val decoded = Base64.decode(encoded, Base64.DEFAULT)
       Log.d(LOG_TAG,"Base64 Decoded Bytes: ${decoded.toHexString()}")


       // Step 4: Get the AEAD primitive for decryption
       val aead = getAead(this@MainActivity)


       // Step 5: Decrypt the ciphertext using AEAD (no associated data)
       Log.d(LOG_TAG,"Accessing AEAD Primitive and Decrypting Decoded Bytes")
       val decrypted = aead.decrypt(decoded, null)


       // Step 6: Convert decrypted bytes back to string and return
       return String(decrypted)
   }


   /**
    * Called when the activity is first created.
    *
    * Initializes the demonstration of secure storage by setting up
    * the cryptographic primitives and performing a save/retrieve cycle.
    */
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)


       // Demonstrate the secure storage functionality
       demonstrateSecureStorage()
   }


   /**
    * Demonstrates saving and retrieving encrypted data from DataStore.
    *
    * This function performs a complete secure storage workflow:
    * 1. Generates a preference key for storing data
    * 2. Saves sensitive test data to encrypted DataStore
    * 3. Retrieves the same data from encrypted DataStore
    * 4. Outputs the retrieved value for verification
    *
    * All operations are performed in a coroutine scope to avoid blocking
    * the main thread during I/O and cryptographic operations.
    */
   private fun demonstrateSecureStorage() {
       // Launch a coroutine for background operations
       Log.d(LOG_TAG,"Starting IncludeSec Secure Storage Demo")
       val secretValue = "SuperDuperSecretValue123456"
       scope.launch {
           // Create the preference key for our sensitive data
           val key = stringPreferencesKey("SuperSecretIncludeSecSauce")


           try {
               // Step 1: Save sensitive data to encrypted DataStore
               Log.d(LOG_TAG,"Securely Storing Secret Value: $secretValue")
               saveSensitiveData(key, "SuperDuperSecretValue123456")


               // Step 2: Retrieve the encrypted data
               val retrieved = getSensitiveData(key)


               // Output the result for verification
               Log.d(LOG_TAG,"Retrieved value: $retrieved")
           } catch (e: Exception) {
               // Handle any errors that occur during the secure storage operations
               e.printStackTrace()
           }
       }
   }
}

示例代码 Logcat 输出

以下logcat输出展示了示例实现的完整执行,com.example.android_secure_data_storage。输出会逐步完成安全存储和检索磁盘敏感数据的完整过程。

在输出中,你可以看到初始化 Tink 和密钥存储支持密钥集、加密明文秘密、对生成的密文进行 Base64 编码、写入 DataStore,然后通过读取、解码和解密来恢复原始秘密值的日志事件。

---------------------------- PROCESS STARTED (26208) for package com.example.android_secure_data_storage ----------------------------

2026-07-06 13:24:19.509 26208-26208 SecureStorageDemo       com...e.android_secure_data_storage  D  
Starting IncludeSec Secure Storage Demo

2026-07-06 13:24:19.513 26208-26224 SecureStorageDemo       com...e.android_secure_data_storage  D  
Securely Storing Secret Value: SuperDuperSecretValue123456

2026-07-06 13:24:19.513 26208-26224 SecureStorageDemo       com...e.android_secure_data_storage  D  
Registering AEAD Configuration with Tink

2026-07-06 13:24:19.513 26208-26224 SecureStorageDemo       com...e.android_secure_data_storage  D  
Building AndroidKeysetManager

2026-07-06 13:24:19.651 26208-26224 SecureStorageDemo       com...e.android_secure_data_storage  D  
Converting Plaintext String to UTF-8: SuperDuperSecretValue123456

2026-07-06 13:24:19.652 26208-26224 SecureStorageDemo       com...e.android_secure_data_storage  D 
 Converted String UTF-8 Bytes: 5375706572447570657253656372657456616c7565313233343536

2026-07-06 13:24:19.653 26208-26224 SecureStorageDemo       com...e.android_secure_data_storage  D  
Encrypted UTF-8 Bytes: 0188f76a9ddf73f11a67fc709f92067c14fad50fa74eb26899a2d3db14fe5b7bd2d7a76114e6e3ba599d297b89e63a37c598ee98165775a71e5208ac

2026-07-06 13:24:19.653 26208-26224 SecureStorageDemo       com...e.android_secure_data_storage  D  
Base64 Encoding Encrypted Bytes and Storing output: AYj3ap3fc/EaZ/xwn5IGfBT61Q+nTrJomaLT2xT+W3vS16dhFObjulmdKXuJ5jo3xZjumBZXdaceUgis

2026-07-06 13:24:19.671 26208-26226 SecureStorageDemo       com...e.android_secure_data_storage  D  
Retrieving and Decoding Stored Base64 Encoded Bytes: AYj3ap3fc/EaZ/xwn5IGfBT61Q+nTrJomaLT2xT+W3vS16dhFObjulmdKXuJ5jo3xZjumBZXdaceUgis

2026-07-06 13:24:19.672 26208-26226 SecureStorageDemo       com...e.android_secure_data_storage  D  
Base64 Decoded Bytes: 0188f76a9ddf73f11a67fc709f92067c14fad50fa74eb26899a2d3db14fe5b7bd2d7a76114e6e3ba599d297b89e63a37c598ee98165775a71e5208ac

2026-07-06 13:24:19.691 26208-26226 SecureStorageDemo       com...e.android_secure_data_storage  D  
Accessing AEAD Primitive and Decrypting Decoded Bytes

2026-07-06 13:24:19.691 26208-26226 SecureStorageDemo       com...e.android_secure_data_storage  D  
Retrieved value: SuperDuperSecretValue123456

关于设备密钥集存储和加密数据的示例

以下adb输出展示了使用这种现代推荐方法时,实际写入磁盘的内容。实现该方法时,应用程序的私有数据目录下会出现两个工件:

  • 一个包含生成的 Tink 密钥集的 SharedPreferences 文件,该密钥集由存储在 Android 密钥库中的主密钥保护。
  • 一个包含敏感数据值base64编码密文的DataStore文件。

值得注意的是,原始明文从未出现在磁盘上。这个例子充分说明,虽然数据会持久化到磁盘,但它们只是通过强健的密码学原语生成的密文形式完成的。

user@HackBookProM4 ~ % adb shell

emu64a:/ # cd data/data/com.example.android_secure_data_storage

emu64a:/data/data/com.example.android_secure_data_storage # cat shared_prefs/secure_storage_prefs.xml                                                   





    1288015871494e371cff16bc71f98437a97f36d008d50801c958abe24c42756076caef087543b06afecb3b6a81ff9fc7b1a652274a8ed182fa53d74ed8092bf30e8b66b86a4836062565414e8dc9921f05a1ef32a187836344075a32e3f6d8b04247173a79a8effd63cb2e1d1fb177d01ebfbdcf261c8b1f96a251aa26a1e10eb66d4b770bffdf304be2e71a4408e495bff00b123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e41657347636d4b6579100118e495bff00b2001



emu64a:/data/data/com.example.android_secure_data_storage # cat files/datastore/secure_storage_prefs.preferences_pbr

SuperSecretIncludeSecSauceT*RAb4PyuSpVa1zrjRNCP8+KCVSYe6TdBlt05XGfsxmPVsUtMFBXvxLTcXP3HGQz2mbJOpJbsETmUzkvIq8

emu64a:/data/data/com.example.android_secure_data_storage #

代码摘要

1. 首次使用时,AndroidKeysetManager生成新的AES-256-GCM密钥集,在Android密钥库中用主密钥保护该密钥,并持久化密钥集元数据(secure_storage_prefs.xml)。

2. 加密:明文→ Tink AEAD.encrypt() → 密文字节→ Base64 → DataStore。

3. 解密:DataStore → Base64 解码→ Tink AEAD.decrypt()(完整性检查)→明文。

4. 密钥轮换:Tink 和 AndroidKeysetManager 在需要时支持密钥轮换。

注:谷歌现提供 androidx.datastore:datastore-tink,提供 AeadSerializer 包装器,实现更简单的集成。上述手动方法依然完全支持、有效、具有教育意义,并让你完全控制整个过程的事件链,从明文和加密数据的转换、密码学原语实例化、执行密码学操作,到数据的存储和检索。例如,请考虑以下伪示例代码,突出 AeadSerializer 实现的简单性:

fun createEncryptedAppSettingsDataStore(context: Context): DataStore {

   val appContext = context.applicationContext

   // --- Step 1: Create keyset with AndroidKeysetManager ---

   // --- *Cache the Keyset handle as a singleton outside of this code in prod

   val keysetHandle = AndroidKeysetManager.Builder()

       .withSharedPref(appContext, "app_settings_keyset", "keyset_prefs")

       .withKeyTemplate(KeyTemplate.createFrom(PredefinedAeadParameters.AES256_GCM))

       .withMasterKeyUri("android-keystore://app_settings_master_key")

       .build()

       .keysetHandle

   // --- Step 2: Create AeadSerializer that wraps your existing serializer ---

   val aeadSerializer = AeadSerializer(

       aead = keysetHandle.getPrimitive(RegistryConfiguration.get(), Aead::class.java),

       wrappedSerializer = ExampleUpdateMeSerializer,

       associatedData = "app_settings.json".encodeToByteArray() //Prevents ciphertext swapping attacks

   )

   // --- Step 3: Build the DataStore using the encrypted serializer ---

   return DataStoreFactory.create(

       produceFile = { File(appContext.filesDir, "app_settings.json") },

       serializer = aeadSerializer,

       corruptionHandler = ReplacementFileCorruptionHandler { AppSettings() },

       scope = CoroutineScope(Dispatchers.IO + SupervisorJob())

   )

}

平台无关的最佳实践(做与不做)

当你必须将敏感数据存储在本地时

  • 使用AEAD加密方案进行加密。
  • 将密钥存储在硬件支持的加密容器中(例如,密钥存储器、密钥链)。
  • 尽量减少存储内容。例如,尽量优先使用短寿命的令牌而非长寿命刷新秘密。这在深度安全上增加了一层防御。
  • 在可行的情况下实施安全删除,并考虑数据保留政策。
  • 定期接受安全评估。

结论

将敏感数据写入磁盘是一种随着攻击复杂性和现代存储介质持久性特性的增加,其风险只会不断增加。虽然操作系统提供了宝贵的防御层,但它们不能替代应用安全的最佳实践。

例如,现实世界的例子不断出现:

CVE-2019-11384详细说明了一个漏洞,即Zalora安卓应用被发现以明文共享偏好存储凭证(即用户名和密码)。最近,CVE-2025-25381也强调了一个类似漏洞,即KSRTC卡纳塔克邦安卓应用也存储了用户凭证以及应用用户特定的个人身份信息(PII)。

更广泛的研究工作持续强化了这一教训。例如,Microsoft的Dirty Stream研究展示了如何利用流行且广泛安装的安卓应用中的路径穿越漏洞来获取私有应用存储(/data/data/)。

在多个实例中,Microsoft 证明这些受影响的应用程序将认证令牌、凭证及其他敏感数据以明文形式写入磁盘,一旦找到允许跨越 Android 沙箱边界的途径,提取变得轻而易举。

我在参与VR领域的重大技术工作时,亲身经历过这种情况。在Android VR应用中发现写入磁盘的敏感数据是很常见的,这也是我首次注意到这种广泛误解和缺乏认知的原因。

通过采用Jetpack DataStore和Tink(或更新的数据存储-Tink集成),Android开发者可以在遵循谷歌当前建议的同时,实现持久化数据的强加密。

最重要的是,在将数据保存到磁盘之前,务必先问一个问题:这些数据是否必须存在于该设备上?

安全性是累积的。你添加的每一层,从数据最小化、磁盘和文件加密、备份退出到最小权限,都构成了防御性安全态势,使利用变得更难。

目标不是完美安全;而是在保持和完善历史安全实践的同时,提高攻击成本,超出对手经济可行的范围。

因此,记得尽量少写入磁盘的敏感数据,并且在写入时,使用强健的密码学结构以确保存储静止时的保护。

参考文献

Android Datastore: https://developer.android.com/jetpack/androidx/releases/datastore

安卓水晶技术: https://developer.android.com/privacy-and-security/cryptography

Java 不可变字符串安全: https://blog.includesecurity.com/2025/11/immutable-strings-in-java-are-your-secrets-still-safe/

什么是TINK?: https://developers.google.com/tink/what-is

AeadSerializer: https://developer.android.com/reference/kotlin/androidx/datastore/tink/AeadSerializer

存储在外部存储中的安卓敏感数据: https://developer.android.com/privacy-and-security/risks/sensitive-data-external-storage
Android 提升应用安全性: https://developer.android.com/privacy-and-security/security-best-practices

CVE-2019-11384(以明文共享偏好存储凭证的 Zalora 应用)): https://nvd.nist.gov/vuln/detail/CVE-2019-11384

CVE-2025-25381(KSRTC应用,以明文共享偏好存储密码和个人信息地址): https://github.com/edwin-0990/CVE_ID/tree/main/CVE-2025-25381

“脏流”攻击:发现并缓解安卓应用中的常见漏洞模式: https://www.microsoft.com/en-us/security/blog/2024/05/01/dirty-stream-attack-discovering-and-mitigating-a-common-vulnerability-pattern-in-android-apps/

本文最初发表于包含安全研究博客.

添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论