全盘加密

全盘加密是指使用加密密钥对 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 码、图案或密码限制。

加密由 initvold 管理。init 调用 vold,而 vold 设置属性以触发 init 中的事件。系统的其他部分也会查看这些属性以执行任务,例如报告状态、请求密码或在发生致命错误时提示恢复出厂设置。要在 vold 中调用加密功能,系统使用命令行工具 vdccryptfs 命令:checkpwrestartenablecryptochangepwcryptocompleteverifypwsetfieldgetfieldmountdefaultencryptedgetpwtypegetpwclearpw

为了加密、解密或擦除 /data/data 必须未挂载。但是,为了显示任何用户界面 (UI),框架必须启动,而框架需要 /data 才能运行。为了解决这个难题,临时文件系统挂载在 /data 上。这允许 Android 提示输入密码、显示进度或根据需要建议数据擦除。它确实施加了限制,即为了从临时文件系统切换到真正的 /data 文件系统,系统必须停止每个在临时文件系统上打开文件的进程,并在真正的 /data 文件系统上重启这些进程。为此,所有服务必须属于以下三个组之一:coremainlate_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 设备的正常首次启动。

  1. 使用 forceencrypt 标记检测未加密的文件系统

    /data 未加密,但由于 forceencrypt 强制要求,因此需要加密。卸载 /data

  2. 开始加密 /data

    vold.decrypt = "trigger_encryption" 触发 init.rc,这会导致 vold 在没有密码的情况下加密 /data。(由于这应该是一台新设备,因此未设置密码。)

  3. 挂载 tmpfs

    vold 挂载 tmpfs /data(使用来自 ro.crypto.tmpfs_options 的 tmpfs 选项)并将属性 vold.encrypt_progress 设置为 0。vold 准备 tmpfs /data 以启动加密系统,并将属性 vold.decrypt 设置为:trigger_restart_min_framework

  4. 启动框架以显示进度

    由于设备实际上没有要加密的数据,因此进度条不会经常出现,因为加密速度非常快。有关进度 UI 的更多详细信息,请参阅加密现有设备

  5. /data 加密后,关闭框架

    voldvold.decrypt 设置为 trigger_default_encryption,这将启动 defaultcrypto 服务。(这将启动以下流程以挂载默认加密的用户数据。)trigger_default_encryption 检查加密类型以查看 /data 是否使用密码加密。由于 Android 5.0 设备在首次启动时加密,因此不应设置密码;因此,我们解密并挂载 /data

  6. 挂载 /data

    然后 init 使用从 ro.crypto.tmpfs_options 中提取的参数,将 /data 挂载到 tmpfs RAMDisk 上,该参数在 init.rc 中设置。

  7. 启动框架

    voldvold.decrypt 设置为 trigger_restart_framework,这将继续正常的启动过程。

加密现有设备

当您加密已迁移到 L 的未加密 Android K 或更早版本设备时,会发生这种情况。

此过程由用户启动,在代码中称为“就地加密”。当用户选择加密设备时,UI 会确保电池已充满电并且已插入交流电源适配器,以便有足够的电量完成加密过程。

警告:如果设备在完成加密之前耗尽电量并关机,则文件数据将处于部分加密状态。设备必须恢复出厂设置,并且所有数据都将丢失。

为了启用就地加密,vold 启动一个循环来读取真实块设备的每个扇区,然后将其写入加密块设备。vold 在读取和写入扇区之前检查扇区是否正在使用,这使得在新设备上加密速度更快,因为新设备几乎没有数据。

设备状态:设置 ro.crypto.state = "unencrypted" 并执行 on nonencrypted init 触发器以继续启动。

  1. 检查密码

    UI 使用命令 cryptfs enablecrypto inplace 调用 vold,其中 passwd 是用户的锁屏密码。

  2. 关闭框架

    vold 检查错误,如果无法加密,则返回 -1,并在日志中打印原因。如果可以加密,则将属性 vold.decrypt 设置为 trigger_shutdown_framework。这会导致 init.rc 停止 late_startmain 类中的服务。

  3. 创建加密页脚
  4. 创建面包屑文件
  5. 重启
  6. 检测面包屑文件
  7. 开始加密 /data

    然后 vold 设置加密映射,这将创建一个虚拟加密块设备,该设备映射到真实块设备,但在写入时加密每个扇区,并在读取时解密每个扇区。vold 然后创建并写出加密元数据。

  8. 在加密时,挂载 tmpfs

    vold 挂载 tmpfs /data(使用来自 ro.crypto.tmpfs_options 的 tmpfs 选项)并将属性 vold.encrypt_progress 设置为 0。vold 准备 tmpfs /data 以启动加密系统,并将属性 vold.decrypt 设置为:trigger_restart_min_framework

  9. 启动框架以显示进度

    trigger_restart_min_framework 导致 init.rc 启动 main 类服务。当框架看到 vold.encrypt_progress 设置为 0 时,它会启动进度条 UI,该 UI 每五秒查询该属性并更新进度条。每次加密分区的一个百分比时,加密循环都会更新 vold.encrypt_progress

  10. /data 加密后,更新加密页脚

    /data 成功加密后,vold 清除元数据中的标志 ENCRYPTION_IN_PROGRESS

    当设备成功解锁后,密码将用于加密主密钥,并且加密页脚将更新。

    如果由于某种原因重启失败,vold 会将属性 vold.encrypt_progress 设置为 error_reboot_failed,并且 UI 应显示一条消息,要求用户按一个按钮以重启。预计永远不会发生这种情况。

