2.1 权限的性质
正如我们在前章所说的,Android应用程序是运行在沙盒的,默认情况下只能访问自己的文件和一组非常有限的系统服务。 为了与系统和其他应用程序进行交互,Android应用程序可以请求安装时授予的一组附加权限,并且无法更改(除了一些例外,这个在后面讨论)。
在Android中,权限只是表示执行特定操作的字符串。 目标操作可以是从访问物理资源(如设备的SD卡)或共享数据(如注册联系人列表)到启动或访问第三方应用程序中的组件的任何操作。 Android带有一组内置的预定义权限。 每个版本都添加了与新功能对应的新权限。
注意:根据应用程序清单中指定的targetSdkVersion
,可以有条件地应用新的内置权限,这些权限会锁定之前不需要权限的功能:定位在新版权限引入之前发布的Android版本的应用程序无法预 知它,因此许可通常是隐式授予的(没有被请求)。 但是,隐式授予的权限仍显示在应用程序安装程序屏幕上的权限列表中,以便用户可以知道它们。 面向更高版本的应用需要明确请求新的权限。
内置权限被记录在平台API参考中。额外的权限称为定制权限(custom permission),可以由系统和用户安装的应用程序定义。
要查看系统当前已知的权限列表,请使用pm list permissions
命令。 要显示有关权限的其他信息(包括定义程序包,标签,说明和保护级别),请在命令中添加-f参数。
权限名称通常以其与字符串.permission
连接的定义程序包作为前缀。 由于内置权限是在android包中定义的,因此它们的名称以android.permission
开头。 例如,在上图中,REBOOT 和BIND_VPN_SERVICE 是内置权限,而GALLERY_PROVIDER 由Gallery应用程序(包com.google.android.gallery3d)定义,RECEIVE_LAUNCH_BROADCASTS 由默认启动器应用程序定义(package com.android.launcher3)。
2.2 请求权限
应用程序通过向其AndroidManifest.xml文件添加一个或多个<uses-permission>
标记来请求权限,并可以使用<permission>
标记定义新的权限。 下图显示了一个请求INTERNET和WRITE_EXTERNAL_STORAGE权限的示例清单文件。
2.3 权限管理
系统软件包管理器服务在安装时将权限分配给每个应用程序(由唯一的软件包名称标识)。 软件包管理器维护一个安装软件包的中央数据库,包括预安装和用户安装的软件包,以及有关每个软件包的安装路径,版本,签名证书和分配的权限的信息,以及设备上定义的所有权限的列表。 (上一节中介绍的pm列表权限命令通过查询软件包管理器来获取此列表。)此软件包数据库存储在XML文件/data/system/packages.xml
中,该文件每次安装应用程序时都会安装,更新或卸载。 下图显示了packages.xml中的典型应用程序条目。
每个包由<package>元素表示,其中包含有关分配的UID(在userId属性中①),签名证书(在<cert>中②)中的信息和分配的权限(在<perms>的子标签中③)。 要以编程方式获取有关已安装软件包的信息,请使用android.content.pm.PackageManager
类的getPackageInfo()
方法,该方法返回封装<package>
标签中包含的信息的PackageInfo实例。 如果所有权限都是在安装时分配的,并且在不卸载应用程序的情况下无法更改或撤销,那么软件包管理器将如何决定是否授予请求的权限? 为了理解这一点,我们需要讨论权限保护级别。
2.4 权限保护级别
根据官方文档,权限的保护级别“表征了许可中隐含的潜在风险,并指出了系统在确定是否授予许可时应遵循的程序。”实际上,这意味着权限是 是否授予取决于其保护级别。 以下各节讨论Android中定义的四种保护级别以及系统如何处理每个级别。
2.4.1 normal
这是默认值。 它定义了对系统或其他应用程序具有低风险的权限。 无需用户确认即可自动授予保护级别正常的权限。 例子有ACCESS_NETWORK_STATE(允许应用程序访问关于网络的信息)和GET_ACCOUNTS(允许访问账户服务中的账户列表)。
2.4.2 dangerous
具有危险保护级别的权限允许访问用户数据或对设备进行某种形式的控制。例如READ_SMS(允许应用程序读取SMS消息)和CAMERA(使应用程序可以访问相机设备)。在授予危险权限之前,Android会显示一个确认对话框,显示有关请求的权限的信息。由于Android要求在安装时授予所有请求的权限,因此用户可以同意安装该应用程序,从而授予所请求的危险权限或取消应用程序安装。
2.4.3 signature
签名权限仅授予使用与声明权限的应用程序相同的密钥签名的应用程序。 这是“最强”的权限级别,因为它要求拥有只有应用程序(或平台)所有者控制的加密密钥。 因此,使用签名权限的应用程序通常由同一作者控制。 内置签名权限通常由执行设备管理任务的系统应用程序使用。 例如NET_ADMIN(配置网络接口,IPSec等)和ACCESS_ALL_EXTERNAL_STORAGE(访问所有多用户外部存储)。
2.4.4 signatureOrSystem
具有此保护级别的权限有点妥协:它们被授予应用程序,这些应用程序既可以是系统映像的一部分,也可以使用与声明权限的应用程序相同的密钥进行签名。 这允许在Android设备上预安装其应用程序的供应商共享需要许可而不必共享签名密钥的特定功能。 在Android 4.3之前,安装在系统分区上的任何应用程序都会自动授予signatureOrSystem
权限。 自Android 4.4以来,应用程序需要安装在/system/priv-app/
目录中,以便通过此保护级别授予权限。
2.5 权限分配
权限在Android中的各个层上实施。 更高级别的组件(如应用程序和系统服务)会查询软件包管理器,以确定哪些权限已分配给应用程序,并决定是否授予访问权限。 像本机守护程序这样的低级组件通常无法访问包管理器,并依赖分配给进程的UID,GID和补充GID来确定授予它的权限。 对设备文件,Unix域套接字(本地套接字)和网络套接字等系统资源的访问由内核根据目标资源的所有者和访问模式以及访问进程的UID和GID进行规定。 首先讨论权限如何映射到操作系统级别的构造,如UID和GID以及如何使用这些进程ID来实施权限。
2.5.1 权限和进程属性
与任何Linux系统一样,Android进程具有许多关联的进程属性,最重要的是真实有效的UID和GID以及一组补充GID。
正如第1章所讨论的,每个Android应用程序在安装时都会被分配一个唯一的UID,并在一个专门的进程中执行。 当应用程序启动时,进程的UID和GID被设置为由安装程序(包管理器服务)分配的应用程序UID。 如果已为应用程序分配了其他许可权,则会将它们映射到GID并将其作为补充GID分配给过程。 在/etc/permission/platform.xml文件中定义了内置权限的GID映射权限。下图显示了Android 4.4设备上的platform.xml文件的摘录。
在这里,INTERNET权限与inet GID①相关联,而WRITE_EXTERNAL_STORAGE权限与sdcard_r和sdcard_rw GID②相关联。因此,已授予INTERNET权限的应用程序的任何进程都与对应于inet的补充GID相关联 组和具有WRITE_EXTERNAL_STORAGE权限的进程将sdcard_r和sdcard_rw的GID添加到关联的补充GID列表中。
<assign-permission>标签用于相反的目的:它用于将更高级别的权限分配给在没有相应包的特定UID下运行的系统进程。 上图显示了使用介质UID(实际上,这是mediaserver的守护程序)运行的进程被分配了MODIFY_AUDIO_SETTINGS③和ACCESS_SURFACE_FLINGER④权限。
Android没有/etc/group文件,所以从组名到GID的映射是静态的,并在android_filesystem_config.h头文件中定义。
android_filesystem_config.h文件还定义了核心Android系统目录和文件的所有者,访问模式和相关功能(用于可执行文件)。 软件包管理器在启动时读取platform.xml并维护权限和关联GID列表。 在安装过程中授予对软件包的权限时,软件包管理器将检查每个权限是否具有关联的GID。 如果是,则将GID添加到与应用程序关联的补充GID列表中。补充GID列表被写为packages.list文件的最后一个字段。
2.5.2 进程属性分配
在我们看到内核和底层系统服务如何检查和强制执行权限之前,我们需要检查Android应用程序进程是如何启动并分配进程属性的。如第1章所述,Android应用程序是用Java实现的,并由Dalvik VM执行。因此,每个应用程序进程实际上都是执行应用程序字节码的Dalvik VM进程。为了减少应用程序内存占用并缩短启动时间,Android不会为每个应用程序启动新的Dalvik VM进程。相反,它使用称为zygote的部分初始化过程,并在需要启动新应用程序时使用它(使用fork()
系统调用)。然而,它不像启动本地进程时那样调用某个exec()
函数,而只是执行指定Java类的main()
函数。这个过程被称为专业化(specialization),因为通用的zygote进程转化为特定的应用进程,就像来自zygote细胞的细胞专门化为执行不同功能的细胞。因此,分叉进程继承了zygote进程的内存映像,该进程已预装大部分核心和应用程序框架Java类。因为这些类永不改变,Linux在分叉进程时使用写时复制机制,zygote的所有子进程(即所有Android应用程序)共享相同的框架Java类副本。
zygote进程由init.rc
初始化脚本启动,并在Unix域套接字(也称为zygote)上接收命令。当zygote收到启动一个新的应用程序进程的请求时,它会自行分叉,而子进程大致执行下面的代码(在dalvik_system_Zygote.cpp中简写为forkAndSpecializeCommon()
),以便使自己专门化,如下图所示。
如上所示,子进程首先使用setgroups()设置其补充GID(对应于权限),由①在setgroupsIntarray()处调用。 接下来,它使用setrlimit()设置资源限制,在②处由setrlimitsFromArray()调用,然后使用setresgid()③和setresuid()④设置真实的,有效的和保存的用户和组ID。
子进程能够更改其资源限制和所有进程属性,因为它最初以root身份执行,就像其父进程zygote一样。 在设置了新的进程属性之后,子进程将使用分配的UID和GID执行,并且无法返回以root身份执行,因为保存的用户标识不为0。
设置完UID和GID后,该过程使用capset()设置其功能,从setCapabilities()⑤调用。 然后,它通过将其自身添加到其中一个预定义的控制组中来设置其调度策略⑥。在⑦该进程设置好名称(显示在进程列表中,通常是应用程序的包名称)和seinfo标签(由SELinux使用, 我们将在第12章中讨论)。最后,它可以在需要时调试。
注意: Android 4.4引入了一个名为Android RunTime(ART)的全新实验性运行时,预计将在未来的版本中取代Dalvik。 虽然ART为当前执行环境带来了许多变化,但最重要的是提前编译(AOT),它使用与Dalvik相同的基于zygote的应用程序执行模型。
在使用ps命令获得的进程列表中,zygote和应用程序进程之间的进程关系很明显,如下图所示。
这里,PID列表示进程ID,PPID列表示父进程ID,NAME列表示进程名称。如您所见,zygote(PID 181②)由init进程启动(PID 1 ①),并且所有应用程序进程都有zygote作为它们的父节点(PPID 181)。每个进程在专用用户下执行,无论是内置(radio,nfc)还是安装时自动分配(u0_a7)。 进程名称设置为每个应用程序的包名称(com.android.phone,com.android.nfc和com.google.android.gms)。
2.6 权限执行
正如前面部分所讨论的,每个应用程序进程在从zygote分出时都会被分配一个UID,GID和补充GID。 内核和系统守护进程使用这些进程标识符来决定是否授予对特定系统资源或功能的访问权限。
2.6.1 内核级执行
访问常规文件,设备节点和本地套接字的方式与任何Linux系统一样。 一个特定于Android的添加需要想要创建属于组inet的网络套接字的进程。 这个Android内核添加被称为“偏执网络安全性”(paranoid network security),并作为Android内核中的附加检查来实现,如下图所示。
不属于AID_INET(GID 3003,name inet)组并且没有CAP_NET_RAW功能(允许使用RAW和PACKET套接字)的调用程序进程会收到拒绝访问错误(①和③)。 NonAndroid内核不定义CONFIG_ANDROID_PARANOID_NETWORK,因此不需要特殊的组成员资格来创建套接字②。为了将inet组分配给应用程序进程,它需要被授予INTERNET权限。 因此,只有具有INTERNET权限的应用程序才能创建网络套接字。 除了在创建套接字时检查进程凭证之外,Android内核还为使用特定GID执行的进程授予某些功能:使用AID_NET_RAW(GID 3004)执行的进程被赋予CAP_NET_RAW功能,并且使用AID_NET_ADMIN(GID 3005) CAP_NET_ADMIN功能。
paranoid network security也用于控制对蓝牙套接字和内核隧道驱动程序(kernel tunneling driver)(用于VPN)的访问。 内核以特殊方式处理的Android GID的完整列表可以在内核源代码的include/linux/android_aid.h
文件中找到。
2.6.2 原生守护程序级执行
虽然Binder是Android中首选的IPC机制,但较低级的本地守护程序通常使用Unix域套接字(Unix domain socket)(本地套接字)用于IPC。 由于Unix域套接字表示为文件系统上的节点,因此可以使用标准文件系统权限来控制访问。
由于大多数套接字是通过只允许访问其所有者和组的访问模式创建的,因此在不同的UID和GID下运行的客户端无法连接到套接字。 系统守护进程的本地套接字在init.rc
中定义,并由init
在启动时使用指定的访问模式创建。 例如,下图显示了如何在init.rc中定义卷管理守护进程(vold):
vold使用0660访问模式声明一个名为vold的套接字,该套接字由root拥有,并且组设置为挂载(mount)①。 vold守护程序需要以超级用户身份运行,以挂载或卸载卷,但挂载组(AID_MOUNT,GID 1009)的成员可以通过本地套接字发送命令,而无需以超级用户身份运行。 Android守护进程的本地套接字是在/dev/socket/
目录中创建的。 下图显示vold套接字①具有在init.rc
中指定的所有者和权限。
Unix域套接字允许使用SCM_CREDENTIALS控制消息和SO_PEERCRED套接字选项来传递和查询客户端凭证。 与作为Binder事务一部分的有效UID和有效GUID一样,与本地套接字关联的对等证书由内核检查,不能由用户级进程伪造。 这允许本地守护进程对它们允许特定客户端执行的操作实施额外的细粒度控制,如下所示,以vold守护进程为例。
vold守护程序仅允许对作为根(UID 0)或系统(AID_SYSTEM,UID 1000)用户运行的客户端执行加密的容器管理命令。 在这里,SocketClient-> getUid()①返回的UID使用getsockopt(SO_PEERCRED)获得的客户端UID进行初始化,如下所示。
本地套接字连接功能封装在android.net.LocalSocket
类中,也可用于Java应用程序,允许更高级别的系统服务与本地守护程序进行通信而无需使用JNI代码。 例如,MountService框架类使用LocalSocket向vold守护进程发送命令。
2.6.3 框架级执行
正如在Android权限介绍中所讨论的,可以通过在封闭应用程序的清单中声明所需的权限来使用权限来控制对Android组件的访问。系统会跟踪与每个组件关联的权限,并在允许访问之前检查调用者是否已被授予所需的权限。由于组件不能在运行时更改它们所需的权限,因此系统的强制实施是静态的。静态权限是声明性安全性的一个例子。在使用声明性安全性时,安全属性(如角色和权限)将放置在组件的元数据(Android中的AndroidManifest.xml文件)中,而不是组件本身中,并由容器或运行时环境强制执行。这具有隔离安全决策与业务逻辑的优点,但可能不如在组件中实施安全检查那么灵活。
Android组件还可以检查调用进程是否已被授予特定权限,而无需在清单中声明权限。这种动态权限执行需要更多的工作,但允许更细粒度的访问控制。动态许可强制是强制性安全的一个例子,因为安全决策是由每个组件制定的,而不是由运行时环境强制执行。
我们来看看如何更详细地实施动态和静态权限执行。
2.6.4 动态执行
正如第1章所讨论的,Android的核心是作为一套协作系统服务来实现的,可以使用Binder IPC机制从其他进程中调用。核心服务向服务管理器注册,任何知道其注册名称的应用程序都可以获得一个Binder引用。因为Binder没有内置的访问控制机制,所以当客户端有引用时,他们可以通过将适当的参数传递给Binder.transact()
来调用底层系统服务的任何方法。因此,访问控制需要由每个系统服务来实现。
在第1章中,我们展示了系统服务可以通过直接检查从Binder.getCallingUid()
获取的调用者的UID来调节对导出操作的访问。但是,此方法要求服务提前知道允许的UID列表,该列表仅适用于众所周知的固定UID,例如根(UID 0)和系统(UID 1000)的UID。另外,大多数服务并不关心调用者的实际UID,他们只是想检查它是否被授予了某种权限。
由于Android中的每个应用程序UID都与一个唯一包关联(除非它是共享用户ID的一部分),并且包管理器记录了授予每个包的权限,因此可以通过查询包管理器服务来实现。检查调用者是否具有某种权限是一种非常常见的操作,Android在可以执行此检查的android.content.Context
类中提供了一些辅助方法。
我们先来看看int Context.checkPermission(String permission, int pid, int uid)
方法是如何工作的。如果传递的UID具有权限,则此方法返回PERMISSION_GRANTED,否则返回PERMISSION_DENIED。如果调用者是root或system,则会自动授予权限。作为性能优化,如果请求的权限已被调用应用程序声明,则不会检查实际许可权。如果不是这种情况,该方法会检查目标组件是公有(导出)还是私有组件,并拒绝访问所有私有组件。 最后,代码将查询包管理器服务,以查看调用者是否已被授予请求的权限。 PackageManagerService类的相关代码如下所示。
在这里,PackageManagerService首先根据传递的UID①确定应用程序的应用程序ID,然后获得授予的权限集。如果GrantedPermission类(包含实际的java.util.Set<String>权限名)包含目标权限,则该方法返回PERMISSION_GRANTED②。如果不是,则检查目标权限是否应自动分配给传入UID ③(基于platform.xml中的<assign-permission>标记)。如果此检查也失败,它最终会返回PERMISSION_DENIED。
Context类中的其他权限检查辅助方法遵循相同的过程。 int checkCallingOrSelfPermission(String permission)
方法为我们调用Binder.getCallingUid()
和Binder.getCallingPid()
,然后使用获取的值调用checkPermission(String permission, int pid, int uid)
。如果权限未被授予,则enforcePermission(String permission, int pid, int uid, String message)
方法不返回结果,而是抛出带有指定消息的SecurityException。例如,BatterStatsService类保证只有具有BATTERY_STATS权限的应用程序才能通过在执行任何其他代码之前调用enforceCallingPermission()
来获得电池统计信息,如下图所示。未被授予权限的调用者会收到SecurityException。
2.6.5 静态执行
当应用程序试图与另一个应用程序声明的组件进行交互时,静态权限执行会发挥作用。强制执行过程会考虑为每个目标组件声明的权限(如果有),并允许在调用者进程已被授予所需权限的情况下进行交互。
Android使用intent来描述它需要执行的操作,并且完全指定目标组件(通过包和类名称)的intent被称为显式。另一方面,隐式intent包含一些允许系统查找匹配组件的数据(通常只有一个抽象操作,例如ACTION_SEND),但它们没有完全指定目标组件。
当系统收到一个隐式intent时,它首先通过搜索匹配的组件来解决它。如果找到多个匹配的组件,则向用户呈现选择对话框。当一个目标组件被选中时,Android会检查它是否有任何关联的权限,如果有,则检查它们是否已被授予给调用者。
一般过程类似于动态执行:调用者的UID和PID使用Binder.getCallingUid()和Binder.getCallingPid()获得,调用者UID被映射到包名称,并且检索关联的权限。如果调用者权限集包含目标组件所需的权限,则组件将启动;否则,抛出SecurityException。
权限检查由ActivityManagerService执行,后者解析指定的intent并检查目标组件是否具有关联的权限属性。如果是,它将权限检查委托给包管理器。根据目标组件,权限检查的时间和具体顺序略有不同。 (接下来,我们将研究如何针对每个组件执行检查。)
2.6.6 活动和服务权限执行
如果传递给Context.startActivity()或startActivityForResult()的intent解析为声明权限的活动,则会执行活动的权限检查。 如果调用者没有所需的权限,则抛出SecurityException。 由于Android服务可以启动,停止和绑定,因此如果目标服务声明权限,则对Context.startService(),stopService()和bindService()的调用都将受到权限检查。
2.6.7 内容提供者权限执行
内容提供者权限可以保护整个组件或特定的导出URI,并且可以为读写指定不同的权限。 如果指定了不同的读写权限,则读权限控制谁可以调用目标提供程序或URI上的ContentResolver.query(), 并且写权限控制谁可以在提供程序或其导出的URI之一上调用ContentResolver.insert(),ContentResolver.update()和ContentResolver.delete()。 当这些方法之一被调用时,这些检查是同步执行的。
2.6.8 广播权限执行
发送广播时,应用程序可以通过使用Context.sendBroadcast(Intent intent, String receiverPermission)
方法来要求接收者保持特定的权限。由于广播是异步的,因此在调用此方法时不会执行权限检查。将意向传送给注册的接收者时执行检查。如果目标接收方不具有所需的权限,则它将被跳过并且不会收到广播,但不会引发异常。反过来,广播接收机可能要求广播机构持有特定的许可,以便能够定位它们。
必需的权限是在清单中或在动态注册广播时指定的。此权限检查也在传送广播时执行,并且不会导致SecurityException。因此,传送广播可能需要两个权限检查:一个用于广播发送者(如果接收者指定了权限),另一个用于广播接收者(如果发送者指定了权限)。
2.6.9 受保护和粘性广播
某些系统广播被声明为受保护的(例如,BOOT_COMPLETED和PACKAGE_INSTALLED),并且只能由作为SYSTEM_UID,PHONE_UID,SHELL_UID,BLUETOOTH_UID或root之一运行的系统进程发送。 如果运行在不同UID下的进程尝试发送受保护的广播,则在调用其中一个sendBroadcast()方法时会收到SecurityException。 发送“粘性”(sticky)广播(如果标签为粘滞,系统在广播完成后保留发送的Intent对象)要求发送方拥有BROADCAST_STICKY权限; 否则,抛出SecurityException并且不发送广播。
2.7 系统权限
Android的内置权限是在android包中定义的,有时也被称为“框架”或“平台”。正如我们在第1章中学到的,核心Android框架是由系统服务共享的一组类,其中一些 通过公有SDK暴露。 框架类打包在/system/framework/目录下的JAR文件中(最新版本中大约有40个)。
除了JAR库之外,框架还包含一个APK文件framework-res.apk。 顾名思义,它打包了框架资源(动画,drawables,布局等),但没有实际的代码。 最重要的是,它定义了android包和系统权限。 由于framework-res.apk是一个APK文件,它包含一个声明权限组和权限的AndroidManifest.xml文件(参见下图)。
如图所示,AndroidManifest.xml文件还会声明系统受保护的广播①。权限组②为一组相关权限指定名称。通过在他们的permissionGroup属性③中指定组名,可以将单个权限添加到组中。
权限组用于在系统UI中显示相关权限,但每个权限仍需要单独请求。也就是说,应用程序不能请求他们被授予组中的所有权限。回想一下,每个权限都有一个使用protectionLevel属性声明的关联保护级别,如④所示。
保护级别可以与保护标志结合使用,以进一步限制授予权限的方式。当前定义的标志是system(0x10)和development(0x20)。系统标志要求应用程序是系统映像的一部分(即,安装在只读系统分区上)以获得许可。例如,允许应用程序管理USB设备的首选项和权限的MANAGE_USB权限仅授予使用平台签名密钥签名并安装在系统分区⑤上的应用程序。开发标志标记开发权限⑥,我们将在提供签名权限后讨论这些权限。
2.7.1 签名权限
正如第1章所讨论的,所有Android应用程序都需要使用由开发人员控制的签名密钥进行代码签名。这也适用于系统应用程序和框架资源包。我们将在第3章中详细讨论软件包签名,但现在让我们谈谈系统应用程序如何签名。
系统应用程序由平台密钥进行签名。默认情况下,当前Android源代码中有四个不同的密钥:平台,共享,媒体和测试密钥(用于发布版本的releasekey)。被认为是核心平台的一部分的所有软件包(系统UI,设置,电话,蓝牙等)都使用平台密钥进行签名;与搜索和联系人相关的包用共享密钥;图库应用和媒体相关提供商使用媒体密钥;和其他所有内容(包括未在makefile中明确指定签名密钥的软件包)用testkey(或releasekey)。定义系统权限的framework-res.apk APK使用平台密钥进行签名。因此,任何尝试请求具有签名保护级别的系统权限的应用都需要使用与框架资源包相同的密钥进行签名。
例如,上面图中显示的NET_ADMIN权限(允许授权应用程序控制网络接口)signature保护级别④声明,并且只能授予使用该平台密钥签名的应用程序。
注意:Android开放源代码存储库(AOSP)包含预生成的测试密钥,默认情况下会在对已编译的软件包进行签名时使用。 它们不应该用于生产版本,因为它们是公开的,并且适用于任何下载Android源代码的人。 发布版本应使用仅生成所有者的新生成的私钥进行签名。可以使用包含在development/tools/AOSP目录中的make_key脚本生成密钥。有关平台密钥生成的详细信息,请参阅build/target/product/security/README文件。
2.7.2 开发权限
传统上,Android权限模型不允许动态授予和撤销权限,并且应用程序授予的权限集在安装时是固定的。但是,自从Android 4.2以来,通过添加许多开发权限(如READ_LOGS和WRITE_SECURE_SETTINGS),这个规则已经放松了一点。 可以使用Android shell上的pm grant
和pm revoke
命令按需授予或撤销开发权限。
注意:当然,这个操作并不适用于所有人,并受到GRANT_REVOKE_PERMISSIONS签名权限的保护。 它被授予android.uid.shell共享用户ID(UID 2000),并授予从Android shell(也作为UID 2000运行)启动的所有进程。
2.8 共享用户ID
使用相同的密钥签名的Android应用程序可以请求以相同的UID运行,并且可以选择在同一个进程中运行。此功能称为共享用户标识(shared user ID),并广泛用于核心框架服务和系统应用程序。因为它可以对进程计数和应用程序管理产生微妙的影响,所以Android团队不建议第三方应用程序使用它,但它也适用于用户安装的应用程序。此外,不支持将不使用共享用户标识的现有应用程序切换到共享用户标识,因此应从一开始就设计和发布需要使用共享用户标识的协作应用程序。
通过将sharedUserId属性添加到AndroidManifest.xml的根元素来启用共享用户标识。清单中指定的用户标识需要采用Java软件包格式(至少包含一个点[.])并用作标识符,与应用程序的软件包名称非常相似。如果指定的共享UID不存在,则创建它。如果已经安装了具有相同共享UID的另一个软件包,则将该签名证书与现有软件包进行比较,如果它们不匹配,则会返回INSTALL_FAILED_SHARED_USER_INCOMPATIBLE错误,并且安装失败。
将sharedUserId属性添加到新版本的已安装应用程序将导致其更改其UID,这将导致无法访问其自己的文件(在某些早期Android版本中就是这种情况)。因此,系统不允许这样做,该系统将使用INSTALL_FAILED_UID_CHANGED错误拒绝更新。简而言之,如果您打算为您的应用使用共享UID,则必须从一开始就为其设计,并且必须在第一次发布后使用它。
共享UID本身是系统包数据库中的第一个类对象,并且与应用程序非常相似:它具有关联的签名证书和许可权。 Android有五个内置的共享UID,当系统被引导时会自动添加:
- android.uid.system (SYSTEM_UID, 1000)
- android.uid.phone (PHONE_UID, 1001)
- android.uid.bluetooth (BLUETOOH_UID, 1002)
- android.uid.log (LOG_UID, 1007)
- android.uid.nfc (NFC_UID, 1027)
下图显示了如何定义android.uid.system共享用户
正如您所看到的,除了拥有一堆可怕的权限(4.4设备上的大约66个)之外,该定义与前面显示的包声明非常相似。 相反,作为共享用户的一部分的软件包没有关联的授予权限列表。 相反,它们继承了共享用户的权限,这是所有当前安装的包使用相同共享用户标识请求的权限的联合。 这样做的一个副作用是,如果某个包是共享用户的一部分,则只要具有相同共享用户标识的某个包已经请求了这些包,就可以访问它未明确请求其权限的API。 虽然软件包已安装或卸载,但是权限会从<shared-user>定义中动态删除,因此可用权限集既不能保证也不能保持不变。
下图显示了如何在共享用户ID下运行的KeyChain系统应用程序的声明。 如您所见,它使用sharedUserId属性引用共享用户,并缺少明确的权限声明:
共享的UID不仅仅是一个包管理结构。 它在运行时也会映射到共享的Linux UID。 下图显示了作为系统用户(UID 1000)运行的两个系统应用程序的示例:
作为共享用户的一部分的应用程序可以在同一个进程中运行,并且因为它们已经具有相同的Linux UID并且可以访问相同的系统资源,所以这通常不需要任何额外的修改。可以通过在需要在一个进程中运行的所有应用程序的清单中的<application>标记的process属性中指定相同的进程名称来请求常见进程。虽然这样做的明显结果是应用程序可以共享内存并直接与其通信,但某些系统服务允许对同一进程中运行的组件进行特殊访问(例如直接访问缓存密码或获取身份验证令牌而不显示UI提示)。 Google应用程序(例如Play服务和Google位置服务)通过请求以与Google登录服务相同的流程运行,以便能够在没有用户交互的情况下同步后台数据。当然,它们使用相同的证书进行签名,并且是com.google.uid.shared共享用户的一部分。
2.9 自定义权限
自定义权限只是第三方应用程序声明的权限。 声明时,可以将它们添加到应用程序组件以供系统静态实施,或者应用程序可以使用Context类的checkPermission()或enforcePermission()方法动态检查调用者是否已被授予权限。 与内置权限一样,应用程序可以定义将自定义权限添加到的权限组。 例如,下图显示了权限组②的声明和属于该组③的权限。
与系统权限一样,如果保护级别正常或危险,当用户确认确认对话框时,将自动授予定制权限。 为了能够控制哪些应用程序被授予自定义权限,您需要使用签名保护级别来声明它,以确保它只会被授予使用相同密钥签名的应用程序。
注意:系统只能授予它所知道的权限,这意味着在安装使用这些权限的应用程序之前,需要安装定义自定义权限的应用程序。 如果应用程序请求系统未知的权限,则该权限将被忽略并且不被授予。
应用程序还可以使用android.content.pm.PackageManager.addPermission()
API动态添加新权限,并使用匹配的removePermision()
API删除它们。 这种动态添加的权限必须属于应用程序定义的权限树。应用程序只能在自己的程序包或另一个作为相同共享用户标识运行的程序包中的权限树中添加或删除权限。权限树名称是反向域名表示法,如果某个权限的名称前面带有权限树名称加点(.),则认为该权限位于权限树中。 例如,com.example.app.permission.PERMISSION2权限是上图①的中定义的com.example.app.permission树的成员。
下图显示了如何以编程方式添加动态权限。
动态添加的权限被添加到包数据库(/data/system/packages.xml)。 它们在重新启动时持续存在,就像在清单中定义的权限一样,但它们具有设置为动态的其他类型属性。
2.10 公有和私有组件
AndroidManifest.xml文件中定义的组件可以是公有或私有的。私有组件只能由声明应用程序调用,而公共应用程序也可用于其他应用程序。
除内容提供者外,所有组件都默认为私有。由于内容提供者的目的是与其他应用程序共享数据,因此内容提供者默认情况下最初是公开的,但是此行为在Android 4.2(API Level 17)中发生了变化。目标API Level 17或更高版本的应用程序现在默认获取私有内容提供程序,但在针对较低的API级别时,它们会保持公开以实现向后兼容。
可以通过将导出的属性显式设置为true来公开组件,或通过声明一个意图过滤器隐式地公开。具有intent过滤但不需要公开的组件可以通过将导出属性设置为false而变为私有。如果未导出组件,则不管已授予调用进程的权限(除非以root或系统身份运行),来自外部应用程序的调用都会被活动管理器阻止。下图显示了如何通过将导出属性设置为false来保持组件私有。
除非明确用于公共消费,否则所有公有组件都应该受到自定义权限的保护。
2.11 活动和服务权限
活动和服务可以通过使用目标组件的权限属性设置的单一权限来保护。 当其他应用程序调用Context.startActivity()或Context.startActivityForResult()时,会检查活动权限,并且intent解析为该活动。 对于服务,当其他应用程序以解析为服务的intent调用Context.startService(),stopService()或bindService()之一时检查权限。 例如,下图显示了两个自定义权限,START_MY_ACTIVITY和USE_MY_SERVICE,分别设置为活动①和服务②。 想要使用这些组件的应用程序需要使用清单中的<uses-permission>标签来请求相应的权限。
2.12 广播权限
与活动和服务不同,广播接收者的权限可以由接收者本身和发送广播的应用程序指定。在发送广播时,应用程序可以使用Context.sendBroadcast(Intent intent)
方法发送广播,以将其发送到所有已注册的接收器,或者使用Context.sendBroadcast(Intent intent, String receiverPermission)
。 receiverPermission参数指定感兴趣的接收者为了接收广播而需要保留的权限。或者,从Android 4.0开始,发件人可以使用Intent.setPackage(String packageName)
将接收者的范围限制为指定包中定义的范围。在多用户设备上,拥有INTERACT_ACROSS_USERS权限的系统应用程序可以通过使用sendBroadcastAsUser(Intent intent, UserHandle user)
和sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission)
方法发送仅传递给特定用户的广播。
接收者可以通过使用清单中的<receiver>标签的permission属性为静态注册的接收者指定一个权限,或者通过将所需的权限传递给Context.registerReceiver(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler)
方法来动态注册接收者。
只有获得所需许可的广播公司才能向该接收机发送广播。例如,实施全系统安全策略的设备管理应用程序(我们将在第9章讨论设备管理)需要BIND_DEVICE_ADMIN权限才能接收DEVICE_ADMIN_ENABLED广播。由于这是具有保护级别签名的系统权限,因此需要权限才能保证只有系统才能激活设备管理应用程序。例如,下图显示了默认的Android电子邮件应用程序如何为其PolicyAdmin接收者指定BIND_DEVICE_ADMIN①权限。
与其他组件一样,专用广播接收器只能接收来自同一应用程序的广播。
2.13 内容提供者权限
如前面的“权限性质”中所述,内容提供者具有比其他组件更复杂的权限模型,因为我们将在本节中详细介绍。
2.13.1 静态提供程序权限
尽管可以使用permission属性指定控制对整个提供者的访问的单个权限,但大多数提供者都使用不同的读写权限,并且还可以指定每个URI的权限。 使用不同权限进行读写的提供者的一个示例是内置的ContactsProvider。 下图显示了它的ContactsProvider2类的声明。
提供程序使用readPermission属性指定一个读取数据的权限(READ_CONTACTS ①),以及使用writePermission属性(WRITE_CONTACTS ②)写入数据的单独权限。因此,仅具有READ_CONTACTS权限的应用程序只能调用提供程序的query()方法,并且调用insert(),update()或delete()会要求调用方拥有WRITE_CONTACTS权限。需要读取和写入联系人提供程序的应用程序需要同时拥有这两个权限。
当全局读写权限不够灵活时,提供者可以指定每个URI的权限来保护其数据的某个子集。每个URI权限的优先级高于组件级权限(如果单独指定,则为读写权限)。因此,如果应用程序想要访问具有相关权限的内容提供者URI,则它只需要保留目标URI的权限,而不是组件级权限。在上图中,ContactsProvider2使用<path-permission>标签来要求试图读取联系人照片的应用程序拥有GLOBAL_SEARCH权限③。由于每个URI权限覆盖全局读取权限,所以感兴趣的应用程序不需要保持READ_CONTACTS权限。在实践中,GLOBAL_SEARCH权限用于授予对Android搜索系统某些系统提供者数据的只读访问权限,该系统不能期望拥有对所有提供者的读取权限。
2.13.2 动态提供程序权限
虽然静态定义的每个URI权限可能非常强大,但应用程序有时需要授予对其他应用程序的特定数据片段(由其URI引用)的临时访问权限,而不需要他们拥有特定的权限。例如,电子邮件或消息应用程序可能需要与图像查看器应用程序合作才能显示附件。由于应用程序无法预先知道附件的URI,因此如果它使用静态的每个URI权限,则需要授予对图像查看器应用程序的所有附件的读取权限,这是不可取的。
为了避免这种情况和潜在的安全问题,应用程序可以使用Context.grantUriPermission(String toPackage, Uri uri, int modeFlags)
方法动态地授予临时的per-URI访问权限,并使用匹配的revokeUriPermission(Uri uri, int modeFlags)
方法撤销访问。通过将全局grantUriPermissions属性设置为true或通过添加<grant-uri-permission>标记来启用临时的每个URI访问,以便为特定的URI启用它。例如,下图显示了Email应用程序如何使用grantUriPermissions属性①来允许临时访问附件而不需要READ_ATTACHMENT权限。
实际上,应用程序很少直接使用Context.grantPermission()
和revokePermission()
方法来允许每个URI的访问。相反,他们将FLAG_GRANT_READ_URI_PERMISSION或FLAG_GRANT_WRITE_URI_PERMISSION标志设置为用于启动协作应用程序的intent(在本例中为图像查看器)。当这些标志被设置时,intent的接收者被授予对intent数据中的URI执行读或写操作的权限。
从Android 4.4(API Level 19)开始,如果接收到的intent设置了FLAG_GRANT_PERSISTABLE_URI_PERMISSION标志,则每个URI访问授权可以通过ContentResolver .takePersistableUriPermission()
方法跨设备重新启动持久保存。授权持久化到/data/system/urigrants.xml文件,并且可以通过调用releasePersistableUriPermission()
方法来撤销。暂时的和持久的每个URI访问授权都由系统ActivityManagerService管理,这些API与内部的每个URI访问调用相关。
从Android 4.1(API级别16)开始,应用程序可以使用ClipData工具的intent添加多个内容URI以临时授予访问权限。
使用FLAG_GRANT_*意向标志之一授予每个URI的访问权限,并在被调用应用程序的任务完成时自动撤销,因此不需要调用revokePermission()
。下图显示了电子邮件应用程序如何创建启动附件查看器应用程序的intent。
2.14 待处理intent(pending intent)
待处理intent既不是Android组件,也不是权限,但由于它们允许应用程序将自己的权限授予其他应用程序,因此我们在此讨论它们。
待处理intent封装了一个intent和一个目标动作来执行它(启动一个活动,发送一个广播,等等)。与“常规”intent的主要区别在于待处理intent还包括创建它们的应用程序的标识。这允许将待处理intent交给其他应用程序,这些应用程序可以使用它们使用原始应用程序的标识和权限执行指定的操作。存储在待处理intent中的身份由系统ActivityManagerService保证,该系统跟踪当前活动的待处理intent。
待处理intent用于在Android中实现警报和通知。警报和通知允许任何应用程序指定需要以其名义执行的操作,既可以在指定的时间进行警报,也可以在用户与系统通知进行交互时执行。当创建它们的应用程序不再运行时,警报和通知可以被触发,并且系统使用未决意图中的信息启动它并代表其执行意向操作。下图显示了电子邮件应用程序如何使用PendingIntent.getBroadcast()
①创建的待处理intent调度触发电子邮件同步的广播。
待处理intent也可以交给非系统应用程序。同样的规则适用:接收待处理Intent实例的应用程序可以使用与创建者应用程序相同的权限和标识来执行指定的操作。因此,在构建基本意图时应该小心,并且基本intent应该尽可能具体(明确指定组件名称)以确保目标组件能够接收intent。
挂起intent的实现相当复杂,但它基于与构建其他Android组件相同的IPC和沙盒原理。当应用程序创建一个挂起的intent时,系统使用Binder.getCallingUid()
和Binder.getCallingPid()
检索它的UID和PID。基于这些,系统检索创建者的包名和用户ID(在多用户设备上),并将它们与基本intent以及任何其他元数据一起存储在PendingIntentRecord中。活动管理器通过存储相应的PendingIntentRecords来保存活动挂起的intent列表,并在触发时检索必要的记录。然后,它使用记录中的信息来承担未决意向创建者的身份并执行指定的操作。从那里开始,这个过程与启动任何Android组件并且执行相同的权限检查时相同。
总结
Android在受限制的沙箱中运行每个应用程序,并要求应用程序请求特定的权限才能与其他应用程序或系统进行交互。权限是表示执行特定操作的能力的字符串。它们是在应用程序安装时授予的,并且(除开发权限外)在应用程序的生命周期内保持不变。权限可以映射到Linux补充组ID,内核在授予对系统资源的访问权限之前会进行检查。
更高级别的系统服务通过使用Binder获取调用应用程序的UID并查找其在包管理器数据库中的权限来强制实施权限。与应用程序清单文件中声明的组件相关联的权限由系统自动执行,但应用程序也可以选择动态执行其他权限检查。除了使用内置权限之外,应用程序还可以定义自定义权限并将它们与其组件关联以控制访问权限。
每个Android组件都可能需要权限,并且内容提供者可以另外指定每个URI的读写权限。待处理intent封装了创建它们的应用程序的身份以及要执行的intent和操作,这允许系统或第三方应用程序代表具有相同身份和权限的原始应用程序执行操作。