安卓应用安全基础知识-全-

安卓应用安全基础知识(全)

原文:zh.annas-archive.org/md5/88CB58B22F96FE00C43EA554A1F58FCE

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在当今技术发达的世界,我们的生活越来越多地数字化,所有这些信息都可以通过移动设备随时随地访问。用户可以下载和尝试成千上万的应用程序。由于移动设备上的应用程序可以轻松访问大量信息,最大的挑战就是保护用户的私人信息并尊重他们的隐私。

第一款 Android 手机在 2009 年问世。从那时起,移动生态系统就不再一样了。平台的开放性和较少限制的应用模型在开发人员社区中引起了兴奋,也促进了创新和实验。但正如每个硬币都有两面一样,开放性也是如此。Android 平台激发了所谓不良分子的想象力。Android 为他们提供了一个完美的试验场来尝试他们的想法。因此,作为开发人员,甚至作为消费者,了解 Android 的安全模型以及如何明智地使用它来保护自己和消费者,这一点非常重要。

Android 应用安全基础》深入探讨了从内核层到应用层的 Android 安全,包含实用的实战示例、图解和日常使用场景。这本书将向你展示如何保护你的 Android 应用和数据。它将为你提供在开发应用时非常有用的技巧和提示。

你将学习 Android 堆栈的整体安全架构。书中详细讨论了使用权限保护组件、在清单文件中定义安全、加密算法和协议、安全存储、以安全为重点的测试以及保护设备上的企业数据。你还将学习在将新技术和用例(如 NFC 和移动支付)集成到你的 Android 应用程序时如何保持安全意识。

本书涵盖的内容

第一章,Android 安全模型 - 大局观,关注从平台安全到应用安全的整个 Android 堆栈的安全性。这一章将为后续章节的构建提供基准。

第二章,应用构建块,从安全的角度介绍了应用组件、权限、清单文件和应用程序签名。这些都是 Android 应用程序的基本组成部分,了解这些组件对于构建我们的安全知识至关重要。

第三章,权限,讨论了 Android 平台现有的权限,如何定义新权限,如何用权限保护应用组件,并提供关于何时定义新权限的分析。

第四章,定义应用程序的策略文件,深入探讨了 manifest 文件的工作机制,这是应用程序的策略文件。我们讨论了如何加强策略文件的技巧和小窍门。

第五章,尊重你的用户,涵盖了妥善处理用户数据的最佳实践。这对于开发者的声誉至关重要,因为声誉取决于用户的评价和评分。开发者还应该小心处理用户的私人信息,以免陷入法律陷阱。

第六章,你的工具 - 加密 API,讨论了 Android 平台提供的加密能力。这些包括对称加密、非对称加密、散列、密码模式和密钥管理。

第七章,保护应用程序数据,全面介绍了应用程序数据在静态和传输过程中的安全存储。我们讨论了如何将私密数据与应用程序一起沙盒化,以及如何在设备上、外部存储卡、驱动器和数据库上安全地存储数据。

第八章,企业中的 Android,讨论了 Android 平台提供的设备安全工具有及其对应用程序开发人员意味着什么。这一章对企业应用程序开发者特别有吸引力。

第九章,安全测试,专注于设计和开发以安全为重点的测试用例。

第十章,未来展望,讨论了移动领域即将到来的用例,以及这对 Android 的影响,尤其是从安全角度出发。

阅读本书所需的条件

如果你能设置好 Android 环境,并且能够实践本书中讨论的概念和示例,那么这本书将更有价值。关于如何设置环境和开始 Android 开发的详细说明,请参考developer.android.com。如果你对内核开发感兴趣,请参考source.android.com

在撰写本书时,Jelly Bean(Android 4.2,API 级别 17)是最新版本。我在这个平台上测试了所有的代码片段。自从 2009 年 Cupcake 的首个版本发布以来,谷歌一直在通过连续的版本更新来增强 Android 的安全性。例如,在 Android 2.2(API 级别 8)中添加了远程擦除和设备管理 API,使 Android 对商业社区更具吸引力。每当相关时,我都会引用开始支持特定功能的版本。

本书适合的读者对象

这本书是任何对移动安全感兴趣的人的绝佳资源。开发人员、测试工程师、工程经理、产品经理和架构师可以在设计和编写应用程序时将这本书作为参考。高级管理人员和技术专家可以利用这本书来更全面地了解移动安全。最好具备一些关于 Android 堆栈开发的先验知识,但不是必需的。

约定

在这本书中,您会发现多种文本样式,这些样式区分了不同类型的信息。以下是一些样式示例,以及它们的含义解释。

文本中的代码字如下所示:"The PackageManager 类处理安装和卸载应用程序的任务。"

代码块设置如下:

<intent-filter>
  <action android:name="android.intent.action.MAIN" />
  <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

当我们希望引起您对代码块中特定部分的注意时,相关的行或项目会以粗体显示:

Intent intent = new Intent("my-local-broadcast");

Intent.putExtra("message", "Hello World!");

LocalBroadcastManager.getInstance(this).sendBroadcast(intent);

命令行输入或输出如下所示:

dexdump –d –f –h data@app@com.example.example1-1.apk@classes .dex > dump

新术语重要词汇以粗体显示。您在屏幕上看到的词,例如菜单或对话框中的,会像这样出现在文本中:"点击下一步按钮将带您进入下一个屏幕"。

注意

警告或重要注意事项会出现在这样的框中。

提示

技巧和诀窍如下所示。

读者反馈

我们始终欢迎读者的反馈。让我们知道您对这本书的看法——您喜欢或可能不喜欢的内容。读者的反馈对我们来说非常重要,以便开发出您真正能从中获得最大收益的标题。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件的主题中提及书名。

如果您在某个主题上有专业知识,并且有兴趣撰写或参与书籍编写,请查看我们在www.packtpub.com/authors上的作者指南。

客户支持

既然您已经拥有了 Packt 的一本书,我们有许多方法可以帮助您充分利用您的这次购买。

勘误

尽管我们已经竭尽全力确保内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能报告给我们,我们将不胜感激。这样做可以避免其他读者感到沮丧,并帮助我们在后续版本中改进这本书。如果您发现任何勘误信息,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误信息得到验证,您的提交将被接受,勘误信息将被上传到我们的网站,或添加到该标题勘误部分的现有勘误列表中。任何现有的勘误信息可以通过选择您的标题从www.packtpub.com/support进行查看。

侵权行为

在互联网上,版权材料的侵权行为是所有媒体持续存在的问题。在 Packt,我们非常重视保护我们的版权和许可。如果您在互联网上以任何形式遇到我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

如果您发现疑似盗版材料,请通过<copyright@packtpub.com>联系我们,并提供相关链接。

我们感谢您帮助保护我们的作者,以及我们向您提供有价值内容的能力。

问题咨询

如果您在书的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。

第一章:安卓安全模型 – 大局观

欢迎阅读《安卓应用安全基础》的第一章!

安卓系统在许多方面与众不同。它是开放的,比其他一些平台更先进,并汲取了之前开发移动平台的经验教训。在本章中,我们将从内核到应用层,介绍安卓安全模型的基础知识。本章中引入的每个安全工件事后都会在后续章节中进行更详细的讨论。

我们从解释为什么安装时应用程序权限评估对安卓平台和用户数据的安全至关重要开始本章。安卓具有分层架构,本章将讨论每个架构层的安全评估。我们以讨论核心安全工件事物作为本章的结束,如应用程序签名、设备上的安全数据存储、加密 API 以及安卓设备的管理等。

谨慎安装

安卓与其他移动操作系统不同的一个特点是应用安装时的权限审查。应用程序所需的所有权限都必须在其清单文件中声明。这些权限是应用程序正常运行所需的权限,例如访问用户的联系人列表、从手机发送短信、拨打电话以及访问互联网等。详细描述这些权限的内容请参考第三章,权限

当用户安装应用程序时,清单文件中声明的所有权限都会呈现给用户。用户可以审查这些权限,并做出是否安装应用的明智决定。用户应仔细审查这些权限,因为这是唯一一次要求用户授权。这一步骤之后,用户对应用程序的控制就极为有限,最多只能卸载应用。有关参考,请查看以下屏幕截图。在这个例子中,应用程序将追踪或访问用户位置,使用网络,读取用户联系人列表,读取手机状态,并使用某些开发能力。在安全审查此应用程序时,用户必须评估授予该应用程序某些权限是否必要。如果这是一个游戏应用,可能不需要开发工具能力。如果这是一个面向儿童的教育应用,它不应该需要访问联系人列表或用户位置。还要注意,开发者可以添加自己的权限,特别是如果他们想要与可能在设备上安装的其他自己开发的应用程序进行通信时。开发者有责任为这类权限提供清晰的描述。

在安装时,框架会确保应用程序中使用的所有权限都在清单文件中声明。操作系统在运行时会强制执行这些权限。

谨慎安装

Android 平台架构

Android 是一个具有分层软件堆栈的现代操作系统。下图说明了 Android 软件堆栈中的层。此软件堆栈运行在设备硬件之上。Android 的软件堆栈可以运行在许多不同的硬件配置上,如智能手机、平板电脑、电视,甚至嵌入式设备如微波炉、冰箱、手表和笔。每一层都提供了安全性,为移动应用程序提供了一个安全运行和执行的环境。在本节中,我们将讨论 Android 堆栈每一层提供的安全保障。

Android 平台架构

Linux 内核

设备硬件之上是Linux 内核。Linux 内核已经被用作一个安全的多用户操作系统数十年,将一个用户与另一个用户隔离开来。Android 使用 Linux 的此属性作为 Android 安全的基础。将 Android 想象成一个多用户平台,每个用户都是一个应用程序,每个应用程序都与其他应用程序隔离。Linux 内核托管设备驱动程序,如蓝牙、摄像头、Wi-Fi 和闪存驱动程序。内核还提供了一种安全的远程过程调用RPC)机制。

每个应用程序在设备上安装时,都会被赋予唯一的用户识别码UID)和组识别码GID)。只要应用程序在设备上安装,这个 UID 就是应用程序的身份标识。

请参考以下屏幕截图。第一列列出了所有应用程序的 UID。请注意突出的应用程序。应用程序com.paypal.com具有 UID app_8,而com.skype.com具有 UID app_64。在 Linux 内核中,这两个应用程序都以其自己的进程运行,并使用这个 ID。

Linux 内核

请参考下一张屏幕截图。当我们在壳层中输入id命令时,内核会显示 UID、GID 以及与壳层关联的组。这是 Android 用来将一个进程与另一个进程隔离的进程沙箱模型。两个进程可以相互共享数据。如何正确进行数据共享的机制将在第四章,定义应用程序的策略文件中进行讨论。

Linux 内核

尽管大多数 Android 应用程序是用 Java 编写的,但有时需要编写本地应用程序。本地应用程序更为复杂,因为开发者需要管理内存和特定于设备的问题。开发者可以使用 Android NDK 工具集来用 C/C++开发应用程序的部分。所有本地应用程序都符合 Linux 进程沙箱化;本地应用程序和 Java 应用程序的安全性没有区别。请记住,与任何 Java 应用程序一样,需要适当的加密、散列和安全的通信等安全构件。

中间件

在 Linux 内核之上是提供代码执行库的中间件。这类库的例子有libSSLlibcOpenGL。这一层还提供了 Java 应用程序的运行时环境。

由于大多数用户在 Android 上用 Java 编写应用程序,一个显而易见的问题是:Android 是否提供了一个Java 虚拟机?这个问题的答案是:不,Android 没有提供 Java 虚拟机。因此,Java 归档文件JAR)在 Android 上无法执行,因为 Android 不执行字节码。Android 提供的是Dalvik 虚拟机。Android 使用一个名为dx的工具将字节码转换为Dalvik 可执行文件DEX)。

Dalvik 虚拟机

Dalvik 虚拟机最初由 Dan Bornstein 开发,他以冰岛的一个渔村 Dalvik 命名,那里住着他的部分祖先。Dalvik 是一个基于寄存器、高度优化、开源的虚拟机。它与 Java SE 或 Java ME 并不一致,其库基于Apache Harmony

每个 Java 应用程序都在其自己的虚拟机中运行。当设备启动时,一个名为Zygote的原始进程会生成一个虚拟机进程。这个 Zygote 进程随后根据请求分叉以创建新的虚拟机进程。

Dalvik 背后的主要动机是通过增加共享来减少内存占用。因此,Dalvik 中的常量池是一个共享池。它还在不同的虚拟机进程之间共享核心的只读库。

Dalvik 依赖 Linux 平台提供所有底层功能,如线程和内存管理。Dalvik 为每个虚拟机设置了独立的垃圾收集器,但会处理共享资源的进程。

Dan Bornstein 在 2008 年的 Google IO 上关于 Dalvik 做了精彩的演讲。你可以在www.youtube.com/watch?v=ptjedOZEXPM找到它。去看看吧!

应用层

开发基于 Java 的应用程序的应用程序开发者与 Android 堆栈的应用层交互。除非你正在创建一个本地应用程序,否则这一层将为你提供创建应用程序所需的所有资源。

我们可以将应用层进一步划分为应用框架层和应用层。应用框架层提供了由 Android 堆栈公开供应用程序使用的类。例如,活动管理器管理活动的生命周期,包管理器管理应用程序的安装和卸载,通知管理器向用户发送通知。

应用层是应用程序所在的层次。这些可以是系统应用程序或用户应用程序。系统应用程序是设备预装的应用程序,如邮件、日历、联系人和浏览器。用户无法卸载这些应用程序。用户应用程序是用户在设备上安装的第三方应用程序。用户可以根据自己的意愿自由安装和卸载这些应用程序。

Android 应用程序结构

要理解应用层的安全性,重要的是要了解 Android 应用程序的结构。每个 Android 应用程序都被创建为一堆组件的栈。这种应用程序结构的美妙之处在于,每个组件都是自包含的实体,甚至可以被其他应用程序独立调用。这种应用程序结构鼓励组件共享。下图展示了 Android 应用程序的组成,包括活动、服务、广播接收器和内容提供者:

Android 应用程序结构

Android 支持四种类型的组件:

  • 活动:这个组件通常是应用程序的用户界面部分。这是与用户交互的组件。活动组件的一个例子是登录页面,用户在该页面输入用户名和密码以对服务器进行身份验证。

  • 服务:这个组件负责在后台运行的过程。服务组件没有 UI。一个例子是,一个与音乐播放器同步并在用户预先选择的情况下播放歌曲的组件。

  • 广播接收器:这个组件是接收来自 Android 系统或其他应用程序消息的邮箱。例如,Android 系统在启动后会触发一个名为BOOT_COMPLETED的 Intent。应用程序组件可以在清单文件中注册,以便监听此广播。

  • 内容提供者:这个组件是应用程序的数据存储。应用程序还可以将此数据与其他 Android 系统的组件共享。内容提供者组件的一个示例用例是,一个应用程序存储了用户为购物保存的物品列表。

所有上述组件都在AndroidManifest.xml(清单)文件中声明。除了组件,清单文件还列出了应用的其他需求,如 Android 所需的最低 API 级别,应用所需的用户权限,例如访问互联网和读取联系人列表,应用使用硬件的权限,如蓝牙和相机,以及应用链接到的库,如 Google Maps API。第四章,定义应用的策略文件,将更详细地讨论清单文件。

活动、服务、内容提供者和广播接收器都通过意图(Intents)相互通信。意图是 Android 的异步进程间通信IPC)机制。组件通过发射意图来执行一个动作,接收组件根据意图采取行动。有单独的机制用于将意图传递给每种类型的组件,因此活动意图只传递给活动,广播意图只传递给广播接收器。意图还包括一个名为Intent对象的信息包,接收组件使用它来采取适当的行动。重要的是要了解意图并不安全。任何监听应用都可以嗅探意图,因此不要在其中放置任何敏感信息!并且想象一下,如果意图不仅被嗅探,还被恶意应用篡改的场景。

例如,下图展示了两个应用,应用 A应用 B,它们各自有一组组件。只要它们有权限,这些组件就可以相互通信。应用 A中的活动组件可以使用startActivity()启动应用 B中的活动组件,也可以使用startService()启动自己的服务

Android 应用结构

在应用级别,Android 组件遵循基于权限的模型。这意味着组件必须拥有适当的权限才能调用其他组件。尽管 Android 提供了应用可能需要的绝大多数权限,但开发者仍有能力扩展此模型。但这种情形应很少使用。

额外的资源,如位图、UI 布局、字符串等,在另一个不同的目录中独立维护。为了提供最佳的用户体验,这些资源应当针对不同的地区进行本地化处理,并针对不同的设备配置进行定制。

接下来的三章将详细讨论应用结构、清单文件和权限模型。

应用签名

Android 的一个区别因素是 Android 应用程序的签名方式。Android 中的所有应用程序都是自签名的。不需要使用证书颁发机构对应用程序进行签名。这与传统的应用程序签名不同,后者通过签名识别作者,并基于签名建立信任。

应用程序的签名将应用程序与作者关联起来。如果用户安装了同一作者编写的多个应用程序,且这些应用程序想要共享彼此的数据,它们需要具有相同的签名,并且在清单文件中设置了SHARED_ID标志。

应用程序签名在应用程序升级时也会被使用。应用程序升级要求两个应用程序具有相同的签名,且没有权限提升。这是 Android 中确保应用程序安全的另一机制。

作为应用程序开发者,保持用于签名应用程序的私钥安全是非常重要的。作为应用程序作者,你的声誉取决于它。

设备上的数据存储

Android 提供了不同的解决方案,用于设备上的安全数据存储。基于数据类型和应用程序用例,开发者可以选择最适合的解决方案。

对于需要在用户会话之间持久化的基本数据类型,如整数(ints)、布尔值(booleans)、长整数(longs)、浮点数(floats)和字符串(strings),最好使用共享数据类型。共享偏好设置中的数据以键值对的形式存储,允许开发者保存检索持久化数据。

所有应用程序数据都与应用程序一起存储在沙盒中。这意味着这些数据只能由该应用程序或具有相同签名且被授权共享数据的其他应用程序访问。最好将私有数据文件存储在此内存中。这些文件将在应用程序被卸载时被删除。

对于大型数据集,开发者可以选择使用 Android 软件堆中捆绑的 SQLite 数据库。

所有 Android 设备允许用户挂载外部存储设备,如 SD 卡。开发者可以编写应用程序,以便将这些大文件存储在这些外部设备上。这些外部存储设备大多数采用 VFAT 文件系统,Linux 访问控制在这里不起作用。敏感数据在存储在这些外部设备上之前应进行加密。

从 Android 2.2(API 8)开始,APK 可以存储在外部设备上。使用随机生成的密钥,APK 被存储在一个名为asec文件的加密容器中。这个密钥存储在设备上。Android 上的外部设备是以noexec方式挂载的。所有的 DEX 文件、私有数据和本地共享库仍然存放在内部存储中。

在任何可以连接网络的地方,开发者可以将数据存储在自己的网络服务器上。建议将可能泄露用户隐私的数据存储在自家服务器上。例如,银行应用程序应将用户账户信息和交易细节存储在服务器上,而不是用户设备上。

第七章,保护应用程序数据,详细讨论了 Android 设备上的数据存储选项。

如视频、电子书和音乐等权利受保护的内容,可以使用 DRM 框架 API 在 Android 上得到保护。应用程序开发者可以使用此 DRM 框架 API 将设备注册到 DRM 方案,获取与内容相关的许可证,提取约束,并将相关内容与其许可证关联。

加密 API

Android 拥有一套全面的加密 API 套件,应用开发者可以使用它来保护数据,无论是在静止状态还是在传输过程中。

Android 提供了用于数据对称和不对称加密、随机数生成、散列、消息认证码和不同密码模式的 API。支持的算法包括 DH、DES、三重 DES、RC2 和 RC5。

安全通信协议如 SSL 和 TLS,结合加密 API,可用于保护传输中的数据。还提供了包括 X.509 证书管理的密钥管理 API。

自 Android 1.6 以来,系统密钥存储已被用于 VPN。Android 4.0 引入了一个名为KeyChain的新 API,允许应用程序访问存储在那里的凭据。此 API 还支持从 X.509 证书和 PKCS#12 密钥存储中安装凭据。一旦应用程序被授予访问证书的权限,它就可以访问与证书关联的私钥。

加密 API 在第六章,你的工具 – 加密 API中进行了详细讨论。

设备管理

随着移动设备在工作场所的普及,Android 2.2 引入了设备管理 API,允许用户和 IT 专业人士管理访问企业数据的设备。使用此 API,IT 专业人士可以在设备上实施系统级安全策略,如远程擦除,密码启用及具体设置。Android 3.0 和 Android 4.0 进一步增强了此 API,增加了密码过期、密码限制、设备加密要求和禁用摄像头等策略。如果你有邮件客户端并在 Android 手机上用它来访问公司邮件,那么你很可能正在使用设备管理 API。

设备管理 API 通过强制执行安全策略来工作。DevicePolicyManager列出了设备管理员可以在设备上执行的所有策略。

设备管理员编写一个应用程序,用户在其设备上安装该程序。安装后,用户需要激活策略以在设备上实施安全策略。如果用户未安装该应用,则不适用安全策略,但用户也无法访问该应用提供的任何功能。如果设备上有多个设备管理应用程序,则最严格的策略优先。如果用户卸载该应用,则策略会被停用。在卸载时,根据其拥有的权限,应用程序可能会决定将手机重置为出厂设置或删除数据。

我们将在第八章,企业中的 Android中更详细地讨论设备管理。

总结

安卓是一个现代操作系统,安全性已经内置于平台中。正如我们在本章中学到的,具有进程隔离特性的 Linux 内核构成了安卓安全模型的基础。每个应用程序及其应用数据都与其它进程隔离。在应用程序层面,组件通过意图相互交流,并且需要具备适当的权限才能调用其他组件。这些权限在久经考验的安全多用户操作系统 Linux 内核中得到执行。开发者拥有一套全面的加密 API,用以保护用户数据。

有了对安卓平台的基本了解,让我们迈向下一章,从安全角度理解应用程序组件和组件间通信。祝您好运!

第二章:应用程序构建块

本章节关注 Android 应用程序的构建块,即应用程序组件和组件间通信。Android 系统中有四种类型的组件:活动(Activities)、服务(Services)、广播接收器(Broadcast Receivers)和内容提供器(Content Providers)。每个组件都特别设计用来完成一个特定任务。这些组件的集合构成了一个 Android 应用程序。这些组件通过 Intents 进行相互通信,Intents 是 Android 的跨进程通信机制。

有几本书讨论了如何构建 Android 组件和 Intents。实际上,Android 开发者网站在介绍使用这些组件进行编程方面也做得相当不错。因此,在本章中,我们不是要涵盖实施细节,而是要讨论每个组件的安全方面,以及如何在应用程序中定义和使用组件及 Intents,以保护我们作为开发者的声誉和消费者的隐私。

本章节的重点是组件和 Intents。对于每个 Android 组件,我们将涵盖组件声明、与组件相关的权限以及特定于该组件的其他安全考虑。我们将讨论不同类型的 Intents 以及在不同情境下最佳 Intents 的使用。

应用程序组件

正如我们在第一章简要提及的,Android 安全模型 - 大蓝图,一个 Android 应用程序是由一系列松散绑定的应用组件组成的。应用组件、清单文件以及应用资源被打包在一个应用包格式 .apk 文件中。一个APK文件本质上是一个采用 JAR 文件格式的 ZIP 文件。Android 系统只识别 APK 格式,因此所有要安装在 Android 设备上的包都必须是 APK 格式。APK 文件随后使用开发者的签名进行签名,以确认作者身份。PackageManager 类负责安装和卸载应用程序的任务。

在本节中,我们将详细讨论每个组件的安全性。这包括在清单文件中声明组件,因此我们会梳理掉松散的结尾以及其他针对每个组件的独特安全考虑。

活动

活动是通常与用户交互的应用程序组件。活动扩展了 Activity 类,并作为视图和片段实现。片段是在Honeycomb中引入的,以解决不同屏幕尺寸的问题。在较小的屏幕上,一个片段显示为一个单一的活动,并允许用户导航到第二个活动以显示第二个片段。片段和活动启动的线程在活动的上下文中运行。因此,如果活动被销毁,与其相关的片段和线程也将被销毁。

一个应用程序可以有多个活动。最好使用一个活动专注于单一任务,并为各个任务创建不同的活动。例如,如果我们正在创建一个让用户在网站上订购书籍的应用程序,最好创建一个用于用户登录的活动,另一个用于在数据库中搜索书籍的活动,另一个用于输入订购信息,再一个用于输入支付信息,等等。这种风格鼓励在应用程序内以及设备上安装的其他应用程序中重用活动。组件的重用有两个主要好处。首先,它有助于减少错误,因为代码重复较少。其次,它使应用程序更安全,因为不同组件之间的数据共享较少。

活动声明

应用程序使用的任何活动都必须在AndroidManifest.xml文件中声明。以下代码段显示了在清单文件中声明的登录活动和订单活动:

<activity android:label="@string/app_name" android:name=".LoginActivity">
  <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
  </intent-filter>
</activity>
<activity android:name=".OrderActivity" android:permission="com.example.project.ORDER_BOOK" android:exported="false"/>

请注意,LoginActivity被声明为一个公共活动,可以被系统中的任何其他活动启动。OrderActivity被声明为一个私有活动(没有意图过滤器的活动是一个私有活动,只能通过指定其确切的文件名来调用)。此外,可以使用android:exported标签指定它是否对应用程序外部可见。值为true使活动在应用程序外部可见,值为false则相反。本章稍后将讨论意图过滤器(Intent Filter)。

所有的活动都可以通过权限进行保护。在上述示例中,除了是私有的,OrderActivity还受到权限com.example.project.ORDER_BOOK的保护。任何尝试调用OrderActivity的组件都应该具有此自定义权限才能调用它。

通常情况下,每当启动一个活动时,它都会运行在声明它的应用程序进程中。将android:multiprocess属性设置为true可以让活动运行在一个与应用程序不同的进程中。这些进程的具体设置可以通过使用android:process属性来定义。如果此属性的值以冒号(:)开头,将为应用程序创建一个新的私有进程;如果以小写字母开头,则活动将在全局进程中运行。

android:configChanges标签允许应用程序处理由于列出的配置更改而重新启动活动。这样的更改包括本地化更改、插入外部键盘和 SIM 卡更改。

保存活动状态

系统通过活动栈管理所有活动(Activities)。当前与用户交互的活动在前台运行。当前活动可以启动其他活动。由于资源限制,处于后台的任何活动都可能被 Android 系统杀死。在配置更改(例如从垂直方向更改为水平方向)期间,活动也可能被重新启动。如前一部分所述,活动可以使用android:configChanges标签自行处理其中一些事件。不建议这样做,因为它可能导致不一致。

在重新启动之前,应该保存活动(Activity)的状态。活动的生命周期由以下方法定义:

public class Activity extends ApplicationContext {
  protected void onCreate(Bundle savedInstanceState);
  protected void onStart();
  protected void onRestart();
  protected void onResume();
  protected void onPause();
  protected void onStop();
  protected void onDestroy();
}

活动可以重写onSaveInstanceState(Bundle savedInstanceState)onRestoreInstanceState(Bundle savedInstanceState)方法,以保存和恢复例如用户偏好和未保存文本的实例值。Android 开发者网站 www.developer.android.com 使用以下流程图精美地说明了这个过程:

保存活动状态

下面的代码片段展示了活动(Activity)如何存储和检索首选语言、搜索结果数量和作者名称。当活动被销毁时,用户偏好作为Bundle存储,它存储名称-值对。当活动重新启动时,这个 Bundle 会被传递给onCreate方法,该方法恢复了活动的状态。需要注意的是,这种存储方法不会在应用程序重新启动后保持。

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
  super.onSaveInstanceState(savedInstanceState);
  savedInstanceState.putInt("ResultsNum", 10);
  savedInstanceState.putString("MyLanguage", "English");
  savedInstanceState.putString("MyAuthor", "Thomas Hardy");
}

@Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
  super.onRestoreInstanceState(savedInstanceState);
  int ResultsNum = savedInstanceState.getInt("ResultsNum");
  String MyLanguage = savedInstanceState.getString("MyLanguage");
  String MyAuthor = savedInstanceState.getString("MyAuthor");
}

保存用户数据

如我们之前所讨论的,活动(Activities)与用户交互,因此它们可能会收集一些用户数据。这些数据可能是应用程序私有的,也可能是与其他人共享的。这类数据的一个例子可能是用户的偏好语言或书籍类别。这种数据通常被应用程序保留以增强用户体验。它对应用程序本身有用,并且不与其他应用程序共享。

一个共享数据的例子可能是用户在浏览商店时不断添加到收藏中的书籍愿望清单。这类数据可能与其他应用程序共享,也可能不共享。

根据数据的隐私性和类型,可以采用不同的存储机制。应用程序可以决定使用SharedPreferences、内容提供者、内部或外部存储的文件,甚至是开发者自己的网站来存储这类数据。本章将讨论内容提供者。其他持久性数据存储机制将在第七章《保护应用程序数据》中详细讨论。

服务(Service)

与活动(Activities)不同,服务(Services)没有视觉界面,主要用于后台长时间运行的任务。理想情况下,即使启动它的活动(Activity)不再存在,服务也应该在后台继续运行。任务完成后,服务应该自行停止。适合使用服务执行的任务例如与数据库同步、从网络上传或下载文件、与音乐播放器交互以播放用户选择的曲目,以及应用程序可以绑定获取信息的全局服务。