使用默认加密启动加密设备

当您启动没有密码的加密设备时,会发生这种情况。由于 Android 5.0 设备在首次启动时加密,因此不应设置密码,因此这是默认加密状态。

  1. 检测到加密的 /data,但没有密码

    检测到 Android 设备已加密,因为 /data 无法挂载,并且设置了标志 encryptableforceencrypt 之一。

    voldvold.decrypt 设置为 trigger_default_encryption,这将启动 defaultcrypto 服务。trigger_default_encryption 检查加密类型以查看 /data 是否使用密码加密。

  2. 解密 /data

    在块设备上创建 dm-crypt 设备,以便设备可以使用。

  3. 挂载 /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 类中的服务(自启动以来)。

  4. 启动框架

    现在,框架使用解密的 /data 启动其所有服务,并且系统已准备好使用。

在没有默认加密的情况下启动加密设备

当您启动设置了密码的加密设备时,会发生这种情况。设备的密码可以是 PIN 码、图案或密码。

  1. 检测到具有密码的加密设备

    检测到 Android 设备已加密,因为标志 ro.crypto.state = "encrypted"

    voldvold.decrypt 设置为 trigger_restart_min_framework,因为 /data 已使用密码加密。

  2. 挂载 tmpfs

    init 设置五个属性以保存为 /data 提供的初始挂载选项,其中参数从 init.rc 传递。vold 使用这些属性来设置加密映射

    1. ro.crypto.fs_type
    2. ro.crypto.fs_real_blkdev
    3. ro.crypto.fs_mnt_point
    4. ro.crypto.fs_options
    5. ro.crypto.fs_flags (以 0x 开头的 ASCII 8 位十六进制数)
  3. 启动框架以提示输入密码

    框架启动并看到 vold.decrypt 设置为 trigger_restart_min_framework。这告诉框架它正在 tmpfs /data 磁盘上启动,并且需要获取用户密码。

    但是,首先,它需要确保磁盘已正确加密。它向 vold 发送命令 cryptfs cryptocompletevold 如果加密成功完成,则返回 0;如果发生内部错误,则返回 -1;如果加密未成功完成,则返回 -2。vold 通过在加密元数据中查找 CRYPTO_ENCRYPTION_IN_PROGRESS 标志来确定这一点。如果设置了该标志,则表示加密过程已中断,并且设备上没有可用的数据。如果 vold 返回错误,则 UI 应向用户显示一条消息,要求用户重启设备并恢复出厂设置,并为用户提供一个按钮以执行此操作。

  4. 使用密码解密数据

    一旦 cryptfs cryptocomplete 成功,框架将显示一个 UI,要求输入磁盘密码。UI 通过向 vold 发送命令 cryptfs checkpw 来检查密码。如果密码正确(这通过在临时位置成功挂载解密的 /data,然后卸载它来确定),vold 会将解密的块设备的名称保存在属性 ro.crypto.fs_crypto_blkdev 中,并向 UI 返回状态 0。如果密码不正确,则向 UI 返回 -1。

  5. 停止框架

    UI 弹出一个加密启动图形,然后使用命令 cryptfs restart 调用 voldvold 将属性 vold.decrypt 设置为 trigger_reset_main,这会导致 init.rc 执行 class_reset main。这将停止 main 类中的所有服务,从而允许卸载 tmpfs /data

  6. 挂载 /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 类中的服务(自启动以来)。

  7. 启动完整框架

    现在,框架使用解密的 /data 文件系统启动其所有服务,并且系统已准备好使用。

失败

无法解密的设备可能有几个原因。设备启动时会执行一系列正常的启动步骤

  1. 检测到具有密码的加密设备
  2. 挂载 tmpfs
  3. 启动框架以提示输入密码

但在框架打开后,设备可能会遇到一些错误

  • 密码匹配但无法解密数据
  • 用户输入错误密码 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 将生成的签名转换为适当长度的密钥。然后,此密钥用于加密和解密主密钥。要存储此密钥

  1. 生成随机的 16 字节磁盘加密密钥 (DEK) 和 16 字节 salt。
  2. 将 scrypt 应用于用户密码和 salt,以生成 32 字节的中间密钥 1 (IK1)。
  3. 用零字节将 IK1 填充到硬件绑定私钥 (HBK) 的大小。具体来说,我们填充为:00 || IK1 || 00..00;一个零字节,32 个 IK1 字节,223 个零字节。
  4. 使用 HBK 签署填充的 IK1,以生成 256 字节的 IK2。
  5. 将 scrypt 应用于 IK2 和 salt(与步骤 2 中的 salt 相同),以生成 32 字节的 IK3。
  6. 使用 IK3 的前 16 个字节作为 KEK,后 16 个字节作为 IV。
  7. 使用 AES_CBC、密钥 KEK 和初始化向量 IV 加密 DEK。

更改密码

当用户选择在设置中更改或删除其密码时,UI 会将命令 cryptfs changepw 发送到 vold,并且 vold 会使用新密码重新加密磁盘主密钥。

加密属性

voldinit 通过设置属性相互通信。以下是可用于加密的属性列表。

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.rcinit.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 的情况下运行。

ro.crypto.fs_type
ro.crypto.fs_real_blkdev
ro.crypto.fs_mnt_point
ro.crypto.fs_options
ro.crypto.fs_flags

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