全盘加密是使用加密密钥对 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 闪存芯片对话。
加密算法是带有密码块链接 (CBC) 和 ESSIV:SHA256 的 128 高级加密标准 (AES)。主密钥通过调用 OpenSSL 库使用 128 位 AES 进行加密。您必须使用 128 位或更多位作为密钥(256 位是可选的)。
注意: OEM 可以使用 128 位或更高位来加密主密钥。
在Android 5.0版本中,有四种加密状态:
- 默认
- 别针
- 密码
- 图案
首次启动时,设备会创建一个随机生成的 128 位主密钥,然后使用默认密码和存储的盐对其进行哈希处理。默认密码为:“default_password” 但是,生成的哈希值也会通过 TEE(例如 TrustZone)进行签名,TEE 使用签名的哈希值来加密主密钥。
您可以在 Android 开源项目cryptfs.cpp文件中找到定义的默认密码。
当用户在设备上设置 PIN/pass 或密码时,只有 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
。下面详细解释每个流程。
使用forcecrypt加密新设备
这是 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
获取的参数(在init.rc
中设置)将/data
挂载到 tmpfs RAMDisk 上。 - 启动框架
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
是否使用密码加密。 - 解密/数据
在块设备上创建
dm-crypt
设备,以便该设备可供使用。 - 挂载/数据
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
(ASCII 8 位十六进制数字,前面带有 0x)
-
- 启动框架提示输入密码
框架启动并看到
vold.decrypt
设置为trigger_restart_min_framework
。这告诉框架它正在 tmpfs/data
磁盘上启动,并且需要获取用户密码。然而,首先需要确保磁盘已正确加密。它将命令
cryptfs cryptocomplete
发送到vold
。如果加密成功完成,则vold
返回 0;如果发生内部错误,则返回 -1;如果加密未成功完成,则 vold 返回 -2。vold
通过在加密元数据中查找CRYPTO_ENCRYPTION_IN_PROGRESS
标志来确定这一点。如果已设置,则加密过程将被中断,并且设备上没有可用的数据。如果vold
返回错误,UI 应向用户显示一条消息,要求用户重新启动设备并恢复出厂设置,并为用户提供一个按钮来执行此操作。 - 使用密码解密数据
一旦
cryptfs cryptocomplete
成功,框架就会显示一个 UI,要求输入磁盘密码。 UI 通过将命令cryptfs checkpw
发送到vold
来检查密码。如果密码正确(通过在临时位置成功挂载解密的/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
。这将停止主类中的所有服务,从而允许卸载 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 生成的密钥来加密主密钥。为了使密钥能够抵御离机攻击,我们通过使用存储的 TEE 密钥对结果密钥进行签名来扩展此算法。然后,通过另一次 scrypt 应用程序将所得签名转换为适当长度的密钥。然后使用该密钥来加密和解密主密钥。要存储此密钥:
- 生成随机 16 字节磁盘加密密钥 (DEK) 和 16 字节盐。
- 将 scrypt 应用于用户密码和盐以生成 32 字节中间密钥 1 (IK1)。
- 将 IK1 用零字节填充到硬件绑定私钥 (HBK) 的大小。具体来说,我们填充为:00 || IK1 || 00..00; 1 个零字节、32 个 IK1 字节、223 个零字节。
- 用 HBK 对 IK1 进行符号填充,生成 256 字节的 IK2。
- 将 scrypt 应用于 IK2 和盐(与步骤 2 相同的盐)以生成 32 字节 IK3。
- 使用 IK3 的前 16 个字节作为 KEK,后 16 个字节作为 IV。
- 使用 AES_CBC、密钥 KEK 和初始化向量 IV 加密 DEK。
更改密码
当用户选择更改或删除设置中的密码时,UI 会将命令cryptfs changepw
发送到vold
,然后vold
使用新密码重新加密磁盘主密钥。
加密属性
vold
和init
通过设置属性来相互通信。以下是可用于加密的属性的列表。
沃尔德属性
财产 | 描述 |
---|---|
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.decrypt 设置为trigger_post_fs_data 之前由vold 设置。 |
vold.post_fs_data_done 1 | 在完成任务post-fs-data 后由init.rc 或init.rc 设置。 |
初始化属性
财产 | 描述 |
---|---|
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 文件系统时应使用的选项。 |
初始化动作
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