全盘加密是指使用加密密钥对 Android 设备上的所有用户数据进行编码的过程。设备加密后,所有用户创建的数据都会在提交到磁盘之前自动加密,并且所有读取操作都会在将数据返回到调用进程之前自动解密数据。
全盘加密在 Android 4.4 中引入,但 Android 5.0 引入了以下新功能
- 创建了快速加密,它仅加密数据分区上已使用的块,以避免首次启动耗时过长。目前只有 ext4 和 f2fs 文件系统支持快速加密。
- 添加了
forceencrypt
fstab 标记以在首次启动时进行加密。 - 添加了对模式和无密码加密的支持。
- 添加了使用可信执行环境 (TEE) 的签名功能(例如 TrustZone 中)对加密密钥进行硬件支持存储的功能。有关详情,请参阅存储加密密钥。
注意:升级到 Android 5.0 然后进行加密的设备可以通过恢复出厂设置返回到未加密状态。在首次启动时加密的新 Android 5.0 设备无法返回到未加密状态。
Android 全盘加密的工作原理
Android 全盘加密基于 dm-crypt
,这是一种在块设备层工作的内核功能。因此,加密适用于嵌入式多媒体卡 (eMMC) 和类似的闪存设备,这些设备以块设备的形式呈现给内核。加密无法用于 YAFFS,后者直接与原始 NAND 闪存芯片对话。
加密算法为 128 位高级加密标准 (AES),采用密码块链接 (CBC) 和 ESSIV:SHA256。主密钥通过调用 OpenSSL 库使用 128 位 AES 进行加密。您必须使用 128 位或更高的密钥(可选 256 位)。
注意:OEM 可以使用 128 位或更高的位数来加密主密钥。
在 Android 5.0 版本中,有四种加密状态
- 默认
- PIN 码
- 密码
- 图案
首次启动时,设备会创建一个随机生成的 128 位主密钥,然后使用默认密码和存储的盐值对其进行哈希处理。默认密码是:“default_password”。但是,生成的哈希值也会通过 TEE(例如 TrustZone)进行签名,后者使用签名的哈希值来加密主密钥。
您可以在 Android 开源项目 cryptfs.cpp 文件中找到定义的默认密码。
当用户在设备上设置 PIN 码/密码或密码时,只会重新加密并存储 128 位密钥。(即,用户 PIN 码/密码/图案更改不会导致用户数据重新加密。)请注意,受管理的设备可能受 PIN 码、图案或密码限制。
加密由 init
和 vold
管理。init
调用 vold
,而 vold 设置属性以触发 init 中的事件。系统的其他部分也会查看这些属性以执行任务,例如报告状态、请求密码或在发生致命错误时提示恢复出厂设置。要在 vold
中调用加密功能,系统使用命令行工具 vdc
的 cryptfs
命令:checkpw
、restart
、enablecrypto
、changepw
、cryptocomplete
、verifypw
、setfield
、getfield
、mountdefaultencrypted
、getpwtype
、getpw
和 clearpw
。
为了加密、解密或擦除 /data
,/data
必须未挂载。但是,为了显示任何用户界面 (UI),框架必须启动,而框架需要 /data
才能运行。为了解决这个难题,临时文件系统挂载在 /data
上。这允许 Android 提示输入密码、显示进度或根据需要建议数据擦除。它确实施加了限制,即为了从临时文件系统切换到真正的 /data
文件系统,系统必须停止每个在临时文件系统上打开文件的进程,并在真正的 /data
文件系统上重启这些进程。为此,所有服务必须属于以下三个组之一:core
、main
和 late_start
。
core
:启动后永不关闭。main
:在输入磁盘密码后关闭然后重启。late_start
:在/data
解密并挂载后才启动。
为了触发这些操作,vold.decrypt
属性设置为各种字符串。为了停止和重启服务,init
命令是
class_reset
:停止服务,但允许使用 class_start 重启。class_start
:重启服务。class_stop
:停止服务并添加SVC_DISABLED
标志。停止的服务不响应class_start
。
流程
加密设备有四种流程。设备只需加密一次,然后遵循正常的启动流程。
- 加密以前未加密的设备
- 使用
forceencrypt
加密新设备:首次启动时的强制加密(从 Android L 开始)。 - 加密现有设备:用户启动的加密(Android K 和更早版本)。
- 使用
- 启动加密设备
- 在没有密码的情况下启动加密设备:启动没有设置密码的加密设备(与运行 Android 5.0 及更高版本的设备相关)。
- 使用密码启动加密设备:启动设置了密码的加密设备。
除了这些流程之外,设备也可能无法加密 /data
。以下详细解释了每个流程。
使用 forceencrypt 加密新设备
这是 Android 5.0 设备的正常首次启动。
- 使用
forceencrypt
标记检测未加密的文件系统/data
未加密,但由于forceencrypt
强制要求,因此需要加密。卸载/data
。 - 开始加密
/data
vold.decrypt = "trigger_encryption"
触发init.rc
,这会导致vold
在没有密码的情况下加密/data
。(由于这应该是一台新设备,因此未设置密码。) - 挂载 tmpfs
vold
挂载 tmpfs/data
(使用来自ro.crypto.tmpfs_options
的 tmpfs 选项)并将属性vold.encrypt_progress
设置为 0。vold
准备 tmpfs/data
以启动加密系统,并将属性vold.decrypt
设置为:trigger_restart_min_framework
- 启动框架以显示进度
由于设备实际上没有要加密的数据,因此进度条不会经常出现,因为加密速度非常快。有关进度 UI 的更多详细信息,请参阅加密现有设备。
- 当
/data
加密后,关闭框架vold
将vold.decrypt
设置为trigger_default_encryption
,这将启动defaultcrypto
服务。(这将启动以下流程以挂载默认加密的用户数据。)trigger_default_encryption
检查加密类型以查看/data
是否使用密码加密。由于 Android 5.0 设备在首次启动时加密,因此不应设置密码;因此,我们解密并挂载/data
。 - 挂载
/data
然后
init
使用从ro.crypto.tmpfs_options
中提取的参数,将/data
挂载到 tmpfs RAMDisk 上,该参数在init.rc
中设置。 - 启动框架
vold
将vold.decrypt
设置为trigger_restart_framework
,这将继续正常的启动过程。
加密现有设备
当您加密已迁移到 L 的未加密 Android K 或更早版本设备时,会发生这种情况。
此过程由用户启动,在代码中称为“就地加密”。当用户选择加密设备时,UI 会确保电池已充满电并且已插入交流电源适配器,以便有足够的电量完成加密过程。
警告:如果设备在完成加密之前耗尽电量并关机,则文件数据将处于部分加密状态。设备必须恢复出厂设置,并且所有数据都将丢失。
为了启用就地加密,vold
启动一个循环来读取真实块设备的每个扇区,然后将其写入加密块设备。vold
在读取和写入扇区之前检查扇区是否正在使用,这使得在新设备上加密速度更快,因为新设备几乎没有数据。
设备状态:设置 ro.crypto.state = "unencrypted"
并执行 on nonencrypted
init
触发器以继续启动。
- 检查密码
UI 使用命令
cryptfs enablecrypto inplace
调用vold
,其中passwd
是用户的锁屏密码。 - 关闭框架
vold
检查错误,如果无法加密,则返回 -1,并在日志中打印原因。如果可以加密,则将属性vold.decrypt
设置为trigger_shutdown_framework
。这会导致init.rc
停止late_start
和main
类中的服务。 - 创建加密页脚
- 创建面包屑文件
- 重启
- 检测面包屑文件
- 开始加密
/data
然后
vold
设置加密映射,这将创建一个虚拟加密块设备,该设备映射到真实块设备,但在写入时加密每个扇区,并在读取时解密每个扇区。vold
然后创建并写出加密元数据。 - 在加密时,挂载 tmpfs
vold
挂载 tmpfs/data
(使用来自ro.crypto.tmpfs_options
的 tmpfs 选项)并将属性vold.encrypt_progress
设置为 0。vold
准备 tmpfs/data
以启动加密系统,并将属性vold.decrypt
设置为:trigger_restart_min_framework
- 启动框架以显示进度
trigger_restart_min_framework
导致init.rc
启动main
类服务。当框架看到vold.encrypt_progress
设置为 0 时,它会启动进度条 UI,该 UI 每五秒查询该属性并更新进度条。每次加密分区的一个百分比时,加密循环都会更新vold.encrypt_progress
。 - 当
/data
加密后,更新加密页脚当
/data
成功加密后,vold
清除元数据中的标志ENCRYPTION_IN_PROGRESS
。当设备成功解锁后,密码将用于加密主密钥,并且加密页脚将更新。
如果由于某种原因重启失败,
vold
会将属性vold.encrypt_progress
设置为error_reboot_failed
,并且 UI 应显示一条消息,要求用户按一个按钮以重启。预计永远不会发生这种情况。
使用默认加密启动加密设备
当您启动没有密码的加密设备时,会发生这种情况。由于 Android 5.0 设备在首次启动时加密,因此不应设置密码,因此这是默认加密状态。
- 检测到加密的
/data
,但没有密码检测到 Android 设备已加密,因为
/data
无法挂载,并且设置了标志encryptable
或forceencrypt
之一。vold
将vold.decrypt
设置为trigger_default_encryption
,这将启动defaultcrypto
服务。trigger_default_encryption
检查加密类型以查看/data
是否使用密码加密。 - 解密 /data
在块设备上创建
dm-crypt
设备,以便设备可以使用。 - 挂载 /data
然后
vold
挂载解密的真实/data
分区,然后准备新分区。它将属性vold.post_fs_data_done
设置为 0,然后将vold.decrypt
设置为trigger_post_fs_data
。这会导致init.rc
运行其post-fs-data
命令。它们创建任何必要的目录或链接,然后将vold.post_fs_data_done
设置为 1。一旦
vold
在该属性中看到 1,它就会将属性vold.decrypt
设置为:trigger_restart_framework。
这会导致init.rc
再次启动main
类中的服务,并首次启动late_start
类中的服务(自启动以来)。 - 启动框架
现在,框架使用解密的
/data
启动其所有服务,并且系统已准备好使用。
在没有默认加密的情况下启动加密设备
当您启动设置了密码的加密设备时,会发生这种情况。设备的密码可以是 PIN 码、图案或密码。
- 检测到具有密码的加密设备
检测到 Android 设备已加密,因为标志
ro.crypto.state = "encrypted"
vold
将vold.decrypt
设置为trigger_restart_min_framework
,因为/data
已使用密码加密。 - 挂载 tmpfs
init
设置五个属性以保存为/data
提供的初始挂载选项,其中参数从init.rc
传递。vold
使用这些属性来设置加密映射ro.crypto.fs_type
ro.crypto.fs_real_blkdev
ro.crypto.fs_mnt_point
ro.crypto.fs_options
ro.crypto.fs_flags
(以 0x 开头的 ASCII 8 位十六进制数)
- 启动框架以提示输入密码
框架启动并看到
vold.decrypt
设置为trigger_restart_min_framework
。这告诉框架它正在 tmpfs/data
磁盘上启动,并且需要获取用户密码。但是,首先,它需要确保磁盘已正确加密。它向
vold
发送命令cryptfs cryptocomplete
。vold
如果加密成功完成,则返回 0;如果发生内部错误,则返回 -1;如果加密未成功完成,则返回 -2。vold
通过在加密元数据中查找CRYPTO_ENCRYPTION_IN_PROGRESS
标志来确定这一点。如果设置了该标志,则表示加密过程已中断,并且设备上没有可用的数据。如果vold
返回错误,则 UI 应向用户显示一条消息,要求用户重启设备并恢复出厂设置,并为用户提供一个按钮以执行此操作。 - 使用密码解密数据
一旦
cryptfs cryptocomplete
成功,框架将显示一个 UI,要求输入磁盘密码。UI 通过向vold
发送命令cryptfs checkpw
来检查密码。如果密码正确(这通过在临时位置成功挂载解密的/data
,然后卸载它来确定),vold
会将解密的块设备的名称保存在属性ro.crypto.fs_crypto_blkdev
中,并向 UI 返回状态 0。如果密码不正确,则向 UI 返回 -1。 - 停止框架
UI 弹出一个加密启动图形,然后使用命令
cryptfs restart
调用vold
。vold
将属性vold.decrypt
设置为trigger_reset_main
,这会导致init.rc
执行class_reset main
。这将停止 main 类中的所有服务,从而允许卸载 tmpfs/data
。 - 挂载
/data
vold
然后挂载解密的真实/data
分区并准备新分区(如果使用擦除选项加密,则可能从未准备好该分区,首次发布时不支持擦除选项)。它将属性vold.post_fs_data_done
设置为 0,然后将vold.decrypt
设置为trigger_post_fs_data
。这会导致init.rc
运行其post-fs-data
命令。它们创建任何必要的目录或链接,然后将vold.post_fs_data_done
设置为 1。当vold
在该属性中看到 1 时,它会将属性vold.decrypt
设置为trigger_restart_framework
。这会导致init.rc
再次启动main
类中的服务,并首次启动late_start
类中的服务(自启动以来)。 - 启动完整框架
现在,框架使用解密的
/data
文件系统启动其所有服务,并且系统已准备好使用。
失败
无法解密的设备可能有几个原因。设备启动时会执行一系列正常的启动步骤
- 检测到具有密码的加密设备
- 挂载 tmpfs
- 启动框架以提示输入密码
但在框架打开后,设备可能会遇到一些错误
- 密码匹配但无法解密数据
- 用户输入错误密码 30 次
如果这些错误未解决,提示用户恢复出厂设置
如果 vold
在加密过程中检测到错误,并且如果尚未破坏任何数据且框架已启动,则 vold
会将属性 vold.encrypt_progress
设置为 error_not_encrypted
。UI 会提示用户重启并告知他们加密过程从未启动。如果错误发生在框架已关闭之后,但在进度条 UI 启动之前,vold
会重启系统。如果重启失败,它会将 vold.encrypt_progress
设置为 error_shutting_down
并返回 -1;但不会有任何东西来捕获该错误。这种情况预计不会发生。
如果 vold
在加密过程中检测到错误,它会将 vold.encrypt_progress
设置为 error_partially_encrypted
并返回 -1。然后,UI 应显示一条消息,说明加密失败,并为用户提供一个按钮来恢复设备出厂设置。
存储加密密钥
加密密钥存储在加密元数据中。硬件支持通过使用可信执行环境 (TEE) 的签名功能来实现。以前,我们使用通过将 scrypt 应用于用户密码和存储的 salt 生成的密钥来加密主密钥。为了使密钥能够抵御盒外攻击,我们通过使用存储的 TEE 密钥对生成的密钥进行签名来扩展此算法。然后,通过再次应用 scrypt 将生成的签名转换为适当长度的密钥。然后,此密钥用于加密和解密主密钥。要存储此密钥
- 生成随机的 16 字节磁盘加密密钥 (DEK) 和 16 字节 salt。
- 将 scrypt 应用于用户密码和 salt,以生成 32 字节的中间密钥 1 (IK1)。
- 用零字节将 IK1 填充到硬件绑定私钥 (HBK) 的大小。具体来说,我们填充为:00 || IK1 || 00..00;一个零字节,32 个 IK1 字节,223 个零字节。
- 使用 HBK 签署填充的 IK1,以生成 256 字节的 IK2。
- 将 scrypt 应用于 IK2 和 salt(与步骤 2 中的 salt 相同),以生成 32 字节的 IK3。
- 使用 IK3 的前 16 个字节作为 KEK,后 16 个字节作为 IV。
- 使用 AES_CBC、密钥 KEK 和初始化向量 IV 加密 DEK。
更改密码
当用户选择在设置中更改或删除其密码时,UI 会将命令 cryptfs changepw
发送到 vold
,并且 vold
会使用新密码重新加密磁盘主密钥。
加密属性
vold
和 init
通过设置属性相互通信。以下是可用于加密的属性列表。
Vold 属性
属性 | 描述 |
---|---|
vold.decrypt trigger_encryption |
不使用密码加密驱动器。 |
vold.decrypt trigger_default_encryption |
检查驱动器以查看是否已使用无密码加密。如果是,则解密并挂载它,否则将 vold.decrypt 设置为 trigger_restart_min_framework。 |
vold.decrypt trigger_reset_main |
由 vold 设置以关闭要求输入磁盘密码的 UI。 |
vold.decrypt trigger_post_fs_data |
由 vold 设置以使用必要的目录等准备 /data 。 |
vold.decrypt trigger_restart_framework |
由 vold 设置以启动真正的框架和所有服务。 |
vold.decrypt trigger_shutdown_framework |
由 vold 设置以关闭完整框架以启动加密。 |
vold.decrypt trigger_restart_min_framework |
由 vold 设置以启动加密进度条 UI 或提示输入密码,具体取决于 ro.crypto.state 的值。 |
vold.encrypt_progress |
当框架启动时,如果设置了此属性,则进入进度条 UI 模式。 |
vold.encrypt_progress 0 to 100 |
进度条 UI 应显示设置的百分比值。 |
vold.encrypt_progress error_partially_encrypted |
进度条 UI 应显示一条消息,说明加密失败,并为用户提供恢复设备出厂设置的选项。 |
vold.encrypt_progress error_reboot_failed |
进度条 UI 应显示一条消息,说明加密已完成,并为用户提供一个按钮来重启设备。预计不会发生此错误。 |
vold.encrypt_progress error_not_encrypted |
进度条 UI 应显示一条消息,说明发生错误,没有数据被加密或丢失,并为用户提供一个按钮来重启系统。 |
vold.encrypt_progress error_shutting_down |
进度条 UI 未运行,因此不清楚谁响应此错误。并且无论如何都不应该发生这种情况。 |
vold.post_fs_data_done 0 |
由 vold 在将 vold.decrypt 设置为 trigger_post_fs_data 之前设置。 |
vold.post_fs_data_done 1 |
由 init.rc 或 init.rc 在完成任务 post-fs-data 后立即设置。 |
init 属性
属性 | 描述 |
---|---|
ro.crypto.fs_crypto_blkdev |
由 vold 命令 checkpw 设置,供 vold 命令 restart 稍后使用。 |
ro.crypto.state unencrypted |
由 init 设置,表示此系统正在未加密的 /data ro.crypto.state encrypted 的情况下运行。由 init 设置,表示此系统正在加密的 /data 的情况下运行。 |
|
当 init 尝试使用从 init.rc 传入的参数挂载 /data 时,会设置这五个属性。vold 使用这些属性来设置加密映射。 |
ro.crypto.tmpfs_options |
由 init.rc 设置,其中包含 init 在挂载 tmpfs /data 文件系统时应使用的选项。 |
init 操作
on post-fs-data on nonencrypted on property:vold.decrypt=trigger_reset_main on property:vold.decrypt=trigger_post_fs_data on property:vold.decrypt=trigger_restart_min_framework on property:vold.decrypt=trigger_restart_framework on property:vold.decrypt=trigger_shutdown_framework on property:vold.decrypt=trigger_encryption on property:vold.decrypt=trigger_default_encryption