安全性和许可

Android是一个权限分离(privilege-separated)的操作系统,其中的每一个运行的程序有一个单独的系统识别(identity)(Linux 用户ID及组ID)。此外系统的不同部分也被分入了不同的识别。通过这种方式Linux使不同的程序和系统之间相互独立。

其他的高级安全特性也由一种称为“权限”的机制所提供,它强行规定了一个进程可以执行哪些特定操作,同时URI级别的权限(per-URI permission)担保了对于特定数据片段的ad-hoc访问。

安全构架

Android安全构架的一个中心设计要点就是默认来说没有程序有进行任何可能对其他程序、操作系统或是用户产生负面影响的任何操作的许可(permission)。这包括了读写用户私人数据(例如通讯录或是电子邮件),读写其他程序的文件,执行网络接入或保持设备不进入睡眠状态等。

因为内核将程序通过沙盒分离,所以程序必须显式地共享资源及数据。它们通过声明许可来获取它们所需的没有通过基本沙盒提供的额外功能。程序静态地声明它们所需的许可,Android系统将在程序安装时向用户征求许可。Android没有动态(在运行时)担保许可的机制,因为这将使安全性受损,使得用户体验变糟。

只有内核负责将程序通过沙盒与其他程序分离。尤其要注意Dalvik VM以及任何可以运行原生代码(native code)的程序(参见Android NDK)并不是安全边界。所有类型的程序——Java,原生(native)或是混合(hybrid)——都以同样方式被沙盒化,相互之间拥有相同的级别的安全性。

应用程序签名

所有的Android应用程序(.apk文件)必须用一个开发者拥有其私钥的认证(certificate)来进行签名。该认证用于识别应用程序的权限。一个认证不需要由认证权威来进行签名:Android程序完全可以,而且通常也会,使用自签名的认证。Android的认证的目的是为了识别应用程序作者。这使得系统可以允许或是拒绝程序获取签名级别(signature-level)的许可以及程序希望获取“相同作者程序”的Linux识别的请求。

用户ID与文件读取

在安装时,Android分给每一个包(package)一个独自的Linux用户ID。该识别将在这个包在设备上的整个周期内保持不变。在不同的设备上,同一个包可能会有不同的UID;不过重要的是在给定的某一个设备上每一个包都有一个独自的UID。

因为安全强化实在进程级别进行的,所以两个不同包的代码无法正常运行于同一个进程,他们需要作为不同的Linux用户运行。可以在每一个包的AndroidManifest.xml的manifest标签中使用sharedUserId属性来将其标为相同的用户ID。这样一来,这两个包就被认为是同一个程序,有同样的用户ID和文件权限了。注意为了保持安全性,只有两个有同样签名(且请求了同样的sharedUserId)的程序才会被分配同样的用户ID。

一个程序存储的任何数据会被分配以这个程序的用户ID,而无法被其他包正常访问。当使用getSharedPreferences(String, int),openFileOutput(String, int)或openOrCreateDatabase(String, int, SQliteDatabase.CursorFactory)来创建新文件时,可以用MODE_WORLD_READABLE和/或MODE_WORLD_WRITEABLE 旗标来允许其他包读/写该文件。当设置了这些旗标后,文件仍然属于原来的程序,但是因为设置了全局读/写许可,所以对任何其他程序都可见。

使用许可

一个基本的Android程序没有与之关联的许可,这意味着它无法进行任何可能影响用户体验或是影响设备上任何数据的操作。要利用这些设备上受保护的特性,就必须在AndroidManifest.xml文件中包含一个或更多的<uses-permission>标签来申明程序所需的许可。

例如,一个需要监视收到的短信的程序应该指定:

在程序安装时,基于用户对声明了许可和/或交互的程序签名的检查,包安装器(package installer)将会认可程序所请求的许可。在程序运行时不会进行任何检查:要么在安装时许可被允许而可以如所希望的那样使用该特性,要么许可不被认可,任何使用该特性的操作将不经过用户判断而直接拒绝。

通常许可错误会导致一个SecurityException被抛回程序。然而,这也不是在所有情况下都会发生的事。例如,sendBroadcast(Intent)方法会在方法调用被返回之后检查数据被传递至每一个接收者的许可,因此即使有许可错误,也不会收到异常。不过,在绝大多数情况下,许可失败都将会被打印于系统日志上。

Android系统所提供的许可可以在Manifest.permission中找到。任何程序也可以定义并执行其自有许可,因此那并非是所有可用许可的完整列表。

一个特定的许可可以在程序执行时被执行于多个地方:

  • 在系统执行一个调用时,阻止应用程序执行某一功能。
  • 在启动一个活动时,阻止应用程序启动属于其他程序的活动。
  • 在发送和接收广播时,控制广播的接收方和发送方。
  • 在读取并操作内容提供者时。
  • 在绑定或启动一个服务时。

声明并执行许可

要执行自有许可,就必须首先在AndroidManifest.xml中通过一个或多个<permission>标签来声明它们。

例如,如果一个程序希望可以控制谁能启动它的活动,它可以像这样为这个操作声明一个许可:

<protectionLevel>属性是必需的,它会按链接文档中描述的那样告诉系统用户将如何被告知程序需要该许可,或是谁将被允许拥有这个许可。

<permissionGroup>属性是可选的,仅仅被用来帮助系统向用户显示该许可。通常会对一个标准系统组(standard system group)(列于android.Manifest.permission_group)或在某些特殊情况下对自己定义的系统组设置该属性。推荐使用一个已有的组,以简化向用户显示的许可界面。