保护服务的第一步是在清单文件中声明服务。接下来,识别正确服务用例并管理服务生命周期非常重要。这包括启动和停止服务,并创建工作线程以避免阻塞应用程序。在接下来的几节中,我们将逐一介绍这些方面。本章的最后一节是关于绑定器(binders),它是大多数 Android 进程间通信(IPC)的基础,并使服务能够以客户端-服务器的方式使用。

服务声明

应用程序计划启动的所有服务都需要在清单文件中声明。服务声明定义了一旦创建服务后,它将如何运行。清单文件中<service>标签的语法在以下代码段中展示:

<service android:enabled=["true" | "false"]
         android:exported=["true" | "false"]
         android:icon="drawable resource"
         android:isolatedProcess=["true" | "false"]
         android:label="string resource"
         android:name="string"
         android:permission="string"
         android:process="string" >
   . . . . .
</service>

根据上述声明语法,一个私有的、在全局进程中运行以将书籍存储在数据库中的应用程序服务可以如下声明:

<service
  android:name="bookService"
  android:process=":my_process"
  android:icon="@drawable/icon"
  android:label="@string/service_name" >
</service>

默认情况下,服务在应用程序的全局进程中运行。如果应用程序想要在不同的进程中启动服务,可以使用android:process属性。如果此属性的值以冒号(:)开头,服务将在应用程序内的新私有进程中启动。如果值以小写字母开头,将创建一个新的全局进程,该进程对 Android 系统的所有应用程序可见和可访问。在上述示例中,服务在其自己的全局进程中运行。应用程序应该有权限创建此类进程。

这个android:enabled属性定义了系统是否可以实例化服务。默认值为true

android:exported属性限制了服务的暴露。值为true意味着此服务对应用程序外部可见。如果服务包含意图过滤器(Intent Filter),则该服务对其他应用程序可见。此属性的默认值为true

要在隔离的进程中运行服务,且没有任何权限,请将android:isolatedProcess属性设置为true。在这种情况下,与服务交互的唯一方式是通过绑定到服务。此属性的默认值为false

与活动一样,服务可以通过权限进行保护。这些服务在清单文件中使用android:permission属性声明。调用组件需要有适当的权限来调用服务,否则从调用中抛出SecurityException

服务模式

服务可以在两个上下文中使用。第一种情况是,服务作为一个辅助服务,组件可以启动它来运行长时间运行的任务。这样的服务被称为启动服务。服务的第二种用例是作为向一个或多个应用程序组件提供信息的服务。在这种情况下,服务在后台运行,应用程序组件通过调用bindService()来绑定服务。这样的服务被称为绑定服务

启动的服务可以扩展Service类或IntentService类。这两种方法的主要区别在于处理多个请求的方式。当扩展Service类时,应用程序需要处理多个请求,这通过onStartCommand()方法完成。

IntentService()类通过排队所有请求并一次处理一个,从而简化了操作,因此开发者无需处理线程问题。如果适用于某种用例,最好使用IntentService类以避免多线程错误。IntentService类为任务启动一个工作线程,并且请求会自动排队。任务在onHandleIntent中完成,就是这样!以下是IntentService类的一个示例:

public class MyIntentService extends IntentService {
  public MyIntentService() {
    super("MyIntentService");
  }
  @Override
  protected void onHandleIntent(Intent intent) {
    // TODO Auto-generated method stub
  }
}

绑定服务是客户端服务器的情况,服务作为服务器,客户端绑定到它以获取信息。这是通过使用bindService()方法完成的。当客户端满意时,它们使用unbindService()从服务中解绑自己。

绑定服务可以服务于一个应用程序的组件,也可以服务于不同应用程序的组件。仅服务于一个应用程序组件的绑定服务可以扩展Binder类并实现返回IBinder对象的onBind()方法。如果一个服务服务于多个应用程序,可以使用信使或Android 接口定义语言AIDL)工具来生成服务发布的接口。使用信使更容易实现,因为它处理多线程问题。

当绑定到一个服务时,检查活动绑定到的服务身份是非常重要的。这可以通过显式指定服务名称来完成。如果服务名称不可用,客户端可以使用ServiceConnection.onServiceConnected()来检查它所连接的服务的身份。另一种方法是使用权限检查。

提示

对于启动的服务,onBind()方法返回 null。

生命周期管理

任何组件都可以通过使用startService()方法并传递一个 Intent 对象来启动一个服务,如下所示:

Intent intent = new Intent(this, MyService.class);
startService(intent);

与其他任何组件一样,启动的 Service 也可以被 Android 系统销毁,以便为用户交互的进程收集资源。在这种情况下,Service 将根据 onStartCommand 方法中设置的返回值重新启动。以下是一个示例:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
  handleCommand(intent);
  // Let the service run until it is explicitly stopped
  return START_STICKY;
}

重新启动 Service 有以下三种选项:

  • START_NOT_STICKY:这个选项表示除非有挂起的 Intent,否则 Android 系统不应重新启动 Service。挂起的 Intent 在本章后面会讨论。这个选项最适合可以安全地稍后重新启动并完成未完成工作的场景。

  • START_STICKY:这个选项表示系统应该启动 Service。如果最初的 Intent 丢失,onStartCommand() 方法会以 null Intent 的形式被启动。这对于即使初始 Intent 丢失,Service 也能恢复其任务的情况最为合适。例如音乐播放器,一旦被系统杀死,可以重新开始播放。

  • START_REDELIVER_INTENT:在这种情况下,Service 会被重新启动,并且挂起的 Intent 会重新传递给 Service 的 onStartCommand()。例如,通过网络下载文件。

需要特别注意的是,Service 与创建线程是不同的。当启动线程的组件被销毁时,线程会立即被杀死。而默认情况下,Service 在全局应用线程中运行,即使调用它的组件被销毁,Service 仍然保持存活。如果 Service 正在进行一些耗时操作,比如下载大文件,最好在一个单独的线程中进行,以避免阻塞应用程序。

启动的 Service 默认在应用线程中运行。任何可能阻塞的活动都应该在一个单独的线程中进行,以避免在运行应用程序时出现潜在的瓶颈。IntentService 类通过生成一个工作线程来处理这种情况。

两种启动服务都应该在任务完成后通过调用 stopSelf() 来停止自己。任何组件也可以通过使用 stopService() 方法来停止 Service。

当没有更多的客户端绑定到 Service 时,系统会销毁绑定的服务。

注意

Service 可以被启动也可以被绑定。在这种情况下,不要忘记调用 stopSelf()stopService() 来停止 Service 在后台继续运行。

绑定器

Binder是大多数 Android IPC 的核心。它是一个内核驱动程序,所有对 Binder 的调用都通过内核进行。信使也是基于 Binder 的。Binder 的实现可能会令人困惑,只有当服务需要为在不同进程中运行的多应用程序提供服务,并希望自行处理多线程时,才应使用 Binder。Binder 框架集成在操作系统中,因此,如果一个进程打算使用另一个进程的服务,它需要将对象封送进基本类型。操作系统然后将它跨进程边界传递。为了使开发者的这项任务更加容易,Android 提供了 AIDL。下图说明了 Binder 是如何成为所有 Android IPC 的核心的。Binder 通过 AIDL 暴露。意图也被实现为 Binder。但这些复杂性对用户是隐藏的。随着我们向更大的同心圆移动,实现变得更加抽象。

Binder

要使用 AIDL 创建绑定服务,我们首先创建 AIDL 文件。然后,使用 Android SDK 工具,我们生成接口。这个接口包含扩展了android.os.Binder类并实现了onTransact()方法的stub方法。客户端接收 Binder 接口的引用,并调用其transact()方法。数据通过这个通道作为一个Parcel对象流动。Parcel对象是可序列化的,因此它可以有效地跨越进程边界。

注意事项

Parcel对象是为了高性能的 IPC 传输而定义的,因此它们不应用于通用目的的序列化。

如果有多个进程正在使用服务,请注意,一旦公开了 AIDL,就尽量不要更改它,因为其他应用程序可能也在使用它。如果这种更改是绝对必要的,那么至少它应该是向后兼容的。

Binder 在系统中全局唯一,对 Binder 的引用可以用作验证可信组件的共享密钥。保持 Binder 私有始终是一个好主意。任何拥有 Binder 引用的人都可以调用它,并且可以调用transact()方法。由服务来响应请求。例如,Zygote,这个系统服务,公开了一个任何 Activity 都可以绑定的 Binder。但是调用它的transact()方法并不意味着它会得到响应。

根据 <service> 标签的 android:process 属性,Binder 可以在同一进程或不同进程中运行。

Binder 通过内核安全地提供调用组件及其权限的身份。可以使用 Binder 的getCallingPid()getCallingUid()方法来检查调用者的身份。Binder 反过来可以调用其他 Binder,在这种情况下,它们可以使用调用 Binder 的身份。要检查调用者的权限,可以使用Context.checkCallingPermission()。要检查调用者或 Binder 本身是否有特定权限,可以使用Context.checkCallingOrSelfPermission()

内容提供者

安卓系统使用内容提供者(Content Providers)来存储数据,如联系人列表、日历和字典。内容提供者是安卓跨进程边界处理结构化数据的机制,也可以在应用内使用。

在大多数情况下,内容提供者的数据存储在 SQL 数据库中。标识符_id用作主键。与 SQL 一样,用户通过编写查询来访问数据。这些可以是rawQuery()query(),具体取决于它们是原始 SQL 语句还是结构化查询。查询的返回类型是一个指向结果行之一的Cursor对象。用户可以使用辅助方法如getCount()moveToFirst()isAfterLast()moveToNext()来导航多行。完成任务后,需要使用close()关闭Cursor

提供者支持许多不同类型的数据,包括整数、长整数、浮点数、双精度数以及作为 64 KB 数组实现的 BLOB(二进制大对象)。提供者还可以返回标准或 MIME 类型。标准 MIME 类型的一个例子是text/html。对于自定义 MIME 类型,值总是vnd.android.cursor.dirvnd.android.cursor.item,分别用于多行和单行。

下图展示了一个可以抽象数据库、文件甚至远程服务器的内容提供者。应用程序的其他组件可以访问它,其他应用组件也可以访问,前提是它们具有适当的权限。

内容提供者

以下各节将讨论正确声明提供者、定义适当的权限以及避免常见的安全陷阱,这些都是安全访问提供者数据所必需的。

提供者声明

应用程序想要使用的任何提供者都必须在清单文件中声明。provider标签的语法如下:

<provider android:authorities="list"
          android:enabled=["true" | "false"]
          android:exported=["true" | "false"]
          android:grantUriPermissions=["true" | "false"]
          android:icon="drawable resource"
          android:initOrder="integer"
          android:label="string resource"
          android:multiprocess=["true" | "false"]
          android:name="string"
          android:permission="string"
          android:process="string"
          android:readPermission="string"
          android:syncable=["true" | "false"]
          android:writePermission="string" >
  . . . . . . .
</provider>

根据前面的声明语法,可以如下声明一个自定义提供者,用于维护用户愿望清单中的书籍列表。该提供者具有读写权限,客户端可以请求对/figures路径的临时访问。

<provider
  android:authorities="com.example.android.books.contentprovider"
  android:name=".contentprovider.MyBooksdoContentProvider"
  android:readPermission="com.example.android.books.DB_READ"
  android:writePermission="com.example.android.book.DB_WRITE">

  <grant-uri-permission android:path="/figures/" />
  <meta-data android:name="books" android:value="@string/books" />
</provider>

字符串android:authorities列出了应用程序公开的提供者。例如,如果一个提供者的 URI 是content://com.example.android.books.contentprovider/wishlist/Englishcontent://是方案,com.example.android.books.contentprovider是权限,而wishlist/English是路径。至少需要指定一个权限。如果有多权限,应该使用分号分隔。它应该遵循 Java 命名空间规则以避免冲突。

布尔型标签android:enabled指定系统可以启动提供者。如果值为 true,系统可以启动。如果值为 false,系统则不能启动提供者。需要注意的是,为了实现这一点,<application>标签和<provider>标签中的android:enabled属性都需要为 true。

如果提供商被发布给其他应用程序,android:exported会被设置为 true。对于将android:targetSdkVersionandroid:minSdkVersion设置为 16 或更低的应用程序,默认值为 true。对于所有其他应用程序,默认值是 false。

属性标签android:grantUriPermissions用于提供一次性访问受权限保护的数据,否则该数据无法被组件访问。如果此功能设置为true,则允许组件绕过android:readPermissionandroid:writePermissionandroid:permission属性施加的限制,并允许访问内容提供商的任何数据。如果此属性设置为false,则权限只能授予<grant-uri-permission>标签中列出的数据集。此标签的默认值为 false。

整数android:initOrder是提供商初始化的顺序。数字越大,初始化越早。如果应用程序中的提供商之间存在依赖关系,这个属性特别重要。

字符串android:label是内容提供商的用户可读标签。

布尔值android:multiprocess属性,如果设置为 true,允许系统在每个与应用程序交互的应用程序进程中创建提供商的实例。这样可以避免进程间通信的开销。默认值为 false,意味着提供商仅在定义它的应用程序进程中实例化。

字符串android:permission标签声明了客户端应具备的与提供商交互的权限。

字符串android:readPermission和字符串android:writePermission分别定义了客户端应具备的读取和写入提供商数据的权限。如果定义了这些权限,它们将覆盖android:permission的值。值得注意的是,尽管字符串android:writePermission只允许对数据库进行写操作,但它通常会使用WHERE子句,一个聪明的工程师可以绕过这些来读取数据库。因此,写权限也应被视为读权限。

android:process属性定义了提供商应该运行在哪个进程中。通常,提供商运行在与应用程序相同的进程中。但是,如果需要将进程运行在单独的私有进程中,可以分配一个以冒号(:)开头的名称。如果名称以小写字母开头,则提供商将在全局进程中实例化,以实现跨应用程序共享。

android:syncable属性通过设置为true允许数据同步到服务器。值为false时不允许数据同步到服务器。

<provider>标签可以包含三个子标签。

第一个是具有以下语法的<grant-uri-permission>

<grant-uri-permission android:path="string"
                      android:pathPattern="string"
                      android:pathPrefix="string" />

另一个是具有以下语法的<path-permission>标签:

<path-permission android:path="string"
 android:pathPrefix="string"
 android:pathPattern="string"
 android:permission="string"
 android:readPermission="string"
 android:writePermission="string" />

第三个是<meta-data>标签,它定义了与提供商相关的元数据,如下所示:

<meta-data android:name="string"
           android:resource="resource specification"
           android:value="string" />

注意

要提供提供者级别的单一读写权限,分别使用android:readPermissionandroid:writePermission。要提供提供者级别的全面读写权限,请使用android:permission属性。要启用临时权限,请设置android:grantUriPermissions属性。你也可以使用<grant-uri-permission>子元素来实现同样的功能。要启用路径级别的权限,请使用<provider><path-permission>子元素。

其他安全考虑

内容提供者扩展了ContentProvider抽象类。这个类有六个方法,如query()insert()update()delete()getType()onCreate(),都需要被实现。如果提供者不支持某些功能,应该返回一个异常。这个异常应该能够跨进程边界进行通信。

如果多个线程正在读取和写入提供者数据,同步可能是一个问题。这可以通过使用关键字synchronize使所有先前提到的方法同步来解决,这样只有一个线程可以访问提供者。另外,可以设置android:multipleprocess=true,以便为每个客户端创建一个实例。在这种情况下,需要平衡延迟和性能问题。

在某些情况下,为了维护数据完整性,可能需要以特定格式在提供者中输入数据。例如,可能需要每个元素都附加一个标签。为了实现这一点,客户端可能会决定不直接调用ContentProviderContentResolver类。相反,可以委托一个活动与提供者进行接口交互。所有需要访问提供者数据的客户端都应该向这个活动发送一个 Intent,然后这个活动执行预期的操作。

如果向查询中输入的值没有得到验证,内容提供者很容易受到 SQL 注入的攻击。以下是发生这种情况的一个示例:

// mUserInput is the user input
String mSelectionClause =  "var = " + mUserInput;

恶意用户可以在这里输入任何文本。它可能是nothing; DROP TABLE *;,这将删除表。开发人员应该对任何 SQL 查询应用同样的判断。用户数据应该是参数化的,并经过检查以排除可能的恶意行为。

用户可能会决定使用正则表达式来检查用户输入的输入语法。以下代码段展示了如何验证用户输入的字母数字字符。该代码段使用了String类的matches函数。

if (myInput.length() <= 0) {
  valid = false;
} else if (!myInput.matches("[a-zA-Z0-9 ]+")) {
  valid = false;
} else {
  valid = true;
}

在数据库中存储数据时,你可能会想在存储之前对敏感信息(如密码和信用卡信息)进行加密。请注意,加密某些字段可能会影响你索引和排序字段的能力。此外,还有一些开源工具,如针对 Android 的 SQLCipher(sqlcipher.net),它使用 256 位 AES 提供了完整的 SQLite 数据库加密。

广播接收器

在 API 级别 1 中引入,广播接收器是一种应用程序从系统或其他应用程序接收 Intent 的机制。接收器的美妙之处在于,即使应用程序没有运行,它仍然可以接收到可以触发进一步事件的 Intent。用户不会察觉到广播。例如,一个应用程序打算在系统启动后立即启动后台服务,可以注册 Intent.ACTION_BOOT_COMPLETE 系统 Intent。想要根据新的时区自定义自己的应用程序可以注册 ACTION_TIMEZONE_CHANGED 事件。下面显示了发送广播 Intent 的服务的一个示例。已使用 Android 系统注册此类广播的接收器将收到广播 Intent。

广播接收器

应用程序可以在清单文件中声明接收器。然后接收器类扩展 BroadcastReceiver 类并实现 onReceive() 方法。或者应用程序也可以动态创建并注册接收器,使用 Context.registerReceiver

接收器声明

接收器可以在清单文件中如下声明:

<receiver android:enabled=["true" | "false"]
          android:exported=["true" | "false"]
          android:icon="drawable resource"
          android:label="string resource"
          android:name="string"
          android:permission="string"
          android:process="string" >
    . . .
</receiver>

作为一个例子,假设有两个应用程序。第一个应用程序允许用户搜索书籍并将书籍添加到愿望清单中。第二个应用程序监听一个书籍被添加到愿望清单的 Intent。第二个应用程序然后将愿望清单与服务器上的列表同步。第二个应用程序清单文件中的示例接收器声明可能如下所示:

<receiver
  android:name="com.example.android.book2.MessageListener" >
  <intent-filter>
    <action
      android:name="com.example.android.book1.my-broadcast" />
  </intent-filter>
</receiver>

接收器 com.example.android.book2.MessageListener 是一个公共接收器,它监听来自应用程序 com.example.android.book1 的事件。intent-filter 标签过滤出 Intent。

应用程序 book1 可以如下发送 Intent:

Intent intent = new Intent();
intent.setAction("com.example.android.book1.my-broadcast");
sendBroadcast(intent);

<receiver> 标签的属性如下所述:

  • android:enabled:将此属性设置为 true 允许系统实例化接收器。此属性的默认值是 true。此标签必须与 <application>android:enabled 属性结合使用。两者都必须为 true,系统才能实例化它。

  • android:exported:将此属性设置为 true 使你的接收器对所有系统中的应用程序可见。如果设置为 false,则它只能接收来自同一应用程序或具有相同用户 ID 的应用程序的 Intent。如果你的应用程序没有 Intent 过滤器,那么默认值是 false,因为它假定这个接收器对你来说是私有的。如果你定义了 Intent 过滤器,那么默认值是 true。在我们前面的示例中,我们确实有 Intent 过滤器,所以接收器对系统的其余部分是可见的。

  • android:name:这是实现接收器的类的名称。这是一个必需的属性,应该是类的完全限定名。声明接收器后,应尽量不要更改名称,因为其他应用程序可能会使用它,更改名称将破坏它们的功能。

  • android:permission:你可以通过权限保护你的接收器。使用此属性,你可以指定发送意图到你的接收器的组件应该具有的权限。如果这里没有列出权限,则使用<application>标签的权限。如果那里也没有指定权限,那么你的接收器将完全不受保护。

  • android:process:默认情况下,接收器在应用程序进程中实例化。如果你愿意,可以在这里声明一个进程的名称。如果名称以冒号(:)开头,它将在应用程序内的私有进程中实例化。如果以小写字母开头,并且你的应用程序有权限这样做,它将在全局进程中运行。

安全地发送和接收广播。

广播分为两种类型:普通广播和有序广播。普通广播通过Context.sendBroadcast()以异步方式发送,所有监听它的接收器都将收到它。有序广播通过Context.sendOrderedBoradcast发送,一次只传递给一个接收器。接收器添加其结果并将其发送给下一个接收器。可以使用 Intent Filter 中的android:priority属性设置顺序。如果有多个具有相同优先级的过滤器,则接收广播的顺序是随机的。

广播是异步的。你发送它们,但不能保证接收器一定能收到。在这种情况下,应用程序必须优雅地处理。

广播可以包含额外的信息。任何监听广播的接收器都可以接收到发送的广播。因此,明智的做法是在广播中不要发送任何敏感信息。此外,可以通过权限保护广播。这是通过在sendBroadcast()方法中提供权限字符串来完成的。只有通过使用<uses-permission>声明适当权限的应用程序才能接收它。同样,可以在sendOrderedBroadcast()方法中添加权限字符串。

当一个进程仍在执行onReceive()时,它被视为前台进程。一旦进程离开了onReceive()方法,它就被视为非活动进程,系统将尝试杀死它。在onReceive()方法中执行的任何异步操作可能会被杀死。例如,当接收到广播时启动服务应该使用Context.startService()

粘性广播会在手机关机或某些组件移除之前一直存在。当广播中的信息更新时,广播会使用新信息进行更新。任何拥有BROADCAST_STICKY权限的应用程序都可以移除或发送粘性广播,因此不要在其中放置任何敏感信息。此外,粘性广播不能通过权限保护,因此应谨慎使用。

可以在接收器上实施权限。如前所述,这可以通过在清单文件中添加权限或在registerReceiver()方法中动态添加来实现。

通过设置Intent.setPackage,从冰淇淋三明治开始,你可以限制广播只被一个应用程序接收。

Intent类中定义了一些系统广播动作。这些事件由系统触发,应用程序无法触发它们。接收者可以注册监听这些事件中的任何一个。这些动作包括ACTION_TIMEZONE_CHANGEDACTION_BOOT_COMPLETEDACTION_PACKAGE_ADDEDACTION_PACKAGE_REMOVEDACTION_POWER_DISCONNECTEDACTION_SHUTDOWN

本地广播

如果广播仅针对应用程序内的组件,最好使用LocalBroadcastManager帮助类。这个帮助类是 Android 支持包的一部分。除了比发送全局广播更有效之外,它还更安全,因为它不会离开应用程序进程,其他应用程序也无法看到它。本地广播不需要在清单中声明,因为它仅限于应用程序内部。

可以如下创建本地广播:

Intent intent = new Intent("my-local-broadcast");
Intent.putExtra("message", "Hello World!");
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);

下面的代码段监听一个本地广播:

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  //  ... other code goes here

  LocalBroadcastManager.getInstance(this).registerReceiver(
    mMessageReceiver, new IntentFilter("my-local-broadcast"));
}

private BroadcastReceiver mMessageReceiver = new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
    String message = intent.getStringExtra("message");
    Log.d("Received local broadcast" + message);
    // ... other code goes here
  }
};

意图

意图是 Android 组件间通信的机制。意图是异步的,组件发出它们,而接收组件有责任验证传入意图的数据并据此采取行动。Android 系统使用意图来启动活动或服务,与服务通信,广播事件或变化,使用待定意图接收通知,以及查询内容提供者。

对于每个组件,有不同的机制来处理意图。因此,发送到活动、服务和广播接收器的意图只由 Android 系统发送给它们各自的对应组件。例如,使用Context.startActivity()发送出去以启动活动的的事件将只解决与意图标准匹配的活动。同样,使用Context.sendBroadcast()发送的广播将只被接收者接收,而不是其他组件。

在意图被发送出去之前,重要的是要检查是否有组件来处理意图。如果没有组件来处理意图,应用程序将会崩溃。可以使用PackageManager类的queryIntentActivities()方法查询匹配的意图。

注意

任何恶意应用程序都可以向暴露的组件发送 Intent。在对其采取行动之前,验证输入是您组件的责任。

Intent 基本上是在组件间传递的序列化对象。这个对象包含了一些被其他组件用来执行操作的信息。例如,使用用户登录凭据登录用户的 Activity 可能会启动另一个 Activity,使用Context.startActivity()加载用户之前选择的书籍。在这种情况下,Intent 可能包含用户的账户名,该账户名将用于从服务器获取存储的书籍。

Intent对象包含以下四种信息:

  1. 组件名称(Component Name):只有在显式 Intent 的情况下才需要组件名称。如果与外部组件通信,它必须是完全限定类名;如果是内部组件,则只需类名。

  2. 动作字符串(Action String):动作字符串是应该执行的动作。例如,动作字符串ACTION_CALL发起电话呼叫。广播动作ACTION_BATTERY_LOW是关于低电量对应用程序的警告。

  3. 数据(Data):这是带有 MIME 类型的数据的 URI。例如,对于ACTION_CALL,数据类型将为tel:。数据和数据的类型是相辅相成的。为了处理某些数据,了解其类型很重要,以便可以适当地处理。

  4. 类别(Category):类别提供了关于组件可以接收的 Intent 类型附加信息,从而增加了进一步的限制。例如,浏览器可以安全地调用具有CATEGORY_BROWSERABLE类别的 Activity。

Intents 是异步的,因此不期望有结果。在 Activity 的情况下,Intent 也可以用于启动一个 Activity 以获取结果。这是通过使用Context.startActivityForResult()完成的,结果通过finish()方法返回给调用 Activity。

用于广播的 Intent 通常是关于刚刚发生动作的通告。广播接收者注册监听这些事件。一些示例包括ACTION_PACKAGE_ADDEDACTION_TIME_TICKACTION_BOOT_COMPLETED。在这种情况下,Intent 就像一个触发器,一旦事件发生就会执行某些动作。

注意

不要在Intent对象中放置任何敏感信息。应使用其他机制,如可以通过权限保护的内容提供者(Content Provider)来在组件间共享信息。

接收组件通过使用getIntent().getExtras()获取附加在Intent类上的额外信息。安全的编程实践要求对此输入进行验证,并确保其值为可接受值。

显式 Intents

组件可以发送一个特定的 Intent,只针对一个组件。为此,组件应知道目标组件的完全限定名称。应用 A中的 Activity 向应用 B中的 Activity 发送显式 Intent,可以图形化表示如下:

显式意图

例如,活动可以使用以下代码显式地与名为ViewBooksActivity的内部活动通信:

Intent myIntent = new Intent (this, ViewBooksActivity.class);
startActivity(myIntent);

如果ViewBooksActivity是一个外部活动,则组件名称应该是类的完全限定名。可以这样操作:

Intent myIntent = new Intent (this, "com.example.android.Books.ViewBooksActivity.class");
startActivity(myIntent);

由于意图可以被任何应用拦截,如果组件名称可用,最好显式调用该组件。

隐式意图

如果不知道组件的完全限定名,可以通过指定接收组件需要用它执行的动作来隐式调用该组件。然后系统通过匹配Intent对象中指定的标准,识别出最适合处理意图的组件。以下是一个隐式意图的说明:应用 A中的活动发出一个意图,系统搜索可以处理此类意图的相关组件(基于它们的意图过滤器和权限)。

隐式意图

以下是一些隐式意图的示例:

// Intent to view a webpage
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.google.com"));

// Intent to dial a telephone number
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:4081112222"));

//Intent to send an email
Intent intent = new Intent(Intent.ACTION_SEND);
emailIntent.setType(HTTP.PLAIN_TEXT_TYPE);
emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] {"me@example.com"});
emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Hello Intent!");
emailIntent.putExtra(Intent.EXTRA_TEXT, "My implicit intent");

意图过滤器

为了让系统解析组件,需要在清单文件中用适当的标识符声明该组件。这项任务是通过意图过滤器完成的。意图过滤器是为活动使用<activity><service><receiver>声明中的<intent-filter>子标签定义的。在解析意图的适当活动时,系统只考虑Intent对象的三个方面:动作、数据(URI 和 MIME 类型)和类别。所有这些意图方面必须匹配才能成功解析。组件名称仅用于显式意图。

意图过滤器必须包含<action>子标签,并且可能包含<category><data>。以下是一些<intent-filter>声明的示例。

以下标签标识了应用的启动点活动:

<intent-filter>
  <action android:name="android.intent.action.MAIN" />
  <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

一个允许用户请求book类型数据的活动可以定义如下:

<intent-filter>
  <action android:name="android.intent.action.GET_CONTENT" />
  <category android:name="android.intent.category.DEFAULT" />
  <data android:mimeType="vnd.android.cursor.item/vnd.example.book"/>
</intent-filter>

意图过滤器不是安全边界,不应依赖它们来保证安全。意图过滤器不能通过权限来保护。此外,具有意图过滤器的任何组件都将成为导出组件,任何应用都可以向这个组件发送意图。

待定意图

在意图的情况下,接收应用以其自身的权限执行代码,就如同它是接收应用的一部分。在待定意图的情况下,接收应用使用原始应用的标识和权限,并代表其执行代码。

因此,待定意图是应用程序提供给另一个应用程序的令牌,以便另一个应用程序可以以原始应用程序的权限和身份执行一段代码。即使发送应用程序的进程被杀死或销毁,待定意图仍将执行。待定意图的此属性可以很好地用于在事件发生后向原始应用程序发送通知。待定意图可以是显式的也可以是隐式的。

