Android FBE 文件级加密:CE/DE 存储域与密钥派生
把一台设备的 /data 分区完整 dd 出来塞到另一台同型号机器上开机大概率是白屏或者 App 集体崩溃——这个现象我在早年间折腾刷机时遇到过不止十次。当时第一反应是密钥跟设备绑定但真正把这条链路捋清楚是后来参与企业级移动设备管理方案、需要跟安全团队一起过加密方案评审的时候。Android 上的 FBEFile-Based Encryption文件级加密不是把整盘加密换个说法那么简单它把加密粒度从分区级压到了文件级而粒度一变密钥派生、存储域划分、启动时序、应用适配全都要跟着重写一遍。这篇文章我打算从为什么必须换讲起把 CE/DE 两个存储域的边界、从锁屏 PIN 一路走到文件内容密钥的密钥链条、内核里 fscrypt 的实际落地方式再到怎么在真机上验证和排查讲透。适合正在做系统层适配、搞移动安全方案或者单纯被 Direct Boot 广播坑过的 Android 开发者。1. 为什么整盘加密最终被放弃FDE 绕不开的三个死结1.1 单密钥模型的全有或全无FDEFull-Disk Encryption时代整个 /data 分区在 dm-crypt 上被一把密钥罩住这把密钥由用户凭据派生。听起来很干净但问题就出在这个一把上只要用户在开机时输了密码这把密钥就在内存里整个 /data 就全部可见用户在开机时不输密码整个 /data 就一个字节都读不到。这种二元状态在功能上是很要命的。举个最常见的例子闹钟。如果用户关机充电闹钟要响系统就必须在用户没解锁的情况下读到闹钟配置——但配置存在 /data 里而 /data 锁着。再比如运营商配置、无障碍服务的部分状态、快速关机的状态保存全都是开机早期就需要访问的内容。FDE 时代只能靠各种绕过手段比如把部分数据放到一个独立不加密的小分区里既不优雅也不安全。真正让这把锁彻底不好用的是多用户这个场景。一台平板上可能有家长和小孩两个用户FDE 只有一个密钥解锁了就是全解锁。你想让用户 A 登录时不暴露用户 B 的数据在 FDE 下基本做不到。1.2 解锁前那段时间的数据真空期FDE 的密钥装载时机在 init 的早期靠 vold 去跟用户交互拿密码。这意味着从内核挂载 /data 到用户输入密码之间系统处于一种半残废状态框架层已经起来了但任何需要读 /data 的组件都会失败。我印象最深的一次是排查某个厂商的无障碍服务为什么在开机后十几秒内不工作。抓了 log 才发现它读的配置文件放在 /data 里而那个时间点用户还没解锁。这类问题在 FDE 下是无解的只能把数据往别处搬。而 FBE 之所以能解决是因为它把解锁这件事从整个分区拆成了按存储域和按目录逐个解锁系统可以在开机早期先解锁一小部分数据剩下的等用户来。这里还有个容易被忽略的点FDE 下用户改密码的过程要做一次全盘重新加密实际上 Android 用的是 crypto footer 里的 key blob 重写不是真的重加密数据但如果密钥直接由 PIN 派生就麻烦了。这直接催生了后面要讲的合成密码机制。1.3 迁移到 FBE 之后究竟哪几件事变了换成 FBE 之后变化可以归纳成三条线。第一条是加密粒度。每个文件有自己独立的内容加密密钥从上层主密钥派生而来不是每个文件存一把密钥加密的是文件内容本身元数据由另一层机制保护。这意味着单一文件的密钥泄露不会牵连其他文件也让按目录设策略成为可能。第二条是密钥来源。不再是PIN 直接派生分区密钥而是PIN 经过拉伸后与 TEE 里的随机数混合得到一个合成密码再由合成密码派生主密钥。这个改动把离线暴力破解的门槛从能算 scrypt抬到了必须拿到 TEE 里的 secret安全等级完全是两码事。第三条是启动模型。系统被明确划分成解锁前和解锁后两个阶段应用可以声明自己在解锁前也需要运行directBootAware数据也可以显式选择放在解锁前可访问的域里。功能和安全之间的取舍第一次变成了开发者可以自己决定的选项而不是被架构强制限死。顺带说一句Android 10 之后的新出厂设备基本都被要求默认启用 FBEFDE 已经退出了历史舞台。所以现在再讨论要不要开 FBE意义不大真正有价值的讨论是FBE 开了之后我的数据和代码该怎么摆。2. CE 与 DE两个存储域划出的数据分界线2.1 目录布局哪些路径属于哪个域FBE 最核心的一个概念就是存储域Storage DomainAndroid 分了两个CECredential Encrypted凭据加密和 DEDevice Encrypted设备加密。名字就说明了它们的解锁条件CE 需要用户凭据DE 只要设备开机就行。落到路径上理解这套布局最快的方式是记住后缀。路径存储域解锁时机/data/system_ce/user_idCE用户解锁后/data/system_de/user_idDE开机后立即可用/data/misc_ce/user_idCE用户解锁后/data/misc_de/user_idDE开机后立即可用/data/vendor_ce/user_idCE用户解锁后/data/vendor_de/user_idDE开机后立即可用/data/user/user_id即 /data/dataCE用户解锁后/data/user_de/user_idDE开机后立即可用/data/media/user_idCE用户解锁后这套命名规律其实很省事看到_ce就是解锁后才有看到_de就是开机就有。应用自己的数据默认落在 CE 域也就是/data/user/0/包名这跟老版本的/data/data/包名是同一个位置。如果应用需要解锁前就能跑它的 DE 数据会落在/data/user_de/0/包名。有个地方很多人会搞混/data/media/0到底是什么。在 Android 10 之前它属于 DE之后被移到了 CE也就是说用户在解锁前是读不到内部存储里的照片的——这也解释了为什么有些相机应用在锁屏状态下启动会看不到之前拍的照片。/sdcard只是对/data/media/user_id的 FUSE 映射本质是同一个位置。2.2 从 createDeviceProtectedStorageContext 到 directBootAware应用切换到 DE 存储靠两个 APIManifest 里声明android:directBootAwaretrue代码里调用context.createDeviceProtectedStorageContext()拿到 DE 的 Context。这两件事必须配合着做只在 Manifest 声明但代码没切 Context数据还是写到 CE 去了。一个典型可用的组合长这样// 在 Application.onCreate 里做一次数据迁移判断 val deContext createDeviceProtectedStorageContext() if (!deContext.getSharedPreferences(boot, MODE_PRIVATE) .getBoolean(migrated, false)) { deContext.moveSharedPreferencesFrom(this, boot) deContext.getSharedPreferences(boot, MODE_PRIVATE) .edit().putBoolean(migrated, true).apply() }这段代码的意图是应用从只在解锁后能用升级成解锁前也能用时要把 CE 里的旧数据搬到 DE 一份。moveSharedPreferencesFrom和moveDatabaseFrom是官方给的迁移接口前者只能搬 SharedPreferences后者只能搬数据库其他文件类型得自己读写复制。广播这块也要改。解锁前收到的广播是Intent.ACTION_LOCKED_BOOT_COMPLETED解锁后才能收到Intent.ACTION_BOOT_COMPLETED。如果应用在 Manifest 里注册了BOOT_COMPLETED但没处理LOCKED_BOOT_COMPLETED那开机早期就是一片安静什么也不会发生。2.3 判断一个目录该放哪一域的实操清单我一般用下面这套判断逻辑来给数据分域省得来回改。这条数据在用户没解锁时是否必须被读到必须才考虑 DE。这条数据泄露后会怎样如果是登录 token、聊天记录、健康数据这类敏感信息坚决放 CE甚至要在 CE 之上再加一层应用级加密。这条数据是否可重建闹钟配置、同步计划表这类可重建的放 DE 风险较低不可重建的用户数据一律 CE。是否需要跨用户共享跨用户数据一般放 DE 或按用户拼接路径分别处理。一个常见的错误做法是把为了开机跑得快当成理由把所有数据都塞进 DE。DE 域的密钥在设备开机后自动可用攻击者只要拿到设备并让系统启动就能读到 DE 域里的全部内容。DE 只适合放不敏感但必须早可用的东西。3. 密钥是怎么一层层拧出来的从锁屏 PIN 到文件内容密钥3.1 密钥层级的全貌很多讲 FBE 的文章只说到密钥存在 TEE 里但真正想排查问题得把整条链子摊开看。从下往上大概分五层。第一层是硬件根密钥KEKKey Encryption Key。它由 TEE 里的 KeymasterAndroid 12 之后叫 KeyMint生成是一把不可导出的 AES 密钥永久留在安全环境里物理存储落在 RPMB 这类防重放区域。应用处理器永远看不到它的明文。第二层是每个用户、每个存储域的密钥材料。Android 会为每个用户各生成一套 CE 和 DE 的密钥材料用第一层的 KEK 包住存成 key blob 落在普通文件系统上。第三层是合成密码Synthetic Password。用户设置的 PIN/密码经过拉伸后跟 TEE 保护的一个随机 secret 混合得到的东西就是它。它是解锁第二层的钥匙。第四层是主密钥Master Key。合成密码派生出来的高熵随机数它本身不直接加密文件。第五层才是真正干活的 fscrypt 密钥。内核通过 HKDF 从主密钥派生出一对密钥一把用于文件内容加密的 XTS 密钥一把用于文件名加密的 CTS-CBC 密钥。这两把密钥通过 ioctl 装载进内核之后对应用完全透明。这五层里真正决定能不能解密的是第三层的合成密码。用户忘了 PIN第三层拿不到后面全废。这也是为什么官方刷机或者恢复出厂设置时很多时候数据是真的找不回来的。3.2 合成密码为什么能挡住离线爆破Android 8.0 之前用户 PIN 直接经过 scrypt 拉伸成密钥。这条路有个致命问题攻击者只要拿到 /data 里的数据哪怕只是 key blob就可以离线拿字典暴力尝试 PIN。scrypt 虽然慢但阻止不了几万次尝试尤其很多人用的还是四位 PIN。合成密码机制换了个思路用户 PIN 拉伸后不是直接当密钥而是跟 TEE 里一个 256 位的随机 secret 做一次混合运算结果才作为合成密码。关键在于那个 secret 只存在 TEE 里攻击者就算把整块 eMMC/UFS 拆下来做镜像也拿不到它。这样一来攻击路径就分成了两岔。想暴力试试 PIN就必须让设备真实参与每次尝试想绕过设备直接离线爆破就必须先攻破 TEE。前者可以被硬件熔断计数器和尝试次数限制挡住Android 侧还有 Weaver 这类限流机制后者是硬件安全芯片的核心价值。两重叠加之后四位 PIN 的实际安全性才勉强够看。这套机制的实现是跨进程的Gatekeeper 负责校验用户凭据密钥库负责管理合成密码和主密钥的加解密vold 负责把最终密钥装载给内核。中间任何一个环节的时序搞错都会出现密码对了但数据还是打不开这种非常难查的问题。3.3 fscrypt 的两把刀XTS 加密内容CTS-CBC 加密文件名内核里的 fscrypt 子系统负责具体加解密。它管两种对象文件内容和文件名。文件内容用AES-256-XTS。选 XTS 而不是 GCM 这类带认证的模式主要是出于性能和随机访问的考虑。文件系统上的数据是按块随机读写的GCM 那种需要保持整体认证状态的模式不适用于块级随机访问。XTS 是专门为磁盘加密设计的每个数据单元独立加解密可以并行还能天然抗 IV 复用代价是它不提供完整性认证——所以 fscrypt 在 metadata 层面靠别的机制防篡改。文件名用AES-256-CTS-CBC。你可能在实机上见过一堆A5YBLQ0WZ4W8C...这样的目录名那就是加密后的文件名。CTS 是 CBC 的一种变体解决了 CBC 处理非整块长度数据时的密文扩展问题。Android 11 之后内核也支持 AES-256-HEH 模式来加密文件名但默认还是保持 CTS 以兼容旧设备升级。这个可以通过属性ro.crypto.volume.filenames_mode查。加密后的文件名长度会变长所以文件系统对明文文件名的长度限制会比不加密时更严。这一点在做文件命名时要注意过长的文件名在加密后可能超过 f2fs 的上限导致创建失败。3.4 Nonce 复用为什么致命以及 inode 号是怎么被用上的XTS 这类模式对 nonce 的依赖是绝对不能重复。同一把密钥下两个不同文件如果用了相同的 nonce攻击者可以通过异或把两个明文的关系推导出来实际上等于把这把密钥废了。fscrypt 的解法是用 inode 号参与构造 nonce。每个文件的 inode 号是唯一的再结合逻辑块偏移量就能保证同一密钥下任意文件的任意数据块的 nonce 组合都不重复。这个设计巧妙地避开了每个文件都要存一个 IV的开销。但这里有个隐藏的坑如果对同一个文件做快照、复制或者备份恢复时把 inode 号也一起复制了就会出现 nonce 复用。所以正规的文件系统工具在做快照时会重新分配 inode或者明确知道自己在改加密文件。这也是为什么直接把加密目录 tar 打包再解到同一台机器上这种做法有时候能读有时候报错——取决于解包时 inode 分配情况。对普通应用开发者来说这件事的实际含义是别去手动搬运 /data/data 下的文件。用官方给的备份恢复接口或者让系统去处理。手动 cp 出来的加密文件即使放回原路径也不一定还能被正确解密。4. 内核与存储侧的落地fscrypt、元数据加密与内联引擎4.1 fscrypt 在 ext4/f2fs 上的挂载与密钥装载fscrypt 不是独立的文件系统它是 ext4 和 f2fs 的一个特性层。文件系统挂载时支持 fscrypt 的挂载点会带上inlinecrypt或者 fscrypt 相关的能力标志。每个启用加密的目录都有一个加密策略encryption policy策略里记着这个目录用哪把密钥、什么模式。密钥装载走的是 ioctl 路径。vold 通过FS_IOC_ADD_ENCRYPTION_KEY把一个密钥描述符加进内核之后内核在处理这个目录下的文件读写时就会自动查表、自动加解密。关键点在于这个 ioctl 加进去的是一个密钥标识 密钥材料的组合密钥材料本身又可以是经过硬件包裹的。Android 13 开始支持 hardware-wrapped keys也就是交给内核的不是明文密钥而是 TEE 用自己密钥包过一层的东西内核解密数据时把数据甩给内联引擎由硬件完成加解密。这样即使内核内存被 dump也拿不到 fscrypt 的明文密钥。升级路径上Android 还留了个加密策略版本的概念。老设备用的是 v1 策略新设备出厂就是 v2。v1 不支持某些新特性比如硬件包裹密钥就必须 v2。做系统适配的时候要注意这个差异尤其是做 OTA 后设备可能处于混合状态。4.2 元数据加密补掉的那些漏网信息光有 FBE 还不够因为文件内容加密了但文件的外壳信息没有——比如目录结构、文件大小、修改时间、扩展属性、文件名。这些东西如果明文存储攻击者不需要解密就能画出一张这台设备上有什么应用、用了多少数据、什么时候活跃过的画像。Android 9 引入的元数据加密Metadata Encryption就是补这块的。它用 dm-default-key 在块设备层做一层加密密钥由 TEE 保护开机时自动装载。也就是说即使 FBE 层面用户还没解锁元数据层已经生效了/data 里所有非文件内容的部分都是密文。这两层加密的分工可以这样理解元数据加密是地毯式覆盖把整个 /data 块设备的裸数据全部罩住FBE 是按文件精确打击决定谁能访问哪些文件内容。两层叠加之后攻击者拿到的镜像如果没有 TEE 参与连目录都列不出来。注意元数据加密依赖 TEE 在开机早期可用。如果设备的 TEE 初始化比较慢或者出问题可能出现 /data 挂载失败导致开机卡在第一屏的情况。排查这类问题要先看内核日志里 dm-default-key 的装载信息。4.3 内联加密引擎与性能的关系加密必然带来性能开销这是绕不过去的。FBE 早期靠软件实现时读写速度的下降在实际使用中是能感觉到的尤其是大文件顺序读写和大量小文件随机读写这两种极端场景。内联加密引擎Inline Crypto EngineICE是把加解密逻辑直接做进存储控制器硬件。数据从应用层到闪存颗粒的路上顺手就把密解了CPU 几乎不参与。Android 6 时代 FDE 就开始用 ICE 加速到了 Android 13对设备的要求进一步收紧内联引擎基本成了标配。ICE 有个硬性约束加解密单元必须和存储的块大小对齐。常规设置是按 4KB 对齐所以有些文件系统格式化参数和分区对齐方式要跟着调整。这也是为什么早期有些设备启用加密后某些非对齐的 I/O 会掉到软件路径性能断崖式下跌。从应用开发的角度看这件事的含义是不要写大量非 4KB 对齐的小随机写。加密场景下这类操作的代价比不加密时更高。数据库和日志写入的场景尤其要注意能用批量写就别一条一条刷。4.4 vold 与 vdc 的分工vold 是卷管理守护进程负责挂载各种卷、装载 fscrypt 密钥、管理 ASEC 这类东西。vdc 是它的命令行客户端通过 socket 发命令给 vold。有几个场景会用到 vdcvdc cryptfs enablefilecrypto之类用于启动配置普通开发者基本用不上。排查的时候可以看 vdc 的日志里关于密钥装载的记录。系统集成测试里会用到 vdc 来检查加密状态。对普通应用开发者来说vold 和 vdc 都是系统层的东西不用直接碰。但知道它们的存在有个好处当你在 logcat 里看到vold相关的报错能立刻定位到是卷挂载层面的问题而不是应用层面的问题。我之前遇到过一次应用读不到自己文件的问题logcat 里刷了一屏应用层异常最后发现是 vold 那边 CE 密钥装载失败了根源是用户凭据校验的时序异常。知道分层查起来就快得多。5. 动手确认与排查怎么判断手上的机器真的开了 FBE5.1 几行命令看清楚加密状态判断一台设备用什么加密方式最直接的是看属性adb shell getprop ro.crypto.type # 输出 file → FBE # 输出 block → FDE # 输出 none → 未加密 adb shell getprop ro.crypto.state # encrypted / unencrypted adb shell getprop ro.crypto.volume.filenames_mode # aes-256-cts / aes-256-heh adb shell getprop ro.crypto.dm_default_key.options_format.version # 有值说明启用了元数据加密再看挂载情况adb shell cat /proc/mounts | grep -E fscrypt|default_key如果 /data 挂载参数里能看到inlinecrypt或者相关的 fscrypt 标志说明内核这一侧是通的。还可以直接看目录名。进到/data/system_ce/0或者/data/misc_ce/0下面如果一堆目录和文件名都是A5YBLQ0WZ4W8C...这种长字符串说明文件名加密生效了。需要 root 权限才能看到这层。5.2 用 run-as 和 Device Explorer 观察加密目录普通应用开发者没有 root但可以用run-as看自己应用的数据目录adb shell run-as com.example.app ls -la /data/user/0/com.example.app这条命令之所以有效是因为 run-as 会以应用自身的 uid 运行从而能访问到应用的 CE 存储。前提是设备上装的是 debuggable 的包并且当前用户已经解锁。如果设备还没解锁这条命令会直接报权限错误——这也顺便成了一个判断用户是否已解锁的小技巧。Android Studio 的 Device File Explorer 也是同样的道理。它读不到 CE 目录时先别怀疑工具坏了先确认设备解锁状态。我见过不止一个同事在这个问题上折腾半天。5.3 常见异常与根因对照这套表我建议保存下来遇到报错直接对照着查。现象可能原因排查方向应用在开机后立即可用但读文件抛异常数据放在 CE 域解锁前不可读换成 DE Context 或用 directBootAware收不到 BOOT_COMPLETED只注册了 BOOT_COMPLETED没处理 LOCKED_BOOT_COMPLETED检查 Manifest 的 directBootAware 和广播接收器配置手动 cp 出来的数据无法读取加密文件被搬动后 inode 变化或密钥域不匹配用官方备份恢复接口别手动搬恢复出厂后旧备份的数据打不开备份时的 TEE 密钥与当前设备不匹配数据只在原设备原用户下可解备份方案要包含密钥处理Android Studio 打不开应用目录设备未解锁或应用不可调试解锁设备或改 build 类型为 debuggable落盘性能明显下降大量非对齐小随机写走了软件加解密路径调整 I/O 模式批量写、4K 对齐5.4 我踩过的三个坑第一个坑以为 DE 域数据不需要保护。早期做方案时图方便把配置里的一个同步 token 也放进了 DE 存储理由是开机后马上要用。后来做安全评估评审直接指出这个 token 能拿到用户的云端数据放在开机即可读的域里等于把设备丢了就全丢了。正确做法是把它放 CE然后在 DE 里放一份是否需要立即同步的布尔标志。第二个坑升级时忘了迁移数据。应用从非 directBootAware 改成支持 Direct Boot 时只改 Manifest 不改迁移逻辑结果老用户升级后开机早期的功能全废了——因为数据还在 CE 域新代码却去找 DE 域。上线前一定要用带旧版本数据的设备做一次真实升级测试别只在干净设备上测。第三个坑把 FBE 当成万能防护。有段时间我一度以为数据在 /data 里就安全了。后来才明白FBE 防的是设备丢失后被物理提取数据它挡不住设备已经解锁状态下的恶意应用、挡不住 root 后的提权读取、也挡不住用户自己截图或者手动导出。真正的敏感数据需要在 FBE 之上再叠一层。6. 应用层还要不要自己加密FBE 的能力边界在哪6.1 先把 FBE 的防护范围说清楚FBE 的威胁模型很明确它防的是设备离开用户控制后的物理访问。具体来说就是设备丢了、被偷了、被拿去数据提取而攻击者拿不到用户的解锁凭据——这种情况下 FBE 能让攻击者看到的只有密文。它不防的东西同样明确。设备已经解锁后的任意提权访问、系统被 root 后的读取、设备管理策略允许的远程管理行为、用户主动分享出去的数据这些场景 FBE 都无能为力。更极端一点如果攻击者拿到了设备并且用户采用的是弱 PIN配合硬件侧的尝试次数限制能拖延时间但拖延不等于绝对安全。所以有了 FBE 还需要应用层加密吗这个问题的答案取决于你的威胁模型如果威胁模型里包含设备已解锁状态下的恶意代码或者需要跨设备安全传输数据那应用层加密是必需的如果只关心设备丢失FBE 加一个合理的锁屏策略基本够了。6.2 必须再加一层应用加密的几种典型场景下面这些场景我建议一律加应用级加密别指望 FBE。第一种是高敏数据本身。支付凭证、健康记录、企业机密文档这类数据即使在解锁状态下也不应该以明文落在磁盘上。原因很简单解锁状态是常态不是异常状态。第二种是需要跨设备同步或者云端备份的数据。FBE 的密钥是绑定到具体设备、具体用户、具体存储域的换个设备就解不开。如果数据需要走出设备就必须自己管密钥。第三种是需要防篡改或者防回滚的场景。FBE 的 XTS 模式不提供完整性保护攻击者有物理访问能力时是可以在密文层面做手脚的虽然元数据层会挡一部分。如果业务需要确认这份数据没被改过得自己加 MAC 或者用 AEAD 模式。第四种是需要审计追踪的场景。FBE 是透明加密系统层面不会有谁在什么时候解密了什么的记录。合规要求下的数据访问轨迹得应用自己记。6.3 Jetpack Security 之外的选择一个务实的技术判断Google 官方的 Jetpack Security Crypto 库androidx.security:security-crypto提供了EncryptedFile和EncryptedSharedPreferences早期确实是个方便的入口。但需要说明的是这个库早已经进入停止维护状态官方推荐的路线是直接用 Android Keystore 配合 Tink或者 Tink 的 Java 版本。一个直接用 Keystore 做 AES-GCM 加密的思路大概是这样private const val KEY_ALIAS app_data_key private const val TRANSFORMATION AES/GCM/NoPadding fun getOrCreateKey(): SecretKey { val ks KeyStore.getInstance(AndroidKeyStore).apply { load(null) } (ks.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry)?.let { return it.secretKey } val gen KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStore) gen.init( KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setKeySize(256) .build() ) return gen.generateKey() }这段代码的关键点在于AndroidKeyStore这个 provider。密钥生成之后就存在 TEE 里应用进程里只拿到一个引用即使进程内存被 dump 也拿不到密钥材料本身。这是它比自己生成一个 AES 密钥存到文件里强的地方——后者等于把钥匙和锁放在同一个盒子里。如果要更进一步可以在KeyGenParameterSpec上加setUserAuthenticationRequired(true)这样每次用密钥都需要用户先通过一次认证。代价是每次解密都要弹指纹或者人脸适合对安全性要求极高、交互频率低的场景比如查看保险箱里的文档。6.4 密钥与用户认证绑定的实际影响setUserAuthenticationRequired(true)这个开关一旦打开会带来几个连锁反应必须在设计阶段就想清楚。第一密钥的使用会受认证有效期约束。Android 允许设置setUserAuthenticationValidityDurationSeconds在这个时间窗口内认证一次可以多次使用。窗口设成 0 意味着每次操作都要现场认证设成 30 秒意味着半分钟内不用重复认证。这个参数直接影响用户体验得根据业务频率来定。第二用户改锁屏密码可能导致密钥失效。Keystore 里有一类密钥在用户凭据变化时会自动失效如果业务依赖这类密钥解密历史数据必须在设计时预留重新加密的流程或者在设置里明确告知用户。第三多用户和访客模式下行为不同。不同 Android 用户拥有独立的 Keystore 空间切换用户后拿到的密钥是完全不同的。如果应用有跨用户共享数据的需求这部分数据不能走 Keystore 加密的路径。第四备份和恢复要单独处理。Keystore 里的密钥不能导出所以用这类密钥加密的数据无法被系统备份直接带走。要在备份前把数据解出来换成备份侧能处理的格式恢复时再重新加密。这块最容易在升级或者换机时出问题测试阶段要专门覆盖。我个人在实际项目里的做法是分两级一级密钥固定在 Keystore 里永不变更用来包裹二级的数据密钥DEK真正加密文件的是 DEK。这样改密码、换设备、备份迁移这些场景下只需要处理二级密钥的重新包裹不用把所有历史数据重新加密一遍。虽然多了一层但长期维护的成本会低很多。最后分享一个排查时的小技巧如果你怀疑某个文件是不是被 FBE 加密了把设备连上电脑用adb shell进到对应目录看文件名是否是一长串 base64 风格的乱码。文件名是明文说明这个目录没启用 fscrypt 策略往往是挂载到了错误的存储域或者目录本身不在 /data 的加密范围内。这个判断方法不需要 root也不用装任何工具看名字就够了。