注意必须同时向许可提供标签(label)和描述(description)。它们是用户在查看一列许可(android:label)或某一许可的详细内容(android:description)时将被显示的字符串资源。标签应当简短,用几个词描述该许可所保护的主要功能。描述可以是说明该许可允许持有者能做什么事的几句句子。习惯上用两句话来描述,第一句描述该许可,第二句告知用户当程序获得该许可后将有哪些可能的不良后果。

这是一个CALL_PHONE许可的标签和描述的例子:

可以通过shell指令adb shell pm list permissions来查看当前系统中所定义的许可。一般来说,’-s’选项将会像展示给用户看到的那样展示这些许可。

在AndroidManifest.xml中执行许可

限制系统或程序的整体组件接入的高级别许可可以在AndroidManifest.xml中被应用。所需要做的只是将android:permission属性包含于所希望的组件之中,并为将被用于控制接入的许可命名。

Activity许可(应用于<activity>标签)限制了谁可以启动相关的活动。该许可在Context.startActivity()和Activity.startActivityForResult()的过程中会被检查;如果调用者没有所需的许可,该调用将抛出SecurityException。

Service许可(应用于<service>标签)限制了谁可以启动或是绑定相关的活动。该许可在Context.startService()、Context.stopService()和Context.bindService()的过程中会被检查;如果调用者没有所需的许可,该调用将抛出SecurityException。

BroadcastReceiver许可(应用于<receiver>标签)限制了谁可以向关联的接收者发送广播。该许可在Context.sendBroadcast()返回后系统试图向给定接收者传递所请求的广播时被检查。因此,许可错误不会向调用者抛出例外;它只是不会传递该意图。同样地,该许可可以用于Context.registerReceiver()控制谁可以向一个在代码中注册的接收者发送广播。另一方面,该许可也可以用于在调用Context.sendBroadcast()时限制哪一个BroadcastReceiver对象可以被允许接收广播(见下文)。

ContentProvider许可(应用于<provider>标签)限制了谁可以读取内容提供者内的数据。(内容提供者拥有一项重要的额外安全机制,允许它们调用URI许可。这将在之后说明。)和其他组件不同,内容提供者可以设置两个不同的属性:android:readPermission限制了谁可以读取该提供者,而android:writePermission限制了谁可以进行写操作。注意,如果一个提供者同时被读和写许可所保护,仅持有写许可并不意味着就可以读取提供者的内容。许可将在第一次检索一个提供者时和对提供者执行操作时被检查(如果没有持有任何一个许可,则将会抛出一个SecurityException)。使用ContentResolver.query()来请求持有读许可;使用ContentResolver.insert()、ContentResolver.update()和ContentResolver.delete()来请求写许可。在所有这些情况下,未持有所需的许可将导致调用抛出SecurityException 。

在发送广播时执行许可

另外,对于限制谁可以向一个注册的BroadcastReceiver发送Intent的许可(如同之前所描述的),还可以在发送广播时指定一个必需的许可。通过调用Context.sendBroadcast()及一个许可字符串,可以要求一个接收者程序必须持有该许可以接收广播。

注意,接收者和广播者都可以请求许可。在这时,Intent必须通过对两种许可的检查才能被传送至相关联的目标。

其他许可执行

强制的细化许可可以对任意服务调用执行。这通过Context.checkCallingPermission()方法执行。以一段所需的许可字符串调用后它将会返回一个整型值,显示该许可是否已被当前调用的进程所许可。注意这只能在执行来自另一进程中的调用时使用(这通常要通过一个由服务发布的IDL接口或是某些其他方法来实现)。

还有许多其他有用的检查许可的方法。如果拥有另一进程的pid,就可以用Context方法的Context.checkPermission(String, int, int)来检查违反该pid的许可。如有有另一个程序的包名,就可以直接用PackageManager方法PackageManager.checkPermission(String, String)来查看特定的包是否已经被某一指定许可所认可。

URI许可

在使用内容提供者时,至今所描述的标准许可系统常常是不足以胜任的。内容提供者可能需要读写许可以保护自己,同时它的直接客户端也需要处理其他程序的特殊URI以供执行操作。典型的例子是邮件程序中的附件。应当通过许可来保护对邮件的访问,因为这是用户敏感数据。不过,如果一个指向图像附件的URI被交给了图像查看器,该查看器将不会有打开附件的许可,因为它没有理由持有访问所有电子邮件的许可。

这个问题的解决方案是单URI许可(per-URI permission):当启动一个活动或是向一个活动返回结果时,调用者可以设置Intent.FLAG_GRANT_READ_URI_PERMISSION和/或Intent.FLAG_GRANT_WRITE_URI_PERMISSION。这将许可接收方访问Intent内的指定数据URI,而不论它是否拥有任何访问和Intent相对应的内容提供者中数据的许可。

这一机制使得常见的能力-风格模型(capability-style model)得以实现,用户交互操作(打开附件,从列表中选择联系人等)可以创建临时的细化许可权限。这是减少程序所需许可至仅有和其行为直接相关许可的工具。

然而,对细化URI许可的授权也需要一些拥有那些URI的内容提供者的配合。强烈建议内容提供者应用这一工具,通过android:grantUriPermissions属性或是<grant-uri-permissions>标签来声明它们支持这一特性。

更为详细的信息可以在Context.grantUriPermission()、Context.revokeUriPermission()和Context.checkUriPermission()方法中找到。

本作品采用知识共享 署名-非商业性使用-禁止演绎 3.0 Unported许可协议进行许可。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注