为了增加安全性,使得只有一个组件接收意图,可以使用setComponent()方法将组件内嵌到意图中。默认情况下,待定意图(pending Intent)不能被接收组件修改。这对于安全来说是有好处的。接收组件唯一可以编辑的部分是extras。然而,发送者可以设置标志,明确允许接收组件编辑PendingIntent。为此,发送者需要使用fillIn(Intent, int)方法的规则。例如,如果发送者希望接收者覆盖数据字段,即使它已经被设置,发送者可以设置FILL_IN_DATA=true。这是一个非常敏感的操作,应当谨慎处理。

总结

在本章中,我们回顾了 Android 系统的四个组件——活动(Activities)、服务(Services)、内容提供者(Content Providers)和广播接收器(Broadcast Receivers),以及组件间通信机制——意图(Intents)和绑定器(Binders)。安全性的起点是这些组件的安全声明。按照安全的一般规则,暴露最少的信息总是一个好主意。所有 Android 组件都通过权限进行保护。意图(Intents)是异步组件,应始终验证其输入。意图过滤器(Intent Filters)是减少应用程序攻击面的好方法,但显式意图(Explicit Intent)仍然可以向其发送意图。现在我们了解了 Android 组件和通信机制,让我们在下一章中详细查看 Android 权限。

第三章:权限

本章的重点是权限。权限是 Android 应用程序的必要组成部分,几乎所有的应用程序开发者和用户都会在不同时间遇到它们。正如我们在第一章中讨论的,Android 安全模型 - 大局观,安装时的应用程序审查是最重要的安全关卡。这一步是用户的全或无决策;用户要么接受所有列出的权限,要么拒绝下载应用。因此,作为 Android 手机的用户,了解权限对于明智地决定安装哪个应用程序至关重要。权限是保护组件和用户数据的基础。

本章从介绍 Android 系统中现有的权限开始。我们讨论了四种权限保护级别:普通、危险、签名和签名或系统。然后,我们将讨论如何使用权限来保护应用程序及其组件。接下来,我们学习如何通过添加用户定义的权限来扩展权限模型。这一节将讨论权限组、权限树以及在清单文件中创建新权限的语法。

权限保护级别

在应用程序层面,Android 安全基于权限。使用这种基于权限的模型,Android 系统保护系统资源,如相机和蓝牙,以及应用程序资源,如文件和组件。应用程序应该有权限去操作或使用这些资源。任何打算使用这些资源的应用程序需要向用户声明它将访问这些资源。例如,如果一个应用程序将发送和读取短信,它需要在清单文件中声明 android.permission.SEND_SMSandroid.permission.READ_SMS

权限也是应用程序之间进行访问控制的有效方法。应用程序的清单文件包含一个权限列表。任何希望访问此应用程序资源的外部应用程序应具备这些权限。下一章将详细讨论这一点。

所有 Android 权限都在 Manifest.permission 类中声明为常量。然而,这个类并没有提到权限的类型。这可以用来检查 Android 源代码。我尝试在以下部分列出一些这些权限。权限列表会根据功能的变化而变化,因此最好参考 Android 源代码以获取最新的权限列表。例如,android.permission.BLUETOOTH 从 API 级别 1 开始就有,而 android.permission.AUTHENTICATE_ACCOUNTS 是在 API 5 中添加的。您可以在 source.android.com 获取获取 Android 源代码的信息。

所有 Android 权限都属于四个保护级别之一。任何保护级别的权限都需要在清单文件中声明。第三方应用只能使用保护级别为 0 和 1 的权限。以下将讨论这些保护级别:

  • 普通权限:这一级别(级别 0)的权限对用户造成不了多大伤害。它们通常不会让用户花钱,但可能会引起用户的一些烦恼。下载应用时,可以通过点击查看全部箭头来查看这些权限。这些权限会自动授予应用。例如,权限android.permission.GET_PACKAGE_SIZEandroid.permission.FLASHLIGHT分别允许应用获取任何包的大小和访问闪光灯。

    以下是本书编写时 Android 系统中存在的一些普通权限列表。

    用于设置用户偏好的权限包括:

    • android.permission.EXPAND_STATUS_BAR 展开状态栏

    • android.permission.KILL_BACKGROUND_PROCESSES 结束后台进程

    • android.permission.SET_WALLPAPER 设置壁纸

    • android.permission.SET_WALLPAPER_HINTS 设置壁纸提示

    • android.permission.VIBRATE 震动

    • android.permission.DISABLE_KEYGUARD 禁用键盘锁

    • android.permission.FLASHLIGHT 闪光灯

    允许用户访问系统或应用信息的权限包括:

    • android.permission.ACCESS_LOCATION_EXTRA_COMMANDS 访问位置额外命令

    • android.permission.ACCESS_NETWORK_STATE 访问网络状态

    • android.permission.ACCESS_WIFI_STATE 访问 WiFi 状态

    • android.permission.BATTERY_STATS 电池统计

    • android.permission.GET_ACCOUNTS 获取账户

    • android.permission.GET_PACKAGE_SIZE 获取包大小

    • android.permission.READ_SYNC_SETTINGS 读取同步设置

    • android.permission.READ_SYNC_STATS 读取同步统计

    • android.permission.RECEIVE_BOOT_COMPLETED 接收开机完成

    • android.permission.SUBSCRIBED_FEEDS_READ 读取订阅源

    • android.permission.WRITE_USER_DICTIONARY 写入用户词典

    用户应该谨慎请求的权限包括android.permission.BROADCAST_STICKY,它允许应用发送粘性广播,即使广播已经送达后仍然保持存活。

  • 危险权限:这一保护级别(级别 1)的权限总是向用户显示。向应用授予危险权限允许它们访问设备功能和数据。这些权限可能导致用户隐私或财务损失。例如,授予如android.permission.ACCESS_FINE_LOCATIONandroid.permission.ACCESS_COARSE_LOCATION之类的危险权限,允许应用访问用户的位置,如果应用不需要这种功能,可能会成为隐私问题。同样,授予应用android.permission.READ_SMSandroid.permission.SEND_SMS权限,允许应用发送和接收短信,这也可能涉及隐私问题。

    用户可以在任何时候进入设置,选择应用来查看已授予应用的权限。参考以下两张图片,它们展示了 Gmail 应用的权限。第一张图片显示了总是向用户显示的危险权限。注意显示全部的下拉菜单按钮。此选项显示了应用请求的所有权限。注意硬件控制权限,以及默认不向用户显示的正常权限。

    权限保护级别

    以下是本书撰写时 Android 系统中一些危险权限的列表。

    一些危险权限对用户来说可能是代价高昂的。例如,发送短信或订阅付费内容的应用可能导致用户花费大量资金。以下是一些其他例子:

    • android.permission.RECEIVE_MMS

    • android.permission.RECEIVE_SMS

    • android.permission.SEND_SMS

    • android.permission.SUBSCRIBED_FEEDS_WRITE

    有权改变手机状态的权限包括以下内容。这些权限应谨慎使用,因为它们可能导致系统不稳定,引起用户烦恼,并使系统安全性降低。例如,挂载和卸载文件系统的权限可以改变手机的状态。任何具有录音权限的恶意应用都可以轻松地用垃圾音频占用手机内存。以下是一些例子:

    • android.permission.MODIFY_AUDIO_SETTINGS

    • android.permission.MODIFY_PHONE_STATE

    • android.permission.MOUNT_FORMAT_FILESYSTEMS

    • android.permission.WAKE_LOCK

    • android.permission.WRITE_APN_SETTINGS

    • android.permission.WRITE_CALENDAR

    • android.permission.WRITE_CONTACTS

    • android.permission.WRITE_EXTERNAL_STORAGE

    • android.permission.WRITE_OWNER_DATA

    • android.permission.WRITE_SETTINGS

    • android.permission.WRITE_SMS

    • android.permission.SET_ALWAYS_FINISH

    • android.permission.SET_ANIMATION_SCALE

    • android.permission.SET_DEBUG_APP

    • android.permission.SET_PROCESS_LIMIT

    • android.permission.SET_TIME_ZONE

    • android.permission.SIGNAL_PERSISTENT_PROCESSES

    • android.permission.SYSTEM_ALERT_WINDOW

    一些危险权限可能存在隐私风险。允许用户读取短信、日志和日历的权限可能被僵尸网络和木马轻易利用,按照远程主人的命令执行有趣的操作。以下是一些其他例子:

    • android.permission.MANAGE_ACCOUNTS

    • android.permission.MODIFY_AUDIO_SETTINGS

    • android.permission.MODIFY_PHONE_STATE

    • android.permission.MOUNT_FORMAT_FILESYSTEMS

    • android.permission.MOUNT_UNMOUNT_FILESYSTEMS

    • android.permission.PERSISTENT_ACTIVITY

    • android.permission.PROCESS_OUTGOING_CALLS

    • android.permission.READ_CALENDAR

    • android.permission.READ_CONTACTS

    • android.permission.READ_LOGS

    • android.permission.READ_OWNER_DATA

    • android.permission.READ_PHONE_STATE

    • android.permission.READ_SMS

    • android.permission.READ_USER_DICTIONARY

    • android.permission.USE_CREDENTIALS

  • 签名权限:这一保护级别(第 2 级)的权限允许同一开发者编写的两个应用程序互相访问对方的组件。如果下载的应用程序具有与声明权限的应用程序相同的证书,则自动授予此权限。例如,应用程序 A 定义了一个权限 com.example.permission.ACCESS_BOOK_STORE。由与应用程序 A 相同证书签名的应用程序 B 在其清单文件中声明了它。由于应用程序 A 和 B 具有相同的证书,因此当安装应用程序 B 时,此权限不会显示给用户。用户当然可以使用查看全部来查看它。应用这一组权限的应用程序可以执行非常强大的操作。例如,有了 android.permission.INJECT_EVENTS 权限,应用程序可以向任何应用程序注入按键、触摸和轨迹球事件,而 android.permission.BROADCAST_SMS 可以广播短信确认。这一保护组中由 Android 系统定义的权限仅保留给系统应用。

    这一级别的某些权限允许应用程序使用系统级别的功能。例如,ACCOUNT_MANAGER 权限允许应用程序使用账户验证器,而 BRIK 权限允许应用程序锁定手机。以下是编写本书时一些签名权限的列表。要获取完整的参考资料,请查看 Android 源代码或 Manifest.permission 类:

    • android.permission.ACCESS_SURFACE_FLINGER

    • android.permission.ACCOUNT_MANAGER

    • android.permission.BRICK

    • android.permission.BIND_INPUT_METHOD

    • android.permission.SHUTDOWN

    • android.permission.SET_ACTIVITY_WATCHER

    • android.permission.SET_ORIENTATION

    • android.permission.HARDWARE_TEST

    • android.permission.UPDATE_DEVICE_STATS

    • android.permission.CLEAR_APP_USER_DATA

    • android.permission.COPY_PROTECTED_DATA

    • android.permission.CHANGE_COMPONENT_ENABLED_STATE

    • android.permission.FORCE_BACK

    • android.permission.INJECT_EVENTS

    • android.permission.INTERNAL_SYSTEM_WINDOW

    • android.permission.MANAGE_APP_TOKENS

    这一级别的某些权限允许应用程序发送系统级别的广播和意图,如发送意图和短信。这些权限包括:

    • android.permission.BROADCAST_PACKAGE_REMOVED

    • android.permission.BROADCAST_SMS

    • android.permission.BROADCAST_WAP_PUSH

    这一级别的其他权限允许应用程序访问第三方应用程序无法访问的系统级别数据。这些权限包括:

    • android.permission.PACKAGE_USAGE_STATS

    • android.permission.CHANGE_BACKGROUND_DATA_SETTING

    • android.permission.BIND_DEVICE_ADMIN

    • android.permission.READ_FRAME_BUFFER

    • android.permission.DEVICE_POWER

    • android.permission.DIAGNOSTIC

    • android.permission.FACTORY_TEST

    • android.permission.FORCE_STOP_PACKAGES

    • android.permission.GLOBAL_SEARCH_CONTROL

  • 签名或系统权限:与签名保护级别一样,此权限授予与定义该权限的应用具有相同证书的应用。此外,此保护级别还包括与 Android 系统映像具有相同证书的应用。这个权限级别主要用于手机制造商、运营商和系统应用构建的应用。第三方应用不允许使用这些权限。这些权限允许应用执行一些非常强大的功能。例如,权限 android.permission.REBOOT 允许应用重启设备。权限 android.permission.SET_TIME 允许应用设置系统时间。

    在撰写本书时,一些 SignatureOrSystem 权限列表如下:

    • android.permission.ACCESS_CHECKIN_PROPERTIES

    • android.permission.BACKUP

    • android.permission.BIND_APPWIDGET

    • android.permission.BIND_WALLPAPER

    • android.permission.CALL_PRIVILEGED

    • android.permission.CONTROL_LOCATION_UPDATES

    • android.permission.DELETE_CACHE_FILES

    • android.permission.DELETE_PACKAGES

    • android.permission.GLOBAL_SEARCH

    • android.permission.INSTALL_LOCATION_PROVIDER

    • android.permission.INSTALL_PACKAGES

    • android.permission.MASTER_CLEAR

    • android.permission.REBOOT

    • android.permission.SET_TIME

    • android.permission.STATUS_BAR

    • android.permission.WRITE_GSERVICES

    • android.permission.WRITE_SECURE_SETTINGS

应用程序级别权限

有两种方法可以将权限应用于整个应用程序。在第一种情况下,应用程序声明应用程序正常运行所需的权限。因此,将发送短信的应用程序将在清单文件中声明此类权限。在第二种情况下,应用程序可以声明尝试与此应用程序交互的其他应用程序应具有的权限。例如,一个应用程序可以声明任何想要与其组件交互的应用程序都应具有访问摄像头的权限。这两种类型的权限都必须在清单文件中声明。让我们逐一了解它们。

这个 <uses-permission> 标签在 <manifest> 内声明,表示应用程序请求正常运行所需的权限。标签的语法如下:

<uses-permission android:name=" " />

用户在下载应用时,必须接受这些权限。android:name 是权限的名称。此标签的声明示例如下。以下权限声明用户即将安装的应用程序将访问用户的短信:

<uses-permission android:name="android.permission.READ_SMS"/>

<application>标签有一个名为android:permission的属性,用于声明组件的通用权限。任何尝试与此应用程序的组件交互的应用程序都需要这些权限。以下代码显示了这一点。以下代码表明,与MyApplication的任何组件交互的应用程序都应该有访问摄像头的权限:

<application android:name="MyApplication" android:icon="@drawable/icon" android:label="@string/app_name""android.permission.CAMERA"/>

正如下一节所讨论的,各个组件也可以设置权限。组件权限会覆盖使用<application>标签设置的权限。上述方法是声明所有组件的通用权限的最佳位置。

组件级别权限

所有 Android 组件都可以使用权限进行保护。以下图示说明了这一概念:

组件级别权限

让我们讨论一下每个组件的权限声明和实施。

活动

任何活动都可以通过在<activity>标签的活动声明中调用权限来进行保护。例如,具有自定义权限com.example.project.ORDER_BOOK的活动OrderActivity将如下声明。任何尝试启动OrderActivity的组件都需要拥有这个自定义权限。

<activity android:name=".OrderActivity" android:permission="com.example.project.ORDER_BOOK" android:exported="false"/>

对于活动,权限实施发生在使用Context.startActivity()Context.startActivityForResult()启动活动时。如果启动组件没有适当的权限,将抛出SecurityException

服务

任何服务都可以通过在<service>标签中列出所需的权限来进行保护。例如,基于关键词识别短信的服务FindUsefulSMS声明了一个权限android.permission.READ_SMS。这个权限将如下声明。任何尝试启动FindUsefulSMS的组件都需要拥有这个权限。

<service android:name=".FindUsefulSMS" android:enabled="true"
android:permission="android.permission.READ_SMS">            
</service>

对于服务的权限实施是在使用Context.startService()启动服务、使用Context.stopService()停止服务以及使用Context.bindService()绑定服务时进行的。如果请求的组件没有适当的权限,将抛出SecurityException

如果服务公开了一个允许其他应用程序绑定的 Binder 接口,可以在绑定到 Binder 时使用Context.checkCallingPermission()来检查调用者的权限。

内容提供者

内容提供者可以通过<provider>标签中指定的权限来保护。以下示例中,任何想要与提供者通信的组件都应该拥有android.permission.READ_SMS权限:

<provider
   android:authorities="com.example.android.books.contentprovider"
   android:name=".contentprovider.MyBooksdoContentProvider" 
   android:grantUriPermissions="true"
   android:Permission="android.permission.READ_CALENDAR"/>

正如在第二章 应用构建块 中所讨论的,<provider>标签也具有细粒度的读写权限属性。为了能够从<provider>标签中读取,应用程序应具有读取权限。这会在ContentResolver.query()过程中进行检查。为了能够更新、删除和插入提供者,组件应具有读写权限。这些权限会在ContentResolver.insert()ContentResolver.update()ContentResolver.delete()过程中进行检查。如果权限不适当,将导致调用时抛出SecurityException

<grant-uri-permission>标签是<provider>标签的子标签,用于在有限时间内授予对提供者某些特定数据集的访问权限。考虑一个将短信保存到数据库的应用程序示例。某些短信可能附有照片。为了使应用程序正确查看短信,它会启动图像查看器,而图像查看器可能无法访问提供者。URI通用资源标识符)权限将允许图像查看器读取特定图片的权限。在前面的示例中,提供者设置了android:grantIriPermissions="true",图像查看器将具有对整个提供者的读取权限。这构成了安全风险。为了提供有限的访问权限,提供者可以声明它希望对 URI 权限开放的部分。

URI 权限的语法如下:

<grant-uri-permission android:path="string"
                      android:pathPattern="string"
                      android:pathPrefix="string" />

注意

URI 权限不是递归的。

我觉得最有趣的是,我们可以使用通配符和模式来定义我们想要强制实施 URI 权限的提供者的哪些部分。以下是一个例子:

<grant-uri-permission android:pathPattern="/picture/" />

记得在任务完成后使用Context.revokeUriPermission()撤销 URI 权限。

广播接收器

广播可以通过两种方式使用权限进行保护。在第一种情况下,接收者使用权限保护自己,因此它只接收想要听到的广播。在另一种情况下,广播者选择哪些接收者可以接收广播。我们将在下一节讨论这两种情况。

任何接收者都可以通过在<receiver>标签中的接收者声明中调用权限来保护。例如,接收者MyListener声明了一个权限android.permission.READ_SMS,这将按如下方式声明。MyListener只接收具有android.permission.READ_SMS权限的广播者的广播。

<receiver android:name=".MyListener"
android:permission="android.permission.READ_SMS">    
        <intent-filter> 
            <action android:name=
                "android.provider.Telephony.SMS_RECEIVED" /> 
        </intent-filter>         
</receiver>

提示

请记住,粘性广播无法通过权限进行保护。

接收广播所需的权限在广播意图传递后被检查,即在调用Context.sendBroadcast()返回之后。因此,如果广播者没有适当的权限,不会抛出异常;只是广播不会被传递。如果通过使用Context.registerReceiver()动态创建接收者,可以在创建此接收者时设置权限。

第二种情况,广播者限制哪些接收者可以接收意图,是通过使用sendBroadcast()方法实现的。以下代码片段定义了一个仅发送给具有android.permission.READ_SMS权限的应用程序接收者的广播示例:

Intent intent = new Intent();
intent.setAction(MY_BROADCAST_ACTION);
sendBroadcast(intent, "android.provider.Telephony.SMS_RECEIVED");

注意

使用组件声明的权限不会授予应用程序。这是尝试与之交互的组件所属的应用程序应具备的权限。

扩展 Android 权限

开发者可以通过添加自己的权限来扩展权限系统。这些权限将在用户下载应用时向用户展示,因此确保它们被适当地本地化和标记是非常重要的。

添加新权限

开发者可以选择仅添加一个新权限或整个权限树。在清单文件中声明新权限。要添加新权限,应用程序可以使用<permission>标签声明,如下面的代码片段所示:

<permission android:name="string"
            android:description="string resource"
            android:icon="drawable resource"
            android:label="string resource"
            android:permissionGroup="string"
            android:protectionLevel=["normal" | "dangerous" | 
                            "signature" | "signatureOrSystem"] />

以下代码片段中用于新权限组的属性描述如下:

  • android:name:这是正在声明的新的权限名称。

  • android:description:这详细描述了正在声明的新的权限。

  • android:icon:这是权限图标。

  • android:label:这是在安装时向用户显示的标签。

  • android:permissionGroup:这将为权限分配一个预先存在的用户定义的组或新组。如果没有指定名称,权限不属于任何组,这也是可以的。我将在本节后面讨论如何创建权限组。

  • android:protectionLevel:这指定了新权限的保护级别。这些保护级别在本章前面已经讨论过。

这样的权限的一个示例可能如下:

<permission android:name="com.example.android.book.READ_BOOKSTORE"
            android:description="@string/perm_read_bookstore"
            android:label="Read access to books database"
            android:permissionGroup="BOOKSTORE_PERMS"
            android:protectionLevel="dangerous"/>

为了便于本地化和维护,使用字符串资源总比使用原始字符串要好。

一旦声明了新权限,请确保在<uses-permission>标签中声明它。

创建权限组

可以使用<permission-group>标签创建权限组。这是一个逻辑上的权限分组,当向用户展示时,它们将一起呈现。权限组是使用以下语法创建的:

<permission-group android:name="string" 
      android:description="string resource"
                  android:icon="drawable resource"
                  android:label="string resource" />

以下代码片段中用于新权限组的属性描述如下:

  • android:name:这是新权限组的名称。这是在<permission>标签中提到的名称。

  • android:description:这详细描述了正在声明的新权限组。

  • android:icon:这是权限组的图标。(注意:这里重复了,根据注意事项,不应该重复输出,所以这里不翻译)

  • android:label:这是在安装时显示的标签。(注意:这里重复了,根据注意事项,不应该重复输出,所以这里不翻译)

一个带有书店权限的权限组声明示例如下:

<permission-group android:description="@string/perm_group_bookstore"
                  android:label="@string/perm_group_bookstore_label"
                  android:name="BOOKSTORE_PERMS" />

创建权限树

如果需要将权限组织为一个命名空间,以便创建权限树,那么应用程序可以声明一个<permission-tree>标签。这样的树的一个示例如下:

com.example.android.book
com.example.android.book.READ_BOOK
com.example.android.book.bookstore.READ_BOOKSTORE
com.example.android.book.bookstore.WRITE_BOOKSTORE

这个标签并不定义任何新权限,它只是为你创建一个用于分组权限的命名空间。我看到这个概念被那些有多款应用的开发商使用,所有这些应用都会相互通信。<permission-tree>标签的语法定义如下:

<permission-tree android:name="string" 
    android:icon="drawable resource"
                 android:label="string resource"  />

以下是对前述代码片段中使用的新权限组属性描述:

  • android:name:这是新权限组的名称。名称至少应由三个由点分隔的部分组成,例如com.example.android是可以的,但com.example则不行。

  • android:icon:这是权限组的图标。

  • android:label:这是在安装时向用户显示的标签。

一个声明示例如下:

<permission-tree android:name="com.example.android.book" 
                  android:label="@string/perm_tree_book"  />

总结

权限是 Android 应用安全的核心,本章详细介绍了权限。我们了解了四种权限保护级别,如何使用权限保护组件,以及如何定义新权限。对于开发者和 Android 手机用户来说,了解和掌握权限模型至关重要。现在我们拥有了关于组件、组件间通信和权限的知识,让我们迈向下一章,学习如何定义应用程序的策略文件。

第四章:定义应用程序的策略文件

本章将把我们迄今为止所学的所有内容汇集在一起。我们将使用应用程序组件、意图和权限,并将它们全部放在一起来定义我们应用程序的策略文件。这个策略文件被称为AndroidManifest.xml,无疑是应用程序最重要的文件。正如您将看到的,这个文件是定义应用程序及其组件访问控制策略的地方。这也是定义应用程序和组件级别特定信息的地方,安卓系统将使用这些信息与您的应用程序交互。

本章从讨论AndroidManfiest.xml开始。我们将讨论到目前为止尚未讨论的两个重要标签:<manifest><application>。接下来,我们将讨论在清单文件中可以执行的操作,例如声明权限、与其他应用程序共享进程、外部存储以及管理组件可见性。在您发布应用程序之前,本章将以策略文件的核对清单作为结束讨论。您应该根据您的使用情况调整核对清单。一旦有了全面的核对清单,您就可以在每次准备发布新版本时参考它。

AndroidManifest.xml文件

所有安卓应用程序都需要有一个清单文件。这个文件必须命名为AndroidManifest.xml,并且必须放在应用程序的根目录中。这个清单文件是应用程序的策略文件。它声明了应用程序组件、它们的可见性、访问规则、库、特性以及应用程序运行的最低安卓版本。

安卓系统使用清单文件进行组件解析。因此,AndroidManfiest.xml文件是整个应用程序中最重要文件,定义时需要特别小心,以加强应用程序的安全性。

清单文件是不可扩展的,因此应用程序不能添加自己的属性或标签。以下是如何嵌套这些标签的完整标签列表:

<uses-sdk><?xml version="1.0" encoding="utf-8"?>

<manifest>
    <uses-permission />
    <permission />
    <permission-tree />
    <permission-group />
    <instrumentation />
    <uses-sdk />
    <uses-configuration />  
    <uses-feature />  
    <supports-screens />  
    <compatible-screens />  
    <supports-gl-texture />  
    <application>
       <activity>
            <intent-filter>
                <action />
                <category />
                <data />
            </intent-filter>
            <meta-data />
        </activity>
        <activity-alias>
            <intent-filter>       </intent-filter>
            <meta-data />
        </activity-alias>
        <service>
            <intent-filter>       </intent-filter>
            <meta-data/>
        </service>
        <receiver>
            <intent-filter>       </intent-filter>
            <meta-data />
        </receiver>
        <provider>
            <grant-uri-permission />
            <meta-data />
            <path-permission />
        </provider>
        <uses-library />
    </application>
</manifest>

我们在前面的章节中已经涵盖了大部分标签。

只有两个标签<manifest><application>是必需的标签。声明组件没有特定的顺序。

<manifest>标签声明了应用程序特定的属性。它的声明方式如下:

<manifest xmlns:android="http://schemas.android.
  com/apk/res/android"
          package="string"
          android:sharedUserId="string"
          android:sharedUserLabel="string resource"
          android:versionCode="integer"
          android:versionName="string"
          android:installLocation=["auto" | "internalOnly" | 
            "preferExternal"] >

</manifest>

下面的代码片段展示了<manifest>标签的一个示例。在这个例子中,包名为com.android.example,内部版本为 10,用户看到的版本为 2.7.0。安装位置由安卓系统根据存储应用程序的空间决定。

<manifest package="com.android.example"
  android:versionCode="10"
  android:versionName="2.7.0"
  android:installLocation="auto"
  >

<manifest>标签的属性如下:

  • package:这是包的名称。这是应用 Java 风格的命名空间,例如 com.android.example。这是您应用的唯一 ID。如果您更改已发布应用的名称,它将被视为一个新应用,自动更新将不起作用。

  • android:sharedUserId:如果两个或更多应用共享同一个 Linux ID,则使用此属性。此属性将在后面的章节中详细讨论。

  • android:sharedUserLabel:这是共享用户 ID 的用户可读名称,仅当设置了 android:sharedUserId 时才有意义。它必须是一个字符串资源。

  • android:versionCode:这是应用内部用来跟踪修订版本的应用版本代码。更新应用至较新版本时会参考此代码。

  • android:versionName:这是向用户展示的应用版本。它可以设置为原始字符串或引用,并且仅用于向用户展示。

  • android:installLocation:此属性定义了 APK 将要安装的位置。此属性将在本章后面详细讨论。

应用标签定义如下:

<application android:allowTaskReparenting=["true" | "false"]
             android:backupAgent="string"
             android:debuggable=["true" | "false"]
             android:description="string resource"
             android:enabled=["true" | "false"]
             android:hasCode=["true" | "false"]
             android:hardwareAccelerated=["true" | "false"]
             android:icon="drawable resource"
             android:killAfterRestore=["true" | "false"]
             android:largeHeap=["true" | "false"]
             android:label="string resource"
             android:logo="drawable resource"
             android:manageSpaceActivity="string"
             android:name="string"
             android:permission="string"
             android:persistent=["true" | "false"]
             android:process="string"
             android:restoreAnyVersion=["true" | "false"]
             android:supportsRtl=["true" | "false"]
             android:taskAffinity="string"
             android:theme="resource or theme"
             android:uiOptions=["none" | 
                "splitActionBarWhenNarrow"] >

</application>

<application> 标签的一个示例在以下代码片段中展示。在此示例中,设置了应用名称、描述、图标和标签。应用不可调试,Android 系统可以实例化组件。

<application android:label="@string/app_name"
    android:description="@string/app_desc"
    android:icon="@drawable/example_icon"
    android:enabled="true"
    android:debuggable="false">

</application>

