Android 从4.4开始就支持一项功能,那就是对设备进行加密。加密自然是为了安全性考虑,由于/system目录是只读的,手机里那些存储设备分区中需要保护的就 剩下/data/分区和sdcard了。显然,/data/和sdcard大量存储了用户数据(比如app运行时存储的数据),对他们进行加密非常非常有 必要。
Android 5.0发布后,为了Android设备在企业中的使用,设备加密这个功能默认就必须启用,但是加密这玩意还是对功耗,性能有影响,而市面上大部分手机还跟 不上Android进化的步伐,所以Google在5.0发布几个月后又推出了Android 5.1,在这个代号为Android Lollipop MR1的版本上,设备加密就不是默认开启的了(我个人觉得可能对于升级现有手机系统而言,Device Encryption默认不开启,而对于新上市的并且配备Android L系统的手机,这个选项很可能是开启的)。
本文目的就是分析下系统中与设备加密工作相关的流程。
- init源码:system/core/init
- init.rc:system/core/rootdir/
- init.flo.rc,fstab.flo等文件:device/asus/flo/
- fs_mgr.c:system/core/fs_mgr/
- vold源码:system/vold/
- device mapper代码:kernel/drivers/md
- 最好有Android 5.1的代码,我在百度云盘上放置了一份,位置在:http://pan.baidu.com/s/1bn4fvVT
- 下载一份Kernel的代码。搞Android系统研究,最好是买一个Nexus。我本人有一个Nexus7。设备代号叫“flo”。kernel代码可安装google官方的方法下载以及编译:http://source.android.com/source/building-kernels.html。
- 当然,Nexus设备有一些设备厂商自有的东西没有源代码。那边我们在编译设备使用的系统时,需要把这些东西(往往是一些二进制库啊,甚至事先编好kernel镜像文件)先下载过来。位置在https://developers.google.com/android/nexus/blobs-preview。在这个网页上把对应设备的私有库下载过来,然后就可以编译能烧到手机并且能正常运转的Android OS了。
在 讲解设备加密前,我们还是要先来回顾下init的工作流程。我其实在《深入理解Android 卷1》一书中曾经详细分析过init的代码。那时候的版本好像还是2.3。不过到今天5.1为止,init的代码看起来改变是很大,但其实书中所分析的 init的软件结构,工作流程等在5.1里没有太大的变化。
init 是Linux用户空间的天字号第一进程,但它干的工作倒是很朴实,就是忠实执行我们在init配置文件(主文件是init.rc,通过import语句加 载其他的rc文件)中设置的各种动作。2.3时,我记得init.rc文件还是扁平化,无层级的。但是随着Android进化,init配置文件也变得有 组织了。
下面列出的5.1中init.rc中一些常见的的结构和代码,重要的地方,我在其中有注释。
简 而言之,对Android设备来说,init.rc等一众配置文件可以说是整个系统中最重要的部分了。没有配置的设备,其实仅仅就是一台未初始化的机器! 所以,当init本身的代码写好后,对我们来说剩下的事情就是写配置文件了。结合上述的init.rc实例,我们可总结5.1中init配置文件的组织结 构如下:
- 配置文件由一段一段的section组成。目前section的标示有三个关键词,分别是import、on和service。
- 其中import用来导入指定的配置文件。
- on section往往包含了一组需要执行的命令(command)。
- service代表了一个服务,它其实是指系统需要启动的一个进程。以及对应需要执行的一些命令。当然,service所代表的进程还可以进一步赋予属性(option)。比如class属性将service分成对应的类,而critical属性则标示该服务的关键性。
对我们代码研究者而言,具体的工作还是由函数来执行的。想要了解init到底针对每条command或者section干了什么,我们需要关注init的关键文件keywords.h。图1所示为其部分内容:
图1 keywords.h
找到这个文件,当想了解配置文件中各种命令的处理方法时,就可以直接找到对应的函数了。
注意:关于init的代码分析我们就不在啰嗦。感兴趣的兄弟们请研究卷1。
对于设备加密来说,mount_all是一个重要的命令。我们先来看它的配置文件。对于flo设备而言,它放在init.flo.rc文件的fs section中
如图1所示,mount_all是一个command,对应的函数是do_mount_all,这个命令有一个参数,就是linux上常见的fstab文件。对flo设备而言,它是fstab.flo。图2所示为此文件的部分内容:
图2 fstab.flo文件部分内容
根据前述内容,data分区是可以加密的,所以在图2的红色框里的最后,加载data分区的时候有一个属性:
- encryptable=/dev/block/.https://blog.51cto.com/metadata。encryptable表示该分区可以被加密。后面的文件名表明一些加密信息(比如password啊,生成加密密钥时使用的盐值等信息存在这个文件里)。
1.2.1 do_mount_all代码
do_mount_all将解析fstab文件,并挂载相应的分区。其代码如下所示:
对do_mount_all来说,它要考虑设备加密的各种情况。仔细想想,应该是有下面三种情况的:
- 系统第一次开机,而且系统设置为设备必须加密,则上述代码走FS_MGR_MNTALL_DEV_NEES_ENCRYPTION。
- 设备加密后的系统开机,它发现设备被加密了,所以走FS_MGR_MNTALL_DEV_MIGHT_BE_ENCYRPTED。
- 设备不用加密,走FS_MGR_MNTALL_DEV_NOT_ENCYRPTED。
来看看fs_mgr_mount_all函数:
mret=0,表示mount成功。如果设备已经加密,则mount是不会成功的。
所以,这里需要考虑第一次开机,然后发现设备被强制要求加密的情况
先卸载这个需要强制加密的设备
//如果mount失败,则需要判断是不是已经加密过的设备。这种情况下,不能通过mount挂载!
fs_mgr_mount_all的流程可总结如下:
- 先mount fstab中的各个项。如果成功的话,表明对应项的块设备肯定没有被加密(加密的设备没法被mount)。
- 如果这个设备是强制需要加密的,则先卸载这个设备,然后设置返回值为FS_MGR_MNTALL_DEV_NEEDS_ENCRYPTION。
- 如果mount失败,则需要判断是不是因为加密设备导致。如果该设备没有被清空(wiped),则先把tmpfs挂载到/data目录下。然后设置返回值为FS_MGR_MNTALL_DEV_MIGHT_BE_ENCRYPTED。
注 意:为什么要挂载tmpfs到/data/目录下呢?这是因为没有这个/data目录,系统根本没办法起来。因为系统很多运行时保存的信息,app自己的 缓存目录等都放在/data下。为了保证系统能启动,所以这里先弄一个临时/data目录。等到后续把加密设备通过device-mapper方式准备好 后,我们再把加密设备挂载到/data/目录下。注意,加密设备的mount不是直接挂载加密设备,而是通过挂载一个device-mapper来实现 的。
回到do_mount_all,我们看看它退出前都干了些什么:
- 如果是强制设备第一次开机,设置“vold.decrypt”属性值为“trigger_encryption”。
- 如果是已经加密设备的后续开机,设置“ro.crypto.state”属性值为“encrypted”,同时设置"vold.decrypt"属性值为"trigger_default_encryption"。
- 非加密设备,设置"ro.crypto.state"值为"unencrypted"。
下面我们分别来分析前两个流程。
1.2.2 强制加密设备的第一次开机
Android 里边,属性可以被设置,也可以获取属性值。除此之外,还可以在属性等于特定值时执行一些特殊的动作,比如在强制加密设备第一次开机的时候,我们需要对这个 设备进行加密。但是do_mount_all仅仅是设置vold.decrypt属性为trigger_enryption就可以触发加密了。这是怎么做 到的呢?
是不是So easy呢?看来,强制加密设备的首次加密将由vdc来处理。这里先透露一下,vdc其实不过是给vold发送enablecrypto命令罢了,真正的加密工作是由vold来完成的。
anyway,还是来看看vdc的代码吧:
vold怎么处理加密呢?我们到最后再说。到此为止,强制设备首次加密的前半部分流程就算完成了。
下面看看加密设备的加载流程。
1.2.2 已加密设备的加载
init也是通过属性来触发已加密设备加载的工作流程的,如下所示:
......,最终工作都是由vold来完成的....具体怎么弄,暂且不表。此时,本文和init相关的工作就告一段落。
Android设备中,具体加密工作都是vold来完成的。前面讲的init是为了解决已加密设备的挂载问题和强制加密设备第一次开机进行加密的问题。当然,强制加密设备第一次开机并且被加密后,后续的问题就会变成已加密设备如何挂载了。
我们先把这些东西放到一边去。来看这样一个东西:系统的设置里边,是可以触发设备加密的。相关选项如图3所示:
图3 设置里的加密设备选项
图3右边界面对应的Activity叫CryptKeeperSettings,相关代码如下所示:
最重要的是代码中的mInitialListener,
一般是没有设置密码控制级别的,所以加密的时候将使用默认加密,密码为””
注 意,在5.1中,系统支持一种默认的加密方式,即加密时默认用户传入的密码是”default_password”。当然,这是在用户没有设置解锁方法的 情况下,系统默认的行为。如果有DevicePolicyManager或者用户自己设置了解锁密码,则系统会用它们做为设备加密密码。
为了方便,我们只讨论默认密码的情况。这个时候,showFinal/confirm/iation函数将被调用。
来看看这个函数,非常简单:
UI小跳转吧,又到了另外一个界面。它和图2右边那个图很类似。
CryptKeeper/confirm/i界面里最下方也有一个按钮,我们直接看它的处理吧。
Blank这个Activity比较关键,代码如下所示:
好 吧。Settings的工作还算比较简单,就是界面显示,然后再触发加密流程。在这个过程中,Settings的工作确实没有什么叹为观止的。但是前面代 码中我们曾提到说,加密过程中,系统会有一个进度栏显示加密进度和所剩余时间。这个其实也是由Settings来实现的。我们先把这部分介绍了。
加密进度显示这个东西说难不难,说简单它也不简单。事情是这样的:
- 当vold在加密设备的过程中,这个时间可能很长很长,根据设备中之前是否有很多数据有关。
- 另 外,挂载加密后的设备时,可能用户用得不是默认密码,那这个时候需要启动framework,然后弹出一个密码输入框让用户输入密码。这也就是为什么加密 设备挂载时会先mount一个tmpfs到/data分区下。因为这个时候framework里那些service是要启动的,还有输入法等所谓的 core app。
所以,在加密过程中或者加密设备挂载前,framework有一些特殊的处理:
当属性vold.decrypt值为“trigger_restart_min_framework“或者为”1“的时候,mOnlyCore为true。mOnlyCore干啥用的?见图4:
图4 coreApp示例
mOnlyCore表示当App的AndroidManifest.xml中有coreApp=”true”的时候,系统启动时,PackageManagerService才会解析这些APK。那么,系统中哪些APK为coreApp呢?如图5所示:
图5 coreApp
图5列出Packages目录和frameworks/base/packages目录中属于coreApp的所有APK。那么,当mOnlyCore为true的时候,系统启动后只会解析这些coreApp。
问题来了,图5中并没有Launcher,那加密过程中进度界面怎么显示?原来,Settings中也有一个能响应Home Intent的Activity,如图6所示:
图6 CryptKeeper
CryptKeeper属于HOME这一类,而Settings又是coreApp。所以,当我们在加密过程中的时候,framework启动完毕后,接着就会启动HOME。这个时候只能由CryptKeeper来响应HOME Intent了。如图7所示:
图7 加密进度显示界面
图6中,状态栏和虚拟按键栏都没法使用了,就是防止在加密过程中用户乱动!!!手欠啊真是....
CryptKeeper比较复杂,除了显示加密进度,还能在加密设备挂载前,让用户输入密码以验证是否允许挂载加密设备。这个时候的UI就变成图8了:
图8 挂载加密设备时需要输入用户设置的密码
关于CryptKeeper,我就不说太多了。这代码没有任何难度的。
注 意:对于挂载加密设备来说,系统其实并不是真的去解密存储设备,然后再挂载。而仅是判断用户输入的密码是不是对的。如果是对的话,它会通过device- mapper去挂载一个虚拟设备到/data下,而这个虚拟设备一方面连着实际的block设备。这样,当用户从/data读取、写入数据时,这个虚拟设 备都会把数据进行加解密。比如用户读数据时,它会从真实设备里读取加密后的数据,然后解密返回给用户。用户写数据时候,它会加密后再写到真实设备中!
我们直接从Settings跳到vold了,因为MountService其实就是和前面提到的vdc一样,发送相关命令给vold。真正干活还是在vold里。
注意,vold的源码,我曾经在《深入理解Android》卷1中讲过了。那时候还是2.3,但是即使到5.1为止,vold的代码和结构都没有太大的变动。
vold中有一个CommandLister,在那里,它注册了自己能处理的各种命令和对应处理模块:
CryptfsCmd专门处理以“cryptfs“开头的命令,处理的地方在其runCommand函数中。对加密一个设备而言,对应的命令格式如下:
enablecrypto对应的处理函数入口是cryptfs_enable或者是cryptfs_enable_default。它们内部都会调用cryptfs_enable_internal,我们重点看这个函数:
3.1.1 第一部分工作
不知道什么原因,上一次加密还没处理完,系统就重启或者vold就重启了。为了处理这种情况,
vold会把加密过程都写到一个地方去,然后要每次加密前都需要检查。因为一旦加密被干扰而又没
正确处理的话,用户数据就会丢失,这是非常严重的事故!
上面这段代码中,比较重要的一个函数就是get_crypt_ftr_and_key,这个函数用于获取之前存储的和设备加密有关的上下文信息。这个信息非常之重要!
(1) get_crypt_ftr_and_key
get_crypt_ftr_info用于获取加密信息的存储位置。按Android的设计,这个信息可以存储在
两个地方。一个是fstab里encryptable=xxxx中xxx这个存储设备里,也可以存储在
需要挂载的设备里最后一段空间里。比如fstab.flo中,
是要挂载的设备,所以加密信息
存储在这个设备的最后一段空间里。但是由于我们还有下面这句话:
所以实际的加密信息存储在metadata文件里
通过上述代码,大家要牢记几个重要知识点:
- 加密所用的上下文信息(包括加密版本号,密钥信息,加密方法,加密进度等)都是存储在某个地方。这个地方可以是加密设备最后一块区域,也可以单独指定一个文件(通过fstab encryptable=xxx来指定)。
3.1.2 第二部分工作
接着看第二部分工作:
注意上述代码:
- 通过设置”vold.decrypt”为“trigger_shutdown_framework“来干一些事情。
- 然后卸载/data/分区
按道理,如果卸载/data/分区的话,应用程序肯定会奔溃。怎么避免呢?很简单,把它们都杀了,然后不再启动就好。来看init.rc
class_reset main#如果查看init代码的话,reset就是干掉这些服务,并且不会自动重启它们
main类别的服务有谁呢?最重要的就是zygote了。它属于main类别,所以上面那个trigger会干掉zygote,并且不会重启。没有zygote,java世界是没可能启动的!
来看第三部分工作
3.1.3 第三部分工作
第三部分工作中,最重要的是prep_data_fs,它其实也是通过设置属性来干活的,直接看init.rc
3.1.4 第四部分工作
这个动作会触发class main类型的service重启动,当然,zygote也就起来了
然后framework都起来了。但是由于SystemServer只加载coreApp,所以我们能看见
图6
利用device-mapper机制创建一个
设备,其实device mapper是一个驱动,叫md(mapped device之意)
为这个device mapper设备设置一个crypt虚拟项。这个crypt虚拟项会和
待加密设备(real_blkdev)关联起来.
是上面device mapper创建的一个虚拟项设备,命名方式为
。xxx为device mapper的minor版本号。
4 设备加解密工作就是在这个crypto_blkdev读写过程中完成的。以后我们挂载这个
到/data分区就可以了。当从这个设备读的时候,它会从底层关联的
读取加密数据,然后解密传递给读者。当往这个设备写的时候,它会加密后
再写到real_blkdev中
加密数据。方法很简单,从real_blkdev读取数据,然后写到crypto_blkdev中。
前面反复讲过了。real_blkdev实际上是和crypto_blkdev绑定好的。
我们先从real_blkdev中读取未加密的数据,从存储空间第一块位置开始读取
然后写到crypto_bldev中。它会先加密,然后对应写回到real_blkdev第一块位置
获取ro.crypto.state的属性,这个属性:
如果值为空:表明是强制加密设备的第一次加密。那么我们要挂载这个已经加密的设备
如果值不为空(此时值应该是“uncrypted”,在init中设置),表明我们是对
以前没有加密的设备进行首次加密
最后,我们来看一下create_crypto_blk_dev函数。
(1) create_crypto_blk_dev
create_crypto_blk_dev和device-mapper的用法有关系。网上有相关教程,这里也不想说太多。整体流程大概就是通过ioctl把device-mapper的虚拟项设置好。
struct dm_ioctl *io;//发送给devicemapper的命令,都是通过ioctl来完成的
为mapped device设置目标,也就是建立虚拟项和实际设备的关系
打开/dev/device-mapper,这个设备对应的驱动在kernel/drivers/md目录下。我们
后续会简单分析下它
//获取这个mappeddevice的状态,其实就是版本号之类的东西
一个mapped device可以注册多个虚拟项,这被称作target项。在驱动中,一个叫"crypt"的
target项会注册到这个mapped device中。下面这个函数就是找到这个MD设备注册的crypt项
的版本信息
有兴趣的话,也看看load_crypto_mapping_table函数:
(2) load_crypto_mapping_table
设置target的名字其实,crypt或者req-crypt,到时候MD会根据这个名字找到对应的target
3.1.5 一点小结
到此为止,vold加密的工作就算介绍完了。其中有一些小地方,我们就先略去不讨论。童鞋们自己对着代码多看两遍也就明白了。
另外:
- 对于强制加密设备第一次加密流程中,vold会直接挂载它。
- 对于其他用默认密码加密的设备,在init里我们看到它会通过vdc发送mountdefaultencrypted给vold来挂载。
下面我们就看看vold挂载默认密码加密时的处理。
来看看真正挂载的地方:
重启framework,这时候设置的属性值不再会导致SystemServer只加载coreApp了
wowo,按我以前的做法,Device Encryption(以后简称DE)的流程其实到这里就算完了。回顾下来,好像DE的流程也没那么复杂:
- 首 先是各种处理,比如强制设备的第一次加密,没有加密的设备被用户在settings中触发加密,加密后设备的启动和挂载等。这个流程涉及到 init,settings,vold,framework。尤其是设置不同的属性以触发不同的处理过程。需要对Android系统有一些比较深的了解。
- 回归到具体的加密而言,借助Device Mapper这种机制,把一个虚拟的Mapped Device和一个真实的block device关联起来,然后使得往虚拟MD读出或者写人的数据解密/加密,这就是DE关于加解密的核心了。
从 上述知识中,我们可以看到Device Mapper的机制在整个DE中扮演了非常重要的角色。这部分代码到没有什么难度,不过涉及kernel方面的知识,我这里不想写那么详细,只是把一些重 要的流程相关的东西给大家展示一下。毕竟,在大部分情况下,我们只需要理解原理就行。
先来看看vold是怎么操作DeviceMapper的,我这里列出一些基本命令:
下面来看看上面这几个函数在MD驱动里是怎么实现的。
DM是一个驱动,好像是RedHat公司的人写的。这个驱动的注册函数是dm_init。
来看看dm_interface_init:
上面对应的ioctl有两个实现,不过最终的处理都在lookup_ioctl函数中。如下所示:
下面我们来看看dev_create函数。
block设备的名字,前面见过了
从代码上看,dm_create将构造一个block device,名字为“dm-xxx”(minor号),并注册了针对这个设备的函数实现。
再来看DM_LIST_VERSION的处理。
list_versions本身没有什么神奇,就是把注册的target模块的版本号取出来。前面一直没提到的是,我们在一个MD上可以注册多个Target模块。比如前面反复见过的“crypt”target,它是通过dm_crypt_init注册上去的:
table_load用于为目标target和一个真实blk设备建立映射关系
分配映射表。本例中,target_count值为1
target_type就是target的名字,此处为"crypt",里边将把参数传递给
crypt target的ctr函数
感兴趣的童鞋可以去看一下crypt target的ctr函数,大概也就是设置一些参数,分配一些资源罢了。在没有介绍Kernel基础知识前,我这里就不多说了。
当MD挂载到/data分区后,应用程序会进程I/O操作,里边有数据来往。当然,我们这里是底层的block device。
4.5.1 读数据并解密
当cyrpt从底层real block设备读取了数据后,它的crypt_endio被调用,我们看看解密的流程。
这个io请求被加到一个workqueue里,对应的处理函数是kcryptd_crypt
kcryptd_crypt这个函数包括了加密和解密功能。我们这里看到的是读操作是怎么触发进入这个函数的。
具体细节就不讨论了.....
4.5.2 写数据并加密
写数据的触发流程如下:
这个io请求被加到一个workqueue里,对应的处理函数是kcryptd_crypt
DE难吗?不难也。其基石说白了就是利用device mapper的crypt target来实现的。当然,这些在kernel里都是现成的,只要了解crypt所需要的参数,理论上非常快就能做完。
但是,DE难,在Android平台上比较难。为什么,因为它需要融合到Android自己的架构里,比如vold,比如framework,比如init以及各种配置文件,比如UI的显示等等等等。