Kitkat 的磁盘加密功能分析

说明

参考英文文章: 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用新的密码重新加密生成主密钥。

 

 

你可能感兴趣的:(android,KitKat,加密功能)