<application> 标签的许多属性作为应用内部声明的组件的默认值。这些标签包括 permissionprocessiconlabel。其他如 debuggableenabled 的属性是为整个应用设置的。以下将讨论 <application> 标签的属性:

  • android:allowTaskReparenting:此值可以被 <activity> 元素覆盖。它允许当 Activity 被带到前台时,Activity 与其有亲缘关系的 Activity 重新分组。

  • android:backupAgent:此属性包含应用的备份代理的名称。

  • android:debuggable:当此属性设置为 true 时,应用可以被调试。在将应用发布到市场之前,此值应始终设置为 false

  • android:description:这是作为对字符串资源的引用的应用的用户可读描述。

  • android:enabled:如果此属性设置为 true,Android 系统可以实例化应用组件。此属性可以被组件覆盖。

  • android:hasCode:如果此属性设置为 true,应用在启动组件时会尝试加载一些代码。

  • android:hardwareAccelerated:当此属性设置为 true 时,应用可以支持硬件加速渲染。它是在 API 级别 11 中引入的。

  • android:icon:这是作为对可绘制资源的引用的应用图标。

  • android:killAfterRestore:如果此属性设置为 true,则在完整系统恢复期间恢复应用程序设置后,应用程序将被终止。

  • android:largeHeap:此属性允许安卓系统为该应用程序创建一个大的 Dalvik 堆,并增加应用程序的内存占用,因此应谨慎使用。

  • android:label:这是应用程序的用户可读标签。

  • android:logo:这是应用程序的标志。

  • android:manageSpaceActivity:此值是管理应用程序内存的活动名称。

  • android:name:此属性包含将在任何其他组件启动之前实例化的子类的完全限定名称。

  • android:permission:此属性可以被组件覆盖,并设置客户端应具有与应用程序交互的权限。

  • android:persistent:通常由系统应用程序使用,此属性允许应用程序始终运行。

  • android:process:这是组件将要运行的进程的名称。这可以被任何组件的 android:process 属性覆盖。

  • android:restoreAnyVersion:此属性允许备份代理在即使当前存储的备份是由比尝试恢复的应用程序更新的新应用程序创建的情况下,尝试恢复。

  • android:supportsRtl:当此属性设置为 true 时,支持从右到左的布局。它是在 API 级别 17 中添加的。

  • android:taskAffinity:此属性让所有活动都与包名称有关联,除非活动明确设置。

  • android:theme:这是对应用程序样式资源的引用。

  • android:uiOptions:如果此属性设置为 none,则没有额外的 UI 选项;如果设置为 splitActionBarWhenNarrow,则在屏幕受限时会在底部设置一个栏。

在以下各节中,我们将讨论如何使用策略文件处理特定要求。

应用程序策略使用场景

本节讨论如何使用清单文件定义应用程序策略。我使用了使用场景,并且我们将讨论如何在策略文件中实现这些使用场景。

声明应用程序权限

安卓平台上的应用程序必须声明它打算使用哪些资源,以便应用程序能够正常运行。这些权限是用户在下载应用程序时显示的权限。正如在第三章 权限 中所讨论的,应用程序也可以定义自定义权限。应用程序权限应该是描述性的,以便用户能够理解它们。此外,与安全相关的普遍规则是,请求所需的最低权限是很重要的。

应用程序权限在清单文件中通过使用 <uses-permission> 标签声明。以下代码段展示了一个使用 GPS 获取位置信息的基于位置的清单文件的示例:

<uses-permissionandroid:name="android.
    permission.ACCESS_COARSE_LOCATION" />
<uses-permissionandroid:name="android.
    permission.ACCESS_FINE_LOCATION" />
<uses-permissionandroid:name="android.
    permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<uses-permissionandroid:name="android.
    permission.ACCESS_MOCK_LOCATION" />
<uses-permissionandroid:name="android.permission.INTERNET" />

这些权限将在用户安装应用程序时显示给他们,并且可以通过进入设置菜单下的应用程序进行检查。以下屏幕截图显示了这些权限:

声明应用程序权限

声明外部应用程序的权限

清单文件还声明了一个外部应用程序(不与相同的 Linux ID 运行)需要访问应用程序组件的权限。这可以是策略文件中的两个位置之一:在<application>标签中或者在<activity><provider><receiver><service>标签中的组件旁边。

如果应用程序的所有组件都需要某些权限,那么在<application>标签中指定它们很容易。如果一个组件需要某些特定的权限,那么可以在特定的组件标签中定义。请记住,任何标签中只能声明一个权限。如果一个组件受到权限保护,那么组件权限将覆盖在<application>标签中声明的权限。

以下是一个应用程序示例,它要求外部应用程序具有android.permission.ACCESS_COARSE_LOCATION权限才能访问其组件和资源:

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:permission="android.
            permission.ACCESS_COARSE_LOCATION">

如果一个服务要求访问它的任何应用程序组件都需要访问外部存储,那么可以按如下方式定义:

<service android:enabled="true"
    android:name=".MyService" 
        android:permission="android.
            permission.WRITE_EXTERNAL_STORAGE">            
</service>

如果策略文件同时具有前面的标签,那么当外部组件请求此服务时,它应该具有android.permission.WRITE_EXTERNAL_STORAGE权限,因为此权限将覆盖应用程序标签中声明的权限。

具有相同 Linux ID 的应用程序运行

应用程序之间共享数据总是很棘手。维护数据保密性和完整性并不容易。必须根据谁可以访问多少数据来建立适当的访问控制机制。在本节中,我们将讨论如何与内部应用程序(由同一开发密钥签名)共享应用程序数据。

安卓是一个分层架构,其应用程序隔离由操作系统本身强制执行。每当在安卓设备上安装一个应用程序时,安卓系统都会给它分配一个由系统定义的唯一用户 ID。请注意,以下屏幕截图中,两个应用程序example1example2是作为不同的用户 ID 运行的,分别是app_49app_50

具有相同 Linux ID 的应用程序运行

然而,一个应用程序可以请求系统分配一个它选择的用户 ID。另一个应用程序也可以请求相同的用户 ID。这会创建紧密耦合,不需要将组件对另一个应用程序可见或创建共享的内容提供者。这种紧密耦合是在所有希望在同一进程中运行的应用程序清单标签中完成的。

下面是两个应用com.example.example1com.example.example2的清单文件片段,它们使用相同的用户 ID:

<manifest xmlns:android="http://schemas.android.
  com/apk/res/android"
    package="com.example.example1"
    android:versionCode="1"
    android:versionName="1.0"
    android:sharedUserId="com.sharedID.example">

<manifest xmlns:android="http://schemas.android.
  com/apk/res/android"
    package="com.example.example2"
    android:versionCode="1"
    android:versionName="1.0"
    android:sharedUserId="com.sharedID.example">

下面的截图展示了当这两个应用在设备上运行时的显示效果。注意,应用com.example.example1com.example.example2现在有了app_113的应用 ID。

具有相同 Linux ID 的应用程序运行

你会注意到共享 UID 遵循一定的格式,类似于包名。任何其他命名约定都可能导致错误,例如安装错误:INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID

提示

所有共享相同 UID 的应用应该具有相同的证书。

外部存储

从 API 级别 8 开始,安卓提供了支持将安卓应用(APK 文件)存储在外部设备上的功能,例如 SD 卡。这有助于释放手机内部内存。一旦 APK 移动到外部存储,应用所占的唯一内存就是存储在内部内存上的应用私有数据。需要注意的是,即使是 SD 卡上的 APK,DEXDalvik 可执行文件)、私有数据目录和本地共享库仍然存储在内部存储上。

在清单文件中添加一个可选属性可以启用这个功能。对于这样的应用,应用信息屏幕会有一个移动到 SD 卡或移动到手机的按钮,具体取决于 APK 当前的存储位置。然后用户可以选择相应地移动 APK 文件。

如果外部设备被卸载或 USB 模式设置为Mass Storage(设备被用作磁盘驱动器),所有在外部设备上运行的活动和服务都会立即被终止。第七章 保护应用数据中对外部存储及其安全性进行了详细分析。在本节中,我们将讨论如何在策略文件中指定外部存储的首选设置。

通过在应用的清单文件的<manifest>元素中添加可选属性android:installLocation,可以启用在外部设备上存储 APK 的功能。属性android:installLocation可以有以下三个值:

  • InternalOnly:安卓系统只会在内部存储上安装应用。如果内部存储空间不足,将返回存储错误。

  • PreferExternal:安卓系统将尝试在外部存储上安装应用。如果外部存储空间不足,应用将被安装在内部存储上。用户可以根据需要将应用从外部存储移动到内部存储,反之亦然。

  • auto:此选项允许 Android 系统决定应用程序的最佳安装位置。默认的系统策略是首先在内部存储上安装应用程序。如果系统内部内存不足,则将应用程序安装在外部存储上。用户可以根据需要将应用程序从外部存储移动到内部存储,反之亦然。

    例如,如果将android:installLocation设置为Auto,那么在运行 Android 2.2 以下版本的设备上,系统将忽略此功能,APK 只能安装在内部存储上。以下是具有此选项的应用程序清单文件中的代码片段:

    <manifest package="com.example.android"
      android:versionCode="10"
      android:versionName="2.7.0"
      android:installLocation="auto"
      xmlns:android="http://schemas.android.
        com/apk/res/android">
    

以下是先前指定的应用程序的屏幕截图。你会注意到在这种情况下启用了移动到 SD 卡功能:

外部存储

在另一个未设置android:installLocation的应用程序中,移动到 SD 卡功能被禁用,如下面的屏幕截图所示:

外部存储

设置组件可见性

应用程序的任何组件,即活动、服务、提供者和接收器都可以被外部应用程序发现。本节讨论了这些场景的细节。

任何活动或服务都可以通过设置android:exported=false变为私有的。这也是活动的默认值。以下是一个私有活动的两个示例:

<activity android:name=".Activity1" android:exported="false" />
<activity android:name=".Activity2" />

然而,如果你给活动添加了一个意图过滤器(Intent Filter),那么该活动就会对意图过滤器中的意图变得可发现。因此,意图过滤器绝不能作为安全边界依赖。以下是意图过滤器声明的示例:

<activity
    android:name=".Activity1"
    android:label="@string/app_name" >
    <intent-filter>
      <action android:name="android.intent.action.MAIN" />
      <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
<activity android:name=".Activity2">
    <intent-filter>
      <action  android:name="com.example.android.
        intent.START_ACTIVITY2" />
    </intent-filter>
</activity>

活动和服务也可以通过外部组件所需的访问权限进行保护。这是通过使用组件标签的android:permission属性来实现的。

内容提供者可以通过使用android:exported=false设置为私有访问。这也是提供者的默认值。在这种情况下,只有具有相同 ID 的应用程序才能访问提供者。通过设置提供者标签的android:permission属性,甚至可以进一步限制此访问。

广播接收器可以通过使用android:exported=false设置为私有。如果接收器不包含任何意图过滤器,这是接收器的默认值。在这种情况下,只有具有相同 ID 的组件才能向接收器发送广播。如果接收器包含意图过滤器,则它变得可发现,且android:exported的默认值为false

调试

在应用程序开发过程中,我们通常将应用程序设置为调试模式。这允许开发者看到详细的日志,并可以进入应用程序检查错误。这通过在<application>标签中设置android:debuggabletrue来实现。为了避免安全漏洞,在发布应用程序之前,将此属性设置为false非常重要。

根据我的经验,敏感信息的示例包括用户名和密码、内存转储、内部服务器错误,甚至一些有趣的个人笔记,包括服务器状态和开发者对一段代码的看法。

android:debuggable的默认值是false

备份

从 API 级别 8 开始,应用程序可以选择一个备份代理将设备备份到云端或服务器。这可以在清单文件的<application>标签中设置android:allowBackuptrue,然后设置android:backupAgent为一个类名来实现。android:allowBackup的默认值设置为true,如果应用程序希望选择退出备份,可以将其设置为falseandroid:backupAgent没有默认值,应该指定一个类名。

这种备份的安全性是有争议的,因为用于备份数据的服务不同,且敏感数据,如用户名和密码可能会被泄露。

整合所有内容

以下示例将我们迄今为止的学习内容应用于分析随 Android SDK 示例RandomMusicPlayer提供的AndroidManifest.xml

清单文件指定这是应用程序com.example.android.musicplayer的第 1 版,它在 SDK 14 上运行,但支持回退至 SDK 7。应用程序使用了两个权限,分别是android.permission.INTERNETandroid.permission.WAKE_LOCK。应用程序有一个名为MainActivity的 Activity 作为入口点,一个名为MusicService的服务,以及一个名为MusicIntentReceiver的接收器。

MusicService定义了名为PLAYREWINDPAUSESKIPSTOPTOGGLE_PLAYBACK的自定义动作。

接收器使用 Android 系统定义的动作意图android.media.AUDIO_BECOMING_NOISYandroid.media.MEDIA_BUTTON

所有组件都没有受到权限保护。以下屏幕截图显示了AndroidManifst.xml文件的一个示例:

整合所有内容

示例清单

在本节中,我尝试整理了一个建议您在准备发布应用程序版本时参考的示例列表。这是一个非常通用的版本,您应该根据自身使用案例和组件进行适配。在创建清单时,要考虑与整个应用程序相关的问题,与特定组件相关的问题,以及可能由组件和应用程序规范设置共同引发的问题。

应用程序级别

在本节中,我列出了一些问题,在定义应用程序特定偏好时,你应该问自己这些问题。它们可能会影响用户如何看待、存储和感知你的应用程序。你可能需要问的一些应用程序级别的问题如下:

  • 你是否希望与其他你开发的应用程序共享资源?

    • 你是否指定了唯一的用户 ID?

    • 你是否故意或无意地为另一个应用程序定义了这个唯一 ID?

  • 你的应用程序是否需要一些功能,如摄像头、蓝牙和短信?

    • 你的应用程序是否需要所有这些权限?

    • 是否有比你所定义的权限更严格的权限?请记住最小权限原则。

    • 你的应用程序的所有组件都需要这个权限,还是只有少数需要?

    • 再次检查所有权限的拼写。即使权限拼写错误,应用程序也可能编译并运行。

    • 如果你定义了此权限,这是否是你需要的正确权限?

  • 应用程序在哪个 API 级别上工作?

  • 你的应用程序能够支持的最小 API 级别是什么?

  • 你的应用程序是否需要任何外部库?

  • 你在发布前是否记得关闭调试属性?

  • 如果你使用了备份代理,那么请记得在这里提及。

  • 你是否记得设置一个版本号?这将有助于你在应用程序升级时。

  • 你是否希望设置自动升级?

  • 你是否记得用你的发布密钥签名应用程序?

  • 有时设置特定的屏幕方向将不允许你的应用程序在某些设备上显示。例如,如果你的应用程序只支持竖屏模式,那么它可能不会在只支持横屏模式的设备上显示。

  • 你想在哪个位置安装 APK?

  • 如果意图没有及时接收,是否有可能会停止工作的服务?

  • 你是否希望有一些其他的应用程序级别设置,比如系统恢复组件的能力?

  • 如果定义了新的权限,请三思是否真的需要它们。可能已经有一个现成的权限可以涵盖你的使用场景。

组件级别

这里列出了一些你需要在策略中考虑的组件级别的问题。这些问题是你在每个组件上都应该问自己的问题:

  • 你是否定义了所有组件?

  • 如果你的应用程序使用了第三方库,你是否定义了你将使用的所有组件?

  • 第三方库是否期望你的应用程序有特定的设置?

  • 你是否希望这个组件对其他应用程序可见?

  • 你需要添加一些意图过滤器吗?

  • 如果组件不应该可见,你是否添加了意图过滤器?请记住,一旦你添加了意图过滤器,你的组件就会变得可见。

  • 其他组件是否需要一些特殊权限来触发这个组件?

  • 核对权限名称的拼写。

  • 您的应用程序是否需要某些功能,如相机、蓝牙和短信?

概述

在本章中,我们学习了如何定义应用程序的策略文件。清单文件是应用程序最重要的工件,应当非常谨慎地定义。这个清单文件声明了应用程序请求的权限以及外部应用程序访问其组件所需的权限。通过策略文件,我们还定义了 APK 输出的存储位置以及应用程序将运行的最低 SDK 版本。策略文件公开了那些对应用程序不敏感的组件。在本章的末尾,我们讨论了开发者在编写清单文件时应该注意的一些示例问题。

本章节结束了本书第一部分的内容,我们学习了关于 Android 应用程序结构的知识。让我们继续学习本书的下一部分,重点关注用户数据的的安全存储。

第五章:尊重你的用户

既然我们已经清楚地理解了 Android 平台和应用程序安全框架及组件,让我们深入到安全最具挑战性的方面:数据保护。正如我之前所述,作为应用程序开发人员,你的信誉取决于你处理用户数据的安全程度。因此,本章的名称是:尊重你的用户

本章构成了理解保护用户数据的重要性和意义的基础。本章从讨论评估数据安全性的基准和 CIA 三原则开始。接下来,我们以我们的书店应用程序为例,通过资产、威胁和攻击场景进行分析。我们讨论移动生态系统以及生态系统的不同组件如何影响用户数据的安全性。最后,我们将回顾 Android 的数字版权管理DRM)框架。

数据安全原则

本节讨论数据安全的三个原则,即保密性、完整性和可用性,通常称为CIA 三原则。存储在设备或服务器上的任何数据都应满足这三个属性以确保安全。理解这些基准可以帮助我们评估我们的数据存储解决方案的安全性。这三个原则通常以 CIA 三合体的形式表达。

数据安全原则

保密性

保密性是安全的第一支柱,它关注数据的隐私。这一原则确保私人数据远离窥探的目光,只对具有适当访问权限的用户可用。例如,Android 应用程序的私人数据应只对该应用程序的组件或其他具有适当权限的组件(如果数据受到权限保护)可访问。Linux 操作系统的沙盒和权限强制执行此保密性。在另一种情况下,可能存在包含敏感数据的加密文件在 SD 卡上。即使设备或 SD 卡遭到破坏,这些信息也不会泄露。这种保密性是通过密码学强制执行的。另一个保密性的例子是设备在一段时间不活动后自动锁定,需要用户凭据才能解锁。请注意,Linux 内核默认不支持文件系统加密,因此在存储之前对敏感数据进行加密对于安全至关重要。

完整性

数据完整性确保数据在传输过程中或静态存储时不会被故意或意外地更改或修改。例如,不恰当地写入数据库表可能会导致意外的完整性问题。因此,除非你对自己的技术非常了解,否则建议使用内置的同步方法来强制执行数据完整性。故意破坏数据完整性的一个例子可能是在应用程序与服务器通信的传输过程中发生。中间人可以监听数据并在其传输过程中进行修改。为了减轻这种欺诈行为,建议在通信时对数据进行加密,并使用安全套接字层SSL)协议。为了额外的安全,可以使用校验和。SSL 还需要证书颁发机构(CA)的证书验证链,这在 Android 应用程序中很少使用。

可用性

数据可用性确保在需要时数据能够获得。我想补充一点,即数据应当在有适当权限的用户需要时可用。这非常重要,因为应用程序不应让未授权用户访问敏感信息,以可用性为名。

识别资产、威胁和攻击

没有绝对的安全。当我们谈论数据安全时,我们需要确定我们正在保护什么以及保护的对象是谁。以下三个问题可以帮助我们规划方法:

  1. 我们试图保护什么? 从 Android 应用程序的角度来看,我们是试图保护用户的用户名和密码,还是用户可能通过你的应用程序进行购买时输入的优惠码和信用卡号码,或者是用户使用你的应用程序购买的保护版权的歌曲或图片?通过回答这个问题,我们可以确定我们的资产。

  2. 我们试图保护资产免受谁的侵害? 换句话说,我们的威胁是什么?我们是否试图保护用户数据不受系统上其他应用程序的侵害,还是试图保护这些信息不受你开发的其他应用程序的侵害?即使设备被盗,我们也想要保护我们的资产吗?

  3. 什么是攻击? 回答这个问题有助于识别我们应用程序中的漏洞。我们要站在黑客的角度思考如何利用应用程序中的漏洞。

回答前面的三个问题将有助于我们确定资产的价值以及我们愿意在保护这些资产上投入的时间和精力。让我们尝试用一个示例应用程序来回答前面的问题。回到我们的书店应用程序,用户可以浏览目录中的书籍,将书籍添加到愿望清单中,并订购书籍以便寄送给用户。我们的应用程序会记住用户的基本信息,例如用户最后浏览的作者和类别以及语言和用户名,这样当用户登录时,应用程序会提供建议,让用户感到宾至如归。我们的应用程序还向用户提供商店的信用卡号、邮寄地址和姓名,以便用户准备好支付书籍费用时可以轻松结账。

让我们尝试回答第一个问题:我们试图保护什么?在前面示例中,我们的资产是:

  • 姓名

  • 信用卡号码

  • 邮寄地址

  • 最后搜索的作者

  • 最后搜索的语言

  • 最后搜索的类别

  • 用户名

  • 密码

  • 书籍愿望清单

下图说明了我们示例中不同的敏感数据工件:

识别资产、威胁和攻击

请注意,并非所有这些资产都需要同等程度地保护。存储机制应根据信息的敏感程度来决定。例如,信用卡号码和密码(如果存储在设备上)需要被强烈保护。您可以加密此类信息,并存储信息的哈希值,而不是以原始形式存储此信息。您将在传输过程中加密信息,并使用 SSL 协议进行安全通信。用户偏好(如语言、作者和类别)的丢失不会带来重大风险。即使这些信息丢失,用户也可以重新设置。

前面的分析还引发了关于 PII 的厚客户端和薄客户端的争论。厚客户端在设备本身上存储大量信息。因此,应用程序最终会在设备上存储 PII。薄客户端依赖后端服务器进行所有繁重的工作。它们在设备上存储最少的信息。这是一个好的方法,因为设备可能会丢失或被盗,然后用户数据的风险就会受到威胁。

接下来,我们要确定攻击场景。以下是一些示例场景的讨论。

假设用户安装了一个恶意应用程序。现在这个应用程序试图以不同的方式窃取用户信息。在第一种情况下,它试图访问不同的数据库表并提取用户信息。这是窃取私人信息的情况。如果数据库表受到权限保护,我们处于安全的位置。如果内容提供者检查组件的身份,我们将会处于更安全的情况。

识别资产、威胁和攻击

在另一种情况下,恶意应用程序可能会发送带有不良数据的广播消息,接收应用程序可能会尝试对此采取行动,或者恶意应用程序可能会尝试启动其他应用程序的组件,并带有格式不良的数据,可能导致其他应用程序崩溃。因此,在采取行动之前,检查调用应用程序的身份并审查输入数据是非常重要的。

从这种攻击场景中我们可以得到的重要教训如下:

  • 除非绝对必要,否则不要暴露组件。保持组件私有是我们的第一道防线。

  • 如果我们暴露了一个组件,我们会确保我们用权限来保护它。这是决定是否希望将其暴露给整个系统还是仅暴露给由您创建的其他应用程序的好地方。如果用例是在同一作者编写的应用程序之间共享组件,我们可以定义自定义权限。

  • 通过指定一些意图过滤器来减少攻击面。

  • 在采取行动之前,请务必检查输入数据。如果数据不是所需的格式或形式,应该有一个优雅退出当前情况的计划。在这种情况下,向用户显示错误消息可以作为一个选项。识别资产、威胁和攻击

其他场景可能包括一个恶意应用程序监听通过流氓 Wi-Fi 连接的设备之间的数据交换。这个应用程序可以拦截信息,修改它,假装成用户正在连接的服务器,或者完全阻止数据流。所有这些场景都是安全风险。

识别资产、威胁和攻击

另一个例子是,当恶意应用程序更改设备上存储的数据时。用户甚至可能没有意识到这些信息已经被更改。假设我们的应用程序在不同语言环境中本地化,并且用户设置了首选语言。在以下场景中,用户的首选语言从英语更改为例如日语。下次用户登录时,应用程序会以日语打开。在我们的案例中,安全风险并不大,这对用户来说是个烦恼,但这个例子证明了信息修改是另一种安全风险。

识别资产、威胁和攻击

最后,我们需要考虑在发生安全漏洞时如何获取损失和我们的行动计划。如果信用卡信息、密码和社会安全号码等私人信息被窃取,这将是一个严重的安全风险。在发生安全漏洞时通知用户的计划必须仔细考虑。如果用户的偏好和愿望清单被不适当地访问,可能会引起用户的不满,但可能不会造成太大的隐私风险。

什么数据应该存储以及存储在哪里

之前的分析使我们意识到应用程序开发人员必须考虑的两个重要决策。

首先,应用程序开发者必须决定他/她想要从用户那里收集哪些信息。正如有最小权限原则一样,也有最少存储原则。最少存储原则导致风险和责任的最小化。应用程序开发者应始终尝试减轻个人识别信息PII)的存储负担。在我们之前的例子中,应用程序可能不想存储信用卡详情、账单地址以及与支付相关的其他信息。支付是一个棘手的领域,而像 PayPal 这样的公司可以帮助用户处理结账过程。此外,任何处理信用卡号码的应用程序都建议遵循支付卡行业PCI)标准。该标准列出了此类应用程序和服务器必须遵守的要求。我的建议是将此类操作交给最擅长这些事情的服务。

第二个需要深思的重要决定是用户数据存储在哪里。在当今分布式的数据存储环境中,开发者有许多存储选项,例如设备上、服务器上、云上或第三方应用程序。移动设备不应被视为安全的存储位置,部分原因是它可能容易被盗或丢失,而且大多数设备没有像台式机和笔记本电脑那样的高级安全机制,如安全元件和双启动。密码、加密密钥、大内容文件、个人识别信息(PII)和其他敏感数据应存储在后端服务器上。同样,重要的是要为这些服务器设置防火墙。

我们将在第七章《保护应用数据》中回到这个例子,根据前面的分析,我们将决定适当的存储选项和保护机制。

端到端的安全

大约十年前,我们把音乐存储在磁带和磁盘上;我们的照片放在相册里,我们只把电话用于紧急情况。快进到今天;我们的生活越来越多地走向数字化。我们的朋友、家人、喜好、不喜欢、照片、联系人列表,甚至我们的购买历史和信用卡号码都在数字化。想象一下用户丢失手机的场景。除了设备的价值和存储在其中的内容的情感价值,最大的风险是存储在设备上的用户个人信息的安全。这些信息可能包括可以识别个人的 PII,如姓名、社会安全号码、出生日期和母亲的婚前姓名。它还可能包括对密码、联系人列表和短信数据的访问。即使设备所有者拥有设备并且设备因恶意软件而受到威胁,这种风险仍然存在。

移动生态系统

如下图所示,移动生态系统中有不同的构件,如设备、网络、用户在设备上安装的应用程序、OEM(原始设备制造商)以及消费者的设备与之交互的其他服务。

移动生态系统

让我们更深入地了解这些组件。

  • 消费者:整个生态系统都围绕着消费者以及消费者如何与生态系统的不同部分互动。

  • 设备制造商:也称为 OEM,这些公司生产设备的硬件。像 HTC、摩托罗拉、三星和 LG 这样的公司都设计和制造 Android 设备。除了设备的大小和样式,每个设备制造商都会加入他们自己的系统芯片SOC)、设备驱动和固件,这些都会影响应用程序在不同设备上的工作方式。如果你在不同的设备上测试过你的应用程序,你可以轻易地注意到这些差异。硬件层面的任何安全缺陷都会影响使用该硬件的所有设备。硬件缺陷也很难修补。

  • 操作系统供应商:Android 是一个开源操作系统,制造商可以自由修改它或使用自己的软件。例如,设备制造商可能会决定使用不同的 WebKit 引擎、音乐播放器或屏幕,而不是 Android 堆栈中捆绑的那个。这将导致应用程序在不同设备上的行为和外观有所不同。这些专有软件包中的安全缺陷可能导致你的应用程序受到威胁。运行特定版本操作系统的所有设备都会受到缺陷的影响。软件层面的缺陷通常可以修补,建议用户始终保持软件更新。

  • 运营商:AT&T、Sprint、Verizon、Orange 和 Vodafone 等运营商提供了使移动设备真正移动的基础设施。他们为我们的设备提供数据和语音计划。他们还与设备制造商(在大多数情况下也是操作系统供应商)合作,将他们的定制应用程序捆绑在系统映像中。他们也可能与 OEM 合作,调整安全规则以满足他们的需求。例如,他们可能要求 OEM 直接加载和安装应用程序,而无需征得用户的同意或显示权限请求。

  • 服务:这些是设备交互的服务,例如用于备份的云服务。在大多数情况下,用户会安装一个与后端交互的客户端。其他服务可能包括像 PayPal 这样的支付服务,像 Gmail 这样的邮件服务,以及像 Facebook 和 Twitter 这样的社交网络服务。这些服务大多数以第三方应用程序的形式提供给用户。

  • 应用开发者:这是指个人应用开发者或小型开发者团队,他们将应用发布到如 Google Play 和亚马逊应用商店等应用商店。这类应用包括实用程序、游戏、内容消费应用等。本书的大部分读者属于这一类。

  • 基础设施:这些技术和协议是移动基础设施的支柱。这包括CDMA码分多址)、GSM全球移动通信系统)、WiMAX微波全球互操作性接入)、WAP无线应用协议)以及如 NFC、RFID 和蓝牙等近场通信技术。这些技术中的安全漏洞可能导致我们的应用易受攻击。

  • 标准和安全:在我们撰写这本书时,这两个移动生态系统的重要组成部分仍在不断发展中。

你可能已经注意到,移动生态系统中有许多参与者,从而增加了风险和威胁表面。此外,移动领域的一些主要玩家并不总是合作,有时甚至相互对抗,导致攻击模型复杂化。同时,制造商为特定市场生产设备。因此,这是一个复杂且不断变化的局面。从端到端的角度来看待安全问题,不难意识到应用开发者唯一能控制的也就是他们创建的应用。设备或操作系统中的任何其他缺陷也可能导致安全漏洞。例如,操作系统中的一个缺陷可能导致权限提升,让应用以 root 权限运行。在这种情况下,这个 root 应用可以访问设备上的所有信息。所有应用都会受到威胁,但如果开发者采用了良好的安全标准,他们的责任将最小化。

