说明
参考英文文章: http://source.android.com/tech/encryption/android_crypto_implementation.html
参考翻译:http://blog.sina.com.cn/s/blog_70753a4801012i4b.html
参考书籍《Android安全机制解析与应用实践》第7章 Android加密文件系统
分析的源代码使用 Android4.4.3
个人对加密的算法没有什么研究,本文只分析Kitkat的磁盘加密功能的流程分析。
Kitkat上的磁盘加密
如果你想在Kitkat设备上打开加密功能,那么需要:
/data文件系统必须基于块设备,eMMC是首选。这是因为磁盘加密功能使用kernel的dm-crypt模块,工作在块设备层。
system/vold/cryptfs.c中的 get_fs_size()函数会假定/data的文件系统是ext4,异常检查确定文件系统没有使用分区上最后16Kbytes,因为 cryptofooter将会保持到该存储空间。对开发人员而言是有用的,因为它的大小会变化,但是无法释放它。如果你使用的不是ext4文件系统,要么删除这个函数及其调用,要么改造它以适用你的文件系统。
大部分控制临时framework装载和卸载的代码都是在通常与设备无关的代码文件中。但是init.<device>.rc文件需要一些修改。所有的services必须属于三个类中的一个:core,main或late_start。Core类中的所有Services在临时 framework 获得磁盘密码时不会关闭或重启。main类中的Services会在真正的framwork重启时重启,late_start中的Services直到临时 framework被重启后才会被启动,放在late_start中的Services在临时framework获得磁盘密码的过程中不会运行。
需要在/data目录下创建的所有目录都是需要写在post-fs-data的Action中,并且在该Action中必须用命令"setpropvold.post_fs_data_done1"来结束。如果你的init.<device>.rc文件没有post-fs-data Action,那么主init.rc的post-fs-data Action结束时必须执行"setpropvold.post_fs_data_done1",三星的manta设备的init.manta.rc文件中的代码如下所示。
/KitKat/device/samsung/manta/init.manta.rc
on post-fs-data mkdir /data/media 0770 media_rw media_rw setprop vold.post_fs_data_done 1
备注: tmpfs是一种虚拟内存的文件系统,典型的tmpfs文件系统完全驻留在RAM中,读写速度远快于内存或硬盘文件系统。 vold会在停止main class服务后将/data 挂载为tmpfs,然后触发post-fs-data action的操作并等待"setprop vold.post_fs_data_done 1",然后继续启动临时framework并开始加密过程。如果 init.rc中不执行"setprop vold.post_fs_data_done 1",那么vold会认为异常从而重启整个系统。 |
Android加密如何工作
Android的磁盘加密基于linux kernel的dm-crypt模块,它是一个linux kernel的一个工作于块设备上的功能。因为YAFFS它会直接访问原始NAND Flash芯片,所以在基于YAFFS文件系统不起作用,但是可以工作在被linux kernel识别为块设备的eMMC及其相似的 Flash设备上。尽管ext4文件系统与设备加密与否无关,但对于加密设备来说推荐使用的ext4文件系统。
实际上加密功能是linux kernel的标准功能,在Android设备上开启加密功能,会有些小问题。Android系统试图尽量避免包含GPL成分,所以不能使用cryptsetup命令并且libdevmapper不是可选功能。所以使用 ioctl(2)通知内核是最好的选择。Android的Vold已经支持了移动应用到SD卡的功能,所以利用它进行整个磁盘的加密。实际用于文件系统加密的首先是128AES 算法,CBC模式和ESSIV:SHA256算法。主键通过调用openssl库使用128bit AES加密。
一旦决定将这个加密功能添加到vold模块中,我们要做的事情就十分明显了:
将一个名字为 cryptfs的新模块添加到Vold中. 并仿照Vold的命令方式,添加相应的加密相关命令,并使用相同的调用方式。在Kitkat中加密相关的命令有 checkpw , restart, enablecrypto, changepw 和cryptocomplete, verifypw, getfield ,setfield . 下面会详细说明。
另外的一个重要问题是如何在boot阶段得到密码. 最初设计在ramdisk实现一个能够被init调用的轻量级UI,并且初始化解密功能和mount/data。然而,UI工程师认为这种做法,工作量太大,并建议在init初始化启动时,通知framework来弹出 password输入对话框,获得密码,然后关闭framework并重新启动真正的framework。这个方案的确定引出下面的设计。详细点说,init通过设置property来告诉framework进入密码输入模式,同时使用properties 在vold,init和framework之间作为平台进行更多通信。详细描述如下所示:
最后,涉及的问题围绕在杀掉服务和重启哪些服务,在进行这些操作过程中,可能会导致/data分区的卸载和重新挂载。启动一个临时framework来获得密码,需要/data挂载tmpfs临时文件系统,否则framework无法启动。但是当卸载/data临时文件系统tmpfs,挂载真正的/data加密文件系统时,所有在/data临时文件系统tmpfs上打开文件的进程会被kill掉,并在真正/data文件系统上重新启动。
这个魔术需要所有属于core, main, late_start三组中的一组Services来完成。 Core Services一旦启动,就不会被关闭,main Services会先关闭,并在输入完磁盘密码后重新启动,late_start Services直到/data挂载并解密后才会启动。
这个魔术去触发Actioin是通过设置vold.decrypt属性的各种字符串来实现,这些字符串在接下来的内容中会说明。
同时,一个新的init命令"class_reset"被发明,作用是停止一个服务,但允许它调用"class_start"命令重启服务。如果用"class_stop"替代"class_reset"命令,则会将SVC_DISABLED标志添加到该已经停止服务的状态中,无论这个服务时什么类型的,这意味着当服务所属class使用class_start时,它也不会被启动。
引导加密系统
当init 挂载/data失败时,它假定文件系统已经加密,并设置几个properties:
ro.crypto.state = "encrypted" vold.decrypt = 1
这意味着/data被挂载为tmpfs ramdisk.
如果init能够正常挂载/data,会设置.
ro.crypto.state ="unencrypted"
framework启动时会检查 vold.decrypt的值,如果值成"1",说明此时 /data 被挂载为tmpfs,必须获得用户密码。 首先,需要先要确定disk是否已经加密。该操作是通过给vold 发送命令 "cryptfscryptocomplete",vold 返回0 表示成功加密完毕,返回 -1 表示内部错误,-2 表示加密没操作有完成。Vold 通过crypto footer的CRYPTO_ENCRYPTION_IN_PROGRESS标志来确定“cryptfs cryptocomplete”的返回值。如果它被设置了,那么表示加密过程被中断了,同时存储设备上没有有用的数据。当vold 返回一个错误时,UI界面应该弹出提示消息框,通知用户需要重启并将设备进行恢复出厂设置,用户只能通过点击提示框中的按钮来被强制进行此操作。
假设"cryptfs cryptocomplete"命令返回成功,那么framework应该弹出UI询问用户磁盘密码,这个UI会发送命令"cryptfs checkpw " 给vold,如果密码正确(密码正确与否是由是否可以成功挂载已解密磁盘到临时路径,然后再卸载它决定的), vold 保存解密块设备的名字到ro.crypto.fs_crypto_blkdev,然后返回状态0 给 UI,如果密码错误,则返回 -1 给UI。
UI显示一个加密启动画面,然后调用"cryptfs restart",vold设置property
调用cryptfs.c的cryptfs_restart(void)函数设置的该值。
这个操作会导致init.rc 执行"class_reset main"操作。
system/core/rootdir/init.rc
/* The init files are setup to stop the class main when vold.decrypt is * set to trigger_reset_main. */ property_set("vold.decrypt", "trigger_reset_main"); class_reset main
该操作会导致停止所有属于main class的 Services,
接着执行cryptfs_restart()函数,开始调用wait_and_unmount()方法卸载/data挂载的文件系统:
<pre name="code" class="cpp">#define DATA_MNT_POINT "/data" if (! (rc = wait_and_unmount(DATA_MNT_POINT)) ) { /* If that succeeded, then mount the decrypted filesystem */ fs_mgr_do_mount(fstab, DATA_MNT_POINT, crypto_blkdev, 0)
卸载tmpfs的/data文件系统,如果卸载成功,则调用fs_mgr_do_mount()方法,vold会挂载解密的真实/data分区,然后准备一个新的分区(如果通过wipe选项加密,可能永远不会准备这个新的分区。首次发布的加密功能的版本中时不包含wipe功能的)。
接着调用如下方法,来加载系统的properties,这里不赘述:
property_set("vold.decrypt", "trigger_load_persist_props");
从下面代码的注释可以看出,是在/data分区中创建一些必须的目录:
/* Create necessary paths on /data */ if (prep_data_fs()) { return -1; }
调用的prep_data_fs()函数的代码如下:
static int prep_data_fs(void) { …… property_set("vold.post_fs_data_done", "0"); property_set("vold.decrypt", "trigger_post_fs_data"); SLOGD("Just triggered post_fs_data\n"); ……. }
首先设置vold.post_fs_data_done 为字符串“0”,同时设置 vold.decrypt 为"trigger_post_fs_data",
on property:vold.decrypt=trigger_post_fs_data trigger post-fs-data
它导致init.rc和init.<device>.rc执行post-fs-data命令,创建必要的目录、连接及设置vold.post_fs_data_done 为"1"。
在prep_data_fs()函数中使用了一个for循环来等待vold.post_fs_data_done 被设置为"1",代码如下:
static int prep_data_fs(void) { …… /* Wait a max of 50 seconds, hopefully it takes much less */ for (i=0; i<DATA_PREP_TIMEOUT; i++) { char p[PROPERTY_VALUE_MAX]; property_get("vold.post_fs_data_done", p, "0"); if (*p == '1') { break; } else { usleep(250000); } } ……. }
当终于将vold.post_fs_data_done 被设置为"1",则prep_data_fs()执行完毕,返回,接着执行cryptfs_restart()函数:
/* startup service classes main and late_start */ property_set("vold.decrypt", "trigger_restart_framework");
最后 vold 设置vold.decrypt 为"trigger_restart_framework",
on property:vold.decrypt=trigger_restart_framework class_start main class_start late_start
通知 init.rc 启动main class和从系统启动后首次启动late_startclass所属services。
现在,framework已经使用解密的/data文件系统启动完成,并准备好可以使用了。
在设备上启用加密
首次发布时,我们只支持inplace加密,要求framework关闭,/data 卸载,然后加密每个区块,以上操作需要reboot设备之后开始执行。详细如下:
用户从UI选择加密设备,UI确认电池电量充足并且插入电源, 以确保有足够电量完成加密过程,因为如果设备在加密过程完成前因为没电而关机,数据会停留在一个不完整的加密状态,设备必须进行恢复出厂设置(所有数据将丢失)。
一旦用户按下按钮开始加密设备,UI将通过给vold发送命令 "cryptfs enable cryptoinplace ",密码使用用户锁屏密码。
Vold接到命令,经过处理会调用cryptfs_enable()函数来进行加密的处理操作。
vold会进行一些错误检查,返回 -1代表不能加密并输出包含原因到log中,如果确定可以加密,执行如下语句:
/* The init files are setup to stop the class main and late start when * vold sets trigger_shutdown_framework. */ property_set("vold.decrypt", "trigger_shutdown_framework");
它会设置vold.decrypt为"trigger_shutdown_framework"字符串,
on property:vold.decrypt=trigger_shutdown_framework class_reset late_start class_reset main
此操作会触发init.rc关闭late_start和main的所属Services。
if (vold_unmountAllAsecs()) { /* Just report the error. If any are left mounted, * umounting /data below will fail and handle the error. */ SLOGE("Error unmounting internal asecs"); }
调用vold_unmountAllAsecs()函数,卸载/mnt/secure/asec和/data/app-asec目录下的文件系统。system/vold/VolumeManager.cpp
extern "C" int vold_unmountAllAsecs(void) { int rc; VolumeManager *vm = VolumeManager::Instance(); rc = vm->unmountAllAsecsInDir(Volume::SEC_ASECDIR_EXT); if (vm->unmountAllAsecsInDir(Volume::SEC_ASECDIR_INT)) { rc = -1; } return rc; }
获取ro.crypto.fuse_sdcard的值,看系统是否使用fuse模式的存储管理模式,代码如下:
property_get("ro.crypto.fuse_sdcard", fuse_sdcard, ""); if (!strcmp(fuse_sdcard, "true")) { /* This is a device using the fuse layer to emulate the sdcard semantics * on top of the userdata partition. vold does not manage it, it is managed * by the sdcard service. The sdcard service was killed by the property trigger * above, so just unmount it now. We must do this _AFTER_ killing the framework, * unlike the case for vold managed devices above. */ if (wait_and_unmount(sd_mnt_point)) { goto error_shutting_down; } }
如果不是fuse模式,则调用wait_and_unmount()方法卸载SD卡。
接着调统一个方法卸载/data文件系统,源码如下所示:
/* Now unmount the /data partition. */ if (wait_and_unmount(DATA_MNT_POINT)) { goto error_shutting_down; }
至此,已经卸载/mnt/sdcard 和 /data文件系统。
/* Now unmount the /data partition. */ if (wait_and_unmount(DATA_MNT_POINT)) { goto error_shutting_down; }
如果进行一个inplace类型的加密,即how 的值为CRYPTO_ENABLE_INPLACE,此时则会调用fs_mgr_do_tmpfs_mount()方法,让 vold将tmpfs挂载/data目录下,并且设置vold.encrypt_progress="0".
已经卸载完所有的存储设备,并将tmpfs挂载到/data目录下了,接下来的就应该重启framework了,进行真正的加密操作了。
代码如下:
/* restart the framework. */ /* Create necessary paths on /data */ if (prep_data_fs()) { goto error_shutting_down; }
调用prep_data_fs()函数,来进行framework重启的一些操作,上文已经分析过该方法,这里就不再分析了,由于tmpfs文件系统被挂载后,/data目录下什么内容也没有,所以需要在/data目录下创建必要的一些目录。
睡眠了2S后,接着执行如下代码:
/* startup service classes main and late_start */ property_set("vold.decrypt", "trigger_restart_min_framework");
创建好目录,就可以重启framework了,这里设置 vold.decrypt= "trigger_restart_min_framework",
/* startup service classes main and late_start */ property_set("vold.decrypt", "trigger_restart_min_framework");
这将触发init.rc启动main class服务进程,当framework看到 vold.encrypt_progress设置为 "0",会启动显示进度的UI,每5s刷新一次进度。此时由于vold服务属于core,所以不会重启,之后vold设置加密映射,它创建了一个虚拟的加密块设备映射到实际的块设备,但加密是一块块写入,解密是一块块读取,vold会创建并写入crypto footer
crypto footer包含了加密类型的详细信息和用于系统解密的秘钥。主密钥通过读取 /dev/urandom 创建的128bit随机值,对用户密码进行哈希加密,加密算法使用SSL库的PBKDF2函数。 footer 也同样包含随机数(也从/dev/urandom 读取), 为了增加熵而使用PBKDF2的hash算法得出,防止彩虹表破解密码。同时,CRYPT_ENCRYPTION_IN_PROGRESS标志设置到crypto footer中防止加密失败,cryptfs.h中有详细的crypto footer布局,crypto footer保存在分区的最后16Kbytes上,且/data文件系统不能使用到这部分。
如果是wipe模式的加密,vold会通过cryptfs_enable_wipe()方法调用命令"make_ext4fs" 为加密块设备创建ext4格式的文件系统,注意分区上不会包含那最后的16Kbytes。
如果是inplace模式,vold调用cryptfs_enable_inplace()函数,开始循环读取每块数据然后写入加密块设备,这在30Gbyte分区的MotorolaXoom上要花费约1小时。这取决于硬件。 加密进度每增加1% 会更新vold.encrypt_progress。UI会以5s间隔检查加密进度的变化。
if (! rc) { /* Success */ /* Clear the encryption in progres flag in the footer */ crypt_ftr.flags &= ~CRYPT_ENCRYPTION_IN_PROGRESS; put_crypt_ftr_and_key(&crypt_ftr); sleep(2); /* Give the UI a chance to show 100% progress */ cryptfs_reboot(0); } else {
当各种加密全部成功时,vold会清除 footer上的ENCRYPTION_IN_PROGRESS标志,然后调用cryptfs_reboot(0)函数重启系统,
该函数的代码如下:
static void cryptfs_reboot(int recovery) { if (recovery) { property_set(ANDROID_RB_PROPERTY, "reboot,recovery"); } else { property_set(ANDROID_RB_PROPERTY, "reboot"); } sleep(20); /* Shouldn't get here, reboot should happen before sleep times out */ return; } ANDROID_R
B_PROPERTY的值如一下代码所示:
system/core/include/cutils/android_reboot.h
#define ANDROID_RB_PROPERTY "sys.powerctl"
可以看出只是根据传入参数recovery的值,对sys.powerctl进行设置,这里传入的是0,则进入if的false分支,写入”reboot”字符串。
system/core/rootdir/init.rc
on property:sys.powerctl=* powerctl ${sys.powerctl}
触发init.rc的命令,执行powerctl reboot命令。
接着就执行到builtins.c的do_powerctl()函数,解析命令,然后调用system/core/libcutils/android_reboot.c的android_reboot()函数,通过系统调用,重启系统。
我们来看一下如果重启失败的处理:
/* hrm, the encrypt step claims success, but the reboot failed. * This should not happen. * Set the property and return. Hope the framework can deal with it. */ property_set("vold.encrypt_progress", "error_reboot_failed"); release_wake_lock(lockid); return rc;
如果因为某些原因reboot失败,vold会设置vold.encrypt_progress="error_reboot_failed" ,UI告知用户强行reboot,这不是预期会发生的情况。
接下来,我们来看一下机密过程失败的处理流程:
error_unencrypted: free(vol_list); property_set("vold.encrypt_progress", "error_not_encrypted"); if (lockid[0]) { release_wake_lock(lockid); } return -1;
如果vold 在加密过程中遇到错误,没有数据遭到破坏并且framework在运行,vold会设置vold.encrypt_progress ="error_not_encrypted" ,UI提示用户reboot并告知加密没有进行。如果错误发生在framework停止之后,但是在UI进度条运行之前,vold会reboot系统,如果reboot失败,会设置 vold.encrypt_progress="error_shutting_down" 并返回 -1,但不会有谁捕捉这个错误,这不是预期发生的。
} else { char value[PROPERTY_VALUE_MAX]; property_get("ro.vold.wipe_on_crypt_fail", value, "0"); if (!strcmp(value, "1")) { /* wipe data if encryption failed */ SLOGE("encryption failed - rebooting into recovery to wipe data\n"); mkdir("/cache/recovery", 0700); int fd = open("/cache/recovery/command", O_RDWR|O_CREAT|O_TRUNC, 0600); if (fd >= 0) { write(fd, "--wipe_data", strlen("--wipe_data") + 1); close(fd); } else { SLOGE("could not open /cache/recovery/command\n"); } cryptfs_reboot(1); } else { /* set property to trigger dialog */ property_set("vold.encrypt_progress", "error_partially_encrypted"); release_wake_lock(lockid); } return -1; }
如果vold在加密过程中遇到错误,设置vold.encrypt_progress= “ error_partially_encrypted ”,并返回-1 。然后,UI显示消息说明加密失败,并为用户提供恢复出厂设置的按钮。
改变密码
改变磁盘加密的密码时,UI发送命令"cryptfs changepw" 给vold,vold用新的密码重新加密生成主密钥。