提示

应用开发者唯一能掌控的就是他们自己的应用。任何恶意用户都可以利用设备硬件、操作系统或运营商应用中的弱点,获取用户数据。

例如,我们的书店应用与数据库通信,向服务器发送信息并缓存一些数据。所有这些情况都需要得到保护。如果设备使用某种近场通信技术,如近场通信NFC)、蓝牙或射频识别RFID)来交换数据,了解这些技术相关的安全风险和新的附加场景是非常重要的。

第六章,你的工具 – 加密 APIs,讨论了可以用来保护传输中数据的加密算法。

数据的三个状态

让我们看看典型移动应用程序中的信息流。再次考虑书店应用程序。在我们的书店应用程序中,用户可以浏览目录中的书籍,将书籍添加到愿望清单中,并订购书籍以便邮寄给用户。我们的应用程序会记住用户的基本信息,例如用户最后浏览的作者和类别、用户的语言和用户名。用户的信用卡号、邮寄地址和姓名也会被存储以方便结账。

下图展示了一个可能的情况。书店应用程序在 Android 设备上使用 SQLite 数据库和平面文件作为缓存。应用程序在外部服务器上存储账户详情、书籍目录和愿望清单,并通过 Wi-Fi 连接到后端服务器。

数据的三个状态

在任何给定时刻,数据可以处于以下三种状态之一:在某个位置静止、从一个节点传输到另一个节点中、或在处理过程中。我们将这三种数据状态称为静止数据、传输中的数据和使用中的数据。让我们更详细地了解这三种状态:

  1. 静止数据:这是存储在某些存储介质上的数据,如 SD 卡、设备内存、后端服务器和数据库。此数据处于非活动状态。在上一个示例中,位于平面文件、SQLite 数据库表和后端服务器上的数据都被认为是静止数据。

  2. 使用中的数据:当前正在处理的数据称为使用中的数据。这类数据的例子包括从数据库表中访问的数据、通过意图发送到应用程序组件的数据,以及当前正在写入或从中读取的文件。

  3. 传输中的数据:当数据正在从一个节点传输到另一个节点时,称为传输中的数据。响应查询,将数据从数据库传输到应用程序是传输中数据的例子。数据的三个状态

在处理数据和考虑端到端安全时,保护三种状态下的数据都是重要的。

数字版权管理

数字版权管理DRM)是针对数字内容如音乐、电子书、应用程序、视频和电影等的访问控制技术。访问控制基于与内容相关联的权利对象。这个权利对象包含限制内容的使用、分发和复制的规则。开放移动联盟OMA)开发了如 OMA DRM v1 和 OMA DRM v2 等 DRM 方案,但许多设备制造商也有自己的专有 DRM 方案。

DRM 系统包含以下组件:

  • 内容服务器:这是设备从中获取媒体内容的服务器。

  • 权利服务器:设备从中获取权利对象的的服务器。权利对象通常是一个带有与内容相关联的权限和约束的 XML 文件。

  • DRM 代理:该代理内置于设备中,是将内容和权限相关联并执行内容权限管理的可信实体。

  • 存储设备:这是存储内容和权利对象的设备。它可以是手机或平板电脑,或者是外部存储,如 SD 卡,甚至是云存储。数字版权管理

你可以在www.openmobilealliance.org阅读关于 OMA DRM 的完整规范。OMA DRM 1.0 支持诸如内容锁定(内容不能转发到另一设备)、联合交付(内容和权利对象一起交付)以及分离交付(内容和权利对象从不同的服务器分别拉取)等模型。OMA DRM v2.0 的安全性基于 PKI,安全性显著提高。制造商可以选择支持其设备上的 DRM 方案。他们还可以根据需要实现或修改 DRM 方案。

安卓从 API 11 开始支持 DRM。安卓对 DRM 的支持是开放的,这样制造商可以选择自己的 DRM 代理。这是通过在两个架构层实现 DRM 框架来实现的。Android 开发者网站(developer.android.com)以下图解地展示了这一点:

数字版权管理

DRM 管理器实现了 DRM 框架,对那些将他们选择的 DRM 代理与此框架集成作为插件的设备制造商来说很有兴趣。框架层抽象了 DRM 管理器的所有复杂性,并向开发者展示了一组统一的 API 以供使用。这些 API 在 Dalvik VM 中运行,与应用程序的其他代码一起。

所有 DRM API 都在android.drm包中。这个包有类和接口来获取权利信息,关联内容与权利,查询 DRM 插件和 MIME 类型。DrmManager类为每个DrmManagerClient提供一个唯一的 ID 以便操作。

应用程序首先需要找出设备上可用的 DRM 插件。这可以通过使用DrmManagerClient类来完成。

DrmManagerClient mDrmManagerClient = new DrmManagerClient(getContext());
String[] plugins = mDrmManagerClient.getAvailableDrmEngines();

下一步是向 DRM 服务器注册并下载权利对象。

DrmManagerClient mDrmManagerClient = new DrmManagerClient(context);
DrmInfoRequest infoRequest = new DrmInfoRequest(DrmInfoRequest.TYPE_RIGHTS_ACQUISITION_INFO, MIME);
mDrmManagerClient.acquireDrmInfo(infoRequest);

第三步是从权利对象中提取许可信息。这是通过使用DrmManagergetConstraints方法完成的。

ContentValues constraintsValues = mDrmManager.getConstraints(String path, int action);
ContentValues constraintsValues = mDrmManager.getConstraints(Uri uri, int action);

现在,我们需要将内容与权利对象关联。这是通过在DrmManagersaveRights方法中指定内容路径和权利路径来完成的。一旦完成这种关联,DRM 代理将继续执行内容的权限管理,无需用户干预。

int status = mDrmManager.saveRights(DrmRights  drmRights, String rightsPath, String  contentPath);

android.drm 包还提供了一些其他实用功能。查看此包以了解所有可用的功能(developer.android.com/reference/android/drm/package-summary.html)。

本章总结

本章节介绍了数据安全的基础知识。我们讨论了数据安全的三个核心原则,即保密性、完整性和可用性。我们通过一个示例应用场景进行了演练,并尝试绘制我们的资产、威胁和攻击场景。我们试图评估安全漏洞相关的成本。我们的数据存储选项以及我们计划在保护数据上投入的时间、精力和资金将取决于这一分析。我们还反思了整个移动生态系统,以及在移动环境中端到端安全的意义。不难意识到,我们仅控制我们所编写的应用程序。我们以回顾 Android 的 DRM 框架和可用功能结束了这一章节。凭借所有这些关于数据安全的知识,让我们迈向下一章,学习应用程序开发人员可以使用的不同工具来保护用户数据。

第六章:你的工具 - 加密 API

为了尊重用户隐私,处理敏感数据的应用程序需要保护这些数据不被窥探。尽管 Android 栈提供了分层的安全架构,安全功能内置于操作系统本身,但在设备上获取 root 访问权限相对容易,从而危及存储在设备上的数据。因此,应用程序开发者了解他们可以用来安全存储数据的工具是很重要的。同样,他们了解如何正确传输数据也是至关重要的。

Android 栈为开发者提供了工具,用于执行诸如加密和解密、散列、生成随机数以及消息认证码等任务。这些工具是栈中各个软件包提供的加密 API。javax.crypto 软件包提供了加密和解密消息、生成消息认证码和密钥协商 API 的能力。java.util.Random 类提供了生成随机数的工具,而 java.security 软件包提供了散列、密钥生成和证书管理的 API。

在本章中,我们将讨论 Android 栈提供的加密 API,应用程序开发者可以利用这些 API 来保护敏感信息。我们首先介绍密码学中使用的基本术语,然后是如何确定可用的安全提供者。接下来,我们将讨论随机数生成,随后是散列函数。接着会讨论非对称和对称密钥密码学以及不同的加密模式,然后是消息认证码。

术语

让我们先了解一些在密码学中使用的术语。随着我们深入本章,这些术语将反复出现,因此在我们继续之前,熟悉它们是很重要的。

  • 密码学:密码学是在不安全环境中以及在对手存在的情况下进行安全通信的研究和实践。随着我们的生活越来越数字化和互联,密码学的重要性日益增加。密码学以算法和协议的形式实践,这些算法和协议是使用数学公式和计算上困难的问题设计的。

  • 明文:也称为纯文本,明文是发送者想要传输的需要保密的消息。如果爱丽丝想要向鲍勃发送一条消息"Hello World",那么"Hello World"就是明文。

  • 密文:也称为编码文本,这是发送给接收者的经过编码或加密的明文消息。让我们继续之前的例子,爱丽丝想要将消息"Hello World"发送给鲍勃。爱丽丝使用了一种替换方法,每个字母被下一个字母替换,形成密文。因此,明文"Hello World"现在变成了"Ifmmp Xpsme"。"Ifmmp Xpsme"就是传送给鲍勃的密文。

  • 加密:加密是将明文转换为密文的过程,这样窃听者无法在传输或存储过程中解读信息,只有知道密码的双方才能理解。在上述示例中,将"Hello World"转换为"Ifmmp Xpsme"的过程称为加密。术语

  • 解密:解密是加密的逆过程。它是在接收端将密文转换回明文以获取信息的过程。因此,将"Ifmmp Xpsme"转换回"Hello World"的过程称为解密。术语

  • 关键:在密码学术语中,密钥是决定加密算法输出的关键信息或数学参数。在上述示例中,将"Hello World"转换为"Ifmmp Xpsme"时,关键信息是给每个字母加一,这就是密钥。在解密过程中,关键信息是从每个字母减一,这就是解密的密钥。

  • 密码:密码是执行消息加密和解密的加密算法。它也被称为加密算法。在上述示例中,密码是一种将"Hello World"加密为"Ifmmp Xpsme"的算法,然后在接收端将"Ifmmp Xpsme"转换回"Hello World"。

安全提供者

就安全提供者而言,Android 堆栈是可定制的。这意味着设备制造商可以添加他们自己的加密提供者。作为应用程序开发者,您也可以自由使用自己的安全提供者。由于 Android 堆栈仅提供 Bouncy Castle 安全提供者的一些功能,因此 Spongy Castle 非常受欢迎。此外,不同版本的 Android 堆栈通过移除不安全的加密算法并添加新的算法,不断更新其加密功能。您可能想要检查在特定时间点提供者及其支持算法的完整列表。同时,确保在不同的设备上测试您的应用程序,以确认加密算法按预期工作。

下面的代码片段展示了如何使用java.security.Providers方法获取加密提供者列表:

for (Provider provider: Security.getProviders()) {
    System.out.println(provider.getName());
}

安全提供者

现在为了获取每个提供者的详细信息,让我们增强函数以记录更多细节,如下所示:

for (Provider provider: Security.getProviders()) {
    System.out.println(provider.getName());
    for (String key: provider.stringPropertyNames()) {
      System.out.println("\t" + key +
        "\t" + provider.getProperty(key));
    }
}

下面的屏幕截图显示了有关一些安全提供者的详细信息:

安全提供者

始终使用知名、行业标准的加密算法。编写加密例程听起来很有趣也很简单,但实际上比看上去要困难得多。我们将在下一节学习的行业标准算法是由加密专家开发并经过彻底测试的。如果发现这些算法有任何弱点,那么这些信息会被公开,开发者可以用更强大的加密算法更新他们的代码。

随机数生成

生成随机数是密码学中最重要的任务之一。随机数作为其他加密功能的种子,如加密和生成消息认证码。模拟真正的随机数生成是很困难的,因为它们来自自然界不可预测的行为。计算机系统生成伪随机数,这意味着它们并不是真正的随机,但看起来是随机的。

计算生成的随机数有两种方法:伪随机数生成器PRNG)和真随机数生成器TRNG)。PRNG 是基于某些数学公式算法生成的。TRNG 基于系统特性,如CPU中央处理单元)周期、时钟、噪声和按键操作等。都柏林三一学院的 Mads Haahr 博士运行着www.random.org,这是任何对随机性感兴趣的人的一个非常有趣的站点。请查看!

随机数的应用场景包括游戏应用,比如用户掷骰子的应用、赌博应用、随机播放歌曲的音乐应用,以及作为加密操作(如哈希、加密或密钥生成)的种子等。并非所有的应用场景都需要强烈的随机性。比如,随机播放曲目的音乐播放器不需要像密钥生成算法那样强烈的随机性。

安卓提供了使用java.util包中的java.util.Random类生成随机数的能力。这个类提供了生成一个或多个随机双精度浮点数、字节、浮点数、整数或长整数的数组的方法。这个类是线程安全的。

下面的代码片段展示了如何在 1 到 100 的范围内生成一个随机数的例子。

int min = 1;
int max = 100;

public int getRandom(int min, int max) {
  Random random = new Random();
  int num = random.nextInt(max - min + 1) + min;
  return num;
}

也可以使用种子生成随机数。然而,由于安卓堆栈有一个伪随机数生成器,它使用一个相当不可预测的初始状态作为种子,实际上种子使得随机数更容易被预测。

哈希函数

哈希函数是处理任意长度数据以产生固定长度输出的算法。对于相同的输入,输出总是相同的,对于不同的输入值,输出总是不同的。这些函数是单向的,这意味着对数据的反向操作是不可能的。

在数学术语中,单向哈希函数可以定义如下:

给定一个消息M,和一个单向哈希函数H,很容易计算出x使得H(M) = x。但是给定xH,要得到消息M是不可行的。这可以数学上如下表示:

H(M) = x

H(x) ≠ M

哈希函数的另一个特性是低碰撞概率。这意味着给定一个消息M,很难找到另一个消息M,使得:

H(M)H(M')

单向哈希函数可用于各种应用。它们用于为可变长度字符串创建固定大小的输出。使用哈希,可以安全地存储给定哈希的值;无法检索原始消息。例如,在表中存储密码的哈希,而不是密码本身。由于给定的消息哈希值始终相同,输入正确的密码将导致生成相同的哈希值。它们用作校验和,以确保消息在传输过程中未被更改。

目前广泛使用的最流行的哈希函数是MD5消息摘要算法)和SHA安全哈希算法)系列的哈希函数。所有这些哈希函数在强度和碰撞概率上都有所不同,你应该选择最适合你应用的一个。通常,使用 SHA-256 是一个不错的选择。许多应用程序仍然使用 MD5 和 SHA-1,但现在这些被认为足够安全。对于需要非常高级别安全的应用程序,应考虑使用更强大的哈希函数,如 SHA-3。以下表格总结了一些常见哈希函数的输出长度:

哈希算法 块长度(位) 输出长度(位)
MD5 512 128
SHA-1 512 160
SHA-256 512 256
SHA-512 1024 512

下面的维基百科图片展示了输入的微小变化如何完全改变输出。这个案例中的哈希函数是 SHA-1:

哈希函数

java.security包中的java.security.MessageDigest类提供了哈希功能。以下代码片段展示了如何使用这个类对字符串s创建一个 SHA-256 哈希。update方法使用字节更新摘要,而digest方法创建最终的摘要。

final MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.update(s.getBytes());
byte messageDigest[] = digest.digest();

公钥密码学

公钥密码学是一种使用两个密钥的密码系统:一个用于加密,另一个用于解密。其中一个密钥是公开的,另一个是私有的。

公钥密码学最常见的用途是针对两个用例。一个是保密性,另一个是认证。在保密性情况下,发送者使用接收者的公钥加密消息并发送。由于接收者持有私钥,接收者使用私钥来解密消息。

在作为数字签名的认证情况下,发送者使用他们的私钥加密消息(在大多数使用场景中,加密的是消息的哈希值而不是整个消息),并将其公开。任何拥有公钥的人都可以访问它,并确信消息来自发送者。

下面的截图展示了两个使用场景:

公钥密码学

在以下章节中,我们讨论了两种常见的公钥密码算法:用于加密和认证的 RSA,以及用于密钥交换的 Diffie-Hellman。

RSA

以其发明者 Ron Rivest、Adi Shamir 和 Leonard Adleman 的名字命名,RSA 是基于公钥密码学的一种算法。RSA 的安全性基于分解两个大素数。算法本身不是秘密,公钥也不是。只有素数是秘密的。

根据所需的强度,使用的 RSA 密钥长度可以是 512、1024、2048 或 4096 位。目前 2048 位的密钥被认为是强的。RSA 非常慢,因此应避免用它来加密大量数据集。需要注意的是,可以用 RSA 加密的消息长度不能超过模数(两个素数的乘积的长度)。由于 RSA 本质上很慢,通常的做法是使用对称密钥加密明文,然后再用 RSA 加密密钥。

RSA 可以用于保密和认证的数字签名。在使用 RSA 时有三种主要操作,如下所述:

密钥生成

实现 RSA 的第一步是生成密钥。在 Android 中,可以通过使用java.security.KeyPairGenerator类来完成。以下代码片段展示了如何生成一个 2048 位的密钥对:

KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair key = keyGen.generateKeyPair();

如果密钥已经以原始形式存在,并且需要从中提取私钥和公钥,那么可以使用java.security.KeyFactory类从密钥规格中提取公钥和私钥,如下所示:

KeyFactory keyFactory = KeyFactory.getInstance("RSA");
keyFactory.generatePublic(keySpecs);

加密

根据使用场景,加密和解密可以通过私钥或公钥来执行。以下代码片段使用接收者的公钥加密数据。这个示例紧接着前面使用java.security.KeyPairGenerator类生成密钥对的方法。以下示例使用java.security.Cipher类来初始化密码并执行操作:

private String rsaEncrypt (String plainText) {
    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    PublicKey publicKey = key.getPublic();
    cipher.init(Cipher.ENCRYPT_MODE, publicKey);
    byte [] cipherBytes = cipher.doFinal(plainText.getBytes());
    String cipherText = new String(cipherBytes,
        "UTF8").toString();
    return cipherText;
}

解密

解密是加密的相反操作。以下代码展示了如何使用私钥来解密数据。接着前面的示例,这是一个发送者使用接收者的公钥加密消息,然后接收者使用他们的私钥进行解密的案例。

private String rsaDecrypt (String cipherText) {
    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    PrivateKey privateKey = key.getPrivate();
    cipher.init(Cipher.DECRYPT_MODE, privateKey);
    byte [] plainBytes = cipher.doFinal(cipherText.getBytes());
    String plainText = new String(plainBytes, "UTF8").toString();
    return plainText;
}

填充(Padding)

在前面的示例中,您会注意到加密算法是用PKCS1Padding进行初始化的。让我们来更多地了解一下填充。RSA 算法没有随机成分。这意味着使用相同的密钥加密相同的明文将产生相同的密文。这种特性可能导致针对加密系统的选定明文攻击。在加密明文之前,通常会用随机数据填充。由 RSA 实验室发布的PKCS#1公钥密码学标准)用于在明文中嵌入结构化随机数据。后来证明,即使是 PKCS#1 填充也不足以避免适应性选定明文攻击。这是一种选定密文攻击,在这种攻击中,随后的密文是根据第一组解密密文的结果来选择的。为了减轻这类攻击,建议使用 PKCS#1 v1.5。另一种可以使用的填充是OAEP光不对称加密填充)。

在示例中,您还会注意到参数中的CBCCipher Block Chaining,密文块链)。这种模式将在本章的块密码模式部分进行讨论。

Diffie-Hellman 算法

由 Whitefield Diffie 和 Martin Hellman 于 1976 年发布,Diffie-Hellman 是最受欢迎的密钥交换算法。这个算法的巧妙之处在于,双方可以在不安全的通道上独立生成一个秘密密钥,而无需交换秘密密钥。然后可以使用这个秘密密钥进行对称加密。

Diffie-Hellman 算法没有验证双方的身份。因此,它容易受到中间人攻击,在这种攻击中,窃听者坐在中间,冒充另一方与双方通信。维基百科下面的插图完美地解释了 Diffie-Hellman 的概念,使用了两方:Alice 和 Bob:

Diffie-Hellman 算法

下面的代码示例显示了生成密钥对的示例实现。使用java.security.KeyPairGenerator类基于 DH 参数生成密钥对。接下来,使用javax.crypto类生成密钥协商:

// DH params
BigInteger g = new BigInteger("0123456789", 16);
BigInteger p = new BigInteger("0123456789", 16);
DHParameterSpec dhParams = new DHParameterSpec(p, g);

// Generate Key pair
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DH");
keyGen.initialize(dhParams, new SecureRandom());

// Generate individual keys
KeyAgreement aKeyAgree = KeyAgreement.getInstance("DH");
KeyPair aPair = keyGen.generateKeyPair();
aKeyAgree.init(aPair.getPrivate());

KeyAgreement bKeyAgree = KeyAgreement.getInstance("DH");
KeyPair bPair = keyGen.generateKeyPair();
bKeyAgree.init(bPair.getPrivate());

// Do the final phase of key agreement using other party's 
  public key
aKeyAgree.doPhase(bPair.getPublic(), true);
bKeyAgree.doPhase(aPair.getPublic(), true);

对称密钥加密

对称密钥加密基于一个秘密密钥,双方都相同。加密和解密都使用相同的密钥。与公钥密码学相比,这是一个问题,因为需要通过某种方式安全地交换秘密密钥。如果窃听者获得了密钥,系统的安全性就被破坏了。

对称密钥加密

对称密钥比公钥快得多,在加密/解密大量数据时非常理想。对称密钥算法的安全性基于密钥的长度。

流密码

流密码是一种对称密钥加密类型,其中每个位或字节的数据是使用称为密钥流的随机位序列单独进行加密的。通常,每个位或字节与密钥流进行异或Exclusive OR)操作。密钥流的长度与数据的长度相同。流密码的安全性取决于密钥流的随机性。如果使用相同的密钥流对多个数据集进行加密,那么算法的漏洞可能会被发现并利用。下图展示了流密码的工作情况:

流密码

流密码的最佳应用场景是数据长度可变的情况,如在 Wi-Fi 或加密语音数据中。它们在硬件中的实现也相对容易。使用流密码技术的一些算法示例包括 RC4、A5/1、A5/2 和 Helix。

由于密钥与需要加密的数据长度相同,流密码在密钥管理上存在严重问题。

块密码

在块密码的情况下,数据块是逐个使用密钥进行加密的。明文被划分为固定长度的块,每个块单独进行加密。下图展示了块密码的基本思想。每个明文被划分为固定大小的数据块。如果块不能均匀划分,它们会使用一组标准的位进行填充,以达到期望的长度。然后每个块使用一个密钥进行加密,并生成固定长度的加密块。

块密码

块密码的一个问题是,如果相同的数据块被重复,输出总是相同的。另一个问题是,如果数据块在传输过程中丢失,没有办法识别出数据块已经丢失。已经设计出各种块密码模式来解决前面提到的问题。块密码在加密算法中得到了广泛的应用,例如 AES、DES、RC5 和 Blowfish。

由于明文被划分为块,通常最后一个块将没有足够的位来填满块。在这种情况下,最后一个块会填充额外的位以达到所需的长度。这个过程被称为填充。

块密码模式

在块密码模式下,明文被划分为多个块,每个块都使用相同的密钥进行加密。在下面一节中,将讨论实现块加密的一些技术。这些模式既用于对称加密,也用于非对称加密,如 RSA。然而,在实际应用中,很少使用非对称密码对大量数据进行加密,因为它们通常速度非常慢。

电子密码本(ECB)

在 ECB 模式下,明文被划分为块,每个块独立地使用密钥进行加密。这种模式可以很容易地进行并行处理,因此速度很快。这种模式不隐藏明文模式。因此,相同的块将产生相同的密文。任何攻击者都可以修改或窃取明文,而发送者却一无所知。

下图展示了在 ECB 模式下如何实现加密和解密:

电子密码本(ECB)

下面的代码演示了如何使用 ECB 模式初始化 RSA 密码:

Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");

同样,为了使用 ECB 初始化 AES 对称算法,可以使用以下代码:

Cipher cipher = Cipher.getInstance("AES/ECB");

密文块链(CBC)

在 CBC 模式下,每个明文块与之前的密文块进行异或操作,然后进行加密。这种模式解决了与 ECB 模式相关的两个缺点。将块与之前的明文块进行异或操作可以隐藏明文中的任何模式。此外,如果除了第一个和最后一个块之外的任何块被删除或更改,接收者可以轻松检测到。

下图说明了使用 CBC 模式对明文块进行加密和解密。注意使用初始化向量IV)为第一个块添加随机性。IV 是一组随机的位,与第一个块进行异或操作:

密文块链(CBC)

下面的代码演示了如何使用 CBC 模式初始化 RSA 密码:

Cipher cipher = Cipher.getInstance("RSA/CBC/PKCS1Padding");

同样,为了使用 CBC 初始化 AES 对称算法,可以使用以下代码:

Cipher cipher = Cipher.getInstance("AES/CBC");

密文反馈链(CFB)

在 CFB 模式下,先对前一个密文块进行加密,然后与明文进行异或操作以生成密文。这种模式同样隐藏了明文模式,并使一个明文块依赖于前一个块。这使得在传输过程中可以跟踪和验证块的一致性。同样,注意第一个块使用了初始化向量(IV)。

密文反馈链(CFB)

下面的代码演示了如何使用 CFB 模式初始化 RSA 密码:

Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");

同样,为了使用 CFB 初始化 AES 对称算法,可以使用以下代码:

Cipher cipher = Cipher.getInstance("AES/CFB");

输出反馈模式(OFB)

OFB 模式与 CFB 模式相似,不同之处在于异或的密文充当同步流密码,这样一位的错误只会影响一位,而不是整个块。同样,使用 IV 来启动该过程,如下所示:

输出反馈模式(OFB)

下面的代码演示了如何使用 OFB 模式初始化 RSA 密码:

Cipher cipher = Cipher.getInstance("RSA/OFB/PKCS1Padding");

同样,为了使用 OFB 初始化 AES 对称算法,可以使用以下代码:

Cipher cipher = Cipher.getInstance("AES/OFB");

高级加密标准(AES)

AES 是最受欢迎的块对称密码。它比其他常见的块对称密码(如 DES 和 DES3)更安全。此密码将明文划分为固定块大小,为 128 位,密钥可以是 128 位,192 或 256 位密钥。AES 速度快,内存要求低。Android 磁盘加密也使用 AES 128 位加密,主密钥也使用 AES 128 位加密。

以下代码段展示了如何生成 128 位 AES 密钥:

//Generate individual keys
Cipher cipher = Cipher.getInstance("AES");
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
generator.init(128);
Key secretKey = keyGen.generateKey();
byte[] key = skey.getEncoded();

接下来,以下代码展示了如何使用 AES 密钥加密明文:

byte[] plaintext = "plainText".getBytes();
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] cipherText = cipher.doFinal(plainText);

继续前面的示例,要使用 AES 解密,可以使用以下代码:

SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(cipherText);

消息认证码

消息认证码MAC)是附加到消息上以确定其真实性和完整性的标签或校验和。通过拥有一个秘密密钥来提供认证,验证消息的意外或有意更改提供了完整性。下图说明了 MAC 的工作原理:

消息认证码

可以使用不同的方法生成 MAC:使用一次性密钥或一次性秘密密钥、使用哈希函数、使用流密码,或者使用块密码并将最后一个块作为校验和输出。最后一种方法的例子是使用 CBC 模式的 DES。

哈希函数用于创建称为哈希 MACHMAC)的校验和。然后使用对称密钥加密此哈希并将其附加到消息中。这是生成 MAC 最受欢迎的方法。这种 MAC 的一些示例是带有 SHA1 的 AES 128 和带有 SHA1 的 AES 256。

Android 通过使用javax.crypto.Mac类提供了生成 HMAC 的能力。以下代码段展示了如何使用 SHA-1 生成摘要:

String plainText = "This is my test string.";
String key = "This is my test key.";
Mac mac = Mac.getInstance("HmacSHA1");
SecretKeySpec secret = new SecretKeySpec(key.getBytes("UTF-8"),
    mac.getAlgorithm());
mac.init(secret);
byte[] digest = mac.doFinal(plainText.getBytes());
String stringDigest = new String(digest);

总结

在本章中,我们讨论了应用程序开发人员可以使用哪些工具来保护其应用程序和用户数据的隐私。我们讨论了用于种子和加密算法初始化向量的随机数生成。讨论了哈希技术,如 SHA-1 和 MD5,开发人员可以使用这些技术来存储密码。它们还非常适合将大量数据压缩成有限且定义的长度。我们讨论了用于交换密钥的公钥密码学和对称密钥算法,如 AES 加密大量数据。我们还讨论了流密码和块密码以及块密码模式。大多数算法都有已发布的测试向量,并且可以在网上找到。开发人员可以针对这些测试向量测试他们的实现。在以下章节中,我们将使用这些工具和技术来保护数据。现在让我们继续下一章,学习如何为不同类型的数据选择最佳的存储选项。

第七章:保护应用数据

应用程序开发者的信誉取决于他们如何安全地处理用户数据。明智的做法是从不在设备上存储大量用户数据。这不仅会占用内存,而且是一个巨大的安全风险。然而,有些用例需要应用程序共享数据,缓存应用程序偏好设置,并在设备上存储数据。这类数据可能是应用程序私有的,也可能是与其他应用程序共享的。此类数据的例子可能是用户的偏好语言或书籍类别。应用程序保存此类数据是为了提升用户体验。它对应用程序本身有用,并且不与其他应用程序共享。共享数据的例子可能是用户在浏览商店时不断添加到收藏中的书籍愿望清单。此类数据可能会也可能不会与其他应用程序共享。

根据隐私和数据类型,可以采用不同的存储机制。应用程序可以选择使用共享偏好设置、内容提供者、存储在内部或外部内存中的文件,甚至是开发者自己的服务器来存储数据。

本章从最重要的问题开始,即确定应用程序应存储的识别信息以及如何决定数据的存储位置。通常情况下,应收集最少量的信息,并在收集敏感信息前获得用户同意。接下来,我们将讨论 Android 中的存储机制,包括共享偏好设置、设备存储、外部存储以及在后端服务器上存储数据。我们将讨论保护传输中数据的协议。我们将以讨论在外部存储上安装应用程序来结束本章。

数据存储决策

在应用程序的背景下,许多因素会影响数据存储的决定。其中大部分是基于开发者应该了解的数据安全方面,如隐私、数据保留和系统实现细节。以下各节将讨论这些问题。

隐私

当今的应用程序收集和使用关于用户的不同类型的信息。用户偏好、位置、健康记录、金融账户和资产等都是其中的一部分。收集此类信息应当谨慎,并得到用户同意,因为收集私人信息可能引发法律和道德问题,并可能被视为侵犯隐私。即使收集了此类信息,也应妥善加密存储并安全传输。本章后半部分的重点是安全数据存储和传输。

隐私以不同的形式表现。首先,在不同的文化和国家中是不同的。每个国家都建立了关于个人识别信息(PII)的规则和法规。例如,欧盟有一个关于处理和转移个人数据的数据保护指令。更多信息可以在欧洲委员会司法总司维护的此网站上找到:ec.europa.eu/justice/data-protection/index_en.htm。关于此方面的印度网络法律可以在deity.gov.in/content/cyber-laws找到。美国采取的是分部门的数据保护方法。这是立法、监管和自我监管的结合,而不是仅由政府执行。

第二,不同的使用场景有不同的法律。例如,如果一个应用程序与医疗或健康相关,那么其规则与跟踪用户位置或进行金融交易的应用程序是不同的。美国一些具体的法律例子包括美国残疾人法案、1998 年的儿童在线隐私法案和 1986 年的电子通信隐私法案。因此,了解与您的使用案例以及您希望运营的国家相关的规则和法规非常重要。如有疑问,可以寻求在其领域内具有专业知识的公司的服务。例如,与其尝试建立自己的支付系统,不如使用像 PayPal 这样的支付提供商,后者已经进行了多年的支付处理,并且符合此领域如 PCI 等的规则和法规。

第三,从一个国家向另一个国家转移私人信息也受到规则和法规的约束。在大多数情况下,另一个国家应具备足够的保护法律以满足另一国家的保护标准。

《世界人权宣言》第 12 条规定了隐私规则如下:

"任何人不得受到任意干涉其隐私、家庭、住宅或通信,也不得受到对其荣誉和声誉的攻击。每个人都有权受到法律的保护,以免受到此类干涉或攻击。"

PII 的一些例子包括全名、电子邮件地址、邮寄地址、驾驶执照、选民登记号、出生日期、母亲的婚前姓、出生地、信用卡号码、犯罪记录和国民身份证号码。在某些情况下,年龄、性别、工作职位和种族可能被视为 PII。有时,隐私可能意味着匿名。

如果您的应用程序正在收集 PII,您将不得不向用户披露,并可能需要获得他们的同意。您可以向他们展示使用应用程序或使用可能需要您的应用程序收集有关用户的敏感信息的特定功能的条款和条件。

数据保留

数据保留是指在一定时间内存储数据。这种数据用于追踪和识别如人员、设备和位置等信息。例如,银行数据通常保存七年。在大多数使用场景中,数据保留不应成为问题,除非是针对特定使用场景的组织,如邮政、银行、政府、电信、公共卫生和安全。在大多数情况下,必须为访问此类个人识别信息(PII)定义适当的访问权限。同样,不同国家和不同使用场景的数据保留规则是不同的。

实施决策

在处理数据并决定最安全的安全机制时,第一个问题是确定数据将存储在哪里。让我们回到我们的书店示例。正如我们在第三章,权限中所确定的那样,我们示例中的数据元素是:

  • 姓名

  • 信用卡号码

  • 邮寄地址

  • 最后搜索的作者

  • 最后搜索的语言

  • 最后搜索的分类

  • 用户名

  • 密码

  • 书籍愿望清单

根据它们的隐私需求对上述资产进行进一步分析,我们确定了 PII 为姓名、信用卡号码、邮寄地址和密码。请注意,这种分类也会根据国家而改变。

接下来是持久性的问题。我们希望数据在应用程序的一个实例中可用,还是在多个实例中?我们希望数据在重置后仍然存在吗?在我们的示例中,我们希望所有资产都能持久化。然而,如果用户偏好(如作者、分类和语言)在重置后不保留,我们并没有丢失有价值的信息,用户可以再次选择它们。

第三项重要的任务是识别哪些数据对应用程序是私有的,哪些数据是共享的。数据的可见性将影响我们选择的存储选项。

第四个问题是数据的大小。大文件最好存储在外部存储上。下图展示了典型安卓手机设备中可用的内存选项:

实施决策

始终建议使用框架提供的存储机制,而不是发明一个新的。在以下各节中,我将讨论 Android 框架为不同存储需求提供的存储机制。

用户偏好

应用程序以两种方式收集用户偏好设置。在第一种情况下,应用程序向用户展示设置屏幕,让用户选择如语言、每页显示的结果数量等偏好设置。这类偏好设置最好使用Preference类进行存储。另一种情况是在用户浏览应用程序时自动获取用户偏好设置。例如,在搜索书籍时,用户选择了特定作者的书籍。应用程序可能希望保存此类偏好设置,以便用户下次登录时使用。这类用户偏好设置最好使用SharedPreferences进行存储。需要注意的是,Preference类在底层也调用了SharedPreferences。请记住,SharedPreferences只能持久化基本数据类型。

共享偏好设置

SharedPreferences类用于以键值对的形式存储基本数据类型。这些基本类型包括intlongBooleanfloatstring setstring。存储在SharedPreferences中的数据在应用程序会话中持久存在。偏好文件以 XML 文件的形式存储在设备上应用程序的data目录中。因此,该文件受到与应用程序相同的 Linux 权限的沙盒保护。即使应用程序被杀死,偏好文件中的数据仍然存在,只有在应用程序被卸载或使用Preference类的方法删除特定值时,文件才会被销毁。

对于任何类型的数据存储,有三个操作:实例化存储、存储数据和检索数据。

创建偏好设置文件

下面的代码片段使用默认文件名实例化SharedPreferences

SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);

在这种情况下,文件名可以通过以下代码获取:

String preferencesName = this.getPreferenceManager().getSharedPreferencesName();

你也可以指定偏好设置文件的名称。在以下示例中,偏好设置文件的名称为MyPref

public Static final String PREF_FILE = "MyPref";
SharedPreferences preferences = getSharedPreferences(PREF_FILE, MODE_PRIVATE);

上面的代码片段引发了一个关于偏好文件可见性和共享的重要讨论。默认情况下,所有偏好文件都是创建它的应用程序私有的。因此,它们的模式是MODE_PRIVATE。如果需要在不同应用程序之间共享偏好文件,可以将其设置为MODE_WORLD_WRITABLEMODE_WORLD_READABLE,分别允许其他应用程序写入和读取偏好文件。

写入偏好设置

下一步是将基本数据存储到偏好文件中。下面的代码片段紧接着前面的代码片段,展示了如何将数据添加到偏好文件中。你会注意到,需要使用SharedPreferences.Editor类来存储值。Editor类中的所有值都是批处理的,需要提交才能使值持久化。在以下示例中,MyString是字符串的键,其值为Hello World!

SharedPreferences.Editor editor = preferences.edit();
editor.putString("MyString", "Hello World!");
editor.commit();

读取偏好设置

下一步是读取偏好文件中的键值对。下面的代码片段紧接着前面的代码片段,展示了如何从偏好文件中读取数据:

String myString = preferences.getString("MyString", "");

提示

SharedPreferences可以被应用程序的所有组件访问。如果设置为MODE_WORLD_WRITABLEMODE_WORLD_READABLE,其他应用程序可以写入和读取偏好设置文件。

要读取另一个应用程序的偏好设置文件,第一步是获取指向另一个应用程序上下文的指针,然后读取该值。

Context myContext = getApplicationContext().createPackageContext("com.android.example", Context.MODE_WORLD_READABLE);
SharedPreferences preference =
myContext.getSharedPreferences("MyPref",Context.MODE_WORLD_READABLE);
String mMyString = preference.getString("MyString", "");

偏好设置 Activity

从 Honeycomb 开始,Android 扩展了Preference类的功能,以从 UI 收集设置。这些值被设置为 XML 文件,而 Activity 从中加载。在幕后,Preference类使用SharedPreferences类来存储键值对。这些设置是应用程序私有的,只有Activity类可以访问。

要选择铃声,需要在res/xml目录下的Preference.xml文件中设置以下代码:

<RingtonePreference
  android:name="Ringtone Preference"
  android:summary="Select a Ringtone"
  android:title="Ringtones"
  android:key="ringtonePref" />

要从该 XML 文件中加载一个 Activity,可以在onCreate()方法中使用以下代码:

public class Preferences extends PreferenceActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    addPreferencesFromResource(R.xml.preferences);
. . . .
}

记得在清单文件中添加这个 Activity。

文件

应用程序也可以使用 Android 的文件系统来存储和检索数据。java.io包提供了这一功能。这个包提供了从文件中读写不同数据类型的类。默认情况下,应用程序创建的文件是私有的,其他应用程序无法访问。文件在重启和应用程序崩溃后仍然存在;只有在应用程序被卸载时才会被删除。

创建文件

下面的代码片段展示了如何创建一个文件。正如我之前所说,默认情况下,所有文件都是应用程序私有的。

FileOutputStream fOut = openFileOutput("MyFile.txt", MODE_WORLD_READABLE);

文件MyFile.txt将被创建在/data/data/<application-path>/files/目录中。前面提到的文件是作为MODE_WORLD_READABLE创建的,这意味着其他应用程序可以读取这个文件。其他选项分别是MODE_WORLD_READABLEMODE_PRIVATEMODE_APPEND,它们允许其他应用程序写入文件、保持文件对应用程序私有或向文件追加内容。决定适当的可见性很重要。在安全性方面,永远只给出所需的最小可见性。

由于MODE_WORLD_READABLEMODE_WORLD_WRITABLE是非常危险的选择,从 API 级别 17 开始,这些选项已被弃用。如果仍需要与同一证书关联的应用程序之间共享文件,可以使用android:sharedUserId选项。如果这些是不同的应用程序,则可以使用封装类来处理文件访问,并提供读写功能。可以通过权限来保护对此封装类的访问。

写入文件

下一步是写入文件。以下代码段展示了如何使用OutputStreamWriter类将字符串写入文件。在java.io包中有许多选项可用于将不同类型的数据写入文件。请检查该包以选择适合您用例的正确选项。

String myString = new String ("Hello World!");
FileOutputStream fOut = context.openFileOutput("MyFile.txt", MODE_PRIVATE);
OutputStreamWriter osw = new OutputStreamWriter(fOut);
osw.write(myString);
osw.flush();
osw.close();

从文件中读取

如前所述,请检查java.io包以找到从文件中读取数据的最佳方法。以下代码段展示了如何从文件中读取字符串。

下面的示例一次从文件中读取一行:

FileInputStream fIn = context.openFileInput("MyFile.txt");
InputStreamReader isr = new InputStreamReader(fIn);
BufferedReader bReader = new BufferedReader(isr);
StringBuffer stringBuf = new StringBuffer();
String in;
while ((in = bReader.readLine()) != null) {
  stringBuf.append(in);
  stringBuf.append("\n");
}
bReader.close();
String myString = stringBuf.toString();

外部存储上的文件操作

文件也可以创建在外部存储上。如果 API 级别为 8 或更高,Android 提供了一个特殊的函数getExternalFilesDir(),用于获取外部存储上的应用程序目录。

File file = new File (getExternalFilesDir(null), "MyFile.txt");

如您在前面的代码段中注意到的,getExternalFilesDir()方法接受一个参数。此参数用于根据媒体类型识别适当的存储目录。例如,要存储图片,使用ENVIRONMENT.DIRECTORY_PICTURES,要存储音乐文件,使用ENVIRONMENT.DIRECTORY_MUSIC。如果这样的目录不存在,它将被创建,然后文件将存储在那里。值null是应用程序的根目录。

File file = new File(
  getExternalFilesDir(ENVIRONMENT.DIRECTORY_PICTURES),
  "MyFile.jpg");

对于 API 级别小于 8 的情况,用户可以使用getExternalStorageDirectory()来获取外部存储的根目录。然后可以在/Android/data/<application-path>/files/目录中创建文件。

要在外部存储上创建文件,应用程序应具有WRITE_EXTERNAL_STORAGE权限。创建在外部存储上的文件将在用户卸载应用程序时被移除。

外部存储缺少内部存储的安全机制。最好假设存储在外部存储上的任何数据都是不安全的,并且可以被全局读取。如果外部存储未挂载,文件将无法访问,必须采用适当的错误处理机制,以便应用程序优雅地失败。

在某些情况下,可能实际上需要外部存储,特别是如果文件不包含个人识别信息(PII),并且旨在跨不同设备共享和可用。媒体扫描器在搜索相关内容时会扫描这些目录。这些目录如下列出。这些目录遵循应用程序的根目录/data/data/<application-path>/

  • 音频(音乐)文件Music/

  • 播客文件Podcasts/

  • 视频文件(除摄像机外)Movie/

  • 铃声Ringtones/

  • 图片Pictures/

  • 杂项下载Downloads/

  • 通知声音Notifications/

  • 闹钟Alarms/

缓存

如果一个应用程序需要缓存数据,那么使用 Android 栈提供的缓存存储机制是明智的。Android 将缓存文件与应用程序一起存储在文件系统中,这样它们就会被创建它们的应用程序沙盒化。所有缓存文件都创建在/data/data/<application-path>/cache/目录中。当系统内存不足时,这些缓存文件会被首先删除。定期修剪这些文件是必要的,因为它们可能会变得很大并占用磁盘空间。

下面的代码片段首先将字符串写入缓存文件,然后从缓存文件中读取相同的字符串。您会注意到,读取和写入与任何文件输入/输出都相同,只是使用getCacheDir()获取文件的位置来写入字符串。

//Write to the cache file
String myString = new String ("Hello World!");
File file = new File (getCacheDir(), "MyCacheFile");
FileOutputStream fOut = new FileOutputStream(file);
OutputStreamWriter osw = new OutputStreamWriter(fOut);
osw.write(myString);
osw.flush();
osw.close();

// Now read from the cache file
File file = new File (getCacheDir(), "MyCacheFile");

FileInputStream fIn = new FileInputStream (file);
InputStreamReader isr = new InputStreamReader(fIn);
BufferedReader bReader = new BufferedReader(isr);
StringBuffer stringBuf = new StringBuffer();
String in;
while ((in = bReader.readLine()) != null) {
  stringBuf.append(in);
  stringBuf.append("\n");
}
bReader.close();
String myString = stringBuf.toString();

与在外部存储上创建文件一样,也可以创建缓存文件。根据 API 级别,方法会有所不同。从 API 级别 8 开始,Android 提供了一个特殊的函数getExternalCacheDir(),用于获取外部存储上的缓存目录。

File file = new File (getExternalCacheDir(), "MyCacheFile");

这个目录与应用程序关联,当应用程序被卸载时,这个目录将不复存在。如果是多用户环境,每个用户都有自己的个人目录。

如果 API 级别小于 8,用户可以使用getExternalStorageDirectory()获取外部存储,然后在/Android/data/<application-path>/cache/目录中创建文件。

要在外部存储上创建缓存,应用程序应具有WRITE_EXTERNAL_STORAGE权限。

在外部存储上创建缓存并非没有安全顾虑。首先,如果外部存储没有挂载缓存文件,那么它是无法访问的,必须为应用程序实施适当的错误处理机制,以便应用程序能够优雅地失败。其次,外部存储本质上是安全的,因此应假定外部存储上的任何内容都是全局可读的。

提示

应定期修剪缓存文件,并移除不需要的文件以保留内存。

数据库

数据库是存储结构化数据的最佳选择。Android 通过android.database.sqlite包支持 SQLite。这个数据库是 Android 栈的一部分,系统管理数据库。对于移动操作系统来说,使用 SQLite 是一个明智的选择,因为它体积小,无需设置或管理,且是免费的!

创建后,数据库文件与应用程序一起被沙盒化,并存储在/data/data/<application-path>/databases/目录中。这个私有数据库将对应用程序的所有组件开放,但不会对外开放。

下面的代码片段展示了如何创建一个位于内部存储上的数据库。这个类将扩展SQLiteOpenHelper类,并使用 SQL(结构化查询语言)的CREATE_TABLE子句。该表存储用户标记为心愿单的书籍列表。我们的表wishlist中有两列,一列是自动递增的 ID,另一列是书名。

你会注意到这里有两个方法,onCreate()onUpgrade()onCreate()将创建一个新的数据库(如果它不存在)以及一个新的数据库表。如果数据库已经存在,则会调用onUpgrade()方法。

public class MySQLiteHelper extends SQLiteOpenHelper {
  public static final String TABLE_NAME = "wishlist";
  public static final String COLUMN_ID = "_id";
  public static final String COLUMN_BOOK = "book";
  private static final String DATABASE_NAME = "bookstore.db";
  private static final int DATABASE_VERSION = 1;
  @Override
  public void onCreate(SQLiteDatabase database) {
    database.execSQL("create table " + TABLE_NAME + "("
      + COLUMN_ID + " integer primary key autoincrement, "
      + COLUMN_BOOK + " text not null);");
  }

  @Override
  public void onUpgrade(SQLiteDatabase database) {
    database.execSQL("drop table if exists " + TABLE_NAME);
    onCreate(db);
  }
. . . .
}

同样,其他数据库查询可以用来添加一行、读取一行和删除一行。任何关于 SQL 的好书都可以帮助你完成这些查询。

也有可能在外部内存中创建一个数据库。创建一个接受目录路径的自定义上下文类可以实现这一点。你还需要拥有对外部存储的写入权限。然而,如果表中有敏感信息,则不建议这样做。

正如我之前所提到的,SQLite 数据库是一个私有数据库,与应用程序一起被沙盒化。如果需要将此数据与其他应用程序共享,则可以通过作为 URI 地址的内容提供者来实现。我们在第二章,应用程序构建块中已经详细介绍了内容提供者。

账户管理器

在存储敏感数据的背景下,存储密码或认证令牌是一个重要的方面。考虑像 Google 邮箱、Twitter 和 Facebook 这样的应用程序,它们允许用户登录。其他应用程序使用像 OAuth2 这样的身份协议所使用的认证令牌。

安卓提供了android.accounts.AccountManager类作为存储用户凭据的中心化存储库。应用程序可以选择使用自己的可插拔认证器来处理账户认证。从存储用户名到身份信息,再到创建你自己的自定义账户管理器,安卓的AccountManager是一个强大的工具。

AccountManager类的功能受到权限保护,因此你的应用程序需要请求android.permission.GET_ACCOUNTS来访问存储在其上的账户列表,以及使用 OAuth2 的android.permission.ACCOUNT_MANAGER

每个账户都采用命名空间格式。例如,Google 账户使用com.google,Twitter 账户使用com.twitter.android.auth.login。以下是如何访问AccountManager

AccountManager am = AccountManager.get(getApplicationContext());

可以使用以下代码获取整个账户列表:

Account[] accounts = am.getAccounts();

auth令牌以Bundle的形式获取,使用名为KEY_AUTHTOKEN的值来检索。

String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);

使用AccountManager时有两大要点需记住。首先,如果你的应用尝试使用 OAuth2 进行认证,你的应用将和服务器进行通信,这可能会导致延迟,因此这些调用应该是异步进行的。其次,凭据以明文形式存储在AccountManager中。所以在已获得根权限的手机上,任何使用adb shell命令的用户都能看到这些凭据。因此,在设备上存储信息时,应避免以明文形式存储密码和个人识别信息(PII),而应通过散列或加密以加密安全的方式存储,这将最小化设备被入侵的风险。

SSL/TLS

我阅读了一项由德国汉诺威莱布尼茨大学和马尔堡菲利普大学的学生进行的研究,非常有趣,关于传输中数据的中间人(MITM)攻击。研究的应用程序使用了 SSL(安全套接层)或 TLS(传输层安全)协议来保护网络上的数据。许多应用程序没有正确使用 SSL/TLS,导致存在漏洞。另一个有趣的观察是,由于 Android 浏览器通常不会显示通常与使用 SSL/TLS 的网站相关的绿色挂锁,用户并不了解他们正在使用一个不安全的网站。查看这篇论文:www2.dcsec.uni-hannover.de/files/android/p50-fahl.pdf。我相信这将是一次有趣的阅读。

前述研究揭示了在应用程序中正确实现协议的重要性。本节介绍了 SSL/TLS 并提供了一些正确实现它的注意事项。SSL 是由 Netscape 开发的,用于在互联网上进行安全通信的协议。该协议遵循客户端和服务器之间的一系列调用,在这些调用中,它们协商用于数据交换的密钥和密码套件。

Android 提供了通过javax.net.sslorg.apache.http.conn.sslandroid.net包集成 SSL/TLS 的能力。以下图示展示了 SSL 的顺序:

SSL/TLS

第一步是设置一个密钥库并导入服务器证书链。接下来是将密钥库链接到DefaultHttpClient,这样它就知道在哪里找到服务器的证书。

在开发阶段,尤其是在企业环境中,我们通过创建自定义TrustManager使 SSL 信任所有证书,并通过SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER允许所有主机名。如果这样的应用程序被发布,它将存在严重的安全缺陷。在发布你的应用程序之前,请检查这一点。因此,在发布应用程序之前,请记得修复这些问题。

在外部存储上安装应用程序

正如在第四章 定义应用程序的策略文件 中所讨论的,从 API 级别 8 开始,应用程序可以选择安装在 SD 卡上。一旦 APK 移动到外部存储,应用程序所占用的唯一内存就是存储在内部内存中的应用程序私有数据。需要注意的是,即使是 SD 卡上的 APK,DEX(Dalvik 可执行)文件、私有数据目录和本地共享库仍然保留在内部存储上。

在清单文件中添加一个可选属性可以启用此功能。对于此类应用程序的 应用程序信息 屏幕上,要么有 移动到 SD 卡 按钮,要么有 移动到手机 按钮,具体取决于 APK 的当前存储位置。然后用户可以选择相应地移动 APK 文件。如果外部设备被卸载或 USB 模式设置为 大容量存储(设备被用作磁盘驱动器),则托管在该外部设备上的所有运行中的活动和服务的进程会被立即结束。

下面的屏幕截图显示了应用程序设置中的 移动到 SD 卡 选项:

在外部存储上安装应用程序

现在,每个应用程序的 ApplicationInfo 对象都有一个名为 FLAG_EXTERNAL_STORAGE 的新标志。对于存储在外部设备上的应用程序,此标志的值为 true。如果这样的应用程序被卸载,该应用程序的内部存储也会被移除。如果外部设备不可用(例如,当 SD 卡被卸载时),内部存储不会被清除。在这种情况下,用户可以通过卸载应用程序来清除内部存储。执行此操作不需要挂载 SD 卡。

同时也添加了两个新的广播。

  • ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE:当 SD 卡被卸载时,会发送此意图。它包含一个禁用应用程序的列表(使用 EXTRA_CHANGED_PACKAGE_LIST 属性)和一个不可用应用程序 UID 的列表(使用 EXTRA_CHANGED_UID_LIST 属性)。

  • ACTION_EXTERNAL_APPLICATIONS_AVAILABLE:当 SD 卡再次可用时,会发送此意图。它包含一个禁用应用程序的列表(使用 EXTRA_CHANGED_PACKAGE_LIST 属性)和一个不可用应用程序 UID 的列表(使用 EXTRA_CHANGED_UID_LIST 属性)。

当一个应用程序从内部存储移动到外部位置时,会触发 ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE。然后,资源和资产会被复制到新位置。应用程序启用后,会再次触发 ACTION_EXTERNAL_APPLICATIONS_AVAILABLE 广播意图。

提示

任何类型的外部设备本质上都是不安全的。例如,由于电源故障(在手机中就是电池耗尽)或不正确的移除方式(没有正确卸载),SD 卡可能会遭受内存损坏。SD 卡在全球范围内可读,因此应用程序可以被读取、写入、复制或删除。

为了在外部设备上安全存储 APK,安卓应用程序被存储在一个加密容器(ASEC 文件)中,以防止其他应用程序或程序修改或损坏它们。ASEC 文件是一个加密的文件系统,其密钥是随机生成的并由设备存储,因此只能由最初安装它的设备进行解密。因此,安装在 SD 卡上的应用程序只适用于一个设备。

挂载 SD 卡(使用 Linux 回环机制)时,这些容器与内部存储上的应用程序以相同的方式挂载。文件系统强制执行权限,使得其他应用程序不能修改其内容,除了系统本身,没有其他人可以通过 ASEC 文件修改任何内容,因为其他应用程序没有相应的密钥。此外,SD 卡以noexec方式挂载,因此没有人可以在那里放置可执行代码。

一个设备可以关联多个 SD 卡,以便轻松更换 SD 卡。只要 SD 卡被挂载,就没有性能问题。

安卓开发者网站(developer.android.com)列出了在 SD 卡上安装应用程序可能导致应用程序表现不稳定的使用场景,尤其是当 SD 卡被卸载时。其中一些,如服务,是基于服务在手机启动时可用顺序的。以下是列出的部分情况:

  • 服务:运行中的服务将被杀死。应用程序可以注册ACTION_EXTERNAL_APPLICATIONS_AVAILABLE广播意图,当安装在扩展存储上的应用程序对系统可用时,该意图会通知你的应用程序。一旦收到该意图,服务就可以重新启动。

  • 闹钟服务:使用AlarmManager注册的闹钟将被取消,并在外部存储重新挂载时手动重新注册。

  • 输入法引擎(IME):IME 是一个控件,允许用户输入文本。如果你的 IME 位于外部存储上,它将被默认的 IME 替换。当外部存储重新挂载时,用户将需要打开系统设置以重新启用自定义 IME。

  • 动态壁纸:如果设置的动态壁纸存储在外部存储上,那么默认的动态壁纸将替换正在运行的动态壁纸。当外部存储重新挂载时,用户将需要重新选择他们自定义的动态壁纸。

  • 应用小部件:如果你的应用小部件存储在外部存储上,它将被从主屏幕移除。在大多数情况下,需要系统重置才能让应用小部件再次出现在主屏幕上。

  • 账户管理器:如果使用AccountManager创建了任何账户,它们将在外部存储重新挂载之前消失。

  • 同步适配器AbstractThreadedSyncAdapter及其所有同步功能将无法工作。需要重新挂载外部存储才能使同步功能再次工作。

  • 设备管理员:这一部分非常重要,因为DeviceAdminReceiver及其所有管理功能将被禁用,即使重新挂载 SD 卡,这些功能可能也无法完全正常工作。

  • 广播接收器:任何监听ACTION_BOOT_COMPLETE广播的广播接收器将停止工作,因为系统在外部存储挂载到设备之前发送此广播。因此,在外部存储上安装的任何应用程序都无法接收此广播。

概述

本章介绍了 Android 上可用的存储机制。我们从理解隐私和数据保留等术语开始。在收集可识别个人信息之前,我们应当始终考虑这些问题,以避免法律和道德问题。需要注意的是,关于隐私和数据安全的规则和法规根据国家和使用情况的不同而有所不同。我们探讨了使用共享偏好存储用户偏好,以及在文件、缓存和数据库上存储、读取和写入数据的方法。我们还讨论了在使用 SSL/TLS 以及在外部存储上安装应用程序时需要考虑的一些重要事项。

接下来的三章将介绍非常有趣的话题,包括设备管理、以安全为重点的测试以及 Android 上的新出现的使用案例。请继续阅读!

第八章:企业中的安卓

随着移动设备的普及,越来越多的员工将设备带到工作中,并要求企业数据能够在他们的个人或企业移动设备上访问。这带来了极大的便利,但同时也带来了日益增加的挑战。由于设备被破坏或丢失而造成的企业数据丢失成本非常高。

随着移动设备在工作场所的普及,IT 部门面临着许多挑战。第一个挑战是各种各样的移动设备,它们有着不同的外形和功能。第二个挑战是让员工接受并在特定应用程序和设备部分上接受企业控制。第三个挑战是持续支持设备管理。

本章节专注于安卓设备的管理。如果你不是为企业开发应用程序,可以安全地跳过这一章,直接进入下一章关于安卓应用程序的安全测试。

本章从设备管理的基础和安卓生态系统的独特挑战开始。其次,我们讨论设置和实施设备管理策略以及为安卓设置接收器的机制。我们还讨论了存储在设备上和传输过程中的数据的安全性。我们以提出为安卓设置设备管理的下一步建议以及设备管理员应了解的政策和合规性指南来结束本章。

基础知识

在企业中设备的背景下,BYOD、MDM 和 MAM 这三个术语经常被使用。我们将在本章的剩余部分继续使用它们,让我们了解它们各自的含义。

第一个术语是自带设备BYOD)。这个术语指的是员工将自己的移动设备带到工作场所,并在个人设备上访问企业数据和应用程序的最新趋势。例如,在个人移动设备上访问电子邮件和办公文档。

第二个经常使用的术语是移动设备管理MDM)。MDM 指的是对企业拥有或员工拥有的移动设备进行远程管理,这些设备访问企业应用程序和数据。例如远程擦除企业数据并要求用户设置密码等功能,这些都是 MDM 的例子。这些功能强化了企业对系统功能的控制。

在此背景下经常使用的第三个术语是移动应用管理MAM)。这个术语指的是管理访问企业数据的移动设备上的软件和服务。MAM 的例子包括应用程序升级、捕获崩溃日志和用户统计信息并将其发送给 IT 部门。MAM 与 MDM 不同,因为后者专注于设备功能,而 MAM 专注于设备上安装的软件和服务。

理解安卓生态系统

Android 是一个具有众多定制版本的挑战性生态系统。下图展示了在撰写本书时 Android 版本的使用情况。如您所注意,在任何给定时间,都有不同版本的 Android 堆栈在使用中。理解每个版本细微差别和特殊需求本身就是一份全职工作。您始终可以在developer.android.com/about/dashboards/index.html查看最新的使用统计数据。

加上前述问题,每个制造商都有一个定制的 Android 堆栈版本,以及他们选择的功能和能力。在此堆栈之上,运营商也添加了他们的定制内容。这导致了市场的高度碎片化。

了解 Android 生态系统

设备管理功能

从 Android 2.2 开始,Android 增加了越来越多的功能,使 Android 准备好迎接企业环境。后续的每个版本都在改进现有功能并增加更多功能。下表列出了特定企业功能在 Android 堆栈中添加的时间。本节将重点介绍其中一些功能:

Android 发布版 企业功能
Froyo(发布版 2.2)
  • 密码策略

  • 远程擦除

  • 远程锁定

|

Gingerbread(发布版 2.3)
  • SIP 支持

|

Honeycomb(发布版 3.0)
  • 平板电脑的加密和密码策略

  • 平板电脑的系统加密

|

Ice cream sandwich(发布版 4.0)
  • 扩展系统加密,对设备实施加密和密码策略

  • 证书管理功能

  • VPN

  • SSL VPN 的开发者接口

  • 人脸识别解锁

  • 网络数据使用监控

  • 离线电子邮件搜索

|

设备管理 API

如前表所示,从 Android 2.2 开始,Android 一直在增加对设备管理的支持。在这方面最大的步骤是在 Android 2.2 中引入了设备管理 API,以支持对需要企业级系统级控制的设备的控制。

设备管理 API 通过四个步骤进行操作:

  1. 系统管理员编写一个应用程序,以远程管理设备政策。

  2. 用户从 Google Play 或其他应用商店下载应用程序。用户还可以通过电子邮件安装应用程序。

  3. 下载完成后,用户安装应用程序。在安装时,用户会看到将在设备上执行的政策。用户必须同意这些政策才能激活应用程序。

  4. 安装后,用户必须遵守这些政策才能访问敏感信息。用户可以卸载应用程序,这将导致无法访问敏感数据。

下图展示了如果用户安装了一个强制执行密码策略的管理应用程序,要求密码必须包含特定类型的字符,流程会是怎样的:

设备管理 API

设备管理 API 被封装为android.app.admin。这个包有三个类:用于定义和实现策略的DevicePolicyManager,包含设备管理类元数据的DeviceAdminInfo,以及用于实现接收器组件的DeviceAdminReceiver

策略

策略是设备管理的重要组成部分。在撰写本书时,设备管理 API 支持的策略与密码、远程擦除、禁用摄像头、设备加密和锁定设备有关。密码策略的示例包括要求密码包含字母数字字符、密码过期和超时,以及最大密码尝试次数。当前策略的列表可以在android.app.admin.DevicePolicyManager中验证。

策略在res文件夹下的 XML 文件中定义。一个示例策略文件可以限制密码,远程重置设备到出厂设置,禁用摄像头,加密存储,并锁定设备。在安装期间,这些策略会展示给用户。

<device-admin >
    <uses-policies>
        <limit-password />
        <force-lock />
        <wipe-data />
        <expire-password />
        <encrypted-storage />
        <disable-camera />
    </uses-policies>
</device-admin>

新版本中会不断添加额外的策略。你可以检查当前构建版本,并根据情况实施策略。

设备管理应用程序包含DevicePolicyManager,它管理一个或多个设备管理接收器的策略。

DevicePolicyManager mDPMgr = 
  (DevicePolicyManager)getSystemService
    (Context.DEVICE_POLICY_SERVICE);

使用以下代码可以远程从手机中擦除数据。需要注意的是,市场上也有一些假的设备管理应用程序。请确保下载管理员建议的正确管理应用程序。不安全或受到木马影响的程序很容易导致数据泄露:

DevicePolicyManager mDPMgr;
mDPMgr.wipeData(0);

要设置加密文件系统的策略,可以使用以下代码片段:

DevicePolicyManager mDPMgr;
ComponentName mMyDeviceAdmin;
mDPMgr.setStorageEncryption(mMyDeviceAdmin, true);

DeviceAdminReceiver

继承DeviceAdminReceiver类来创建设备管理应用程序。这个类包含了一些回调函数,当发生特定事件时会被触发。这些意图由系统发送。因此,接收器应该能够处理ACTION_DEVICE_ADMIN_ENABLED意图。

DeviceAdminReceiver需要BIND_DEVICE_ADMIN权限。BIND_DEVICE_ADMIN是一个特殊的权限,只有系统可以访问;应用程序无法访问。这确保只有系统与接收器交互。

接收器还引用了我们在上一节中讨论的元数据策略文件。以下代码片段展示了示例声明。

<receiver android:name="MyDeviceAdminReceiver"
        android:label="@string/my_device_admin_receiver"
        android:description="@string/my_device_admin_desc"
        android:permission="android.permission.BIND_DEVICE_ADMIN">
    <meta-data android:name="android.app.my_device_admin"
                android:resource="@xml/my_device_admin" />
    <intent-filter>
      <action   android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
    </intent-filter>
</receiver>

以下屏幕截图展示了企业电子邮件的 Exchange ActiveSync 设置。这只是一个示例来说明流程。在第一张截图中,需要填写 Exchange 的实际账户详情。这些将是企业账户详情。注意选择加密的 SSL 连接:

DeviceAdminReceiver

在下一步中,用户选择应同步到设备的功能。在我们的案例中,用户勾选了 Exchange ActiveSync 提供的所有功能,即邮件联系人日历。这如下面的截图所示:

设备管理接收器

在第三步中,如下截图所示,用户必须确认他们同意在设备上实施的安全策略,如果他们决定安装应用程序并能够访问敏感信息。如果用户拒绝同意,将不会安装应用程序(在我们的案例中,邮件联系人日历将不会同步)。

设备管理接收器

在下一步中,用户将审查通过同步电子邮件实施的政策。这些是在前面示例中定义的策略文件中的政策。在我们的示例中,如下截图所示,设备管理员可以在设备丢失、员工停止为企业工作或任何其他原因的情况下远程擦除员工设备上的所有数据。第二个政策是设备管理员将设置密码规则。这些密码规则可能是以下任何一种:

设备管理接收器

保护设备上的数据

MDM 的主要要求是保护设备上的企业数据。Android 设备通常有两种数据存储形式:内部存储和外部(可移动)存储介质。从 Honeycomb 开始,内部文件系统挂载在/mnt/sdcard,外部存储挂载在/mnt/external#(其中#是外部设备的数量)。早期版本将内部存储挂载在/mnt/sdcard,SD 卡挂载在/mnt/sdcard/external_sd。Android 堆栈的定制版本可能会也可能不会遵循这些指导方针。

Android 通过全盘加密和支持加密算法来解决企业数据保护在设备上的问题。

加密

在 Android 3.0 中增加了支持全盘加密的能力,以防止未经授权访问用户数据。文件系统使用dm_crypt内核特性进行加密,并在块设备层上工作。密钥源自用户密码,使用的加密方式是带有 CBC 和 ESSIV: SHA-256 的 AES-128。主密钥或加密密钥通过使用 Open SSL 和 AES-128 进行加密。

为了使全盘加密工作,设备需要使用密码进行保护(图案密码将不起作用)。在访问文件系统之前,必须使用密码解锁设备。设备管理员可以设置一个政策,限制密码尝试次数,超过该次数设备将重置为出厂设置。

用户必须手动同意加密设备。注意,当设备首次进行加密时,设备应有足够的电量来完成加密过程。如果设备电量耗尽,那么必须将其恢复到出厂设置,所有用户数据将会丢失。

提示

只有设备上的文件系统被加密。外部存储,如 SD 卡,没有被加密。

如在第六章中讨论的,你的工具 - 加密 API,安卓堆栈支持加密和散列等加密算法。在需要将信息存储在 SD 卡上的情况下,可以使用堆栈支持的加密功能。设备管理员可以强制执行政策,要求所有存储在 SD 卡上的数据必须加密。

即使设备完全加密,用户仍然需要注意几个问题。首先是肩窥问题,在拥挤的地方,有人可能会从别人肩膀上偷看密码。人们应该注意这个问题。第二,尽管输入密码解锁手机相当麻烦,但这是为了保护企业数据的安全,建议选择更复杂的密码,而不是倾向于选择简单的密码。设备策略可能会对此提出要求。第三,请注意,只有文件系统的数据分区被加密。虽然很容易在其他地方存储数据,但出于安全考虑,任何企业数据都应该存储在数据分区中。

备份

谷歌为安卓设备提供备份服务。备份内容包括壁纸、设置、词典和浏览器设置等。当手机恢复出厂设置时,这些设置会被恢复。敏感数据,如密码、屏幕锁定 PIN 码、短信和通话记录不会被备份。备份服务只能通过使用BackupManager API 进行访问。用户需要在设置下的隐私选项手动开启备份功能。

谷歌不保证备份的安全性,因为不同的安卓版本实现备份的方式不同。此备份服务可能不支持所有类型的安卓设备。

安全连接

安卓设备原生支持 VPN。管理员可能会建议使用自定义 VPN,并可能要求所有通信都开启 VPN。这尤其在连接开放热点时特别有用。后者功能仅在安卓 4.2 及以上版本中提供。以下截图展示了我手机上支持的一些 VPN 协议:

安全连接

当用户连接 Wi-Fi 时,应选择安全的无线连接。在这种情况下,用户将被提示输入密码密钥。以下截图展示了这一过程:

安全连接

身份

Android 支持证书存储,用于在设备上存储证书,并允许授权应用在诸如电子邮件、Wi-Fi 和VPN虚拟专用网络)的使用场景中使用它进行身份验证。Android 支持 DER 编码的 X.509 证书。它还支持存储为 PKCS#12 密钥库文件的 X.509 证书。

Android 支持 Bouncy Castle,并预装了证书。它们位于cacerts.bks密钥库中。

用户还可以从设备内存中安装证书。在位置与安全设置下,通过选择从 SD 卡安装选项,可以在设备上安装新的证书。用户在安装证书时应注意,安装非合法证书可能会危及设备的安全。

要删除证书,用户可以进入个人 | 安全 | 凭据存储 | 信任的凭据,并禁用或删除证书。

下一步

既然我们已经了解了 Android 支持 BYOD 的能力,本节将讨论如何利用这些知识在企业中推出 Android 支持。

设备特定决策

要充分利用 Android 设备的功能,设备必须与 Google 账户关联。这使用户能够访问 Google Play、位置服务以及其他一系列应用程序,如 Gmail、Drive、日历和 YouTube。设备管理员面临的重要问题是,他们是否希望员工使用个人 Google 账户还是单独的企业账户。

另一个重要的问题是启用位置服务,这可能会对一些高价值员工造成隐私风险,因为他们可能不希望被追踪。另一方面,启用位置服务可以帮助确定设备如果被盗时的位置。

第三个重要问题是备份和存储。与位置服务的情况一样,备份和存储是重要的功能,但可能会引发隐私问题。设备管理员可能会强制使用加密存储或指定一个私有的企业云。但这很快就会增加维护成本。要启用备份,用户必须明确进入设置 | 隐私,并选择备份我的数据,如下面的截图所示:

设备特定决策

在这里,重要的是要解决 root 过的 Android 设备的问题。root 一个 Android 设备并不需要太多时间,而且相关指导随处可见。在澳大利亚、欧洲和美国,root 设备是合法的。root 过的设备不符合企业使用的安全标准。因此,检测 root 过的设备是设备管理员的一个重要考虑因素。检测 root 过的设备并不容易,因为 root 设备的方法有很多种。

然后,还有一个问题是从哪个应用商店下载企业应用程序。除了 Google Play,Android 应用程序还可以从其他应用商店下载,如亚马逊应用商店和 GetJar。上次我查看时,有超过 128 个应用商店,应用程序可以托管在任何一家。应用程序也可以从网站下载,或者通过电子邮件或侧加载获取。设备管理员可以选择建立一个企业应用商店来解决这个问题。这样可以确保这里只有合法的应用程序。为了从除了 Google Play 以外的外部位置下载应用程序,用户必须明确选择以下截图所示的未知来源选项:

针对特定设备的决策

设备管理的核心理念是,设备应该是可用的、直观的,并且在不妨碍安全的前提下保留原生体验。这是一个艰难的平衡,也是一个确定的挑战。

要紧跟 Android 不断进化和充满活力的生态系统,需要一位热爱 Android、对 Android 生态系统的即将到来的变化保持关注的 Android 专家和爱好者。为了保持你的知识是最新的,需要对用户如何与他们的设备互动以及该领域即将到来的创新有敏锐的理解。这位 Android 专家应该成为企业在 Android 设备上部署应用程序的权威和主要联系人。

了解你的社区

在此部署中的第二步是了解你的员工队伍的偏好、需求和需要。这一步很重要,以便做出关于你的员工需要哪些应用程序和服务的明智决定,以及需要创建何种访问控制和政策。收集有关他们的设备偏好(他们喜欢手机还是平板电脑)、他们习惯的应用程序、他们在设备上需要的访问量等信息是重要的。另一个因素是地理多样性。没有一种解决方案适合所有情况。不同的地点有不同的首选设备,他们自己的首选应用程序,以及与设备上的企业数据的互动程度。

定义边界

明确哪些设备是被接受,哪些不被接受,将有助于处理 Android 的碎片化问题。这些边界应该基于功能而不是版本或发布,因为不同的制造商和运营商会在不同的设备上以不同的方式移植相同版本。

另一个需要定义的边界是信任。公司的信息技术 (IT) 部门应根据设备能力的提升允许增加访问权限。例如,如果一个设备不支持全磁盘加密,它们只能读取数据,而不能将数据存储在设备上。鉴于 Android 开放的 应用生态系统,对用户在设备上安装的应用程序进行定期监控也很重要。

第三种边界是用户可以在其设备上安装的应用程序。Android 应用程序可以从不同的来源安装,这些来源在安全性上并没有像苹果 App Store 那样严格筛选应用程序。定义哪些应用程序允许安装,哪些不允许,将大大有助于保持设备的安全。

安卓兼容性计划

开放性是 Android 生态系统的目标。然而,为了在不同设备上提供一致的用户体验,OEM 必须参与 Android 兼容性计划。此计划向 OEM 提供工具和指导,以便他们正确标记设备并确保应用程序按预期在设备上运行。这对于 IT 人员来说是一个有趣的计划,因为他们可以根据兼容性级别定义自己的边界。

兼容性计划提供了三个关键组件:

  • 兼容性定义文档 (CDC): 这是关于兼容性的政策文件。它定义了兼容堆栈的要求。例如,它列出了一系列被认为是 Android 堆栈核心的 Intents,并且应该始终得到支持。

  • 兼容性测试套件 (CTS): CTS 是一个免费的测试套件,可在桌面上运行,用于在模拟器或设备上自动执行兼容性测试。在撰写本书时,CTS 包括单元测试、功能测试和健壮性以及性能测试的参考测试,未来还有计划增加更多测试。一些例子包括检查硬件特性,比如 Wi-Fi 和蓝牙。

  • 兼容性测试套件验证器 (CTS Verifier): CTS 是一个免费的测试套件,在桌面上运行,需要手动输入以在模拟器或设备上运行兼容性测试。它是 CTS 的补充。

根据上述标准,市场上存在三种类型的 Android 设备。以下表格展示了每种兼容性类型的关键特性:

谷歌领先设备 谷歌体验设备 其他(开放)设备

|

  • 纯正的 Android,100%由谷歌引领

  • 无 OEM 或运营商定制

  • 例子:三星 Galaxy Nexus,摩托罗拉 Xoom,HTC Nexus One

|

  • CTS 合规

  • OEM 和运营商定制

  • 应符合谷歌升级承诺

  • 例子:三星 Galaxy S11,HTC Rezound

|

  • 非 CTS 合规

  • 高度由 OEM 和运营商定制

  • 例子:Kindle Fire,摩托罗拉 ET1 平板电脑

|

你可能会决定只支持提供一致特性并有一定定制体验的领先设备。

推出支持工作

规划分阶段的方法来推出对 Android 设备支持。IT 部门可以先进行试点推广,然后逐渐扩大范围。这有助于两个方面:首先,IT 部门可以确定他们的支持基础设施是否能够随着用户数量的增加而扩展;其次,他们可以根据收集到的使用统计数据调整支持。随着支持范围扩大到更多员工,可以修复任何错误和遗漏的需求。

在此推广过程中,通过培训、维基、海报和警报教育员工,将帮助员工了解正在发生的事情。这也帮助他们理解为什么允许某些设备而排除其他设备,他们可以期待什么,以及如何在设备上安全访问企业数据。

政策与合规性

在回顾所有前面的步骤时,不要忽视该领域新兴的标准和合规性。同时,要紧跟 BYOD、MDM 和 MAM 领域的研究进展,以及不同公司采用的创新方法。

金融行业监管局(FINRA)

金融行业监管局(FINRA) 是美国所有证券公司中最大的独立监管机构。FINRA 的使命是通过确保证券行业的公平和诚信运作来保护美国投资者的利益。他们发布了关于监管其会员公司移动设备电子通信的指导方针。这些方针需要与公司自身的分析相结合来考虑。更多信息请访问 FINRA 的网站:www.finra.org。FINRA 发布了三项通知,以应对个人移动设备和社交网站日益增多的情况。在所有情况下,它都建议应对所有员工进行适当的培训,包括维护记录、审慎在社交媒体网站发布内容,以及持续的监管。

FINRA 在 2007 年 12 月发布了他们的第一个监管通知 07-59(www.finra.org/web/groups/industry/@ip/@reg/@notice/documents/notices/p037553.pdf)。该通知提供了通过移动设备监管电子通信的核心指导原则。它建议企业电子邮件应始终通过企业电子邮件系统流转,不应通过个人账户转发。这些企业电子邮件应只通过受监控的网络流转。这将使得电子邮件得到适当的监管。

美国金融业监管局(FINRA)在 2010 年 1 月发布的第二份监管通知 10-06,主要关注社交媒体网站和博客的使用(www.finra.org/web/groups/industry/@ip/@reg/@notice/documents/notices/p120779.pdf)。该通知建议员工不应在社交媒体网络上使用商业账户。这些网站应持续监控以避免员工发布误导性信息,因为这可能会对投资者产生不利影响。

2011 年 8 月发布的第三份监管通知 11-39,进一步扩展了关于个人设备和社会媒体网站指导方针(www.finra.org/web/groups/industry/@ip/@reg/@notice/documents/notices/p124186.pdf)。该通知指出,只要确保信息可检索且与个人通信分离,员工可以使用个人设备进行通信。对设备的持续监督和培训至关重要。

安卓更新联盟

遵守标准并不总是容易的。在 2011 年 5 月的 Google I/O 大会上,谷歌与许多其他设备制造商承诺,将在任何新版本安卓发布后的 18 个月内更新设备。这个联盟被称为安卓更新联盟。这个想法很高尚,受到了好评,但原始设备制造商(OEM)很难跟上这一承诺。

摘要

在本章中,我们重点关注了公司及员工所有访问企业数据的设备的管理。携带个人设备办公(BYOD)面临的问题包括信任、合规性、治理和隐私,因为越来越多的员工要求在他们的移动设备上访问企业数据。用户体验与安全之间需要精妙的平衡。我们从复杂的安卓生态系统开始,接着讨论设备管理的实施细节以及安卓堆栈提供的企业其他功能。我们以关于合规性和政策的讨论结束本章,并考虑下一步如何在企业领域支持安卓。

现在,是时候进入下一章,从安全的角度讨论测试安卓应用程序。祝阅读愉快!

第九章:安全测试

这无疑是本书最重要的章节。作为开发者,我们都在努力编写优美、可用且安全的代码。我们都体验过伟大想法的兴奋和看到它实现的冲动。我们的工作安排和截止日期也常常很疯狂。因此,出现 bug 是自然的,而 bug 测试是任何编码生命周期的一部分。

目前大多数测试用例关注于可用性、功能性和压力测试。在大多数情况下,当涉及到安全测试时,测试工程师往往不知所措。当合规性和安全被忽视时,有时应用程序需要重新设计或实现。以创建消息摘要为例,用于确保完整性。开发者可能会选择 SHA-1,它产生一个 160 位的摘要。在服务器端,数据库被设计为容纳 160 位数据。一个非道德的黑客入侵了应用程序。在进行安全审查时,决定 SHA-1 对于该用例来说不够强大,需要更新到 SHA-256。由于数据库仅被设计为容纳 160 位,因此在客户端快速修复变得具有挑战性,因为整个设计必须改变。事情现在变得严重了。尤其是在考虑到移动生态系统快速发展和变化的情况下,这真是浪费时间。

本章节旨在介绍以安全为重点的测试概念。章节首先概述了测试。如果你已经熟悉测试,可以轻松跳过这一部分。下一节将讨论安全测试以及你可以如何测试应用程序的安全性,即安全审查、手动测试和利用工具的自动化测试。接下来的部分讨论一些可以作为编写测试基线的安全测试用例示例。章节最后讨论了开发者和测试工程师在开发测试用例和进行安全测试时可以使用的一些工具和资源。

测试概述

由于设备能力、外形和版本的不同,Android 是最具挑战性的操作系统之一进行测试。即使是获取基本的功能和用户体验本身就是一个挑战。下图说明了通常在 Android 应用程序开发背景下执行的测试。正如我们这个时代伟大的密码学家 Bruce Schneier 恰当指出的那样,“安全不是一个产品,而是一个过程”,因此你会注意到,我已经将安全测试添加到应用程序测试的整个生命周期中。

测试概述

让我们花点时间了解从 Android 的角度看,单元测试、集成测试和系统测试各自的含义。

  • 单元测试:在大多数情况下,编写模块的开发人员也会开发单元测试。开发人员在将代码交给测试工程师之前,应该编写并单元测试他们的模块。Android SDK 附带了用于单元测试的仪器化 API。这个框架是基于 JUnit 实现的,JUnit 是 Java 单元测试中一个流行的框架。单元测试可以很容易地自动化。这些测试涵盖了边界测试、输入验证测试以及与后端的连接测试。

  • 集成测试:单元测试完成后,不同的组件开始集成时,进行集成测试以确保不同的组件能够协同工作。这些测试是在组件捆绑在一起时执行的。设想有两个团队分别独立工作,一个负责登录模块,另一个负责搜索结果页面。一旦模块开发完成并且它们被集成在一起,就应该执行测试,检查这两个模块的组合。如今,大多数开发环境都使用持续集成来执行一些基本的健全性测试,以确保这两个模块能够一起编译。

  • 系统测试:这些测试针对整个应用程序以及应用程序与 Android 平台的交互进行测试。系统测试的一些示例包括在不同平台上测试搜索功能以及基于 Android 的设备差异如何影响搜索结果的显示。

    安全测试应该在测试的每个阶段执行。例如,在单元测试级别,开发人员应该测试不一致和错误的输入值、缓冲区溢出以及用户的访问级别。

    在集成级别,工程师可以测试两个模块之间安全数据传输以及传递错误数据时的行为。

    在系统测试阶段,工程师可以测试他们的应用程序在不同 Android 平台上的外观和行为。在 Android 的情况下,由于不同厂商和运营商的 Android 设备和堆栈能力存在差异,这一阶段尤为重要。

    流程中提到的任何测试套件通常都包含不同类型的测试混合。这些在以下图中有所说明。请注意,我再次在混合中加入了安全测试。

    测试概览

  • 功能测试:这类测试检查应用程序是否表现出预期行为。例如,登录功能的功 能测试将检验用户输入用户名和密码并按下回车的情况;如果凭据有效,用户将能够登录系统,否则将显示错误。你可能还想验证在不同错误情况下是否生成了正确的错误消息。

  • 本地化测试:如今,大多数应用程序都是全球性的,在不同的国家可用。为了支持不同的地区,应用程序必须进行本地化和国际化。本地化指的是语言翻译,而国际化是指根据特定地区的规范调整应用程序。例如,考虑一个适应接受日语地址的视图的情况(即你想支持日本作为国家)。本地化会将地址行 1、地址行 2、城市、州、邮编和国家翻译成日语对应的内容。然而,在日本,地址系统与罗马系统不同,接受地址的视图将不得不重新设计,一些标签可能需要重新排列。

    安卓有一个非常用户友好的框架,用于存储字符串和本地化视图,开发者应该充分利用这一点。在新的市场推出应用程序时,最好咨询本地化专家的意见。

  • 可用性测试:也称为 UI 测试,这些测试关注用户界面的外观和感觉,确保用户能够轻松输入、在屏幕上阅读信息,以及改变应用程序的美观和总体流程。在屏幕空间受限的设备和不同屏幕尺寸的设备上,可用性非常重要。

  • 硬件兼容性测试:这一系列测试将针对在不同设备上测试应用程序中使用的硬件特性。例如,如果一个应用程序使用了设备相机,就应该进行测试,以检查代码是否在不同设备相机和不同的对焦能力下正常工作。

  • 回归测试:这些通常是自动化测试,在应用程序每次更改后运行,以确保应用程序仍然按预期工作。例如,在书店应用程序中,您可能会确定关键功能,如登录、登出、搜索书籍以及将书籍添加到愿望清单。每当添加新功能或更新现有功能时,都会执行这些健全性测试,以确保没有破坏任何功能。

以下各节将详细讨论安全测试。

如您所料,这些测试案例中的大多数是相互协作的。例如,要测试新国家的地址页面,本地化和 UI 测试必须同时进行。

安全测试基础

本章节概述了安全测试。我们讨论了可以围绕其开发安全测试的安全支柱。第二部分讨论了不同类型的安全测试。

安全原则

任何类型的应用程序安全测试都应遵循安全的六个原则,即认证、授权、可用性、保密性、完整性和不可否认性。我们在第六章,您的工具 – 加密 API中涵盖了这些概念的大部分内容以及如何实现它们。

安全原则

认证是识别用户的措施。你可以使用来自 Facebook、Twitter、LinkedIn 和 PayPal 等公司的认证 API。主要使用的协议是 OAuth 和 OpenID Connect。这些技术将认证任务从应用程序中卸载。这对于应用程序开发者和用户来说都是一个双赢的局面。应用程序开发者不需要实现自己的方案,可以使用内置的认证机制。用户不需要与不信任的应用程序共享个人信息。这对于开发此类方案的公司来说非常有用,因为它可以引导流量到他们的网站。这些技术大多数基于为用户提供一个认证令牌。

第十章,展望未来,讨论了认证方面的一些新进展。

授权是指访问控制——确定用户是否有适当的权限来访问资源。在 Android 的情况下,这可以通过保护应用程序组件的权限并在可能的情况下检查调用者身份来实现。

可用性意味着数据应在需要时对授权用户可用。使用带有数据的广播和意图可以确保这一安全措施。

保密性是指确保数据安全,只向预期的方透露数据。对数据加密、使用适当的权限、并符合 Android 沙盒可以帮助实现这一安全措施。

完整性意味着数据在传输或静态存储过程中未被修改。如果数据被篡改,这种篡改可以被识别出来。添加消息摘要、数字签名以及无论是在传输过程中还是静态存储时对数据进行加密,都可以帮助保持数据的完整性。

不可否认性可以通过使用数字签名、时间戳和证书来实现,确保发送方不能否认发送数据。在第六章,你的工具 - 加密 API中讨论的 DRM(数字版权管理)被实施,以使用户不能否认接收内容。

安全测试类别

在牢记刚刚讨论的安全原则的基础上,安全测试可以分为三个类别:应用程序审查、手动测试和自动化测试。

应用程序审查

安全测试的第一步是应用程序审查过程。这个过程关注于理解应用程序,并识别应用程序所使用的硬件、不同技术和功能。一旦识别出这些特性,审查者就会尝试访问这些功能中的安全漏洞。审查过程识别出的问题包括清单文件中明显的问题、使用破损或弱加密、协议的不安全使用以及在开发过程中可能遗漏的技术和硬件安全问题。它涵盖了合规性和标准,以及是否适当遵守了这些标准。

在清单文件中可以识别的一些安全问题示例包括:清单文件中不必要的权限,应用程序实际上不需要但为了调试目的而添加的权限,未使用权限保护组件,忘记关闭调试模式,以及日志语句等。

合规性基于用例。根据应用程序编写的用例,应用不同的标准。例如,支付和商务用例应用程序可能会关注 PCC-DSS 标准。基于地理位置的应用程序则必须注意隐私问题。

目前有一些专门从事移动应用程序审查过程的安全审计公司。如果你有疑问,应该使用这些公司。

手动测试

如其名称所示,手动安全测试是在开发过程中或由测试工程师手动完成的。工程师通过输入不同的数据来观察应用程序在不同场景下的行为。例如,查看日志以验证没有敏感信息泄露,多次返回上一个活动以观察应用程序的表现,尝试破坏应用程序的认证方案,以及检查用户是否具有适当的访问权限。无法自动化的场景也属于这一类。有一些公司如 uTest(www.utest.com)雇佣手动测试人员可以为你的应用程序执行手动测试。

动态测试

也称为自动化测试,这些测试理想情况下是通过编写测试脚本来执行的。如输入验证、压力测试、模糊测试和边界测试等测试可以很容易地自动化。这些测试大多数可以轻松地成为标准开发/测试周期的一部分,并在添加新功能时作为健全性测试。你可能会决定使用专门从事此领域的安全公司的服务,如 Device Anywhere(www.deviceanywhere.com)。

样本测试案例场景

在本节中,我尝试列举了一些从安全角度出发有趣的样本测试案例。它们没有特定的顺序,你可以在为特定用例识别测试案例时参考它们。

服务器端测试

移动生态系统非常有趣;它年轻且仍在不断发展。应用程序可能希望将数据发送到服务器,但服务器接收到的内容可能大相径庭。这可能是因为通信通道出现问题,黑客窃听并更改传输中的内容,或者是因为客户端的问题。无论原因是什么,仅测试应用程序是不够的,服务器测试对你的应用程序的安全性至关重要。这些测试关注的是服务器端是否接收到了预期的内容,PII 是否以明文形式存储在服务器上,如果业务逻辑驻留在客户端,那么它是否正常工作。我们在第六章,你的工具 - 加密 API中讨论了这一点。

这个测试领域已经成熟,并且有大量的示例和工具可用于服务器端测试。使用像 Nmap 这样的端口扫描工具可以轻松检查开放端口和防火墙。

测试网络

基础设施层是移动设备的支柱,使移动性无处不在。它也带来了新的挑战和测试案例。设备使用不同的协议与服务器通信,每种协议都带来独特的安全漏洞。GSM 容易被破解;Wi-Fi 本质上不安全,特别是当你连接到恶意热点时。长期演进LTE)是一种新的高速无线数据通信标准,基于 IP 但尚未经过彻底测试,而 NFC、蓝牙和 RFID 等邻近技术带来了完全不同的测试范式。因此,测试你的应用程序所使用的技术并围绕它构建测试案例是非常重要的。

保护传输中的数据

如果你的应用程序使用传输层安全TLS),这是好的,但请确保它被正确实施,因此需要测试它。测试客户端和服务器之间的所有通信是否都已加密,并且没有 PII 或密钥在明文中传输。记住,序列化不是加密,混淆也不是加密。确保服务器正在检查证书验证和证书过期。检查所使用的加密算法和协议是否当前、对于你的用例足够安全。

安全存储

将敏感数据如私钥、用户名、密码及其他个人识别信息(PII)存储在客户端始终不是一个好主意。理想情况下,这些信息应当存储在服务器上。将密钥与它加密的数据一起存储违背了安全的目的。如果必须在客户端存储密钥,首先,它们不应当以明文形式存储;其次,它们不应当存储在文件、缓存文件或共享首选项中。密钥应当存储在keystore中,密码在AccountManager中,所有敏感信息应以加密方式存储。在大多数情况下,可以存储哈希值而不是密码。

行动前的验证

验证在不同应用程序组件之间以及来自其他应用程序传递的输入、数据和调用者。任何 Activity 都可以在 Intent 中封装任何类型的数据,而接收组件有责任在采取行动之前进行测试和验证。我们在第二章中详细讨论了这一点,应用程序构建块。在这种情况下,测试将包括向组件传递无效和错误的数据,并观察它的行为。

在某些情况下,你可能可以在处理来自他们的请求之前检查调用者的身份。使用它!特别是在启动敏感操作之前,检查调用者身份和你将要处理的数据。

最小权限原则

这类测试包括检查不同应用程序组件的权限,并确保它们具有正常功能所需的最小权限。这包括检查文件、缓存文件和SharedPrferences的可见性和可访问性权限。检查它们是否真的需要MODE_WORLD_READABLEMODE_WORLD_WRITABLE权限。

检查你的应用程序请求的权限。例如,如果你不需要精确的位置访问权限,只需请求粗略的位置权限,如果你只需要读取短信,就不要请求读取和写入短信的权限。随着消费者对移动领域安全问题的认识不断提高,如果应用程序请求的权限没有意义,他们可能会对你的应用程序产生怀疑。对于浏览图书的应用程序来说,访问用户联系人列表和设备摄像头是没有意义的。

管理责任

了解你所在领域的规则和法规。涉及责任诉讼是一件麻烦的事,最好避免。另外,如果使用专门处理这些问题的第三方工具和服务是有意义的,那么请务必使用它们。如果你的应用程序收集用户数据,请确保你已经从用户那里获得了适当的同意,并且你收集的所有内容都已列出。例如,加州在线隐私保护法案规定,如果一个应用程序在加州收集信息,那么它应该被公开。

让我们以一个处理支付的应用程序为例。不要试图自己开发,而应使用现有的支付解决方案,如 PayPal。支付处理涉及用户的资金,有诸如 PCI-DSS 这样的指导方针来规定如何使用这些功能。

同样,与其设计和开发自家的安全算法和协议,不如使用经过时间和行业测试的安全套件和库。

了解你的应用程序在支持的国家将如何被使用。不同的国家有不同的规则和法规。个人识别信息(PII)的定义也有所不同。

清理

首先,不要记录敏感信息。在将应用程序发布到野外之前,确保关闭调试。从文件、cookies 和缓存中清理所有敏感信息;即,清零内存。

易用性与安全性的权衡

平衡易用性和安全性是一门棘手且微妙的艺术。应用程序可能为了方便而持久保存用户名、密码和会话令牌,但这也会使安全性降低。如果你的应用程序中有记住用户身份的功能,要权衡便利性和安全性。你可能决定限制会话长度,以及限制保持 cookies 和令牌存活的时间。

认证方案

这里的问题是,你是想要验证设备还是用户?设备可能会丢失或被盗。基于设备特性如 IMEI、IMSI 或 UDID 来识别用户可能不是一个很好的验证方案。这些会导致远程擦除和重置。你可能需要评估基于生物特征的验证机制或双因素验证方案来验证用户。

以黑客的思维思考

以黑客的思维去测试,想象他们会如何尝试攻击你的应用程序。利用互联网上已有的工具和漏洞。使用黑客的工具进行测试可以揭示黑客在试图破解你的应用程序时能看到什么,能得到什么信息。有一些工具,比如 Fiddler (www.fiddler2.com),你可以用来监控应用程序的网络流量。重要的是要记住,混淆代码并不是安全。

谨慎集成

无论是与硬件(内部和外部)还是第三方应用程序集成,都要小心谨慎。

如果应用程序使用了某些硬件组件,如摄像头、蓝牙、NFC 芯片、加速度计、麦克风或 GPS,那么测试它们的安全性也很重要。任何硬件的缺陷都可能影响应用程序的整体安全性。

同样,第三方库中的错误可能导致应用程序受到威胁。在与这样的外部库集成时,要求查看他们的测试结果,在线查找并寻求推荐。

安全测试资源

本节重点介绍可以创造性地用于测试应用程序安全的工具、技术和其他资源。

OWASP

OWASP开放网络应用安全项目)是一个致力于移动安全的组织。它们在移动安全领域提供工具和研究。查看它们的网站 www.owasp.org。这是寻找与安全相关问题的好地方,为开源贡献,创新并参与移动安全讨论。OWASP 每年都会整理一份十大安全漏洞列表,并挑战社区去解决这些问题。

安卓工具

Android 提供了一系列可以创造性地用于测试应用程序的工具。除了测试,这些工具还可以帮助开发者调试他们的应用程序。

Android 调试桥

Android 调试桥ADB)可用于日志、内存检查等多种用途。你可以在开发者网站上查看 ADK 提供的完整功能列表。下方的截图展示了一些 ADB 的使用示例:

Android 调试桥

之前的截图展示了使用adb logcat命令的示例日志。

设置设备

为了设置用于监控 Web 应用程序的高级设置,你可以开启高级功能,以便在渗透测试期间获取更多数据。

设置设备

之前的截图显示了如何启用 JavaScript 和插件来检查信息泄露。

SQlite3

使用 SQLite3 工具,用户可以探索它创建的数据库以及一些与平台捆绑的其他数据库。

SQLite 工具允许用户查询数据库并检查数据库中的值。这种数据库的查询和检查可以指出诸如以明文存储 PII 等问题。

Dalvik 调试监控服务

Dalvik 调试监控服务DDMS)是 Android 框架提供的另一个重要工具。DDMS 提供了如端口转发、屏幕截图、线程和堆信息、logcat进程、以及无线电状态信息、来电和短信欺骗、位置数据欺骗等功能。下方的截图展示了 DDMS 的窗口。你可以在 Android 开发者网站上查看这些功能的具体详情。

Dalvik 调试监控服务

还有一些其他第三方工具,如由 iSecPartners 开发的 Intent Sniffer 和 Manifest Explorer(www.isecpartners.com)。其他 Linux 工具如straceprocrank也可以使用。你可以使用下面章节讨论的 BusyBox 来实现这一目的。

BusyBox

BusyBox 被称为嵌入式 Linux 的瑞士军刀,提供了许多 Unix 工具,如viwhoamiwatchdog等。这些工具可以在不 root 手机的情况下用于测试。在 Android 上安装 BusyBox 非常简单。只需从www.busybox.net下载即可。

BusyBox

如前一个截图所示,busybox可以轻松地推送并安装。安装后,可以轻松执行 Linux 命令。

反编译 APK

反编译 APK 并阅读其内容相对简单。进行这项练习可以帮助你理解黑客将如何接近你的 APK。

APK 文件实际上只是一个 ZIP 文件,将 APK 文件重命名为 ZIP 文件后,你可以使用任何 ZIP 文件浏览器打开它。这些文件位于/data/app目录下。你可以使用adb pull命令将其拉到你的机器上。在其中,你可以看到清单文件、资源、资产以及其他文件。

APK 反编译

接下来,使用 Android 提供的dexdump工具,可以转储/data/dalvik-cache目录下的类。

APK 反编译

举个例子,要将data@app@com.example.example1-1.apk@classes.dex转储到一个名为dump的文件中,需要使用的命令是:

dexdump –d –f –h data@app@com.example.example1-1.apk@classes .dex > dump

以下是在转储文件中收集的数据类型的截图:

APK 反编译

这个转储将以跳转语句的形式出现,难以阅读。可以使用如baksmalidedexer这样的 DEX 反编译器,使这些文件更具可读性。

总结

安全测试是一个相对较新的领域。模式和测试策略仍在发展中,安全已成为识别应用弱点、提高应用质量的重要基准。在本章中,我们将之前所有章节的学习成果汇集起来,用它来定义应用程序的测试用例。这只是一个开始,你应该根据你的使用情况,定义你认为合适的测试用例。

我们首先概述了测试的基本原理。然后,我们讨论了围绕六个安全支柱设计的测试用例。我们讨论了一些示例测试用例,这些用例应该为你测试应用程序提供了一个基础。最后,我们讨论了你可以用于以安全为重点的测试资源和工具,以此结束了这一章节。

现在让我们走向本书的最后一章,看看在 Android 领域有哪些新的挑战在考验我们的安全基础知识。

第十章:展望未来

恭喜你阅读到了最后一章!那么让我们在这一章中找些乐趣,尝试预测未来。

移动是一个相对较新的领域。它正处于实验阶段,一些技术和用例取得了成功,而其他技术可能没有得到预期的那么多的关注。本章的重点是研究移动领域内一些新技术和用例。

本章分为多个部分,每部分讨论了在移动领域对某些技术或用例的实验。我们将从移动商务的讨论开始,重点关注产品发现、支付和利用移动设备进行销售点的操作。接下来将讨论近距离技术,如 NFC、RFID 和蓝牙。接下来的部分将谈论移动设备在医疗保健和身份验证方面的应用。在最后一节中,我们将从安全的角度讨论硬件方面的最新进展。

移动商务

消费者行为正在改变商务模式。如今,商务已不再仅仅是到商家或商店选购产品并付款的简单行为。如下所示,随着新技术的出现,移动商务包括使用地理围栏进行产品发现、店内和在线研究、通过自助扫描和自助结账进行支付、与朋友分享购买内容,以及管理账户。我们也看到在线和离线商务之间的界限越来越模糊。

移动商务

在接下来的几节中,我们将从安全的角度讨论商务的不同组成部分。

使用移动设备进行产品发现

产品发现是寻找产品的过程。商家使用不同的机制,要么吸引顾客到实体店,要么鼓励他们在线购买。产品发现还包括如购物清单、比较购物和有关产品的信息等功能,这些都有助于消费者购买产品。移动设备非常适合这种用例,因为消费者可以实时获取产品信息并检查产品的可用性。

移动领域的一些示例应用包括条形码扫描、基于位置的购物、有针对性的广告、用户进入零售店时所获得的积分和优惠、创建购物清单并在消费者接近拥有购物清单商品的商店时提醒他们,以及将会员卡存储在钱包中的能力。

从安全的角度来看,最大的挑战是隐私问题。针对性的广告和地理围栏是基于对用户数据及其购物模式的分析。应用程序开发者在使用和收集用户数据及偏好时,应了解相关法律法规,并在使用或共享数据时遵守这些法律法规。在几乎所有情况下,收集信息之前都需要用户同意。此同意声明应包括正在收集的内容以及是否将与第三方共享。在添加新功能或更新或扩展现有功能时,请注意更新此用户同意。

移动支付

支付是移动商务中最大的组成部分。在任何支付用例中,都有三个主要实体:消费者(也称为买家)、卖家或商家以及支持支付的基础设施层。

配置

消费者可能正在使用移动设备搜索并支付产品,商家可能正在使用移动设备,或者消费者和商家可能都在使用移动设备。理想情况下,在交互期间这三个实体都应该连接。这是完全连接的情况,到目前为止,这是支付的最安全渠道。用户从 eBay 的移动网站购买商品是一个完全连接的例子,如下面的图所示:

配置

然而,在某些情况下,它们可能会断开连接。当消费者和商家都连接到基础设施,但彼此之间并未连接时,这就是以基础设施为中心的连接性案例。一个例子是地理围栏,当用户靠近商店时,他们会收到商店的优惠券。在这种情况下,商店和用户都在与基础设施对话(第三方或运营商),但彼此之间并未交流。另一个案例是用户通过使用销售点终端的设备结账。在这种情况下,用户将设备用作身份验证机制,但不一定连接到基础设施层。这是以商家为中心的连接性案例,商家连接到消费者和基础设施,但消费者断开了连接。还有一种情况是,消费者同时与基础设施和商家对话,但商家断开了连接。例如,当用户从自动售货机购买苏打水时,售货机可能会在特定时间间隔与后端同步,否则可能会断开连接。以下图示了部分连接配置:

配置

在部分连接中,基本的 security 挑战是端到端的安全问题。由于在任何时候都只有三个连接中的两个,客户端或服务器端的任何陈旧状态都很难被检测到。然后,还有客户端-商家身份验证、通信认证和隐私问题,如下面的图所示:

配置

然后还有一种断开连接的情况,即商家和消费者相互交流,但没有人与基础设施层交流。在这种情况下,维护设备的完整性是一个挑战。以消费者在销售点终端尝试使用优惠券为例。

消费者可能会继续多次使用优惠券,而无法与服务器同步以更新优惠券状态的PoS销售点)终端将无法检测到欺诈行为。同样,客户端证书可能已经过期或被撤销,但商家设备将不会意识到这一点。如果你的应用程序被设置为在这样的场景下工作,那么离线应该只有有限的功能集。涉及 PII 或金钱的情况最好是完全连接或至少是部分连接的情况。

作为一名应用程序开发者,应该了解你的用例是如何工作的。如果你的应用程序可以在部分或无连接的情况下工作,那么在处理支付时你需要采取额外的安全措施。

PCI 标准

支付卡行业PCI)是一个独立组织,致力于提高支付用例中安全性的意识。他们开发了一套通用的支付标准,以确保用户安全不受损害。PCI PTSPIN 交易安全)适用于接受支付的附加设备;PCI P2PE端到端加密)适用于基于硬件的安全,而 PCI DSS数据安全标准)适用于安全管理、政策、程序、网络架构、软件设计以及其他关键保护措施。最新的版本是 2.0,它帮助组织有效地保护用户数据。它有六个核心目标,作为十二个核心要求实施。这些在以下图表中列举:

PCI 标准

作为处理支付的应用程序开发者,应该了解 DSS。支付是复杂的,以安全的方式正确处理支付本身就是一项挑战。因此,你可能希望使用现有的支付提供商,例如 PayPal。

关于 PCI 的更多信息可以在他们的网站 pcisecuritystandards.org 上找到。

销售点

移动销售点(PoS)是随着移动设备的普及和本章前面讨论的邻近技术的使用而变得可行的一种应用。你的移动设备本质上充当销售点终端,可以管理你的账本和当天的所有交易。像 PayPal 和 Square 这样的公司提供的解决方案使用手机音频插孔来接入刷卡设备。该设备读取信用卡详细信息,并以加密形式将其发送到设备上。其他解决方案还包括移动销售点终端。

作为应用开发者,最好是与现有解决方案集成,而不是试图重新发明轮子。但是,在选择解决方案之前,记得要问一些问题。首先,你需要询问解决方案提供商是否采取了适当的安全措施来加密数据。请注意 PCI DSS 和 PCI PTN,正如我们在前面的章节中讨论的那样。处理、存储或传输信用卡号码的零售商必须符合 PCI DSS 标准,否则他们可能会失去处理信用卡支付的能力。由于不同国家之间的信用卡基础设施存在差异,必须采用不同的技术来读取信用卡/借记卡。例如,在欧洲,芯片和密码技术是标准,因此你的 PoS 支付提供商应该在各个领域都有解决方案。你可能希望选择一个提供商,通过它可以管理你的信用卡以及支票、现金和其他形式的支付。

销售点

前一个图展示了一些移动销售点解决方案的例子。第一张图片是北美地区的 PayPal 卡片阅读器及其管理的所有支付模式的应用程序。

销售点

前面的图片是 PayPal 在欧洲使用的密码和芯片解决方案,它通过使用蓝牙工作。

销售点

上图是另一个移动销售点的例子。通常由快递员和销售代表使用。

近距离技术

近距离技术在几英寸或厘米的范围内工作。这些技术包括近场通信NFC)、蓝牙和射频识别RFID)。这些技术中的大多数已经存在了一段时间,但移动设备的普及为它们带来了许多新的使用场景。这些技术现在被用于移动支付、不同设备的配对、识别和认证。

蓝牙现在已经成为大多数手机的标准。这是一项将设备配对的好技术。随着市场上出现如眼镜和手表等设备,这可能是将它们连接在一起的技术。

NFC 和 RFID 都通过产生在一定频率上调制的电磁场来工作。由于这些标签是全球可读的,当它们被用作标签或识别机制时,这些标签会带来隐私风险。第一部支持 NFC 的 Android 手机,Nexus S,在 2010 年推出。Android SDK 附带了使用 NFC 标签的 API。

由于操作范围有限,近距离技术被错误地认为是安全的。然而,事实并非如此。快速搜索将揭示所有情况下的攻击场景。数据调制、数据干扰和隐私是与这些技术相关的一些风险。

社交网络

目前应用商店中存在着大量的社交网络应用,并且每天都有新的使用案例在测试中。这些应用程序让朋友、熟人、邻居、同事以及有特殊兴趣的人们能够分享、协作,并基本上保持相互之间的联系。一些成功的例子包括 Facebook、Twitter、Pinterest、Google Hangout 和 LinkedIn。

社交网络作为将实体联系在一起的网络的图形。图中的任何不良节点都有可能对其他节点进行垃圾信息攻击或感染。在下图中,节点 A 和 B 之间的消息被拦截并替换为垃圾信息。这将导致所有连接到 B 的节点被感染。这种情况会持续,如您所想,会迅速在节点间传播:

社交网络

社交网络应用最大的挑战是隐私问题。首先,用户必须注意他们与联系人分享了什么。在大多数情况下,用户使用的是他们的真实姓名和其他私人信息。

其次,用户需要意识到垃圾信息和恶意软件的存在。不是每个人都是你的朋友。你的朋友们玩的游戏并不都是由好人编写的。也没有必要点击你关注的人分享的所有链接。

第三,应用开发者必须注意他们如何存储和处理用户的敏感信息。第一道防线是明确询问用户他们想分享什么以及与谁分享。这种用户同意可以保护开发者免受责任问题的困扰。其次,他们必须根据用户偏好定义适当的访问控制。第三,他们必须保证用户的详细信息和个人识别信息(PII)在静态存储和传输过程中都是安全的。

社交网络网站的另一个问题是身份盗窃。恶意用户很容易通过使用他人的身份来创建账户。

医疗保健

开发医疗保健的移动应用程序是另一个非常注重安全的使用案例。在医疗保健使用案例中,开发者需要处理用户身份识别、电子病历、实验室检测和处方药物。泄露这些信息可能会影响患者的健康。

移动设备在医疗保健方面可以发挥巨大作用,因为它们非常个人化,我们始终随身携带。因此,提醒我们按时服药、就诊、为医生和患者记笔记的应用程序、即时通知实验室结果以及提醒处方药物需要重新配药的应用程序都是重要且有用的。

在紧急情况下,移动设备也可以被用来帮助患病的人,用户可以通过移动设备实时分享视频并实时与医生交谈以获得帮助。

医疗保健开发的另一方面是在嵌入式设备中使用 Android 平台,例如扫描仪、放射学、X 光机、机器人手术和超声波设备。

在医疗保健中准确识别一个人至关重要。还要记住一个重要的安全规则:信任但要验证。因此,你识别了一个人,但想再确认一次以确信。访问控制和个人识别信息(PII)的安全存储和传输同样重要。

了解医疗保健领域的标准和规定,例如健康保险携带和责任法案HIPAA)。

认证

认证是识别实体的行为。在我们的案例中,认证通常与识别一个人有关。当前的认证方法是使用用户名和密码。由于密码复杂,在小设备上输入困难,因此通常使用电话号码和个人识别码(PIN)来认证用户。

双因素认证

当前最常见的方法是双因素认证。这是基于这样的理论:为了唯一识别一个人,该人应提供以下三个标识符中的两个:

  • 用户拥有的东西;这包括数字签名、安全令牌、电话、标签等。

  • 用户知道的东西;这包括密码、秘密、PIN 或只有用户才应知道的问题的答案。

  • 用户自身的东西;例如视网膜扫描、指纹和面部识别。

双因素认证的一个例子是使用用户名/密码或电话/PIN 登录,然后输入发送到用户设备的短信中的秘密代码。另一个例子可能是输入用户名和密码,然后回答如下图像所示的挑战问题:

双因素认证

在 Android 上实现双因素认证很容易。谷歌认证器通过使用短信或语音通话来实现双因素认证。

生物识别

生物识别认证是使用用户的生物学属性来识别用户。这些包括使用指纹、面部识别、视网膜扫描和虹膜扫描。基于虹膜扫描,印度实施了世界上最大的识别系统,称为 Aadhar。这个雄心勃勃的项目将使用所有五岁及以上印度公民的人口和生物识别信息为他们提供唯一的编号。查看印度唯一身份认证机构UIDAI)的网站,网址为 www.uidai.gov.in

安卓上有些应用程序使用生物识别作为关键。使用此类应用程序时的重要考虑因素是确保用户识别规格没有存储在设备上。其次,如果这些信息存储在服务器上,它们是如何传输和存储的?第三,你如何访问这些信息?

提示

生物识别技术与可以轻松更改密码或更新 RSA 安全令牌的双因素认证不同。生物识别技术是个人信息,泄露这些信息会带来巨大风险。

有两种场景会使用生物识别。在第一种情况下,通过使用一些生物学属性(如指纹)来验证用户。这会与设备中存储的副本进行比对。使用面部登录手机就是验证的一个例子,如下所示:

生物识别

第二种情况是识别,其中生物识别身份与数据库中存储的身份进行比对以找到匹配项。印度正在实施的生物识别身份识别系统就是一个例子。下图说明了这个过程:

生物识别

硬件方面的进步

移动操作系统已经取得了长足的进步。当我开始从事移动设备工作时,我们使用的是功能仅限于打电话和基本工具(如计算器和显示时间和日期的小部件)的糖果棒形状的手机。为了支持移动设备的高级用例,必须在硬件本身构建安全性。我将在以下各节中讨论这方面的一些努力。

硬件安全模块

硬件安全模块,也称为安全元件,是嵌入硬件中的一块硬件(芯片),用于存储加密密钥和其他敏感信息。这个想法是提供一个隔离的、防篡改的环境来存储 PII(个人识别信息)。在某些情况下,安全元件也可以随设备携带。安全元件的例子包括由移动网络运营商控制的增强型 SIM 卡、嵌入设备本身的芯片,或带有特殊电路的微型 SD 卡。许多 Android 手机都配备了安全元件。

在某些情况下,安全模块还可以作为安全加速器工作。这些加速器除了存储密钥外,还在硬件中执行加密功能,如加密、解密、散列和随机数生成。这大大减轻了 CPU 的负担,从而提高了性能。

为了让开发者能够使用安全元件,必须通过 API 将其暴露出来。Android 的安全元件评估工具包SEEK)就是朝这个方向迈出的一步。基于开放的移动 API,这套被称为智能卡 API 的 API 的目标是提供一种机制,让应用程序能够与嵌入式安全元件、SIM 卡或其他设备加密模块进行通信。更多信息可以在code.google.com/p/seek-for-android上查看。以下来自code.google.com的图片非常有效地说明了 SEEK 的概念:

硬件安全模块

基于 Android 的权限机制,智能卡 API 需要一种特殊权限,称为 android.permission.SMARTCARD,以便应用程序访问这些 API。智能卡 API 远程进程注册了智能卡唯一的 UID/GID。请注意,这种安全机制在已获得根权限的设备上不再起作用。GoogleOtpAuthenticator 是使用双因素认证在智能卡 API 上实现的。

TrustZone

由 ARM 开发,现在与 GlobalPlatforms 合作,TrustZone 技术是设备的安全解决方案。它基于系统芯片,TrustZone 为应用程序(如支付、内容流和管理、访问控制以及其他 PII)提供了一个可信执行环境。TrustZone 的酷炫功能是每个应用程序都在其自己的封闭环境中运行,完全与其他应用程序隔离。

你可能想访问www.arm.com/products/processors/technologies/trustzone.php网站了解详细信息。以下来自前述网站的画面展示了这项技术的高级视图。许多移动处理器,例如德州仪器和 Nvidia 的 Tegra 核心,都是基于 TrustZone 技术构建的。

TrustZone

如前图所示,通过虚拟化,处理器被划分为两个虚拟域:一个用于正常模式,另一个用于执行敏感过程的安全模式。通过使用监控模式,进程可以从一个模式过渡到另一个模式。所有敏感代码、数据和资源都远离设备上的正常操作系统环境、软件和内存进行处理。这种隔离由 SoC 架构强制执行,因此它对软件和探测攻击具有高度鲁棒性。

移动可信模块

2010 年,可信计算组TCG)发布了移动可信模块MTM)1.0 版本。TCG 是一个国际标准化机构,与会员合作开发标准和规范。MTM 的目标是将现有的 TCG 技术适应于移动和嵌入式使用。

可信计算基于硬件信任根,被称为可信平台模块TPM)。它检测恶意软件并检查系统的完整性。这种能力被称为可信平台模块。TPM 的安全始于启动过程。硬件信任根(通常是一个密钥)烧录在处理器本身中。启动安全建立在这个信任根上。启动软件的逐步阶段通过加密验证,以确保设备只执行正确、授权的软件。

访问他们的网站 www.trustedcomputinggroup.org。这对于内核开发者来说更为相关,但对于任何人来说都是一个非常有趣的阅读。

应用程序架构

现在有三种编写应用程序的方法:原生、移动网络和混合。

原生应用特定于一个平台,并且使用该平台的本地语言编写。这些应用使用操作系统制造商提供的本地工具和 SDK。这些应用性能更好,并且可以使用本地功能和 API 进行安全数据存储。以下图示说明了原生应用和混合应用的工作原理:

应用架构

移动网页应用是用网页技术编写的,比如 HTML5、CSS、PHP、JavaScript 和 ASP.net。这些应用跨平台,一旦编写完成,就可以在任何拥有浏览器的平台上运行。它们提供了集中更新的便利,但也继承了所有浏览器的漏洞。在编写移动网页应用时,要警惕浏览器漏洞。浏览器代码对所有人来说都很容易获取。此外,URL 漏洞也是这类应用的风险,因为应用程序并不驻留在设备上,只能通过有效的 URL 访问。以下是说明移动网页应用工作原理的图示:

应用架构

编写应用的第三种方式是开发混合应用。这种应用结合了原生应用和移动网页应用的优势。应用使用网页技术编写一次。用户需要像安装原生应用一样安装该应用,并且它使用设备的浏览器引擎在原生环境中运行。这样,应用可以在离线模式下运行,可以访问设备功能,并且开发者可以针对多个平台。

选择哪种架构的决定取决于你的使用场景。原生应用程序比混合型或移动网页应用更安全,它们在速度和用户体验方面的表现也更好。另一方面,混合型和移动网页应用通过使用网页技术,开发起来更容易、更快速,并且跨平台。

概述

本章重点介绍了即将到来的使用场景和技术,以及它们与移动安全的一般关系。我们讨论了移动商务、近场技术、移动医疗安全以及身份验证。我们以硬件领域安全增强的视角结束了本章。正如你所注意到的,移动领域正在发生很多事情,我认为在事情稳定下来之前,这种情况还会持续一段时间。

通过本章,我们已到达本书的结尾。希望你在本书中学到了一些新知识,并且和我一样享受这段旅程。

posted @ 2024-05-23 11:07  绝不原创的飞龙  阅读(24)  评论(0编辑  收藏  举报