安卓应用安全指南-全-

龙哥盟 / 2024-10-06 / 原文

安卓应用安全指南(全)

原文:Android Apps Security

协议:CC BY-NC-SA 4.0

一、安卓架构

谷歌进入手机市场的方式只有价值数十亿美元的公司才能承受得起:它收购了一家公司。2005 年,谷歌公司收购了安卓公司。当时,安卓相对不为人知,尽管有四个非常成功的人作为它的创造者。由安迪·鲁宾、里奇·迈纳、克里斯·怀特和尼克·西尔斯于 2003 年创立的安卓系统并不引人注目,它开发了一个手机操作系统。为了开发一款更了解主人偏好的智能手机,Android 操作系统背后的团队秘密地辛勤工作。该团队只承认他们正在开发手机软件,但在 2005 年收购之前,他们对 Android 操作系统的真实性质保持沉默。

在谷歌资源的全力支持下,Android 开发快速增长。截至 2011 年第二季度,Android 已经在面向终端用户的手机操作系统中占据了近 50%的市场份额。四位创始人在收购后留任,鲁宾担任移动业务高级副总裁。Android 1.0 版本的正式推出发生在 2008 年 9 月 23 日,第一个运行它的设备是 HTC Dream(见图 1-1 )。

9781430240624_Fig01-01.jpg

图 1-1 宏达梦想(迈克尔·奥瑞尔提供)

Android 操作系统的一个独特之处是,它的二进制文件和源代码是作为开源软件发布的,这使得它得以快速发展。您可以下载 Android 操作系统的完整源代码,它大约占用 2.6 GB 的磁盘空间。理论上,这允许任何人设计和制造运行 Android 的手机。保持软件开源的想法一直延续到 3.0 版本。Android 包括 3.0 及以上版本仍然是闭源。在接受彭博商业周刊采访时,鲁宾说 3.x 版本的代码库采取了许多捷径来确保它能快速上市,并能与非常特殊的硬件一起工作。如果其他硬件供应商采用这个版本的 Android,那么负面的用户体验将是可能的,谷歌希望避免这种情况。 1

Android 架构的组件

Android 架构分为 以下四个主要组件(见图 1-2 ):

9781430240624_Fig01-02.jpg

图 1-2 Android 架构

  1. 内核
  2. 库和 Dalvik 虚拟机
  3. 应用框架
  4. 应用

内核

Android 运行在 Linux 2.6 内核之上。内核是与设备硬件交互的第一层软件。与运行 Linux 的台式电脑类似,Android 内核将负责电源和内存管理、设备驱动程序、进程管理、网络和安全。安卓内核在 http://android.git.kernel.org/.有售

作为一名应用开发人员,修改和构建一个新的内核不是您想要考虑的事情。一般来说,只有硬件或设备制造商想要修改内核,以确保操作系统与他们特定类型的硬件一起工作。

图书馆

库组件还与运行时组件共享其空间。库组件充当内核和应用框架之间的转换层。这些库是用 C/C++编写的,但是通过 Java API 向开发人员公开。开发人员可以使用 Java 应用框架来访问底层的核心 C/C++库。一些核心库包括:

  • LibWebCore :允许访问网络浏览器。
  • 媒体库:允许访问流行的音频和视频录制和播放功能。
  • 图形库:允许访问 2D 和 3D 图形绘制引擎。

运行时组件由 Dalvik 虚拟机组成,它将与应用交互并运行应用。虚拟机是 Android 操作系统的重要组成部分,执行系统和第三方应用。

达尔维克虚拟机

丹·博恩施泰因最初编写了达尔维克虚拟机。他以冰岛的一个小渔村命名,因为他相信他的一个祖先曾经起源于那里。Dalvik VM 主要用于在资源非常有限的设备上执行应用。通常,移动电话会属于这一类,因为它们受到处理能力、可用内存量和电池寿命短的限制。

什么是虚拟机?

虚拟机是在另一个主机操作系统中运行的独立的客户操作系统。虚拟机将执行应用,就像它们在物理机上运行一样。虚拟机的主要优势之一是可移植性。不管底层硬件如何,您编写的代码都可以在虚拟机上运行。对于开发人员来说,这意味着您只需编写一次代码,就可以在任何运行兼容 VM 的硬件平台上执行。

达尔维克虚拟机执行。dex 文件。一个。dex 文件是通过取编译好的 Java 制作的。类别或。jar 文件,并将所有常量和数据整合到每个文件中。类文件放入一个共享常量池中(参见图 1-3)。Android SDK 中包含的 dx 工具执行这种转换。转换后,。dex 文件的文件大小明显较小,如表 1-1 所示。

9781430240624_Fig01-03.jpg

图 1-3 将. jar 文件转换为. dex 文件

表 1-1 。文件大小比较(以字节为单位)。罐子和。dex 文件

应用 未压缩。冲突 压缩的。冲突 未压缩。DEXEDRINE 的简称
通用系统库 21445320 = 100% 10662048 = 50% 10311972 = 48%
网络浏览器应用 470312 = 100% 232065 = 49% 209248 = 44%
闹钟应用 119200 = 100% 61658 = 52% 53020 = 44%

应用框架

应用框架是最终系统或最终用户应用的构建块之一。该框架提供了一套服务或系统,开发者在编写应用时会发现这些服务或系统很有用。该框架通常被称为 API(应用编程接口)组件,将为开发人员提供对按钮和文本框等用户界面组件的访问,提供通用内容供应器以便应用可以在它们之间共享数据,提供通知管理器以便设备所有者可以收到事件警报,并提供活动管理器来管理应用的生命周期。

作为开发人员,您将编写代码并使用 Java 编程语言中的 API。清单 1-1 ,摘自 Google 的样例 API 演示(developer . Android . com/resources/samples/API demos/index . html),演示了如何使用应用框架播放视频文件。粗体的 import 语句允许通过 Java API 访问核心 C/C++库。

清单 1-1 。 一个视频播放器演示(由谷歌公司提供)

/*

*版权所有(C)2009 Android 开源项目

*根据 Apache 许可证 2.0 版(“许可证”)获得许可;

*除非符合许可协议,否则您不得使用此文件。

*您可以从以下网址获得许可证副本

*www.apache.org/licenses/LICENSE-2.0

*除非适用法律要求或书面同意,否则软件

*根据许可证分发是基于“原样”分发,

*没有任何明示或暗示的担保或条件。

*有关管理权限的特定语言,请参见许可证

*许可证下的限制。

*/

包 com . example . Android . APIs . media;

导入 com . example . Android . APIs . r;

导入 Android . app . activity;

导入 Android . OS . bundle;

导入 Android . widget . media controller;

导入 android . widget . toast

导入 android . widget . videoview

公共类 VideoViewDemo 扩展活动{

/**

  • TODO:将 path 变量设置为流式视频 URL 或本地媒体

*文件路径。

*/

私有字符串路径= " ";

mVideoView 专用视频视图:

@覆盖

公共 void onCreate(捆绑冰柱){

很好,oncreate(icic);

setContentView(请参阅 layout.videoview):

mvideoview =(video view)findviewbyid(r . id . surface _ view);

if (path == "") {

//告诉用户提供媒体文件 URL/路径。

烤面包。makeText(

VideoViewDemo.this,

请编辑视频视图演示活动,并设置路径

+"媒体文件 URL/路径的变量",

吐司。长度 _ 长)。show();

} else {

/*

*或者,对于流媒体,您可以使用

  • mvideoview . set video uri(uri . parse(URL string));

*/

mvideoview . setvideoplath(path);

mVideoView.setMediaController(新媒体控制器(this));

mVideoView.requestFocus():

}

}

}

应用

Android 操作系统的应用组件最接近最终用户。这是联系人、电话、信息和愤怒的小鸟应用所在的地方。作为开发人员,您的成品将通过使用 API 库和 Dalvik VM 在这个空间中执行。在本书中,我们将广泛地研究 Android 操作系统的这个组件。

尽管 Android 操作系统的每个组件都可以修改,但你只能直接控制你自己的应用的安全性。然而,这并不意味着您可以随意忽略如果设备受到内核或虚拟机漏洞攻击会发生什么。确保您的应用不会因为无关的利用而成为攻击的受害者也是您的责任。

这本书是关于什么的

现在你已经对 Android 架构有了一个总体的了解,让我们转向你在这本书里将要而不是学到的东西。首先,你不会在这本书里从头开始学习如何开发 Android 应用。你会看到很多例子和源代码清单;虽然我将解释代码的每一部分,但你可能会有在本书中找不到答案的其他问题。你需要在为 Android 平台编写 Java 应用方面有一定的经验和技能。我还假设您已经使用 Eclipse IDE 设置了 Android 开发环境。在本书中,我将重点介绍如何为 Android 操作系统开发更安全的应用。

Android 也有相当多的安全挫折和一系列值得研究和借鉴的恶意软件。掌握了在哪里寻找和如何解决 Android 开发的安全问题,不一定会让你成为一个更好的程序员,但它会让你开始对最终用户的隐私和安全更加负责。

我试图以一种能帮助你理解与你开发的应用相关的安全概念的方式来写这本书。在大多数情况下,我发现我能做到这一点的最好方法是通过实例教学。因此,你通常会发现我要求你先编写并执行源代码清单。然后,我会继续解释我们所涉及的具体概念。考虑到这一点,让我们来看看 Android 操作系统上可用的一些安全控件。

安全

安全不是一个肮脏的词,黑爵士!

——梅尔切特将军,黑爵士 IV

安全 是一个庞大的主题,适用于许多领域,这取决于它所处的环境。我写这本书是为了介绍安全性的一小部分。本文旨在让您更好地了解 Android 应用安全性。然而,这到底意味着什么呢?我们要保护什么?谁将从中受益?为什么重要?让我们试着回答这些问题,并可能提出一些新的问题。

首先,让我们认清你到底是谁。你是开发者吗?也许你是一名从事研究的安全从业者。或者,您可能是一个对保护自己免受攻击感兴趣的最终用户。我愿意认为我符合这些类别中的每一个。毫无疑问,你会适合其中的一个或多个。然而,绝大多数人都符合一个类别:希望以不损害隐私和安全的方式使用编写良好的应用的功能的最终用户。如果你是一名开发人员,我猜你是,如果你拿起这本书,这是你的目标受众:最终用户。您编写应用来分发给您的用户。你可以选择出售或者免费赠送。不管是哪种情况,你正在编写的应用最终会被安装在其他人的设备上,可能在几千英里之外。

保护您的用户

您的应用应该努力提供尽可能好的功能,同时注意保护用户的数据。这意味着在开始开发之前要考虑安全性。

你的用户可能并不总是知道你在应用“幕后”采用的安全措施,但是你的应用中的一个漏洞就足以确保他所有的 Twitter 和脸书追随者发现。在应用的开发阶段之前规划和考虑安全性,可以避免差评的尴尬和付费客户的流失。最终用户几乎不会很快原谅或忘记。

在此过程中,您将了解识别敏感用户数据和创建保护这些数据的计划的原则和技术。目标是消除或大大减少应用可能造成的任何意外伤害。那么,您真正要保护最终用户免受什么危害呢?

安全风险

与台式电脑用户相比,移动设备用户面临一些独特的风险。除了设备丢失或被盗的可能性更高之外,移动设备用户还面临着丢失敏感数据或隐私泄露的风险。为什么这与桌面用户不同?首先,存储在用户移动设备上的数据质量往往更加个人化。除了电子邮件,还有即时消息、SMS/MMS、联系人、照片和语音邮件。“那又怎么样?”你说。"其中一些东西存在于台式电脑上."没错,但是考虑一下这个:你移动设备上的数据很可能比你桌面上的数据更有价值,因为你一直把它带在身边。它是计算机和手机的融合平台,包含更丰富的个人数据。因为智能手机上的用户交互水平更高,所以数据总是比台式电脑上的数据更新。即使您已经配置了与远程位置的实时同步,这也只能防止您丢失数据,而不能保护您的隐私。

还要考虑存储在移动设备上的数据格式是固定的。每部手机都有短信/彩信、联系人和语音邮件。功能更强大的手机将拥有照片、视频、GPS 定位和电子邮件,但所有这些都是通用的,与操作系统无关。现在考虑一下所有这些信息对最终用户有多重要。对于没有备份的用户来说,丢失这种性质的数据是不可想象的。丢失重要的电话号码、视频中捕捉到的女儿迈出第一步的珍贵瞬间,或者重要的短信,对于日常电话用户来说都是灾难性的。

对于在手机上同时进行商务和个人活动的用户来说呢?如果有人从你的手机上复制了你的 office 服务器群的整个密码文件,你会怎么做?或者,如果一封包含商业秘密和提案保密定价的电子邮件泄露到互联网上呢?丢了孩子学校的地址怎么办?假设一个跟踪者获得了这些信息以及更多信息,比如你的家庭住址和电话号码。

很明显,在大多数情况下,手机上存储的数据远比手机本身更有价值。最危险的攻击类型是无声无息、远程进行的攻击;攻击者不需要物理接触你的电话。这些类型的攻击可能在任何时候发生,并且由于设备上其他地方的安全薄弱而经常发生。这些安全性上的失误可能不是因为您的应用不安全。它们可能是由于内核或 web 浏览器中的错误造成的。问题是:即使攻击者通过不同的途径访问设备,您的应用能否保护其数据免受攻击?

Android 安全架构

正如我们之前讨论的,Android 运行在 Linux 2.6 内核之上。我们还了解到 Android Linux 内核负责操作系统的安全管理。让我们来看看 Android 的安全架构。

权限分离

Android 内核在执行应用时实现了权限分离模型。这意味着,像在 UNIX 系统上一样,Android 操作系统要求每个应用都使用自己的用户标识符(uid)和组标识符(gid)来运行。

系统架构本身的各个部分以这种方式分离。这确保了应用或进程没有访问其他应用或进程的权限。

什么是特权分离?

权限分离是一项重要的安全特性,因为它拒绝了一种更常见的攻击类型。在许多情况下,首先进行的攻击并不是最有效的攻击。它通常是更大攻击的垫脚石或入口。通常,攻击者会首先利用系统的一个组件;一旦到了那里,他们就会试图攻击系统中更重要的组件。如果这两个组件以相同的权限运行,那么对于攻击者来说,从一个组件跳到下一个组件是一件非常简单的事情。通过分离权限,攻击者的任务变得更加困难。他必须能够将其权限升级或更改为他希望攻击的组件的权限。通过这种方式,攻击被停止,如果不是减慢的话。

因为内核实现了权限分离,这是 Android 的核心设计特性之一。这种设计背后的理念是确保任何应用都不能读取或写入其他应用、设备用户或操作系统本身的代码或数据。因此,应用可能无法随意使用设备的网络堆栈来连接到远程服务器。一个应用可能无法直接读取设备的联系人列表或日历。这个特性也被称为沙箱。两个进程在各自的沙箱中运行后,它们相互通信的唯一方式是显式请求访问数据的权限。

权限

我们举个简单的例子。我们有一个应用,记录来自设备内置麦克风的音频。为了让这个应用正常工作,开发人员必须确保在应用的 AndroidManifest.xml 文件中添加对 RECORD_AUDIO 权限的请求。这允许我们的应用请求使用处理录音的系统组件的权限。但是谁来决定是允许还是拒绝访问呢?Android 允许最终用户执行这一最终批准过程。当用户安装我们的应用时,会出现如图图 1-4 所示的屏幕提示。值得注意的是,当应用执行时,不会出现权限提示。相反,需要在安装时授予权限。

9781430240624_Fig01-04.jpg

图 1-4 。Android 权限请求屏幕

如果我们没有明确设置我们对 RECORD_AUDIO 权限的需求,或者如果设备所有者在我们请求后没有授予我们权限,那么 VM 将抛出一个异常,应用将失败。开发人员需要知道如何请求权限,并通过捕获相关异常来处理权限未被授予的情况。要请求此权限,项目的 AndroidManifest.xml 文件中必须包含以下标记:

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

本书的附录中给出了权限的完整列表。

申请代码签名

任何要在 Android 操作系统上运行的应用都必须经过签名。Android 使用个人开发者的证书来识别他们,并在操作系统中运行的各种应用之间建立信任关系。操作系统将不允许未签名的应用执行。不需要使用证书颁发机构来签署证书,Android 将愉快地运行任何使用自签名证书签署的应用。

与权限检查一样,证书检查仅在应用安装期间进行。因此,如果您的开发人员证书在您的应用安装到设备上后过期,则该应用将继续执行。在这一点上,唯一的区别是您需要在签署任何新的应用之前生成一个新的证书。Android 要求应用的调试版本和发布版本有两个单独的证书。通常,运行 Android 开发工具(ADT)的 Eclipse 环境已经设置好,可以帮助您生成密钥并安装证书,这样您的应用就可以自动打包并签名。Android 模拟器的行为与物理设备相同。像物理设备一样,它将只执行已签名的应用。我们将详细介绍应用代码签名,以及在线发布和销售您的应用。

摘要

正如我们到目前为止所看到的,由于谷歌对 Android 的收购,Android 在资源和关注度方面获得了巨大的提升。同样的关心和关注帮助推动 Android 成为当今世界上增长最快的智能手机操作系统之一。Android 的开源模式帮助其数量增长,主要是因为许多不同的硬件制造商可以在他们的手机上使用该操作系统。

我们也看到了 Android 的核心是基于 Linux 内核的。内核的两个主要任务是(1)充当硬件和操作系统之间的桥梁,以及(2)处理安全性、内存管理、进程管理和网络。当不同的硬件制造商开始采用 Android 与其硬件一起工作时,内核通常是将被修改的主要组件之一。

围绕 Android 内核的下一层是运行时层,包括核心库和 Dalvik 虚拟机。Dalvik VM 是在 Android 平台上执行应用的基础部分。正如您将在接下来的章节中看到的,在资源受限的环境中安全高效地执行应用时,Dalvik VM 有一些独特的特性。

接下来要添加的上层分别是框架和应用。您可以将框架层视为 Java API 和本机代码以及运行在下面的系统进程之间的又一座桥梁。这是所有 Android Java APIs 存在的地方。您希望在程序中导入的任何库都是从这里导入的。应用层是您的应用最终生活和工作的地方。您将与其他开发者应用和 Android 的捆绑应用(如电话、日历、电子邮件和消息应用)共享这个空间。

然后,我们简要地看了一下安全风险,你如何有责任保护你的终端用户,以及 Android 促进这一点的一些方式。我们研究的三个领域是特权分离、权限和应用代码签名。在接下来的章节中,我们将探讨如何不仅利用这些特性,还增加您自己的安全级别和最终用户保护。

1 彭博社《商业周刊》,《谷歌捧蜂巢紧》,阿什利·万斯和布拉德·斯通,www . business week . com/technology/content/mar 2011/TC 2011 03 24 _ 269784 . htm,2011 年 3 月 24 日。

二、信息:一个应用的基础

所有有意义的应用的基础是信息,我们设计和构建应用来交换、创建或存储信息。移动应用也不例外。在当今连接良好的移动领域,信息交换是游戏的名称。为了说明这一点,想象一部没有移动网络或 WiFi 覆盖的 Android 手机。虽然这种手机仍有用武之地,但你将无法访问手机上一些更重要的应用。例如,电子邮件、即时消息、网页浏览和任何其他需要互联网的应用现在都将无法运行。

在后面的章节中,我们将集中精力检查传输中的信息以及如何保护它。在本章中,我们将主要关注存储的信息发生了什么变化。

保护您的应用免受攻击

当创建或接收数据时,数据需要存储在某个地方。这些信息的存储方式最终将反映出您的应用的安全性。向公众发布你的应用应该像在互联网上建立一个网站一样小心谨慎。您应该假设您的应用在某个时候会受到直接或间接的攻击,并且您的应用是最终用户隐私和数据保护之间的唯一障碍。

间接攻击

尽管最后一句话听起来很有戏剧性,但它并非毫无根据。在我们进一步讨论之前,让我们看看我散布恐惧是否有道理。2010 年后期和 2011 年初,分别在 Android 和 2.3 版本中发现了两个漏洞。该漏洞本质上是相同的,在该漏洞中,攻击者可以复制存储在设备 SD 卡上的任何文件,而无需许可,甚至没有可见的提示。该漏洞的工作原理如图图 2-1 所示。

9781430240624_Fig02-01.jpg

图 2-1 。数据窃取漏洞

以下是最值得注意的几点:

  1. 用户访问托管文件的恶意网站,如 evil.html。
  2. 由于漏洞的一部分,在没有提示用户的情况下,evil.html 文件被下载并保存到设备 SD 卡。
  3. 由于该漏洞的另一部分,保存的文件可以在保存后立即执行 JavaScript 代码。同样,对最终用户没有提示。
  4. 由于此漏洞的最后一部分,前面执行的 JavaScript(因为它运行在设备的“本地”上下文中)将完全有权将存储在 SD 卡上的文件上传到攻击者选择的网站。

为了便于讨论,假设您的应用将所有保存的信息写入 SD 卡,存储在它自己的目录下。由于刚才讨论的漏洞,您的应用使用的数据有被窃取的风险。任何运行您的应用和易受攻击的固件版本的 Android 设备都会给其最终用户带来数据被盗的风险。这是对您的应用进行间接攻击的一个例子。

您的应用对于间接攻击的脆弱性很大程度上取决于您在开始编写一行代码之前,在设计考虑安全方面方面投入了多少努力。你可能会问这样的问题,“我只是一个计划在网上低价出售我的应用的小应用开发者,所以我真的需要浪费时间事先做这么多计划吗?”我会响亮地回答你:“是的!”无论您是 30 名开发人员团队的一员,还是在家工作的个人,一个架构良好的应用都是您应该努力创建的。我希望这是你将从本书中学到的东西。

直接攻击

直接攻击明显不同,可以采取多种不同的形式。直接攻击可以分为直接针对您的应用的攻击。因此,攻击者希望利用应用设计中的弱点来收集应用用户的敏感信息,或者攻击应用与之对话的服务器。以一个移动银行应用为例。攻击者可能会追踪属于特定银行的移动应用。如果应用设计薄弱 — 例如,如果敏感的用户数据以明文形式存储,或者应用和服务器之间的通信没有受到 SSL — 的保护,那么攻击者可以专门针对这些弱点发起攻击。这是对特定应用的直接攻击。我将在本书第九章的中更详细地讲述直接攻击。

项目 1:“Proxim”和数据存储

让我们从一个名为 Proxim 的简单例子开始。我已经签约编写一个应用,当用户在一组 GPS 坐标的一定范围内时,该应用可以向特定的、已定义的联系人发送 SMS。例如,使用这个应用,用户可以将他的妻子添加为联系人,并且每当他在他的工作场所和家的三英里范围内时,就让应用给她发短信。这样,她就知道他什么时候离家和办公室近了。

您可以从 Apress 网站的源代码/下载区域(【www.apress.com】)下载并检查 Proxim 应用的整个源代码。为了清楚起见,让我们看一下最重要的区域。

数据存储程序如清单 2-1 中的所示。

清单 2-1 。 保存例程,SaveController。java

包 net . Zen consult . Android . controller;

导入 Java . io . file;

导入 Java . io . file notfounindexception;

导入 java.io.FileOutputStream:

导入 java . io 异常:

导入 net . Zen consult . Android . model . contact;

导入 net . Zen consult . Android . model . location;

导入 Android . content . context;

导入 Android . OS . environment;

导入 android.util.Log:

公共类 SaveController {

私有静态最终字符串标签= " save controller ";

公共静态 void saveContact(上下文 Context,Contact contact) {

if ( isReadWrite ()) {

尝试{

File output File = new File(context . getexternalfilesdir(null),contact . get first name());

FileOutputStream outputStream = new FileOutputStream(outputFile);

output stream . write(contact . getbytes());

output stream . close();

} catch(file notfounindexception e)}

日志。 e ( 标签,“找不到文件”);

} catch(io exception e)}

Log. e ( TAG ,"IO Exception");

}

} else {

日志。 e ( TAG ,“读写模式下打开媒体卡出错!”);

}

}

公共静态 void saveLocation(上下文上下文,位置位置){

if ( isReadWrite ()) {

尝试{

File outputFile = new File(context.getExternalFilesDir(null),location.getIdentifier());

FileOutputStream outputStream = new FileOutputStream(outputFile);

output stream . write(location . getbytes());

output stream . close();

} catch(file notfounindexception e)}

日志。 e ( 标签,“找不到文件”);

} catch(io exception e)}

日志记录。 e ( 标记,“IO 异常”);

}

} else {

日志。 e ( TAG ,“读写模式下打开媒体卡出错!”);

}

}

私有静态布尔值 isReadOnly() {

日志。 e ( 标记,环境

.get xternalstoragestate);

回归环境。媒体装载只读。等于(环境

.get xternalstoragestate);

}

私有静态布尔 isReadWrite() {

日志。 e ( 标记,环境

.get xternalstoragestate);

回归环境。介质安装。等于(环境

.get xternalstoragestate);

}

}

每次用户选择“保存位置”按钮或“保存联系人”按钮时,都会触发前面的代码。让我们更详细地看看位置(见清单 2-2 )和联系人(见清单 2-3 )类。虽然我们可以实现一个主要的保存例程,但我将它分开,以防需要以不同的方式对不同的对象进行操作。

清单 2-2 。Location.java 外景班

包 net . Zen consult . Android . model;

publicclass location

私有字符串标识符;

私人双 latitude

私人双倍长度;

公共位置(){

}

publicdouble get attitude()±

返回纬度;

}

publicvoid setLatitude(双纬度){

this.latitude =纬度;

}

public double get length()= & gt

返回经度;

}

publicvoid setLongitude(双经度){

this.longitude =经度;

}

publicvoid setidentifier(字符串标识符)

this.identifier =标识符;

}

public string getiidentificar()= & gt

返回标识符;

}

公共字符串 toString() {

string builder ret = new string builder();

ret . append(get identifier());

ret . append(string . value of(get distance()));

ret . append(string . value of(get length()));

return ret.toString():

}

公有字节[] getBytes() {。

返回到 String().getBytes();

}

}

清单 2-3 。【Contact.java】的触点类,的

net . Zen consult . Android . model;

publicclass contact

串名;

私有字符串姓氏;

私有字符串 address1

私有字符串 address2

私人字符串邮件;

私人串线电话;

公共联系人(){

}

public 字符串 getFirstName()>

返回名字;

}

public voidset first name(String first name){

这个。firstName =名字;

}

publicString get last name(){

返回姓氏;

}

public voidset last name(String last name){

这个。lastName =姓氏;

}

public 字符串 get ddress 1()= >

返回地址 1;

}

publicvoid setAddress1(字符串地址 1) {

this. address 1 = address 1;

}

public 字符串 getaddress 2()>

返回address 2;

}

publicvoid setAddress2(字符串地址 2) {

这个. address 2 = address 2;

}

public 字符串 getemail()>

回复邮件;

}

发布【set email(string email)】

这个。email = email

}

public String getPhone() {

返回电话;

}

public voidset phone(String phone){

这个。电话=电话;

}

public String toString() {

StringBuilder ret = string builder();

ret . append(get first name()+|);

ret . append(get astname()+|);

ret . append(get address 1()+|);

ret . append(get address 2()+|);

ret . append(get mail()+|);

ret . append(get microphone()+|);

返回ret . tostring();

}

公有字节[] getBytes() {。

返回tostring()getbytes();

}

}

位置和联系类是标准类,用于保存特定于每种类型的数据。它们中的每一个都包含了 toString() 和 getBytes() 方法,这些方法将类的全部内容作为一个字符串或一个字节数组返回。

如果我们要手动添加一个联系人对象,那么我们很可能会使用类似于清单 2-4 中所示的代码。

清单 2-4 。 代码,增加一个新的联系对象

期末联系人= 联系人();

contact . set first name(" Sheran ");

contact . set last name(" Gunasekera ");

contact.setAddress1(" ")

contact.setAddress2(" ")

contact . set email(" sheran @ Zen consult . net ");

contact . set phone(" 12120031337 ");

现在假设当用户填充屏幕向应用添加新联系人时,调用清单 2-4 中的代码。您将使用显示在主视图上的每个 EditText 对象的 getText() 方法,而不是看到硬编码的值。

如果您在您的 Android 模拟器中执行代码 save controller . save Contact(getApplicationContext()、Contact)), SaveController 将获取新创建的联系人并将其存储在外部媒体源中(回头参考清单 2-1 )。

注意使用 getExternalFilesDir() 方法在 Android 设备上查找 SD 卡的位置始终是一个好的做法。因为 Android 可以在大量不同规格的设备上运行,所以 SD 卡目录的位置可能并不总是在 /sdcard 。 getExternalFilesDir() 方法将向操作系统查询 SD 卡的正确位置,并将该位置返回给您。

让我们一次看一行,从 saveContact() 方法的构造函数开始:

public static void saveContact(Context context, Contact contact) {
        if (*isReadWrite*()) {
                        try {

前面的代码片段需要一个上下文对象和一个联系人对象。Android 上的每个应用都有自己的环境。一个上下文对象包含应用特定的类、方法和资源,它们可以在应用中的所有类之间共享。例如,上下文对象将包含关于 SD 卡目录位置的信息。要访问它,您必须调用 context . getexternalfilesdir()方法。该方法接受参数后,将检查设备上的 SD 卡是否已安装,以及是否可写。 isReadWrite() 方法将执行并返回一个真或假值来表明这一点:

File outputFile = new File(context.getExternalFilesDir(null),contact.getFirstName());

这段代码创建了一个指向 SD 卡目录位置的文件对象。我们使用联系人对象的名字作为文件名:

FileOutputStream outputStream = new FileOutputStream(outputFile);
outputStream.write(contact.getBytes());
outputStream.close();

使用这段代码,我们创建了一个指向我们的文件对象的位置的文件输出流。接下来,我们使用 getBytes() 方法将联系人对象的内容写入输出流,以返回一个字节数组。最后,我们关闭文件输出流。

当执行完成时,我们应该有一个名为“Sheran”的文件写入设备上的 SD 卡目录。我在 Mac OS X 雪豹上使用安卓模拟器。因此,当我导航到模拟器的位置时,我可以看到如图图 2-2 所示的屏幕。

9781430240624_Fig02-02.jpg

图 2-2 Max OS X 上的 SD 卡镜像文件

当通过导航到 Android/data/net . Zen consult . Android/files 来挂载该图像时,新创建的联系人文件名可见(参见图 2-3 )。

9781430240624_Fig02-03.jpg

图 2-3 被写入文件的联系对象

如果我们在文本编辑器中打开文件,我们可以看到从应用中保存的纯文本数据(见图 2-4 )。

9781430240624_Fig02-04.jpg

图 2-4 。联系对象的内容

信息分类

当我开始从事移动应用开发时,我所面临的一个问题是,我必须从一开始就开始编写代码。我会在脑海中构思这些特征,并在进行过程中编写代码。很多时候,我会花时间修改我的代码,然后中途回去写一个计划。这对我的截止日期和交付成果产生了毁灭性的影响。这也对我的应用的安全性产生了不利影响。

从那以后,我认识到写一份我即将着手的项目的简要大纲将有助于我提前考虑事情。虽然这似乎是一件显而易见的事情,但我所接触过的许多开发人员都没有遵循这个简单的步骤。我也开始认真做的另一件事是找时间查看我的应用将要处理的信息或数据。例如,我使用一个类似于表 2-1 中所示的表来对我的应用处理的数据进行分类。桌子很基础;然而,通过将它写在纸上,我能够想象我的应用将处理的数据类型,而且,我能够制定一个计划来保护这些信息。

表 2-1 。数据分类表

数据类型 私人的? 敏感? 创造 商店 发送 接收
名字 X X x
电子邮件地址 X X x
电话号码 X X
地址 X X

如果你仔细看一下表 2-1 中的数据分类表,你会发现有些标题非常主观。不同的人对什么是敏感信息或个人信息会有不同的看法。然而,通常最好是尝试并集中在一个共同的参考框架上,以确定什么是敏感信息和个人信息。在本节中,您将首先查看表格标题,然后查看每一列:

  • 数据类型:您将在您的应用中处理这些数据。这是不言自明的。
  • 个人?:此栏表示数据类型是否归类为个人信息。
  • 敏感?:此栏表示数据类型是否属于敏感信息。
  • Create :您的应用允许这个用户创建这个数据类型吗?
  • Store :您的应用将这种数据类型存储在设备上还是远程服务器上?
  • Sent :这种数据类型是通过网络发送给另一方还是服务器?
  • 接收:这种数据类型是通过网络从另一方接收的吗?

什么是个人信息?

个人信息可以归类为你和你的社交圈内有限数量的人所知道的数据。个人信息通常是你的隐私,但你愿意与亲密的朋友和家人分享。个人信息的例子可以是您的电话号码、地址和电子邮件地址。泄露这些信息通常不会对你或你的家庭成员造成严重的身体或精神伤害。相反,它可能会给你带来极大的不便。

什么是敏感信息?

敏感信息比个人信息更有价值。敏感信息通常是您在大多数情况下不会与任何人共享的信息。这类数据包括您的密码、网上银行凭证(如 PIN 码)、手机号码、社会保险号或地址。如果敏感信息被泄露,那么后果可能会给你带来身体或精神上的伤害。无论信息是在传输中还是在存储中,都应该始终受到保护。

警告敏感信息的丢失会对您的身体或情感造成怎样的伤害?考虑丢失您的网上银行凭证。攻击者会偷走你所有的钱,给你造成巨大的经济(身体和情感)损失。跟踪者掌握了你的电话号码或地址,会对你或你家人的身体健康造成严重威胁。

代码分析

如果我们回到本章前面讨论的间接攻击,很明显,在 SD 卡上清晰可见地保存数据是一个巨大的风险,应该不惜一切代价避免。数据失窃或泄露已经成为企业财务和声誉损失的主要原因之一。但是,仅仅因为你为智能手机的单一用户编写应用,并不意味着你应该对数据盗窃掉以轻心。就 Proxim 而言,明文数据存储的这一弱点是存在的。任何能够访问该设备 SD 卡的人都可以复制个人信息,如姓名、地址、电话号码和电子邮件地址。

我们可以追踪原始代码中的缺陷,直到我们保存数据的地方。数据本身没有以任何方式隐藏或加密。如果我们加密数据,那么个人信息仍然是安全的。让我们看看如何在我们的原始 Proxim 代码中实现加密。第五章将深入讨论公钥基础设施和加密;因此,出于这个练习的目的,我们将介绍一个非常基本的高级加密示例标准(AES)加密。公钥加密或非对称加密是一种通过使用两种不同类型的密钥来加密或混淆数据的方法。每个用户有两个密钥,一个公钥和一个私钥。他的私钥只能解密由公钥加密的数据。这个密钥被称为公共密钥,因为它是免费提供给其他用户的。其他用户将使用这个密钥来加密数据。

在哪里实现加密

我们会在将数据保存到 SD 卡之前对其进行加密。这样,我们就永远不会以任何人都可以读取的格式将数据写入 SD 卡。收集您的加密数据的攻击者必须首先使用密码来解密数据,然后才能访问它。

我们将使用 AES 通过密码或密钥加密我们的数据。加密和解密数据都需要一个密钥。这也称为对称密钥加密。与公钥加密不同,该密钥是唯一用于加密和解密数据的密钥。这个密钥需要安全地存储,因为如果它丢失或泄露,攻击者可以用它来解密数据。清单 2-5 显示了加密程序。

清单 2-5 。 一个加密例程

私有字节【加密】字节键、字节数据{。

SecretKeySpec =newSecretKeySpec(key," AES ");

密码密码;

字节[]塞浦路斯文本=null;

试试 {

密码=密码。getInstance【AES】;

cipher.init(密码。 ENCRYPT_MODE ,sKeySpec);

密文= cipher.doFinal(数据);

}catch(nosuchin 算法异常 e)}

日志。 e ( 标签,“nosuchalgrimetricexception”);

}catch(nosuchtpaddingexception e)}

日志。 e ( 标签,“NoSuchPaddingException”);

} catch (非法块阻止异常 e)}

日志。 e ( 标签,“IllegalBlockSizeException”);

}catch(badpaddingexception e)}

日志。 e ( 标签,“BadPaddingException”);

}catch(invalidkeyexception e)>

日志。 e ( 标签,“InvalidKeyException”);

}

返回密文;

}

让我们一段一段地检查代码。第一位代码初始化 SecretKeySpec 类,并创建一个新的密码类实例,为生成 AES 密钥做准备:

SecretKeySpec sKeySpec = new SecretKeySpec(key,"AES");
Cipher cipher;
byte[] ciphertext = null;

前面的代码还初始化了一个字节数组来存储密文。下一位代码为密码类使用 AES 算法做准备:

cipher = Cipher.*getInstance*("AES");
cipher.init(Cipher.*ENCRYPT_MODE*, sKeySpec);

cipher.init() 函数初始化密码对象,因此它可以使用生成的密钥执行加密。下一行代码加密纯文本数据,并将加密的内容存储在密文字节数组中:

ciphertext = cipher.doFinal(data);

为了让前面的例程工作,它应该总是有一个加密密钥。重要的是,我们对解密程序使用相同的密钥。否则就会失败。通常最好编写自己的密钥生成器,它将生成一个基于随机数的密钥。这将使攻击者比普通密码更难猜到。在这个练习中,我使用了清单 2-6 中所示的密钥生成算法。

清单 2-6 。 一种密钥生成算法

publicstatic byte[]generate key(byte[]randomained)>

SecretKey sKey = null

试试 {

key generator keygen = key generator。getinstance(" AES ");

securerandom = securerandom。getinstance(" sha 1 prng ");

random . setseed(randomNumberSeed);

keyGen.init(256,随机);

skey = key gen . generate key();

}catch(nosuchin 算法异常 e)}

日志。 e ( 标签,“无此类算法异常”);

}

returnskey . get encoded();

}

现在,让我们分析代码。这两行代码初始化 KeyGenerator 类,这样它就可以生成特定于 AES 的密钥,然后初始化设备的随机数生成器,这样它就可以生成随机数:

KeyGenerator keyGen = KeyGenerator.*getInstance*("AES");
SecureRandom random = SecureRandom.*getInstance*("SHA1PRNG");

这些随机数用 SHA1 编码。SHA1 或安全哈希算法 1 是一种加密哈希函数。该算法将对具有任意长度的一段数据进行操作,并将产生固定大小的短字符串。如果被散列的数据的任何部分被改变,那么产生的散列将会变化。这表明一部分数据已经被篡改。

下一段代码使用提供的随机数种子,通过这个随机数生成一个 256 位密钥:

random.setSeed(randomNumberSeed);
keyGen.init(256,random);
sKey = keyGen.generateKey();

只需运行一次密钥生成算法,并保存生成的密钥以供解密例程使用。

加密的结果

当我们检查 SD 卡中的同一个联系人对象时,内容出现乱码(见图 2-5 ),任何不经意的窥探者或蓄意攻击者都无法读取。

9781430240624_Fig02-05.jpg

图 2-5 联系对象的加密内容

返工项目 1

我们对 Proxim 项目的更改主要影响了 saveController() 方法(参见清单 2-7 )。

清单 2-7 。【返工 SaveController.java】法

包 net . Zen consult . Android . controller;

导入 Java . io . file;

导入 Java . io . file notfounindexception;

导入 java.io.FileOutputStream:

导入 java . io 异常:

导入 Java . security . invalidkeyexception;

导入 Java . security . nosuchalgorithm exception;

导入 javax . crypto . badpaddingexception:

导入 javax . crypto . cipher;

导入 javax.crypto。非法块异常:

导入 javax . crypto . key generator;

导入 javax . crypto . nosucpaddingexception:

导入 javax . crypto . spec . secretkeyspec;

import net . zenconsultant . Android . crypto . crypto;

导入 net . Zen consult . Android . model . contact;

导入 net . Zen consult . Android . model . location;

导入 Android . content . context;

导入 Android . OS . environment;

导入 android.util.Log:

公共类 SaveController {

private static final String TAG = " save controller ";

公共静态 void saveContact(上下文 Context,Contact contact) {

if (isReadWrite()) {

尝试{

文件输出文件 = 新文件(context.getExternalFilesDir image

(null),contact . get first name();

fileoutput stream output stream = new file output streamimage

(输出文件);

字节[] key =加密。generate keyimage

("randomtext".getBytes());

outputStream.write(encrypt(key,contact . getbytes()));

output stream . close();

} catch(file notfounindexception e)}

Log.e(标签,“找不到文件”);

} catch(io exception e)}

Log.e(标记,“io exception”);

}

} else {

Log.e(标签,“以读/写模式打开媒体卡时出错!”);

}

}

公共静态 void saveLocation(上下文上下文,位置位置){

if (isReadWrite()) {

尝试{

文件输出文件 = 新文件(context.getExternalFilesDir image

(null)、location . geti identifier();

fileoutput stream output stream = new file output streamimage

(输出文件);

字节[] key =加密。generate keyimage

("randomtext".getBytes());

outputStream.write(encrypt(key,location . getbytes()));

output stream . close();

} catch(file notfounindexception e)}

Log.e(标签,“找不到文件”);

} catch(io exception e)}

Log.e(标记,“io exception”);

}

} else {

Log.e(标签,“以读/写模式打开媒体卡时出错!”);

}

}

私有静态布尔值 isReadOnly() {

Log.e(标签,环境

。getexternalstragraestate());

回归环境。MEDIA_MOUNTED_READ_ONLY.equals(环境

。getexternalstragraestate());

}

私有静态布尔 isReadWrite() {

Log.e(标签,环境

。getexternalstragraestate());

回归环境。MEDIA_MOUNTED.equals(环境

。getexternalstragraestate());

}

私有静态字节[]加密(字节[]密钥,字节[]数据){

SecretKeySpec = new SecretKeySpec(key,“AES”);

密码密码;

字节[]塞浦路斯文本= null

尝试{

cipher = cipher . getinstance(" AES ");

cipher.init(密码。ENCRYPT_MODE,sKeySpec);

密文= cipher.doFinal(数据);

} catch(nosuchcalgorithexception e)}

Log.e(标记“nosuchcalgorithexception”);

} catch(nosuchtpaddingexception e)}

Log.e(标记“nosuchcpaddingexception”);

} catch(非法块异常 e)}

Log.e(标记," illegal block exception ");

} catch(badpaddingexception e)}

Log.e(标签," badpaddingexception ");

} catch(invalid key exception e)}

Log.e(标记," invalidkeyexception ");

}

返回密文;

}

}

锻炼

There are many ways to encrypt the data in our Proxim application. What I have done is to encrypt it at storage time. Your exercise is to rewrite the Proxim application so that the data is encrypted as soon as it is created.Tip Do not modify the SaveController.java file. Look elsewhere.Use the Android API reference and write a simple decryption routine based on the same principle as the encryption routine. Create a new class called LoadController that will handle the loading of information from the SD Card.

摘要

在移动设备上存储纯文本或其他容易阅读的数据是你应该不惜一切代价避免的事情。即使您的应用本身可能是安全编写的,来自设备上完全不同区域的间接攻击仍然可以收集和读取您的应用编写的敏感或个人信息。在应用设计期间,请遵循以下基本步骤:

  1. 首先,确定应用存储、创建或交换什么数据类型。接下来,将它们分类为个人数据或敏感数据,这样您将知道在应用执行期间如何处理这些数据。
  2. 拥有一个可以在应用中重用的加密例程集合。最好将此集合保存为一个单独的库,可以包含在项目中。
  3. 为您编写的每个应用生成一个不同的密钥。编写一个好的密钥生成器算法,创建冗长且不可预测的密钥。
  4. 在创建或存储时加密数据。

三、Android 安全架构

在第二章中,我们看了一个如何使用加密保护信息的简单例子。然而,这个例子没有利用 Android 内置的安全和权限架构。在这一章中,我们将看看 Android 在安全性方面能够为开发者和最终用户提供什么。我们还将了解一些可能发生在应用上的直接攻击,以及如何采取必要的保护措施来最大限度地减少私有数据的丢失。

Android 平台有几个控制系统和应用安全性的机制,它试图确保每个阶段的应用隔离和划分。Android 中的每个进程都有自己的特权集,如果没有最终用户提供的明确许可,任何其他应用都无法访问该应用或其数据。尽管 Android 向开发人员公开了大量的 API,但如果不要求最终用户授权访问,我们就无法使用所有这些 API。

重新审视系统架构

让我们从再次查看 Android 架构开始。我们在第一章中讨论了 Android 系统架构,在那里你会记得每个进程都运行在它自己的隔离环境中。除非明确允许,否则应用之间不可能进行交互。能够进行这种交互的机制之一是使用权限。再次在第一章中,我们看了一个简单的例子,我们需要设置 RECORD_AUDIO 权限,这样我们的应用就可以使用设备的麦克风。在这一章中,我们将更详细地了解权限架构(见图 3-1 )。

9781430240624_Fig03-01.jpg

图 3-1 。Android 系统架构

图 3-1 描绘了一个比第二章中的 Android 架构更简单的版本;具体来说,该图更侧重于应用本身。

正如我们之前看到的,Android 应用将在 Dalvik 虚拟机(DVM)上执行。DVM 是字节码或者最基本的代码块将执行的地方。它类似于今天存在于个人计算机和服务器上的 Java 虚拟机(JVM)。如图图 3-1 所示,每个应用——甚至是内置系统应用——都将在自己的 Dalvik 虚拟机实例中执行。换句话说,它在一个有围墙的花园中运行,没有其他应用之间的外部交互,除非明确允许。由于启动单个虚拟机可能非常耗时,并且可能会增加应用启动和启动之间的延迟,因此 Android 依靠预加载机制来加速该过程。这个过程被称为合子,它有两个功能:首先作为新应用的发射台;第二,作为所有应用在其生命周期中都可以引用的实时核心库的存储库。

Zygote 进程 负责启动虚拟机实例,并预加载和预初始化虚拟机所需的任何核心库类。然后,它等待接收应用启动的信号。合子进程在引导时启动,工作方式类似于队列。任何 Android 设备都会运行一个主要的 Zygote 进程。当 Android Activity Manager 收到启动应用的命令时,它会调用作为 Zygote 进程一部分的虚拟机实例。一旦这个实例被用来启动应用,一个新的实例就会被派生出来取代它的位置。启动的下一个应用将使用这个新的 Zygote 过程,依此类推。

Zygote 进程 的存储库部分将始终使核心库集在应用的整个生命周期中可用。图 3-2 显示了多个应用如何利用主 Zygote 进程的核心库。

9781430240624_Fig03-02.jpg

图 3-2 。应用如何使用 Zygote 的核心库库

了解权限体系结构

正如我们在第一章中所讨论的,运行在 Android 操作系统上的应用都使用它们自己的一组用户和组标识符(分别是 UID 和 GID)。应用执行的受限方式使得一个应用无法从另一个应用读取或写入数据。为了促进应用之间的信息共享和进程间通信,Android 使用了一个权限系统。

默认情况下,某个应用无权执行任何类型的活动,这些活动可能会对设备上的其他应用造成损害或严重影响。它也不能与 Android 操作系统交互,也不能调用任何受保护的 API 来使用相机、GPS 或网络堆栈。最后,默认应用无法读取或写入任何终端用户的数据。Linux 内核处理这项任务。

为了让一个应用访问高特权 API 或者甚至获得对用户数据的访问,它必须获得最终用户的许可。作为开发人员,在向公众发布应用之前,您必须了解应用需要哪些权限。一旦你列出了所有你需要的权限,你需要把它们添加到你的 AndroidManifest.xml 文件中。然后,当首次安装应用时,设备会提示终端用户根据应用的要求授予或拒绝特定权限。因此,一个好的做法是以这样一种方式开发应用,如果用户不提供特定的权限,该方式将在模块化上失败。例如,假设您编写了一个使用 GPS 位置查询、访问用户数据和发送 SMS 消息的应用。最终用户授予您的应用三种权限中的两种,但不包括 SMS 消息发送。您应该能够编写这样的应用,即需要 SMS 发送的功能将会自行禁用(除非忽略此权限会破坏整个应用)。这样,最终用户仍然可以使用功能减少的应用。

在进一步探索权限之前,您需要熟悉 Android 软件开发和安全环境中使用的几个主题:内容供应器意图 。虽然您很可能已经听说过这些术语,但还是让我们在这里过一遍,以确保您的理解是完整的。

内容供应器

内容提供者与数据存储同义。它们充当应用可以读写的信息库。由于 Android 架构不允许公共存储区域,内容供应器是应用交换数据的唯一方式。作为开发人员,您可能对创建自己的内容提供者感兴趣,这样其他应用就可以访问您的数据。这就像在 android.content 包中子类化 ContentProvider 对象一样简单。我们将在本书的后续章节中更详细地介绍自定义 ContentProvider 对象的创建。

除了允许创建自己的内容供应器,Android 还提供了几个内容供应器,允许您访问设备上最常见的数据类型,包括图像、视频、音频文件和联系信息。Android provider 包, android.provider ,包含许多方便的类,允许你访问这些内容提供者;表 3-1 列出了这些。

表 3-1。 内容供应器类

类别名 描述
闹钟 包含一个意向动作和附加动作,可以用来启动一个活动,在闹钟应用中设置一个新的闹钟。
浏览器
浏览器。书签栏 在书签 _URI 提供的混合书签和历史项目的列定义。
浏览器。搜索列 搜索历史表的列定义,可从搜索 _URI 获得。
呼叫日志 包含有关已拨和已接呼叫的信息。
CallLog。通话次数 包含最近的通话。
接触冲突 联系人提供者和应用之间的合同。
接触冲突。聚合知觉〔〕 联系人聚合例外表的常数,该表包含覆盖自动聚合所用规则的聚合规则。
联系人联系人。常见数据种类 存储在 ContactsContract 中的通用数据类型定义的容器。数据表。
联系人联系人。CommonDataKinds.Email 表示电子邮件地址的数据类型。
联系人联系人。CommonDataKinds.Event 表示事件的数据类型。
联系人联系人。common data kinds . group membership 小组成员。
接触冲突。CommonDataKinds.Im 表示 IM 地址的数据类型。您可以使用为 ContactsContract 定义的所有列。数据,以及下面的别名。
联系人联系人。CommonDataKinds .昵称 表示联系人昵称的数据类型。
联系人联系人。CommonDataKinds.Note 关于联系人的注释。
联系人联系人。common data kinds . Organization 代表组织的数据类型。
联系人联系人。CommonDataKinds.Phone 代表电话号码的数据类型。
联系人联系人。CommonDataKinds.Photo 代表联系人照片的数据类型。
联系人联系人。公共数据类型.关系 表示关系的数据类型。
联系人联系人。CommonDataKinds.SipAddress 代表联系人的 SIP 地址的数据类型。
联系人联系人。common data kinds . structured name 表示联系人正确姓名的数据类型。
联系人联系人。common data kinds . structured postal 表示邮政地址的数据类型。
联系人联系人。CommonDataKinds.Website 表示与联系人相关的网站的数据类型。
接触冲突。联系人〔〕 Contacts 表的常量,该表包含代表同一个人的每个原始 Contacts 集合的记录。
联系人联系人。联系人.聚集建议 包含所有聚合建议(其他联系人)的单个联系人聚合的只读子目录。
接触冲突。联系人。日期 单个联系人的子目录,包含所有组成的 raw contactContactsContract。数据行
联系人联系人。联系人.实体 联系人的子目录,包含其所有的 contacts contact。原始联系人,以及联系人。数据行。
接触冲突。联系人。照片 单个联系人的只读子目录,包含该联系人的主要照片。
接触冲突。日期〔〕 包含与原始联系人关联的数据点的数据表的常数。
联系人联系人。目录 代表一组联系人。
联系人联系人。群组 组表的常数。
接触冲突。试〔〕 包含用于创建或管理涉及联系人的意图的助手类。
接触冲突。尝试插入〔〕 包含用于创建联系意图的字符串常量的便利类。
接触冲突。phone lookup〔〕 表示查找电话号码结果的表(例如,查找来电显示)。
接触冲突。快速联络〔〕 帮助器方法显示 QuickContact 对话框,允许用户在特定的联系人条目上旋转。
接触冲突。rawcontacts〔〕 原始联系人表的常量,该表包含每个同步帐户中每个人的一行联系人信息。
接触冲突。RawContacts .日期 单个原始联系人的子目录,包含其所有的 contacts contact。数据行。
接触冲突。RawContacts.Entity 单个原始联系人的子目录,包含其所有的 contacts contact。数据行。
接触冲突。rawcontact sensity〔〕 原始 contacts 实体表的常量,可以认为是数据表的 raw_contacts 表的外部连接。
联系人联系人。设置 各种账户的联系人特定设置
接触冲突。状态更新〔〕 状态更新链接到一个 ContactsContract。Data row 并通过相应的源捕获用户的最新状态更新。
接触冲突。SyncState 为同步适配器提供的用于存储专用同步状态数据的表。
LiveFolders 一个 LiveFolder 是一个特殊的文件夹,其内容由一个 ContentProvider 提供。
媒体商店 媒体提供程序包含内部和外部存储设备上所有可用媒体的元数据。
媒体商店。音频 所有音频内容的容器。
媒体商店。音频专辑 包含音频文件的艺术家。
媒体商店。音频艺术家 包含音频文件的艺术家。
媒体商店。音频.艺术家.专辑 每个艺术家的子目录,包含出现该艺术家歌曲的所有专辑。
媒体商店。音频类型 包含所有类型的音频文件。
媒体商店。音频.流派.成员 包含所有成员的每个流派的子目录。
媒体商店。音频媒体
媒体商店。音频.播放列表 包含音频文件的播放列表。
媒体商店。音频.播放列表.成员 包含所有成员的每个播放列表的子目录。
媒体商店。文件 媒体提供者表,包含媒体存储器中所有文件的索引,包括非媒体文件。
媒体商店。图像 包含所有可用图像的元数据。
媒体商店。图像.媒体
媒体商店。图像.缩略图 允许开发者查询获得两种缩略图: MINI_KIND (512 × 384 像素)和 MICRO_KIND (96 × 96 像素)。
媒体商店。视频
媒体商店。视频媒体
媒体商店。视频.缩略图 允许开发者查询获得两种缩略图: MINI_KIND (512 × 384 像素)和 MICRO_KIND (96 × 96 像素)。
搜索最近建议 提供对 searchrecentsuggestionprovider 的访问的工具类。
设置 包含全局系统级设备首选项。
设置。名称值表 名称/值设置表的公共库。
设置。安全 包含应用可以读取但不允许写入的系统偏好设置的安全系统设置。
设置。系统 包含各种系统偏好设置的系统设置。
同步状态合同 用于将数据与任何数据阵列帐户相关联的 ContentProvider 契约。
同步状态合同。常数〔〕
SyncStateContract。助手
用户词典 为输入法提供用户定义的单词,以用于预测文本输入。
用户词典。单词 包含用户定义的单词。

访问内容供应器需要预先了解以下信息:

  • 内容提供者对象(联系人、照片、视频等。)
  • 此内容提供者所需的栏
  • 获取此信息的查询

如前所述,内容供应器的行为方式类似于关系数据库,如 Oracle、Microsoft SQL Server 或 MySQL。当您第一次尝试查询时,这一点变得很明显。例如,您访问媒体商店。Images.Media 用于查询图像的内容供应器。假设我们想要访问存储在设备上的每个图像名称。我们首先需要创建一个内容供应器 URI 来访问设备上的外部商店:

Uri images = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;

接下来,我们需要为将要获取的数据创建一个 receiver 对象。简单地声明一个数组就可以做到这一点:

String[] details = new String[] {MediaStore.MediaColumns.DISPLAY_NAME};

为了遍历得到的数据集,我们需要创建并使用一个 managedQuery ,然后使用得到的 Cursor 对象来遍历行和列:

Cursor cur = managedQuery(details,details, null, null null);

然后我们可以使用我们创建的光标对象迭代结果。我们使用 cur.moveToFirst() 方法移动到第一行,然后读取图像名称,如下所示:

String name = cur.getString(cur.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME));

之后,我们通过调用 cur.moveToNext() 方法将光标移动到下一条记录。为了查询多条记录,这个过程可以包装在一个用于循环的中,或者包装在 do / while 块中。

请注意,有些内容提供者是受控制的,您的应用在试图访问它们之前需要请求特定的权限。

意图

意图是一个应用发送给另一个应用以控制任务或传输数据的消息类型。Intents 与三种特定类型的应用组件一起工作:活动、服务和广播接收器。让我们举一个简单的例子,您的应用需要启动 Android 设备浏览器并加载 URL 的内容。一个意图对象的一些主要组成部分包括意图动作和意图数据。对于我们的例子,我们希望我们的用户查看浏览器,所以我们将使用意图。ACTION_VIEW 常量来处理 URL 上的一些数据,【http://www.apress.com】。我们的意图对象将如下创建:

Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse([`www.apress.com);`](http://www.apress.com);)

为了调用这一意图,我们将这段代码称为:

startActivity(intent);

为了控制哪些应用可以接收 intent,可以在分派之前向 intent 添加权限。

检查权限

我们非常简要地介绍了内容提供者和意图,包括 Android 操作系统如何通过使用权限来控制对这些对象的访问。在第一章的中,我们看到了应用如何向最终用户请求与系统交互的特定权限。让我们看看权限检查实际上是如何进行的,以及在哪里进行的。

验证机制将处理 Android 操作系统中的权限检查。当您的应用进行任何 API 调用 时,权限验证机制将检查您的应用是否具有完成调用所需的权限。如果用户授予权限,则处理 API 调用;否则,抛出一个安全异常。

API 调用分三步处理。首先,调用 API 库。其次,该库将调用一个私有代理接口,该接口是 API 库本身的一部分。最后,这个私有代理接口将使用进程间通信来查询系统进程中运行的服务,以执行所需的 API 调用操作。这个过程如图 3-3 中的所示。

9781430240624_Fig03-03.jpg

图 3-3 。API 调用过程

在某些情况下,应用也可以使用本机代码来执行 API 调用。这些本机 API 调用也以类似的方式受到保护,因为除非通过 Java 包装器方法调用,否则不允许继续进行。换句话说,在调用本机 API 调用之前,它必须通过一个包装的 Java API 调用,然后该调用服从标准的权限验证机制。所有权限验证都由系统进程处理。此外,需要访问蓝牙、写 _ 外部 _ 存储和互联网权限的应用将被分配到一个 Linux 组,该组有权访问与这些权限相关的网络套接字和文件。这一小部分权限在 Linux 内核中进行验证。

使用自定义权限

Android 允许开发者创建和执行他们自己的权限。与系统权限一样,您需要在 AndroidManifest.xml 文件中声明特定的标记和属性。如果您编写的应用提供其他开发人员可以访问的特定类型的功能,您可以选择用自己的自定义权限来保护某些功能。

在您的应用的 AndroidManifest.xml 文件中,您必须如下定义您的权限:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.zenconsult.mobile.testapp" >
    <permission android:name="net.zenconsult.mobile.testapp.permission.PURGE_DATABASE"
        android:label="@string/label_purgeDatabase"
        android:description="@string/description_purgeDatabase"
        android:protectionLevel="dangerous" />
    ...
</manifest>

您在 android:name 属性中定义您的权限名称。需要 android:label 和 android:description 属性。它们是指向您在 AndroidManifest.xml 文件中定义的字符串的指针。这些字符串将标识权限是什么,并描述该权限对浏览设备上存在的权限列表的最终用户做了什么。您可能希望用一些描述性的内容来设置这些字符串,如下例所示:

<string name=" label_purgeDatabase ">purge the application database </string>
<string name="permdesc_callPhone">Allows the application to purge the core database of the information store. Malicious applications may be able to wipe your entire application information store.</string>

android:protectionLevel 属性是必需的。它将权限分类为前面讨论的四个保护级别之一。

或者,您也可以添加一个 android:permissionGroup 属性,让 android 将您的权限与系统组或您自己定义的组一起分组。将您的自定义权限与现有的权限组进行分组是最好的方法,因为这样,您可以在浏览权限时向最终用户呈现更清晰的界面。例如,要将 purgeDatabase 权限添加到访问 SD 卡的组中,您需要将以下属性添加到 AndroidManifest.xml 文件中:

android:permissionGroup=" android.permission-group.STORAGE"

需要注意的一点是,您的应用需要在任何其他依赖应用之前安装在设备上。通常是这种情况;但是在开发过程中,需要记住这一点,因为如果没有首先安装应用,您可能会遇到困难。

保护等级

创建您自己的权限时,您可以选择根据您希望操作系统提供的保护级别对权限进行分类。在前面的例子中,我们将清除数据库的权限的保护级别定义为“危险”。【危险】保护级别表示,通过授予该权限,最终用户将允许应用以可能对其产生不利影响的方式修改私人用户数据。

标有保护级别 【危险】或更高的权限将自动触发操作系统提示或通知最终用户。这种行为是为了让最终用户知道正在执行的应用有可能造成伤害。它还为用户提供了一个机会,通过授予或拒绝所请求的 API 调用来表明对应用的信任或不信任。权限保护级别的描述见表 3-2 。

表 3-2。 权限保护等级

常数 价值 描述
正常 0 一种低风险权限,允许应用访问独立的应用级功能,对其他应用、系统或用户的风险最小。系统会在安装时自动将这种权限授予发出请求的应用,而不需要用户的明确批准(尽管用户总是可以选择在安装前检查这些权限)。
危险 1 一种高风险权限,允许发出请求的应用以可能对用户产生负面影响的方式访问私有用户数据或控制设备。因为这种类型的权限会带来潜在的风险,所以系统可能不会自动将它授予请求应用。应用请求的任何危险许可都可以显示给用户,并在继续之前要求确认,或者可以采取一些其他方法,以便用户可以避免自动允许使用这些设施。
签名 2 只有当发出请求的应用使用与声明该权限的应用相同的证书签名时,系统才会授予该权限。如果证书匹配,系统会自动授予权限,而不通知用户或要求用户明确批准。
签字追踪系统 3 系统仅将此权限授予 Android 系统映像中使用相同证书签名的包。请避免使用此选项,因为签名保护级别应该足以满足大多数需求,并且无论应用安装在哪里,它都可以正常工作。此权限用于某些特殊情况,其中多个供应商将应用构建到系统映像中,并且这些应用需要显式共享特定功能,因为它们是一起构建的。

样本代码 为自定义权限

本节中的示例代码提供了如何在 Android 应用中实现自定义权限的具体示例。项目包和类结构如图 3-4 所示。

9781430240624_Fig03-04.jpg

图 3-4 。示例的结构和类

Mofest.java 文件包含一个名为 permissions 的嵌套类,该类保存将由调用应用调用的权限字符串常量。源代码在清单 3-1 中。

清单 3-1 。 最富阶层

net . Zen consult . libs;

公共类 Mofest {

public Mofest(){

}

公共类权限{

public permission(){

最终字符串清除 _ 数据库= image

”net . Zen consult . libs . mo fest . permission . purge _ DATABASE”;

}

}

}

此时,DBOps.java 文件并不重要,因为它不包含任何代码。ZenLibraryActivity.java 的文件包含了我们应用的入口点。它的源代码在清单 3-2 中给出。

清单 3-2 。【Zen library activity】类

net . Zen consult . libs;

导入Android . app . activity;

导入Android . OS . bundle;

公共类 ZenLibraryActivity 扩展 Activity {

/**首次创建活动时调用。*/

@覆盖

公见oncreate(bundle savedinstancestate)>

超级。oncreate(savedinstancestat):

set content view(r . layout .main);

}

}

同样,这个类没有做什么值得注意的事情;它启动了这个应用的主要活动。真正的变化在于这个项目的 AndroidManifest.xml 文件,如清单 3-3 所示。这是定义和使用权限的地方。

清单 3-3 。 项目的 AndroidManifest.xml 文件

【1.0】编码=【utf-8】?>

【http://schemas . Android . com/apk/RES/Android】

package =" net . Zen consult . libs "

android:版本代码=【1】

android:版本名称=【1.0】>

【10】/>

<权限 Android:name = " net . Zen consult . libs . mofest . permission . purge _ DATABASE"

Android:protection level ="危险

Android:label ="@ string/label _ purge database

Android:description ="@ string/description _ purge database

Android:permission group ="Android . permission-group。成本 _ 金钱 "/ >

<uses-permission Android:name ="net . Zen consult . libs . mofest . permission****image

。清除 _ 数据库/>

" Android . permission . set _ WALLPAPER "/>

" @ drawable/icon "Android:label =" @ string/app _ name ">

".禅藏馆活动"

Android:permission ="net . Zen consult . libs . mofest . permission****image

。清除 _ 数据库

Android:label =" @ string/app _ name "

《Android . intent . action . main》/>

" Android . intent . category . launcher "/>

如您所见,我们在这个应用中都声明并使用了 PURGE_DATABASE 权限。粗体显示的代码都与这个应用的自定义权限实现有关。

为了确保安装程序将提示权限请求屏幕,您必须将项目构建为。 apk 归档并签字。接下来,上传。apk 文件到网络服务器或复制到设备。单击此文件将启动安装过程;此时,设备将向最终用户显示权限请求屏幕。图 3-5 显示了这个屏幕的样子。

9781430240624_Fig03-05.jpg

图 3-5 。权限请求屏幕

摘要

在这一章中,我们讨论了 Android 权限,包括内置权限和自定义权限。我们还详细研究了意图、内容提供者以及如何检查权限。讨论的要点如下:

  • Android 有一套处理应用隔离和安全的核心机制。
  • 每个应用都将在自己的隔离空间中运行,具有唯一的用户和组标识符。
  • 应用不允许交换数据,除非它们明确请求用户的许可。
  • 内容供应器存储并允许访问数据。它们的行为类似于数据库。
  • 意图是在应用或系统进程之间发送的消息,用于调用或关闭另一个服务或应用。
  • 使用权限来控制对特定 API 的访问。权限分为四个类别,类别 1、2 和 3 权限将始终通知或提示最终用户。由于这些权限可能会对用户数据和体验产生负面影响,因此将它们交给用户进行最终确认。

可以创建自定义权限来保护您的单个应用。希望使用您的应用的应用需要通过使用 AndroidManifest.xml 文件中的 < uses-permission > 标签来明确请求您的许可。

四、概念实战:第一部分

在这一章中,我们将把前几章讨论过的所有主题合并在一起。如果您还记得,我们讨论过 Proxim 应用,通过它我们了解了数据加密。我们将在这里详细分析它的源代码。我们还将学习一些需要和使用权限的应用示例。

Proxim 应用

Proxim 项目的结构应类似于图 4-1 中的所示

9781430240624_Fig04-01.jpg

图 4-1 。Proxim 应用结构

让我们从活动开始,这是你的程序通常会开始的地方(见清单 4-1 )。在活动中,我们将创建一个新的联系人对象,其中包含一些信息。

清单 4-1 。 主要活动

package net.zenconsult.android;
import net.zenconsult.android.controller.SaveController;
import net.zenconsult.android.model.Contact;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class ProximActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        final Contact contact = new Contact();
        contact.setFirstName("Sheran");
        contact.setLastName("Gunasekera");
        contact.setAddress1("");
        contact.setAddress2("");
        contact.setEmail("sheran@zenconsult.net");
        contact.setPhone("12120031337");
    final Button button = (Button) findViewById(R.id.button1);
        button.setOnClickListener(new OnClickListener() {
           public void onClick(View v) {
              SaveController.saveContact(getApplicationContext(), contact);
           }
        });
    }
}

正是这一行创建了一个联系人对象:

Contact contact = new Contact();

在方法名开头设置了的代码行只需将相关数据添加到联系人对象中。要理解联系人对象的样子,请看一下清单 4-2 。如你所见,对象本身非常简单。它有一组gettersetter分别用于检索和插入数据。考虑一下名字变量。要将一个人的名字添加到该对象中,您需要调用 setFirstName() 方法,并传入一个类似于 Sheran 的值(如主活动所示)。

清单 4-2 。Proxim 应用的联系对象

package net.zenconsult.android.model;
public class Contact {
    private String firstName;
    private String lastName;
    private String address1;
    private String address2;
    private String email;
    private String phone;
    public Contact() {
    }
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    public String getAddress1() {
        return address1;
    }
    public void setAddress1(String address1) {
        this.address1 = address1;
    }
    public String getAddress2() {
        return address2;
    }
    public void setAddress2(String address2) {
        this.address2 = address2;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getPhone() {
        return phone;
    }
    public void setPhone(String phone) {
        this.phone = phone;
    }
    public String toString() {
        StringBuilder ret = new StringBuilder();
        ret.append(getFirstName()  +  "|");
        ret.append(getLastName()  +  "|");
        ret.append(getAddress1()  +  "|");
        ret.append(getAddress2()  +  "|");
        ret.append(getEmail()  +  "|");
        ret.append(getPhone()  +  "|");
        return ret.toString();
    }
    public byte[] getBytes() {
        return toString().getBytes();
    }
}

既然我们正在讨论数据存储对象(或者是模型-视图-控制器编程概念中的模型,那么让我们也来看看清单 4-3 中的位置对象。这又是一个普通的、日常的、简单明了的带有 getters 和 setters 的 Location 对象。

清单 4-3 。 定位物体

package net.zenconsult.android.model;
public class Location {
    private String identifier;
    private double latitude;
    private double longitude;
    public Location() {
    }
    public double getLatitude() {
        return latitude;
    }
    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }
    public double getLongitude() {
        return longitude;
    }
    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }
    public void setIdentifier(String identifier) {
        this.identifier = identifier;
    }
    public String getIdentifier() {
        return identifier;
    }
    public String toString() {
        StringBuilder ret = new StringBuilder();
        ret.append(getIdentifier());
        ret.append(String.valueOf(getLatitude()));
        ret.append(String.valueOf(getLongitude()));
        return ret.toString();
    }
    public byte[] getBytes() {
        return toString().getBytes();
    }
}

太棒了!我们已经解决了这个问题,现在让我们更仔细地看看我们的保存控制器和加密例程。我们可以分别在清单 4-4 和清单 4-5 中看到这些。

清单 4-4 。

package net.zenconsult.android.controller;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import net.zenconsult.android.crypto.Crypto;
import net.zenconsult.android.model.Contact;
import net.zenconsult.android.model.Location;
import android.content.Context;
import android.os.Environment;
import android.util.Log;
public class SaveController {
    private static final String TAG = "SaveController";
    public static void saveContact(Context context, Contact contact) {
        if (isReadWrite()) {
           try {
               File outputFile = new File(context.getExternalFilesDir(null),contact.getFirstName());
               FileOutputStream outputStream = new FileOutputStream(outputFile);
               byte[] key = Crypto.generateKey("randomtext".getBytes());
               outputStream.write(encrypt(key,contact.getBytes()));
               outputStream.close();
           } catch (FileNotFoundException e) {
               Log.e(TAG,"File not found");
           } catch (IOException e) {
               Log.e(TAG,"IO Exception");
           }
       } else {
       Log.e(TAG,"Error opening media card in read/write mode!");
       }
    }
    public static void saveLocation(Context context, Location location) {
        if (isReadWrite()) {
           try {
              File outputFile = new File(context.getExternalFilesDir(null),location.getIdentifier());
              FileOutputStream outputStream = new FileOutputStream(outputFile);
              byte[] key = Crypto.generateKey("randomtext".getBytes());
              outputStream.write(encrypt(key,location.getBytes()));
              outputStream.close();
           } catch (FileNotFoundException e) {
              Log.e(TAG,"File not found");
           } catch (IOException e) {
              Log.e(TAG,"IO Exception");
           }
        } else {
        Log.e(TAG,"Error opening media card in read/write mode!");
        }
    }
    private static boolean isReadOnly() {
        Log.e(TAG,Environment
              .getExternalStorageState());
        return Environment.MEDIA_MOUNTED_READ_ONLY.equals(Environment
              .getExternalStorageState());
    }
    private static boolean isReadWrite() {
        Log.e(TAG,Environment
              .getExternalStorageState());
        return Environment.MEDIA_MOUNTED.equals(Environment
              .getExternalStorageState());
    }
    private static byte[] encrypt(byte[] key, byte[] data){
        SecretKeySpec sKeySpec = new SecretKeySpec(key,"AES");
        Cipher cipher;
        byte[] ciphertext = null;
        try {
            Cipher = Cipher.getInstance("AES");
            Cipher.init(Cipher.ENCRYPT_MODE, sKeySpec);
            Ciphertext = cipher.doFinal(data);
        } catch (NoSuchAlgorithmException e) {
            Log.e(TAG,"NoSuchAlgorithmException");
        } catch (NoSuchPaddingException e) {
            Log.e(TAG,"NoSuchPaddingException");
        } catch (IllegalBlockSizeException e) {
            Log.e(TAG,"IllegalBlockSizeException");
        } catch (BadPaddingException e) {
            Log.e(TAG,"BadPaddingException");
        } catch (InvalidKeyException e) {
            Log.e(TAG,"InvalidKeyException");
        }
        return ciphertext;
    }
}

清单 4-5 。

package net.zenconsult.android.crypto;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

import android.util.Log;

public class Crypto {
 private static final String TAG = "Crypto";

    public Crypto() {
    }

    public static byte[] generateKey(byte[] randomNumberSeed) {
        SecretKey sKey = null;
        try {
            KeyGenerator keyGen = KeyGenerator.getInstance("AES");
            SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
            random.setSeed(randomNumberSeed);
            keyGen.init(256,random);
            sKey = keyGen.generateKey();
        } catch (NoSuchAlgorithmException e) {
            Log.e(TAG,"No such algorithm exception");
        }
        return sKey.getEncoded();
    }
}

总结

在这一章中,我们已经讨论了前几章中提到的两个关键概念:在存储数据之前加密数据和在应用中使用权限。具体来说,我们查看了两个包含这些概念的应用,并研究了使用不同参数运行每个应用的各种结果。数据加密的概念可能相当容易理解,但 Android 应用权限的话题可能不会立即显现出来。在大多数情况下,您需要的权限与访问设备本身的各种功能有关。这方面的一个例子是连通性。如果您的应用需要与互联网通信,那么您需要互联网权限。我们的示例应用更多地处理创建和使用自定义应用权限。现在,让我们继续讨论传输中的数据加密和 web 应用。**

五、数据存储和密码学

我们在第四章中非常简要地提到了密码学。本章将更多地关注使用加密技术来混淆和保护将要存储或传输的用户数据的重要性。首先,我们将介绍密码学的基础知识,以及它们在应用开发中的应用。接下来,我们将看看 Android 平台上存储数据的各种机制。在这个过程中,我将举例说明如何从不同的机制中存储和检索数据,并概述每个存储最适合执行的功能。

要记住的非常重要的一点是,除非你熟悉加密主题,否则你应该永远不要试图编写你自己的加密例程。我见过许多开发人员试图这样做,但最终都在移动设备和 web 应用中得到易受攻击的应用。密码学本身是一门庞大的学科;而且,在我看来,我认为最好是留给那些为这个主题奉献一生的人。作为一名应用开发人员,您只会对密码学中特定的主题子集感兴趣。

我不会涉及密码学的历史。您只需要记住一件事:让您的敏感用户数据对未经授权的用户不可读。如果攻击者使用间接或直接攻击危及你的应用,那么你的加密附加层(见图 5-1 )不会让他轻易窃取敏感的用户数据。相反,他有一个额外的层,他必须攻击。这一原则类似于美国国家安全局制定的深度防御的信息保证原则。

9781430240624_Fig05-01.jpg

图 5-1 纵深防御原则的一个例子

公钥基础设施

既然我们是在密码学的主题上,学习一点关于公钥基础设施(PKI)的知识是值得的。PKI 基于基于可信第三方的身份和信任验证原则。让我们来看一个说明相关原理的场景。请记住,这个例子暂时与应用开发无关。我们很快就会深入探讨这个问题。

Krabs 先生拥有 Krusty Krab,这是该市最受欢迎的快餐店之一。他把它受欢迎的原因归功于他著名的大钳蟹肉饼(一种非常湿润、美味的汉堡)。除了克莱伯先生,没有人知道大钳蟹馅饼的超级秘方。鉴于他的受欢迎程度,他最近开始向他的餐馆出售特许经营权。由于他的特许经营下的大多数新分店在地理上相距遥远,克莱伯先生决定通过快递将他的秘方发送给店主。这种方法的唯一问题是,克莱伯先生的竞争对手谢尔登·詹姆斯·普兰克以前曾试图窃取他的秘方,而且很可能还会再次尝试。

我喜欢食物,尤其是汉堡,所以我决定在我的城市开一家 Krusty Krab 连锁店。我联系了克莱伯先生。除了相关的文书工作,他还附上了一份文件,告诉我应该如何接收和保护他的大钳蟹馅饼秘方。我将省去无数页的细节和法律术语,而只列出最突出的要点。指令 声明我要做以下事情:

  1. 通过 IV 部门在最近的警察局注册 KK 项目。
  2. 从警察局第四部门领取一把配有一把钥匙的挂锁。
  3. 把挂锁交给我的警察部门。
  4. 用我的生命守护钥匙。
  5. 接收并打开将通过快递寄给我的钢盒。

果然,在我完成这些步骤后,一个包裹邮寄到了。奇怪的是,外部的纸板包装似乎被篡改了,但挂锁或里面的坚固的钢盒子却没有被篡改。钥匙很容易打开挂锁!我有大钳蟹馅饼的秘方。后来,我听克莱伯先生说,普兰克曾试图劫持并打开钢盒,但没有成功。这解释了我注意到的外包装篡改。

为了避免我的愚蠢,我将把这个故事中的角色和对象与 PKI 相关的元素联系起来(见表 5-1 )。

表 5-1 。故事和 PKI 的关系

故事元素 PKI 元素
克莱伯先生 消息发送者
消息接收者
浮游生物 袭击者
秘方 消息/敏感数据
钢盒子 加密的消息
我的挂锁 我的公钥
我挂锁的钥匙 我的私人钥匙
警察局 认证机构(CA)
KK 计划 CA 域
第四部 注册机构(RA)

当您查看表 5-1 时,很明显 PKI 的设置和运行非常复杂。然而,所有的元素都是必不可少的,并且服务于一个非常特定的目的,以确保消息密钥以一种安全可信的方式交换。我们来分析一下每一个元素。

  • Krabs 先生和我:他们分别是发送者和接收者。我们需要交换敏感数据(秘方),并遵循 PKI 策略和程序来这样做。
  • 浮游生物:他就是攻击者。他想要访问敏感数据,并决定在传输过程中攻击这些数据。
  • 秘方:这是敏感数据。我们想交换这份食谱,并且保密。
  • 钢盒:这是加密信息。发送方将加密或锁定它,这样只有密钥持有者才能打开它。钥匙持有者(我)是接收者。
  • 我的挂锁:这是我的公钥。当你思考这个故事时,你可能会想一把挂锁怎么也能是一把钥匙,但是从隐喻的角度来看。我的挂锁是任何人都可以用来锁定或加密信息的东西。我不怕给任何人我的挂锁或公钥,因为只有我能打开消息。我可以有无限数量的挂锁给任何想安全地给我发信息的人。
  • 我挂锁的钥匙:这是我的私人钥匙。它是私有的,因为没有其他人有副本。只有我能用这把钥匙打开我的挂锁。我必须时刻保护好这把钥匙,因为如果攻击者获得了这把钥匙,那么他就可以打开所有用我的挂锁锁住的钢盒,从而获得敏感数据。
  • 警察局:这是认证机构(CA)。作为 PKI 的基本组件之一,CA 相当于可信的第三方。Krabs 先生和我都信任我们当地的警察部门,因此他们是 CA 的优秀候选人。我们依靠他们来维护法律和诚信行事。因此,即使某个我不认识或从未见过面的人想要给我发送安全消息,我也不必担心是否信任这个人。我只需要相信告诉我这个人就是他所说的那个人的权威。
  • 对于我们的故事来说,这是一个 CA 域。例如,警察局或 CA 可以在许多不同的场景中充当可信的第三方。CA 域将确保所有事务发生在相同的上下文中。因此,KK 项目的存在只是为了处理克莱伯先生的特许经营权。
  • 第四部门:这是我们的注册机构(RA)。如果一个人想要发送或接收安全消息,他首先必须向 RA 注册。RA 将要求您用官方颁发的文件证明您的身份,如国民身份证或护照。RA 将确定该文件的真实性,并可能使用其他方法来确定该人是否是他所说的那个人。在令人满意地满足注册局的注册要求后,该人将被注册并获得一把公共和私人钥匙。

你可能会有这样一个问题:两个不同城市,甚至两个不同国家的两个警察部门是如何相互信任的?我们将假设所有警察部门通过内部机制建立信任,在一定程度上,许多部门可以作为一个实体。

总而言之,我和 Krabs 先生将使用可信任的第三方来确保我们避免向冒名顶替者发送或接收消息。那么攻击这个系统呢?攻击该系统有两种主要方式: 1) 攻击者可以试图欺骗注册过程并劫持合法用户的身份,以及 2) 攻击者可以试图对传输中的加密消息进行物理攻击。

这种基础设施的好处是,如果浮游生物试图冒充克莱伯先生或我,他必须通过欺骗 CA 的注册过程来这样做。在许多情况下,由于身份证明阶段,这很难完成。为了减轻在传输过程中对消息的物理攻击,系统采用了坚固的、牢不可破的锁。这些锁是使用的加密算法。

密码学中使用的术语

在这一章中,我要感谢 Bruce Schneier 和他的书应用密码学(约翰·威利&的儿子们,1996)。我在很多场合都提到过它,包括在写这本书的时候。它为密码学提供了很好的基础,并且非常全面。如果你想对密码学有更深入的了解,那么我强烈推荐这本书。

学习正确的密码学术语至关重要。没有学习正确的术语,你仍然可以掌握密码学,但是速度可能会慢一些。表 5-2 列出了在编写和保护你自己的应用时用到的密码学术语。

表 5-2 。密码学中使用的术语

学期 描述
纯文本/纯文本 这是你的信息。它是您编写的文本文件、您存储的用户数据,或者您希望防止他人窥探的原始消息。一般每个人都可读。
加密 该过程用于获取明文并使其不可读或模糊。
密文 这就是加密明文的结果。这是加密信息。
[通信]解密 这是加密的逆过程。这是一个将混乱的密文变回可读的明文的过程。
密码算法/算法/密码 这是用于加密和解密明文的特定类型的数学函数。
钥匙 该值将唯一地影响正在使用的加密或解密算法。可以有单独的密钥用于加密或解密。最常用的算法依赖于一个密钥来工作。
共享密钥/对称密钥 这是一个既能加密又能解密数据的密钥。发送方和接收方都有这个密钥;因此,它被定义为共享密钥
非对称密钥 这是指一个密钥用于加密,另一个密钥用于解密。您可以使用这种类型的密钥为特定的人加密数据。你所要做的就是用这个人的公钥加密数据,然后他可以用他的私钥解密。因此,有一个密钥用于加密(公钥),另一个用于解密(私钥)。
密码分析学 这是指在没有密钥或算法的先验知识的情况下破解密文的研究。

移动应用中的加密技术

为一般的、每天的应用实现 PKI 似乎有些矫枉过正,尤其是当您考虑到所涉及的工作量和复杂性时。当您考虑移动应用时,由于可用资源有限,您将面临更艰巨的任务。然而,这是可能的,2008 年在新加坡举行的第 11 届 IEEE 新加坡国际会议上发表了一篇详细介绍移动环境中轻量级 PKI(LPKI)理论的论文(【http://ieeexplore.ieee.org/xpl/freeabs_all.jsp?arnumber = 4737164】)。

但是我们不会在任何应用中使用 PKI 或 LPKI。相反,我们将试图找到一个平衡点,并以一种适合移动计算环境的有限资源的方式使用来自密码学的技术。因此,让我们检查一下我们希望密码学如何适应我们的应用。正如我在前面章节中提到的,保护你的用户数据是至关重要的。如果你回头看一下第二章中关于联系对象加密的例子,你能确定我们使用的是什么类型的密钥吗?我们使用了高级加密标准 (AES)算法。这是一个对称密钥算法,因为加密和解密只有一个密钥。如果你仔细观察,你会开始质疑我使用一个随机的 256 位密钥的合理性。你可能会问,如果我们一开始只是用一个随机密钥来加密数据,我们如何解密数据?我希望你在第二章的结尾的练习中回答了这个问题。如果您还没有,那么让我们现在就着手解决这个问题。

对称密钥算法

AES 是对称密钥算法 或分组密码。正如我们所见,这意味着在加密和解密中只使用一个密钥。算法用于加密或解密数据。如何处理这些数据导致了对称算法的进一步划分。例如,我们可以一次处理固定数量的数据位,称为数据块;或者我们可以一次处理一位数据,称为流。这种区别给了我们分组密码和流密码。通常,AES 被认为是对 128 位长的数据组进行操作的分组密码。128 位长的明文块将具有相同长度的密文块。AES 允许 0 到 256 位的密钥大小。在我们的例子中,我们使用了最大密钥大小。对于这本书,我将使用 AES 分组密码。我已经在表 5-3 中包含了一些 Android 自带的其他著名的分组密码。为其他分组密码生成密钥的原理与清单 5-1 中的一样,在下一节中显示。只需将 AES 的 key generator . getinstance()方法中的算法名称替换为表中列出的一种分组密码。

表 5-3 。可以在 Android 2.3.3 中使用的分组密码

分组密码 块大小 密钥大小(位)
俄歇电子能谱 128 位 0–256
山茶 128 位 128, 192, 256
河豚 64 位 0–448
双鱼 128 位 128, 192, 256

密钥生成

密钥是加密技术不可或缺的一部分。大多数现代加密算法都需要密钥才能正常工作。在我们第二章的例子中,我使用了一个伪随机数发生器(PRNG)来生成我们的加密密钥(见清单 5-1 )。我使用的一个好的经验法则是总是选择算法的最大密钥大小。如果我在测试时发现我的应用严重滞后,那么我会将密钥减小到下一个更小的值。在密码学中,你总是希望为你的算法使用尽可能大的密钥长度。这样做的原因是为了更难对您的密钥进行暴力攻击。

为了说明,让我们假设您选择了 16 位的密钥大小。这意味着攻击者必须尝试 1 和 0 的组合总共 2 16 或 65,536 次。然而,如果你选择了完整的 256 位密钥大小,那么攻击者必须进行 2 次 256 或 11.6 次 77 (1.16e77)尝试来破解你的密钥,这将花费他几年的时间。当然,这个持续时间可以随着计算能力的进步而减少,但这在密码分析的所有领域都是如此。因此,大的密钥大小和强大的算法确保了攻击者不能轻易破坏您的密文。

在大多数情况下,加密数据对追求唾手可得的果实的攻击者起着威慑作用。他们不会花时间去破解你的密码,而是会转向下一个容易被攻击的应用 — ,当然,前提是你的数据的价值不会超过攻击者愿意为破解你的密码而投入的时间、精力和资源的价值。

注意当攻击者通过基于不同字符集(如 A-Z、A-Z、0-9 和特殊字符)的组合连续创建和尝试密码,不断尝试猜测正确的密码时,就会发生对密钥或密码的暴力攻击。最终,在尝试所有可能的组合的过程中,她很可能猜出正确的密码。

我知道一些开发人员仍然认为加密密钥等同于密码。不是的。不完全是。在我们的密钥生成示例中,我们使用一个随机的 256 位密钥。一般来说,这些加密程序都发生在幕后;而且虽然用户密码可以变成密钥,但我不建议这么做。避免这样做的一个原因是,用户密码几乎总是不超过 10 到 12 个字节,这甚至还不到密钥长度的一半(256 / 8 = 32 个字节)。根据我们对暴力攻击的了解,最好选择允许的最大密钥长度。

清单 5-1。 一种密钥生成算法

**public static****byte[]** generateKey(byte[] randomNumberSeed) {
                SecretKey sKey  =  null;
                **try** {
                     KeyGenerator keyGen  =  KeyGenerator.*getInstance*("AES");
                     SecureRandom random  =  SecureRandom.*getInstance*("SHA1PRNG");
                     random.setSeed(randomNumberSeed);
                     keyGen.init(256,random);
                     sKey  =  keyGen.generateKey();
                } **catch** (NoSuchAlgorithmException e) {
                     Log.*e*(*TAG*,"No such algorithm exception");
                }
                **return** sKey.getEncoded();
        }

数据填充

到目前为止,我已经讨论了处理固定数据块大小的对称算法。但是,当您的数据小于算法要求的输入块大小时会出现什么情况呢?考虑图 5-2 中的情况。这里,我们有两个数据块,但其中只有一个包含完整的块大小(为了简化,我们将使用 8 字节的块大小);第二个仅包含 4 位。如果我们用 AES 算法运行这最后一块,它会失败。为了应对这种情况,有几种不同的填充选项可用。

9781430240624_Fig05-02.jpg

图 5-2 两个数据块没有正确对齐

当你遇到图 5-2 中的情况时,你的第一个想法可能是用零填充剩余的 4 位。这是可能的,被称为零填充。存在其他不同的填充选项。在这一点上,我不会说得太详细,但是你需要记住,你不能简单地将明文通过分组密码。分组密码总是以固定的输入块大小工作,并且总是具有固定的输出块大小。图 5-3 和 5-4 显示了零填充和 PKCS5/7 填充的例子。

9781430240624_Fig05-03.jpg

图 5-3 。两个带零填充的数据块。填充以粗体显示。

9781430240624_Fig05-04.jpg

图 5-4 。具有 PKCS5/7 填充的两个数据块。填充以粗体显示。

注意 PKCS5/7 填充是取需要填充的剩余位的长度,并将其用作填充位。例如,如果还剩下 10 位来将块填充到正确的大小,则填充位为 0A(十六进制为 10)。类似地,如果有 28 位要填充,那么填充位将是 1C。

我在第二章中的例子没有指定任何填充。默认情况下,Android 将使用 PKCS5 填充。

分组密码的操作模式

分组密码有各种加密和解密机制。最简单的加密形式是将一个明文块加密成一个密文块。然后对下一个明文块进行加密,得到下一个密文块,依此类推。这被称为电子代码簿(ECB)模式。 图 5-5 显示了 ECB 加密的可视化表示。

9781430240624_Fig05-05.jpg

图 5-5 。ECB 加密(维基百科提供)

尽管简单,ECB 模式并不能防止模式识别密码分析。这意味着如果消息文本包含两个相同的明文块,那么也将有两个对应的密文块。进行密码分析时,使用的技术之一是识别和定位密文中的模式。在模式被识别之后,可以更容易地推断出使用了 ECB 加密,因此,攻击者只需要专注于解密密文的特定块。他不需要解密整个消息。

为了防止这种情况,分组密码有几种其他的操作模式: 1) 密码分组链接(CBC)2)传播密码分组链接(PCBC)3)密码反馈(CFB)和 4) 输出反馈(OFB)。在这一节中,我只介绍加密例程(只需颠倒加密模式中的步骤就可以得到解密例程):

  • CBC 模式 : 密码块链接模式(见图 5-6 )使用一个称为初始化向量 (IV)的附加值,该值用于对第一个明文块执行 XOR 运算。在此之后,每个结果密文块与下一个明文块进行异或运算,依此类推。这种类型的模式确保每个结果密文块依赖于前一个明文块。【??

    9781430240624_Fig05-06.jpg

    图 5-6 CBC 加密(维基百科提供)

  • PCBC 模式 : 传播密码块链接模式(见图 5-7 )与 CBC 模式非常相似。不同之处在于,PCBC 模式不是仅对第一块的 IV 和后续块的密文进行异或运算,而是对第一块的 IV 密文进行异或运算,然后对附加块的明文密文进行异或运算。这种模式的设计使得密文中的微小变化会在整个加密或解密过程中传播。【??

    9781430240624_Fig05-07.jpg

    图 5-7 PCBC 加密(维基百科提供)

  • *模式 : 密码反馈模式(参见图 5-8 )在 CBC 模式的 IV 和明文之间切换位置。因此,不是将明文异或并加密,随后将密文与明文异或;CFB 模式将首先加密 IV,然后将其与明文进行 XOR 运算以获得密文。然后,对于后续的块,密文再次被加密,并与明文进行异或运算,以给出下一个密文块。

    9781430240624_Fig05-08.jpg

    图 5-8T19 .中心纤维体加密(维基百科提供

    )*
    ** *模式 : 输出反馈模式(见图 5-9 )与 CFB 模式非常相似。不同之处在于,它不是使用 XORd IV 和密文,而是在 xor 运算发生之前使用。因此,对于第一个块,IV 用密钥加密,并用作下一个块的输入。然后,来自第一块的密文与第一块明文进行异或运算。在 xor 运算之前,使用前一个块的密文进行后续加密。【??

    9781430240624_Fig05-09.jpg

    图 5-9OFB 加密(维基百科提供)**

XOR (用符号^表示)是逻辑运算异或(又称异或)的标准缩写。其真值表如下:

0 ^ 0 = 0

0 ^ 1 = 1

1 ^ 0 = 1

1 ^ 1 = 0

如果您查看我的原始示例,您会发现我没有使用特定的加密模式。默认情况下,Android 将使用 ECB 模式来执行加密或解密。作为开发人员,您可以选择更复杂的加密模式,如 CBC 或 CFB。

现在,您对 AES 对称算法的内部工作原理有了更多的了解,我将向您展示如何在加密时更改填充和操作模式。回到我们最初的例子,将代码改为与清单 5-2 中的相同。请注意粗体代码行。我们只做了几处改动。首先我们把 AES 改成了 AES/CBC/pkcs 5 padding;其次,我们将初始化向量(IV)添加到我们的 init() 方法中。正如我之前提到的,当您只指定 AES 编码时,Android 将使用的默认模式是 AES/ECB/PKCS5Padding。您可以通过运行程序两次来验证这一点,一次使用 AES,一次使用 AES/ECB/PKC5Padding。两者都会给你相同的密文。

清单 5-2。 用 CBC 加密模式重做加密程序

private static byte[] encrypt(byte[] key, byte[] data, byte[] iv){
                SecretKeySpec sKeySpec  =  new SecretKeySpec(key,"AES");
                Cipher cipher;
                byte[] ciphertext  =  null;
                try {
                     **cipher**  =  **Cipher.getInstance("AES/CBC/PKCS5Padding");**
                     **IvParameterSpec ivspec**  =  **new IvParameterSpec(iv);**
                     cipher.init(Cipher.ENCRYPT_MODE, sKeySpec, ivspec);
                     ciphertext  =  cipher.doFinal(data);
                } catch (NoSuchAlgorithmException e) {
                     Log.e(TAG,"NoSuchAlgorithmException");
                } catch (NoSuchPaddingException e) {
                     Log.e(TAG,"NoSuchPaddingException");
                } catch (IllegalBlockSizeException e) {
                     Log.e(TAG,"IllegalBlockSizeException");
                } catch (BadPaddingException e) {
                     Log.e(TAG,"BadPaddingException");
                } catch (InvalidKeyException e) {
                     Log.e(TAG,"InvalidKeyException");
             }
             return ciphertext;

        }

假设您选择了自己选择的密钥。你可以编写一个类似于清单 5-3 所示的程序,而不是使用随机数生成器来生成你的密钥。在这个清单中, stringKey 是用来加密数据的密钥。

清单 5-3。 修改了固定键值的密钥生成示例

**public static byte**[] generateKey(String stringKey) {
                **try** {
                     SecretKeySpec sks  =  **new**
                     SecretKeySpec(stringKey.getBytes(),"AES");

                } **catch** (NoSuchAlgorithmException e) {
                     Log.e(TAG,"No such algorithm exception");
                }
                **return** sks.getEncoded();
  }

```**  **Android 中的数据存储

我想在一章中涵盖密码学和数据存储的主题,因为我相信你可以将两者联系起来,提供一个更安全的应用。Android 在独立的安全环境中运行应用。这意味着每个应用将使用自己的 UID 和 GID 运行;当一个应用写入数据时,其他应用将无法读取该数据。如果您想要在应用之间共享数据,那么您将需要通过使用内容提供者来显式地启用这种共享。我可以看到你的问题正在形成:“如果 Android 已经保护了数据,为什么还要涵盖所有的加密主题?”正如我在本章开始时提到的,我们可以在 Android 安全层上建立另一层安全,只是为了那些不可预见的漏洞、病毒或木马抬头的时候。

Android 允许你使用五种不同的选项来存储数据(参见表 5-4 )。显然,您需要根据您的需求决定在哪里存储您的特定于应用的数据。

表 5-4。的机制将的数据存储在 Android 上

| 存储方法 | 描述 | 数据保密 |
| --- | --- | --- |
| 共享偏好设置 | 允许您存储原始数据类型(例如, int 、 Boolean 、 float 、 long 和 String ),这些数据类型将在整个设备会话中保持不变。即使您的应用没有运行,您的数据也将持续存在,直到设备重新启动。 | 可以设置四种隐私模式: MODE_PRIVATE 、 MODE_WORLD_READABLE 、 MODE_WORLD_WRITABLE 和 MODE_MULTI_PROCESS 。默认模式是 MODE_PRIVATE |
| 内部存储器 | 允许您将数据存储在设备的内部存储器中。通常,其他应用甚至最终用户都无法访问这些数据。这是一个私人数据存储区。存储在此处的数据即使在设备重新启动后仍将存在。当最终用户删除你的应用时,Android 也会删除你的数据。 | 可以设置三种隐私模式: MODE_PRIVATE 、 MODE_WORLD_READABLE 和 MODE_WORLD_WRITABLE 。默认模式是模式 _ 私有。 |
| 外部存储器 | 存储在这里的数据是全球可读的。设备用户和其他应用可以读取、修改和删除这些数据。外部存储器与 SD 卡或设备内部存储器(不可移动)相关联。 | 默认情况下,数据是全局可读的。 |
| SQLite 数据库 | 如果您需要为您的应用创建一个数据库来利用 SQLite 的搜索和数据管理功能,请使用 SQLite 数据库存储机制。 | 应用中的任何类都可以访问您创建的数据库。外部应用无法访问该数据库。 |
| 网络连接 | 您可以通过 web 服务远程存储和检索数据。你可以在第六章中读到更多这方面的内容。 | 基于您的 web 服务设置。 |

选择哪种机制来存储数据在很大程度上取决于您的需求。在第二章中查看我们的 Proxim 应用,我们也可以考虑将我们的数据存储在 SQLite 数据库中,因为这将使我们免于不必要地决定实施数据结构。让我们看几个例子,看看如何使用这些机制来存储和检索数据。

共享偏好设置

共享偏好设置对于储存应用设置非常有用,这些设置在设备重新启动之前一直有效。顾名思义,存储机制最适合保存用户对应用的偏好。假设我们必须存储关于电子邮件服务器的信息,我们的应用需要从该服务器检索数据。我们需要存储邮件服务器的主机名、端口以及邮件服务器是否使用 SSL。我已经给出了存储(见清单 5-4 )和检索(见清单 5-5)数据到共享首选项的基本代码。 StorageExample1 类将所有这些放在一起(参见清单 5-6 ),伴随的输出显示在图 5-10 中。

***清单 5-4。*** 将 数据存储到 SharedPreferences 的代码

```java
package net.zenconsult.android;

import java.util.Hashtable;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.preference.PreferenceManager;

public class StoreData {
        public static boolean storeData(Hashtable data, Context ctx) {
                SharedPreferences prefs  =  PreferenceManager
                     .getDefaultSharedPreferences(ctx);
                String hostname  =  (String) data.get("hostname");
                int port  =  (Integer) data.get("port");
                boolean useSSL  =  (Boolean) data.get("ssl");
                Editor ed  =  prefs.edit();
                ed.putString("hostname", hostname);
                ed.putInt("port", port);
                ed.putBoolean("ssl", useSSL);
                return ed.commit();
        }
}

清单 5-5。 从 SharedPreferences 中检索 数据的代码

package net.zenconsult.android;

import java.util.Hashtable;

import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;

public class RetrieveData {
        public static Hashtable get(Context ctx) {
                String hostname  =  "hostname";
                String port  =  "port";
                String ssl  =  "ssl";

                Hashtable data  =  new Hashtable();
                SharedPreferences prefs  =  PreferenceManager
                     .getDefaultSharedPreferences(ctx);
                data.put(hostname, prefs.getString(hostname, null));
                data.put(port, prefs.getInt(port, 0));
                data.put(ssl, prefs.getBoolean(ssl, true));
                return data;
        }
}

清单 5-6。 StorageExample1,主类

package net.zenconsult.android;

import java.util.Hashtable;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.widget.EditText;

public class StorageExample1Activity extends Activity {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.main);
                Context cntxt  =  getApplicationContext();

                Hashtable data  =  new Hashtable();
                data.put("hostname", "smtp.gmail.com");
                data.put("port", 587);
                data.put("ssl", true);

                if (StoreData.storeData(data, cntxt))
                     Log.i("SE", "Successfully wrote data");
                else
                     Log.e("SE", "Failed to write data to Shared Prefs");

                EditText ed  =  (EditText) findViewById(R.id.editText1);
                ed.setText(RetrieveData.get(cntxt).toString());
        }

}

9781430240624_Fig05-10.jpg

图 5-10T3。StorageExample1 应用的输出

内存储器

正如我们所见, SharedPreferences 非常适合键值对数据类型。这有点类似于一个散列表或者甚至是标准的 Java 属性对象。 SharedPreferences 机制的限制是您只能存储原始数据类型。你将无法存储更复杂的类型,如向量或哈希表。如果你想存储原始类型之外的数据,你可以看看内存。内部存储机制将允许您通过输出流写入数据。因此,任何可以序列化为字节字符串的对象都可以写入内部存储。让我们首先创建我们的 StorageExample2 类(参见清单 5-7 )。和以前一样,我在单独的清单中展示了存储和检索模块(分别参见清单 5-8 和清单 5-9 )。图 5-11 显示了输出。

清单 5-7。 StorageExample2,主类

package net.zenconsult.android;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.widget.EditText;

public class StorageExample2Activity extends Activity {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.main);

                Context ctx  =  getApplicationContext();

                // Store data
                Contact contact  =  new Contact();
                contact.setFirstName("Sheran");
                contact.setLastName("Gunasekera");
                contact.setEmail("sheran@zenconsult.net");
                contact.setPhone("  +  12120031337");

                StoreData.storeData(contact.getBytes(), ctx);

                // Retrieve data

                EditText ed  =  (EditText) findViewById(R.id.editText1);
                ed.setText(new String(RetrieveData.get(ctx)));

        }
}

清单 5-8。 使用 StoreData.java 将数据存储在内部存储器中

package net.zenconsult.android;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import android.content.Context;
import android.util.Log;

public class StoreData {
        public static final String file  =  "contacts";

        public static void storeData(byte[] data, Context ctx) {

                try {
                     FileOutputStream fos  =  ctx.openFileOutput(file, ctx.MODE_PRIVATE);
                     fos.write(data);
                     fos.close();
                } catch (FileNotFoundException e) {
                     Log.e("SE2", "Exception: "  +  e.getMessage());
                } catch (IOException e) {
                     Log.e("SE2", "Exception: "  +  e.getMessage());
                }
        }
}

清单 5-9。 使用 RetrieveData.java 从内存中检索数据

package net.zenconsult.android;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

import android.content.Context;
import android.util.Log;

public class RetrieveData {
        public static final String file  =  "contacts";

        public static byte[] get(Context ctx) {
                byte[] data  =  null;
                try {
                     int bytesRead  =  0;
                     FileInputStream fis  =  ctx.openFileInput(file);
                     ByteArrayOutputStream bos  =  new ByteArrayOutputStream();
                     byte[] b  =  new byte[1024];
                     while ((bytesRead  =  fis.read(b)) !  =  -1) {
                     bos.write(b, 0, bytesRead);
                     }
                     data  =  bos.toByteArray();

                } catch (FileNotFoundException e) {
                     Log.e("SE2", "Exception: "  +  e.getMessage());
                } catch (IOException e) {
                     Log.e("SE2", "Exception: "  +  e.getMessage());
                }
                return data;
        }

}

9781430240624_Fig05-11.jpg

图 5-11T3。StorageExample2 应用的输出

注意清单 5-7 中的使用了 Proxim 示例中旧的联系人对象来存储数据。

SQLite 数据库

我将跳过外部存储的例子,因为您已经知道如何在外部存储数据(例如,看看 Proxim 应用的源代码)。它将其所有数据存储在外部存储器中。相反,让我们关注如何使用 Android 的 SQLite 数据库对象创建、存储和检索数据。我将创建一个数据库表,我们可以用它来存储来自 Proxim 应用的联系人对象。表 5-5 显示了工作台的布局。我采取了简单的方法,将所有列指定为文本。当您创建自己的表时,请确保根据您的数据类型指定数字、日期或时间列。

表 5-5。Contacts db SQLite 数据库中的联系人表 ??

列名 列数据类型
名字 正文
姓氏 正文
电子邮件 正文
电话 正文
地址 1 正文
地址 2 正文

在您的开发环境中创建一个名为 StorageExample3 的新项目,其结构如图 5-12 所示。如果需要联系人对象,从 Proxim 示例中复制它。

9781430240624_Fig05-12.jpg

图 5-12T3。存储示例 3 项目结构

StorageExample3 类显示了使用 SQLite 数据库的主类 ,创建了一个包含数据的联系人对象(参见清单 5-10 )。清单 5-11 显示了一个可以用来操作 SQLite 数据库的助手类,而清单 5-12 显示了如何使用一个类将数据从 Contact 对象写入数据库。最后,图 5-13 展示了如何从 SQLite 数据库中获取数据并返回一个联系对象。一旦您有机会仔细阅读这些清单,我们将仔细看看这段代码的每一部分是做什么的,以及它是如何做到的。

清单 5-10。存储示例 3

package net.zenconsult.android;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.EditText;

public class StorageExample3Activity extends Activity {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.main);

                // Store data
                Contact contact  =  new Contact();
                contact.setFirstName("Sheran");
                contact.setLastName("Gunasekera");
                contact.setEmail("sheran@zenconsult.net");
                contact.setPhone("  +  12120031337");

                ContactsDb db  =  new ContactsDb(getApplicationContext(),"ContactsDb",null,1);
                Log.i("SE3",String.valueOf(StoreData.store(db, contact)));

                Contact c  =  RetrieveData.get(db);

                db.close();

                EditText ed  =  (EditText)findViewById(R.id.editText1);
                ed.setText(c.toString());

        }
}

清单 5-11。ContactsDB 助手类 处理我们的 SQLite 数据库

package net.zenconsult.android;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase.CursorFactory;

public class ContactsDb extends SQLiteOpenHelper {
        public static final String tblName  =  "Contacts";

        public ContactsDb(Context context, String name, CursorFactory factory,
                     int version) {
                super(context, name, factory, version);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
                String createSQL  =  "CREATE TABLE "  +  tblName
                     + " ( FIRSTNAME TEXT, LASTNAME TEXT, EMAIL TEXT,"
                     + " PHONE TEXT, ADDRESS1 TEXT, ADDRESS2 TEXT);";
                db.execSQL(createSQL);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
                // Use this to handle upgraded versions of your database
        }
}

清单 5-12。StoreData 类 将数据从联系人对象写入数据库

package net.zenconsult.android;

import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;

public class StoreData {
        public static long store(ContactsDb db, Contact contact) {
                // Prepare values
                ContentValues values  =  new ContentValues();
                values.put("FIRSTNAME", contact.getFirstName());
                values.put("LASTNAME", contact.getLastName());
                values.put("EMAIL", contact.getEmail());
                values.put("PHONE", contact.getPhone());
                values.put("ADDRESS1", contact.getAddress1());
                values.put("ADDRESS2", contact.getAddress2());

                SQLiteDatabase wdb  =  db.getWritableDatabase();
                return wdb.insert(db.tblName, null, values);
        }
}

清单 5-13。retrieve Data 类 从 SQLite 数据库中获取数据并返回一个联系对象

package net.zenconsult.android;

import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

public class RetrieveData {
        public static Contact get(ContactsDb db) {
                SQLiteDatabase rdb  =  db.getReadableDatabase();
                String[] cols  =  { "FIRSTNAME", "LASTNAME", "EMAIL", "PHONE" };
                Cursor results  =  rdb.query(db.tblName, cols, "", null, "", "", "");

                Contact c  =  new Contact();
                results.moveToLast();
                c.setFirstName(results.getString(0));
                c.setLastName(results.getString(1));
                c.setEmail(results.getString(2));
                c.setPhone(results.getString(3));
                return c;
        }
}

9781430240624_Fig05-13.jpg

图 5-13T3。StorageExample3 应用的输出

根据我的经验,我很少不得不使用平面文件来存储数据。除非我处理纯二进制数据(例如,照片、视频或音乐),否则我存储的大部分数据要么以键值对的形式存储,要么存储在 SQLite 数据库中。因此,我可以使用 Android 的 SharedPreferences 或 SQLiteDatabase 来做这件事。这两种机制都提供了非常好的可管理性,这是对我最大的吸引力。如果您以前没有使用过 SQLite 数据库,那么您可能需要考虑更深入地了解它。事实上,大多数现代移动操作系统,包括苹果的 iOS 和 RIM 的黑莓智能手机操作系统,都提供了对 SQLite 数据库的原生支持。好的一面是 SQLite 数据库非常便于移植,您可以在几乎任何主流操作系统上创建、读取和修改 SQLite 数据库,包括 Mac OS X、Linux 和 Windows。

让我们分析一下 StorageExample3 项目的源代码。清单 5-10 是主类,它创建了一个联系人对象,其中包含数据:

Contact contact  =  new Contact();
contact.setFirstName("Sheran");
contact.setLastName("Gunasekera");
contact.setEmail("sheran@zenconsult.net");
contact.setPhone("  +  12120031337");

接下来,它使用了 ContactsDb 类 ( 清单 5-11 ),该类子类化了 SQLiteOpenHelper 类:

ContactsDb db  =  new ContactsDb(getApplicationContext(),"ContactsDb",null,1);

如果你想创建自己的数据库,那么子类化 SQLiteOpenHelper 是一个不错的选择。然后,代码使用 StoreData 类的(清单 5-12 ) store() 方法保存刚刚创建的联系人对象。我们调用 store() 方法,并传递我们新创建的 SQLite 数据库和我们的 Contact 对象。 StoreData 将把联系人对象分解成内容值对象:

ContentValues values  =  new ContentValues();
values.put("FIRSTNAME", contact.getFirstName());
values.put("LASTNAME", contact.getLastName());
values.put("EMAIL", contact.getEmail());
values.put("PHONE", contact.getPhone());
values.put("ADDRESS1", contact.getAddress1());
values.put("ADDRESS2", contact.getAddress2());

提示如果您正在创建自己的数据对象,并且您知道您将使用 SQLite 数据库机制来存储您的数据,您可能想要考虑为您的数据对象扩展 ContentValues 。这使得在存储和检索数据时更容易传递给。

接下来,我们将这些值写入数据库表。 SQLiteOpenHelper 对象可以检索一个可写数据库或者一个可读数据库。当从表中插入或查询数据时,我们使用最合适的方法:

SQLiteDatabase wdb  =  db.getWritableDatabase();
return wdb.insert(db.tblName, null, values);

RetrieveData 类处理从数据库中检索数据。这里,我们只对插入的最后一行值感兴趣。在生产应用中,我们将迭代我们的光标来获取每一行:

SQLiteDatabase rdb  =  db.getReadableDatabase();
String[] cols  =  { "FIRSTNAME", "LASTNAME", "EMAIL", "PHONE" };
Cursor results  =  rdb.query(db.tblName, cols, "", null, "", "", "");

从表中获取数据后,我们重新创建一个返回的 Contact 对象:

Contact c  =  new Contact();
results.moveToLast();
c.setFirstName(results.getString(0));
c.setLastName(results.getString(1));
c.setEmail(results.getString(2));
c.setPhone(results.getString(3));
return c;

输出(见图 5-13 )看起来和前面的例子一样。

将数据存储与加密相结合

在这一章中,我们讨论了两个非常重要的问题,但是我们是分开讨论的。如果你尝试了第二章中的练习,那么你已经对我们下一步需要做什么有了一个公平的想法。我们可以清楚地看到,无论我们选择哪种存储机制,我们存储的任何数据都被放置在明文中。我们可以依靠 Android 来确保我们的数据不被未经授权的应用读取,但如果下周一种全新的病毒被释放到野外怎么办?这种病毒只影响 Android 手机,能够绕过 SQLite 数据库权限,读取设备上的所有数据库。现在,您保持数据隐私的唯一希望已经被破坏,您的所有数据都很容易被从您的设备上复制下来。

我们在前面的章节中讨论了这种攻击,并将它们归类为间接攻击。它们是间接的,因为病毒不会直接攻击您的应用。相反,它盯上了 Android 操作系统。目的是复制所有 SQLite 数据库,希望病毒作者可以复制存储在那里的任何敏感信息。然而,如果你增加了另一层保护,那么病毒作者看到的将是乱码数据。让我们构建一个可以在所有应用中重用的更永久的加密库。让我们首先创建一组简短的规范 :

  • 使用对称算法:我们的库将使用对称算法或分组密码来加密和解密我们的数据。我们将解决 AES,虽然我们应该能够在以后修改它。
  • 使用固定密钥:我们需要能够包含一个可以存储在设备上的密钥,用于加密和解密数据。
  • 存储在设备上的密钥:密钥将驻留在设备上。虽然从直接攻击的角度来看,这对我们的应用来说是一个风险,但它应该足以保护我们免受间接攻击。

让我们从我们的密钥管理模块开始(参见清单 5-14 )。因为我们计划使用一个固定的密钥,所以我们不需要像在过去的例子中那样生成一个随机的密钥。因此按键管理器将执行以下任务:

  1. 接受一个键作为参数( setId(byte[] data) 方法)
  2. 接受一个初始化向量作为参数( setIv(byte[] data) 方法)
  3. 将密钥存储在内部存储区的文件中
  4. 从内部存储的文件中检索密钥(方法 getId(byte[] data)
  5. 从内部存储的文件中检索 IV(方法 getIv(byte[] data)

清单 5-14。 按键管理器模块

package net.zenconsult.android.crypto;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import android.content.Context;
import android.util.Log;

public class KeyManager {
        private static final String TAG  =  "KeyManager";
        private static final String file1  =  "id_value";
        private static final String file2  =  "iv_value";

        private static Context ctx;

        public KeyManager(Context cntx) {
                ctx  =  cntx;
        }

        public void setId(byte[] data) {
                writer(data, file1);
        }

        public void setIv(byte[] data) {
                writer(data, file2);
        }

        public byte[] getId() {
                return reader(file1);
        }

        public byte[] getIv() {
                return reader(file2);
        }
        public byte[] reader(String file) {
                byte[] data  =  null;
                try {
                     int bytesRead  =  0;
                     FileInputStream fis  =  ctx.openFileInput(file);
                     ByteArrayOutputStream bos  =  new ByteArrayOutputStream();
                     byte[] b  =  new byte[1024];
                     while ((bytesRead  =  fis.read(b)) !  =  -1) {
                     bos.write(b, 0, bytesRead);
                     }
                     data  =  bos.toByteArray();
                } catch (FileNotFoundException e) {
                     Log.e(TAG, "File not found in getId()");
                } catch (IOException e) {
                     Log.e(TAG, "IOException in setId(): "  +  e.getMessage());
                }
                return data;
        }

        public void writer(byte[] data, String file) {
                try {
                     FileOutputStream fos  =  ctx.openFileOutput(file,
                     Context.MODE_PRIVATE);
                     fos.write(data);
                     fos.flush();
                     fos.close();
                } catch (FileNotFoundException e) {
                     Log.e(TAG, "File not found in setId()");
                } catch (IOException e) {
                     Log.e(TAG, "IOException in setId(): "  +  e.getMessage());
                }
        }

}

接下来,我们做加密模块(见清单 5-15 )。这个模块负责加密和解密。我在模块中添加了一个 armorEncrypt() 和 armorDecrypt() 方法,以便更容易地将字节数组数据转换为可打印的 Base64 数据,反之亦然。

清单 5-15。 密码模块

package net.zenconsult.android.crypto;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import android.content.Context;
import android.util.Base64;

public class Crypto {
        private static final String engine  =  "AES";
        private static final String crypto  =  "AES/CBC/PKCS5Padding";
        private static Context ctx;

        public Crypto(Context cntx) {
                ctx  =  cntx;
        }

        public byte[] cipher(byte[] data, int mode)
                     throws NoSuchAlgorithmException, NoSuchPaddingException,
                     InvalidKeyException, IllegalBlockSizeException,
                     BadPaddingException, InvalidAlgorithmParameterException {
                KeyManager km  =  new KeyManager(ctx);
                SecretKeySpec sks  =  new SecretKeySpec(km.getId(), engine);
                IvParameterSpec iv  =  new IvParameterSpec(km.getIv());
                Cipher c  =  Cipher.getInstance(crypto);
                c.init(mode, sks, iv);
                return c.doFinal(data);
        }

        public byte[] encrypt(byte[] data) throws InvalidKeyException,
                     NoSuchAlgorithmException, NoSuchPaddingException,
                     IllegalBlockSizeException, BadPaddingException,
                     InvalidAlgorithmParameterException {
                return cipher(data, Cipher.ENCRYPT_MODE);
        }

        public byte[] decrypt(byte[] data) throws InvalidKeyException,
                     NoSuchAlgorithmException, NoSuchPaddingException,
                     IllegalBlockSizeException, BadPaddingException,
                     InvalidAlgorithmParameterException {
                return cipher(data, Cipher.DECRYPT_MODE);
        }

        public String armorEncrypt(byte[] data) throws InvalidKeyException,
                     NoSuchAlgorithmException, NoSuchPaddingException,
                     IllegalBlockSizeException, BadPaddingException,
                     InvalidAlgorithmParameterException {
                return Base64.encodeToString(encrypt(data), Base64.DEFAULT);
        }
        public String armorDecrypt(String data) throws InvalidKeyException,
                     NoSuchAlgorithmException, NoSuchPaddingException,
                     IllegalBlockSizeException, BadPaddingException,
                     InvalidAlgorithmParameterException {
                return new String(decrypt(Base64.decode(data, Base64.DEFAULT)));
        }

}

您可以在任何需要加密数据存储的应用中包含这两个文件。首先,确保您的密钥和初始化向量有一个值,然后在存储数据之前对数据调用任何一种加密或解密方法。清单 5-16 显示了对 StorageExample3 类所需的更改。此外,清单 5-17 和 5-18 分别显示了对 StoreData 和 RetrieveData 文件所需的更改。

清单 5-16。 新存储例 3 带加密

package net.zenconsult.android;

import net.zenconsult.android.crypto.Crypto;
import net.zenconsult.android.crypto.KeyManager;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.EditText;

public class StorageExample3Activity extends Activity {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.main);

                String key  =  "12345678909876543212345678909876";
                String iv  =  "1234567890987654";

                KeyManager km  =  new KeyManager(getApplicationContext());
                km.setIv(iv.getBytes());
                km.setId(key.getBytes());

                // Store data
                Contact contact  =  new Contact();
                contact.setFirstName("Sheran");
                contact.setLastName("Gunasekera");
                contact.setEmail("sheran@zenconsult.net");
                contact.setPhone("  +  12120031337");

                ContactsDb db  =  new ContactsDb(getApplicationContext(), "ContactsDb",
                     null, 1);
                Log.i("SE3", String.valueOf(StoreData.store(new Crypto(
                     getApplicationContext()), db, contact)));

                Contact c  =  RetrieveData.get(new Crypto(getApplicationContext()), db);

                db.close();

                EditText ed  =  (EditText) findViewById(R.id.editText1);
                ed.setText(c.toString());

        }
}

清单 5-17。 修改 StoreData 类

package net.zenconsult.android;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import net.zenconsult.android.crypto.Crypto;
import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

public class StoreData {
        public static long store(Crypto crypto, ContactsDb db, Contact contact) {
                // Prepare values
                ContentValues values  =  new ContentValues();
                try {
                     values.put("FIRSTNAME", crypto.armorEncrypt(contact.getFirstName()
                     .getBytes()));
                     values.put("LASTNAME", crypto.armorEncrypt(contact.getLastName()
                     .getBytes()));
                     values.put("EMAIL", crypto.armorEncrypt(contact.getEmail()
                     .getBytes()));
                     values.put("PHONE", crypto.armorEncrypt(contact.getPhone()
                     .getBytes()));
                     values.put("ADDRESS1", contact.getAddress1());
                     values.put("ADDRESS2", contact.getAddress2());
                } catch (InvalidKeyException e) {
                     Log.e("SE3", "Exception in StoreData: "  +  e.getMessage());
                } catch (NoSuchAlgorithmException e) {
                     Log.e("SE3", "Exception in StoreData: "  +  e.getMessage());
                } catch (NoSuchPaddingException e) {
                     Log.e("SE3", "Exception in StoreData: "  +  e.getMessage());
                } catch (IllegalBlockSizeException e) {
                     Log.e("SE3", "Exception in StoreData: "  +  e.getMessage());
                } catch (BadPaddingException e) {
                     Log.e("SE3", "Exception in StoreData: "  +  e.getMessage());
                } catch (InvalidAlgorithmParameterException e) {
                     Log.e("SE3", "Exception in StoreData: "  +  e.getMessage());
                }
                SQLiteDatabase wdb  =  db.getWritableDatabase();
                return wdb.insert(ContactsDb.tblName, null, values);
        }
}

清单 5-18。 修改后的检索数据类

package net.zenconsult.android;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import net.zenconsult.android.crypto.Crypto;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

public class RetrieveData {
        public static Contact get(Crypto crypto, ContactsDb db) {
                SQLiteDatabase rdb  =  db.getReadableDatabase();
                String[] cols  =  { "FIRSTNAME", "LASTNAME", "EMAIL", "PHONE" };
                Cursor results  =  rdb.query(ContactsDb.tblName, cols, "", null, "", "",
                     "");

                Contact c  =  new Contact();
                results.moveToLast();

                try {
                     c.setFirstName(crypto.armorDecrypt(results.getString(0)));
                     c.setLastName(crypto.armorDecrypt(results.getString(1)));
                     c.setEmail(crypto.armorDecrypt(results.getString(2)));
                     c.setPhone(crypto.armorDecrypt(results.getString(3)));
                } catch (InvalidKeyException e) {
                     Log.e("SE3", "Exception in RetrieveData: "  +  e.getMessage());
                } catch (NoSuchAlgorithmException e) {
                     Log.e("SE3", "Exception in RetrieveData: "  +  e.getMessage());
                } catch (NoSuchPaddingException e) {
                     Log.e("SE3", "Exception in RetrieveData: "  +  e.getMessage());
                } catch (IllegalBlockSizeException e) {
                     Log.e("SE3", "Exception in RetrieveData: "  +  e.getMessage());
                } catch (BadPaddingException e) {
                     Log.e("SE3", "Exception in RetrieveData: "  +  e.getMessage());
                } catch (InvalidAlgorithmParameterException e) {
                     Log.e("SE3", "Exception in RetrieveData: "  +  e.getMessage());
                }

                return c;
        }
}

图 5-14 显示了任何人在没有解密信息的情况下访问 SQLite 数据库的会是什么样子。为了复制这个,我没有让 RetrieveData 类解密任何数据。

9781430240624_Fig05-14.jpg

图 5-14T3。如果不解密,数据会是什么样子

摘要

在这一章中,我们讲述了密码学的基础知识。我们研究了 PKI 和可信第三方是如何工作的,以及对于我们的目的来说,PKI 甚至 LPKI 是如何变得多余的。然后,我们看了加密数据的简单机制,并学习了术语。我们看到加密不像选择对称算法那样简单,您必须考虑不同的方面,如填充和操作模式。

然后我们看了 Android 上存储数据的各种机制。我们讨论了每一个例子,并选择 SQLite 数据库和 SharedPreferences 来存储应用数据。然后,我们研究了如何使用加密来混淆我们的数据,我们构建了一个通用库来执行加密和解密。这个库可以包含在我们未来需要以安全的方式存储数据的任何程序中。**

六、与网络应用交互

在某些时候,您将不得不与 web 应用进行交互。无论您是与第三方的 RESTful API 对话,还是与您自己的后端 web 应用交换数据,您的移动应用都需要接受与其他应用交互的想法。当然,作为一个负责任的开发人员,您的工作是确保数据交换完成,这样攻击者就不能访问或更改属于最终用户的私有数据。在前面的章节中,当我们研究数据存储和加密时,我们花了时间来探索“静态数据”。在本章中,我们将讨论“传输中的数据”

最初,我不打算花太多时间讨论加密传输中的数据的好处。通常,SSL 或 TLS 将处理传输中数据的安全部分。然而,最近对荷兰 DigiNotar 认证机构的入侵让我重新考虑这个选项(更多信息见 http://en.wikipedia.org/wiki/DigiNotar 的 )。最后,作为开发人员,我将让您来决定如何保护您的传输数据;但是很明显,最近的这次攻击让我想到,即使信任 SSL 也不总是最好的选择。因此,我将介绍一些与 web 应用安全性相关的主题,以及您的移动应用应该如何与这样的 web 应用进行交互。我还将简要介绍开放 Web 应用安全项目(OWASP );这是保护您的 web 应用的一个非常好的资源。

考虑清单 6-1 中的源代码有多安全。现在问问你自己,你会做些什么来使它更安全?(查看本章末尾的解决方案,并比较您自己的笔记,看看您的思路是否正确。)

清单 6-1 。 客户端登录

package net.zenconsult.android.examples;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;

import android.util.Log;

public class Login {
  private final String TAG = "HttpPost";

  public Login() {

  }

  public HttpResponse execute() {
       HttpClient client = new DefaultHttpClient();
       HttpPost post = new HttpPost(" [`logindemo1.appspot.com/logindemo`](http://logindemo1.appspot.com/logindemo)");
       HttpResponse response = null;

       // Post data with number of parameters
       List  <  NameValuePair  >       nvPairs = new ArrayList  <  NameValuePair  >  (2);
       nvPairs.add(new BasicNameValuePair("username", "sheran"));
  nvPairs.add(new BasicNameValuePair("password", "s3kretc0dez"));

       // Add post data to http post
       try {
       UrlEncodedFormEntity params = new UrlEncodedFormEntity(nvPairs);
       post.setEntity(params);
       response = client.execute(post);

       } catch (UnsupportedEncodingException e) {
       Log.e(TAG, "Unsupported Encoding used");
       } catch (ClientProtocolException e) {
       Log.e(TAG, "Client Protocol Exception");
       } catch (IOException e) {
       Log.e(TAG, "IOException in HttpPost");
       }
       return response;
       }

}

准备我们的环境

让我们从设置测试环境开始。我们显然需要一个现成的 web 应用托管基础设施。当我需要快速部署或测试 web 应用时,我通常依赖 Google App Engine。这为我节省了很多时间,而且我不必担心设置硬件、web 服务器和应用服务器。有了 Google App Engine,我可以用最小的设置开销开始编码。

让我们首先在 Google App Engine 上注册一个帐户(如果您已经有了 Gmail 的 Google 帐户,那么您可以跳过以下步骤使用它):

  1. 导航到(参见图 6-1 )。

    9781430240624_Fig06-01.jpg

    图 6-1T14 .谷歌应用引擎主页

  2. 点击注册链接。出现提示时,使用您的 Gmail 帐户登录。然后你将被带到你的应用列表(见图 6-2 )。

    9781430240624_Fig06-02.jpg

    图 6-2T13 .应用列表

  3. 单击创建应用按钮。下一页允许您选择有关您的应用的详细信息。(参见图 6-3 )。由于您的应用将公开可见,Google 为您提供了一个子域。appspot.com。这个子域池在应用引擎开发者的整个用户群中共享;因此,在某些情况下,您可能收不到您想要的应用名称。例如,你不太可能收到登录域名 1 子域名,因为我已经注册了。您可以通过单击“检查可用性”按钮来检查子域的可用性。

    9781430240624_Fig06-03.jpg

    图 6-3T14 .为您的应用命名

  4. 填写你想要的应用的子域;应该是类似 < 你的名字>ogindemo 1. appspot . com(见图 6-3 )。给你的应用一个标题,比如登录演示 1。保持其余选项不变,然后单击 Create Application。

  5. 如果一切顺利,您将会看到一个类似于图 6-4 的页面,告诉您您的应用已经成功创建。接下来,您可以通过单击“dashboard”链接来查看您的应用的状态。你的应用还没有做任何事情,所以统计数据仍然是空的(见图 6-5 )。

9781430240624_Fig06-04.jpg

图 6-4 。成功创建应用 ??

9781430240624_Fig06-05.jpg

图 6-5 。应用仪表板

接下来,您必须下载用于 Google App Engine 的 SDK,以便在将应用发布到 Google App Engine 服务器之前,您可以在本地计算机上编写、运行和调试应用。我在大部分开发中使用 Eclipse,我将概述下载 SDK 并将其直接与 Eclipse 集成所需的步骤。此外,由于我们正在覆盖 Android,我们将坚持 Java SDK 的应用引擎。

你可以在以下网址找到如何安装 Eclipse 的 Google Apps 插件的详细说明:code.google.com/eclipse/docs/getting_started.html。即使最终的 URL 发生了变化,您也应该能够通过导航到基本 URL,即 http://code.google.com/eclipse 的,到达文档部分。

我们还不打算写任何后端代码。首先,让我们编写一个存根应用,我们可以从它开始并在其上进行构建。在您的 Eclipse IDE 中,通过转到 FileimageNewimageWeb Application Project 来创建一个新的 Google App Engine 项目。将项目名填写为 LoginDemo,将包名填写为 net.zenconsult.gapps.logindemo,取消选中使用 Google Web Toolkit 旁边的复选框(参见图 6-6 )。完成后,点按“完成”。您将最终得到一个名为 LoginDemo 的项目;在命名的包中,您会发现一个名为的文件。该文件包含清单 6-2 中的代码。目前,它没有什么特别的。代码等待一个 HTTP GET 请求,然后用纯文本进行响应:“Hello,world。”

9781430240624_Fig06-06.jpg

图 6-6 创建一个新的谷歌应用引擎项目

清单 6-2 。 默认存根应用包 ,net . Zen consult . gapps . logindemo

import java.io.IOException;
import javax.servlet.http.*;

@SuppressWarnings("serial")
public class LoginDemoServlet extends HttpServlet {
  public void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
            resp.setContentType("text/plain");
            resp.getWriter().println("Hello, world");
  }

}

让我们将这个应用部署到 Google App Engine,看看我们能否通过 web 浏览器访问它。要部署应用,在 Eclipse 包管理器中右键单击它,然后单击 Google image Deploy to App Engine。

然后会提示您选择您在 Google 网站上创建的远程应用的名称。在应用 ID 字段中输入您创建的名称(参见图 6-7 )并点击确定。在下一个窗口中,单击 Deploy 将您的应用上传到 Google(参见图 6-8 )。

9781430240624_Fig06-07.jpg

图 6-7 。选择远程应用的名称

9781430240624_Fig06-08.jpg

图 6-8 。将应用部署到 Google

成功部署应用后,您可以通过导航到创建应用时选择的 URL(<yourname>log in demo 1 . appspot . com)来检查应用。在我的例子中,当我导航到 http://logindemo1.appspot.com 的,时,我会看到“你好,世界”响应消息(参见图 6-9 )。

9781430240624_Fig06-09.jpg

图 6-9 。访问登录 servlet

我们现在有了自己的工作 web 应用,可以随心所欲地使用它。你可能已经注意到设置一个 Google App Engine 应用是多么的方便。这无疑节省了我们构建服务器、安装操作系统、安装服务器软件和配置服务器的时间和精力。让我们来看一些与 web 应用相关的理论。

HTML、网络应用和网络服务

任何 web 开发人员都知道 HTML 是什么。它是任何现代网站的基本组成部分之一。HTML(超文本标记语言)始于 1991 年的一份草稿;这是一种非常简单的语言,可以用来创建基本的网页。快进到 2008 年,HTML 版本 5 的草案发布了。纯 HTML 页面 被称为静态页面。换句话说,它们呈现在最终用户的浏览器上,并保持在那里,直到用户导航到另一个页面。

一个网络应用 是终端用户通过网络 — 访问的一个软件,就像 HTML 页面一样。然而,web 应用比普通的 HTML 包含更多的动态元素。例如,现代网络应用有许多服务器端语言。这些语言(例如 PHP、JSP 和 ASP)在运行时根据最终用户的输入动态生成静态 HTML。web 应用安装在 web 服务器上,并托管在最终用户可以通过网络(如互联网)访问的硬件上。服务器端应用框架负责呈现用户界面、任何应用逻辑(例如,搜索、计算或任何其他过程)以及数据存储或检索功能。最终用户所要做的就是带着他最喜欢的网络浏览器出现在聚会上。换句话说,因为所有复杂的处理都发生在后端或服务器端,所以更薄、更轻的 web 浏览器只不过是一种与用户界面交互的机制。

网络应用为开发者提供了许多优势 并且是当今网络生活中无处不在的一部分。它们最大的优势之一是能够向服务器推出更新或补丁,而不必担心更新成百上千的客户端。web 应用的另一大优势是,最终用户只需要一个瘦客户端——web 浏览器——仅此而已。因此,您不仅可以接触到来自个人计算群体的大量用户,还可以接触到移动计算群体。

一个 web 服务 类似于一个 web 应用,因为它可以通过网络远程访问。它的相似之处还在于它也运行某种服务器软件。然而,主要的区别是用户不能交互地访问服务。在大多数情况下,web 服务与其他客户端或服务器应用进行交互。在大多数情况下,web 服务能够描述它提供的服务以及其他应用如何访问它们。它使用 Web 服务描述语言(WSDL)文件来完成这项工作。其他应用可以通过处理发布的 WSDL 文件来了解如何使用 web 服务。通常,web 服务使用特定的 XML 格式来交换信息。其中一个流行的协议是 SOAP(简单对象访问协议)。SOAP 由基于特定应用的各种 XML 有效载荷组成。清单 6-3 显示了一个 SOAP 消息的例子。

清单 6-3 。 一个 SOAP 消息的例子(由维基百科提供)

POST /InStock HTTP/1.1
Host:[www.example.org](http://www.example.org)
Content-Type: application/soap  +  xml; charset = utf-8
Content-Length: 299
SOAPAction: "[`www.w3.org/2003/05/soap-envelope`](http://www.w3.org/2003/05/soap-envelope)"

“1.0”?>

>

>

IBM

web 服务的另一种工作方式是公开 RESTful API。REST 或表述性状态转移是一种架构,它使用底层的、无状态的客户端-服务器协议来公开 web 服务的端点。REST 的前提是使用一种简单得多的访问介质(如 HTTP ),对每个资源使用单独的 URIs,而不是依赖更复杂的协议,如 SOAP(使用单个 URI 和多个参数)。

你可以在罗伊·菲尔丁的学位论文www . ics . UCI . edu/∞Fielding/pubs/disserious/REST _ arch _ style . htm或维基百科的en . Wikipedia . org/wiki/representative _ state _ transfer上阅读更多关于 REST 的内容。尽管使用 RESTful web 服务很简单,但它仍然可以执行与使用 SOAP 的 web 服务相同的任务。以清单 6-3 中的 SOAP 为例。如果我们的 web 服务将它作为 RESTful API 公开给我们,那么我们会做这样的事情:

 [`www.example.com/stocks/price/IBM`](http://www.example.com/stocks/price/IBM) 

请注意,这是请求的范围。它可以作为一个简单的 HTTP GET 请求发送给服务器,然后服务器可以做出响应。有时,服务器可以用几种不同的表示形式返回数据。例如,如果我们向服务器请求 XML 输出,我们可以添加一个扩展 xml 。如果我们想要 JSON 格式的,我们可以添加一个 json 扩展名,如下所示:

 [`www.example.com/stocks/price/IBM.xml`](http://www.example.com/stocks/price/IBM.xml) 
 [`www.example.com/stocks/price/IBM.json`](http://www.example.com/stocks/price/IBM.json) 

现在是谈论 HTTP(超文本传输协议)的好时机。HTTP 是驱动 web 的协议。虽然超文本最初指的是普通的老式 HTML,但现在可以扩展到包括 XML(可扩展标记语言)。XML 遵循 HTTP 的规则,但是它包括可以使用的自定义 HTML 标签(或关键字)。HTTP 作为一种请求-响应协议。请求-响应循环发生在称为客户端和服务器的两方之间。客户端,或者说用户代理(一个网络浏览器),向网络服务器发出请求,网络服务器返回一个 HTML 或者 XML 的响应。大多数经验丰富的 web 开发人员有时也会期待与 XML 类似的格式,比如 JSON(JavaScript Object Notation)。

HTTP 请求被进一步细分为请求类型,或方法 。有几种方法,最常用的是 GET 和 POST 。 GET 请求用于检索数据, POST 请求用于提交数据。如果你正在填写注册表格,点击提交按钮会提示浏览器将你的数据发送到服务器。如果你回头看本章开头的清单 6-1 ,你会看到这一行:

HttpPost post = new HttpPost(" [`logindemo1.appspot.com/logindemo`](http://logindemo1.appspot.com/logindemo) ");

这是对特定 URL 的 HTTP POST 请求的创建。您可能知道,URL(统一资源定位器)是一种地址,它告诉用户代理从哪里检索特定的资源。资源可以是远程存储在服务器上的文件、文档或对象。HTTP 请求和响应都有相似的结构。两者都包含标题和内容区域。你可以在 www.w3.org 的 ?? 找到很多关于 HTTP 的附加信息。

Web 应用中的组件

Web 应用由不同的层组成。典型的 web 应用有三层(见图 6-10 ):表示层、逻辑层和数据层。根据应用的要求和复杂性,层数可能会增加。拥有多层应用有许多优势:其中之一是系统所有者可以独立于其他层替换或扩展硬件或服务器配置。考虑公司需要增加数据存储量的情况;IT 部门可以升级这一层,而无需对其他层进行重大更改。下一个优势是安全团队可以在每一层进行更精细的控制。每一层都有不同的功能,因此有不同的要求和相关的安全控制。多层应用允许所有者在各个层拥有更多锁定的控制,而不是留下空白,因为所有三层都在一个系统上。

因此,基于三层架构 ,一个 web 应用将包含一个呈现其数据的 web 服务器,一个处理所有数据交换请求的应用服务器,以及一个存储和检索数据的数据库服务器。

9781430240624_Fig06-10.jpg

图 6-10T3。一个三层的网络应用(由维基百科提供)

让我们通过一个例子来看看每一层是如何涉及的。

登录流程

客户端与服务器进行的标准用户身份验证会话如下所示:

  1. 客户端从 web 服务器【Web 服务器/表示层】请求登录页面。
  2. 客户端将凭证发送到 web 服务器【Web 服务器/表示层】
  3. 应用服务器接收数据并检查其是否符合验证规则【应用服务器/逻辑层】
  4. 如果数据是好的,那么应用服务器查询数据库服务器以发现是否存在匹配的凭证【应用服务器/逻辑层】
  5. 数据库服务器响应应用服务器成功或失败【数据库服务器/数据层】
  6. 应用服务器告诉 web 服务器向客户端提供其门户(如果凭证正确)或错误消息(如果凭证不匹配)【应用服务器/逻辑层】
  7. Web 服务器显示来自应用服务器【Web 服务器/表示层】的消息。

虽然这是一个简化的示例,但它确实说明了流程如何从外部移动到内部 — ,然后再返回。

Web App 技术

web 应用的每一层都可以使用多种技术。您可以从许多 web 服务器、应用框架、应用服务器、服务器端脚本语言和数据库服务器中进行选择。您的选择标准取决于多种因素,如应用要求、预算以及对您选择的技术的支持的可用性。

因为 Android 开发主要是在 Java 上完成的,所以我决定在我们的 web 应用中也坚持使用 Java。除了 Java,您还可以使用其他服务器端技术。这里列举了其中的一些:

  • PHP:www.php.net
  • python:www.python.org
  • 决哥: www .决哥 project.com
  • perl:(不太常用,但有时仍会使用)
  • 冷聚变:www.adobe.com/product/coldfusion-family.html
  • ASP。净:
  • ruby on Rails:www.rubyonrails.org

类似地,根据您的需求,您可以将许多流行的数据库用于您的数据层应用。存在许多免费的和商业的数据库。这是您或您的应用架构师最初必须做出的又一个决定。这里有一个流行数据库的简短列表和一个 URL ,表明您可以在哪里了解更多关于它们的信息:

  • Oracle:www . Oracle . com
  • 微软 SQL Server:www.microsoft.com/sqlserver
  • MySQL:www.mysql.com
  • PostgreSQL:www.postgresql.org
  • couch db:couchdb.apache.org
  • 莽哥布:www .mon 哥布. org

现在让我们花几分钟时间来完成我们的 web 应用,以便它支持基本的密码检查。请注意,我故意让这个例子非常简单。实际 web 应用的身份验证例程将更加复杂。查看清单 6-4 中的代码。

清单 6-4 。 新的凭证验证码

package net.zenconsult.gapps.logindemo;

import java.io.IOException;
import javax.servlet.http.*;

@SuppressWarnings("serial")
public class LoginDemoServlet extends HttpServlet {
  private String username = "sheran";
  private String password = "s3kr3tc0dez"; // Hardcoded here intended to
  // simulate a database fetch

  public void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws IOException {
    resp.setContentType("text/plain");
  resp.getWriter().println("Hello, world");
  }

  public void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws IOException {
    String user = req.getParameter("username"); // No user input validation
                                                    // here!
    String pass =   req.getParameter("password"); // No user input validation
                                                    // here!

    resp.setContentType("text/plain");
    if (user.equals(username) && pass.equals(password)) {
      resp.getWriter().println("Login success!!");
    } else {
      resp.getWriter().println("Login failed!!");
      }

    }
}

下一步是发布您的代码,就像您第一次设置 Google App Engine 帐户时所做的那样,然后创建一个新的处理身份验证的 Android 项目(项目结构见图 6-11 )。清单 6-5、 6-6、 6-7、和 6-8 分别包含了登录、登录民主客户端 1 活动、字符串. xml 和 main.xml 文件的源代码。确保将这一行添加到您的 AndroidManifest.xml 文件中,因为您将需要访问互联网来访问您的 Google App Engine 应用:

<uses-permission Android:name =【Android . permission . internet】>

9781430240624_Fig06-11.jpg

图 6-11T3。项目结构

清单 6-5 。 登录类

package net.zenconsult.android.examples;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;

import android.util.Log;

public class Login {
  private final String TAG = "HttpPost";
  private String username;
  private String password;

  public Login(String user, String pass) {
       username = user;
       password = pass;
  }

  public HttpResponse execute() {
       Log.i(TAG, "Execute Called");
       HttpClient client = new DefaultHttpClient();
       HttpPost post = new HttpPost("[`logindemo1.appspot.com/logindemo`](http://logindemo1.appspot.com/logindemo)");
       HttpResponse response = null;

       // Post data with number of parameters
       List< NameValuePair  >       nvPairs = new ArrayList  <  NameValuePair  >  (2);
       nvPairs.add(new BasicNameValuePair("username", username));
       nvPairs.add(new BasicNameValuePair("password", password));

       // Add post data to http post
  try {
            UrlEncodedFormEntity params = new UrlEncodedFormEntity(nvPairs);
            post.setEntity(params);
            response = client.execute(post);
            Log.i(TAG, "After client.execute()");

       } catch (UnsupportedEncodingException e) {
       Log.e(TAG, "Unsupported Encoding used");
       } catch (ClientProtocolException e) {
            Log.e(TAG, "Client Protocol Exception");
       } catch (IOException e) {
            Log.e(TAG, "IOException in HttpPost");
  }
  return response;
  }

}

6-5 中列出的代码包含登录程序。类构造函数 Login 有两个参数,分别是用户名和密码。 execute() 方法 将使用这些参数向服务器发出 HTTP POST 请求。

清单 6-6 。logindemoclient 1 活动类

package net.zenconsult.android.examples;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;

public class LoginDemoClient1Activity extends Activity implements
  OnClickListener {
  private final String TAG = "LoginDemo1";
  private HttpResponse response;
  private Login login;

  /** Called when the activity is first created. */
  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);

      Button button =     (Button) findViewById(R.id.login);
      button.setOnClickListener(this);

  }

  @Override
  public void onClick(View v) {
      String u = ((EditText) findViewById(R.id.username)).toString();
      String p = ((EditText) findViewById(R.id.password)).toString();

      login = new Login(u, p);

      String msg = "";
      EditText text =         (EditText) findViewById(R.id.editText1);
  text.setText(msg);

  response = login.execute();
      Log.i(TAG, "After login.execute()");

      if (response ! = null) {
  if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
      try {
  BufferedReader reader =     new BufferedReader(
  new InputStreamReader(response.getEntity()
  .getContent()));
  StringBuilder sb =     new StringBuilder();
  String line;
      while ((line = reader.readLine()) ! = null) {
  sb.append(line);
  }
  msg = sb.toString();
  } catch (IOException e) {
  Log.e(TAG, "IO Exception in reading from stream.");
      }

  } else {
      msg = "Status code other than HTTP 200 received";
  }
  } else {
  msg = "Response is null";
  }
      text.setText(msg);
  }
}

6-6 中列出的代码是一个标准的 Android 活动。这可以被认为是应用的入口或起点。

清单 6-7 。strings . XML 文件

<?xml version = "1.0" encoding = "utf-8"?>
<resources>
  <string name = "hello"  >  Web Application response:</string>
  <string name = "app_name"  >  LoginDemoClient1</string>
  <string name = "username"  >  Username</string>
  <string name = "password"  >  Password</string>
  <string name = "login"  >  Login</string>
</resources>

清单 6-8 。main . XML 文件

<?xml version = "1.0" encoding = "utf-8"?>
<LinearLayout xmlns:android = " [`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android) "
  android:orientation = "vertical"
  android:layout_width = "fill_parent"
  android:layout_height = "fill_parent"
  android:weightSum = "1">
  <TextView android:textAppearance = "?android:attr/textAppearanceLarge"
  android:id = "@  +  id/textView1" android:layout_height = "wrap_content"
  android:layout_width = "wrap_content" android:text = "@string/username">
  </TextView>
<EditText android:layout_height = "wrap_content"
  android:layout_width = "match_parent" android:id = "@  +  id/username">
</EditText>
<TextView android:textAppearance = "?android:attr/textAppearanceLarge"
  android:id = "@  +  id/textView2" android:layout_height = "wrap_content"
  android:layout_width = "wrap_content" android:text = "@string/password">
</TextView>
<EditText android:layout_height = "wrap_content"
  android:layout_width = "match_parent" android:inputType = "textPassword"
  android:id = "@  +  id/password">
</EditText>
<Button android:text = "@string/login" android:layout_height = "wrap_content"
  android:layout_width = "166dp" android:id = "@  +  id/login">
</Button>
<TextView android:text = "@string/hello" android:layout_height = "wrap_content"
  android:layout_width = "fill_parent">
</TextView>
<EditText android:id = "@  +  id/editText1" android:layout_height = "wrap_content"
  android:layout_width = "match_parent" android:inputType = "textMultiLine"
  android:layout_weight = "0.13">
  <requestFocus  >  </requestFocus>
</EditText>
</LinearLayout>

strings.xml 和 main 。 xml 文件分别包含我们定义的字符串常量集和应用的图形布局。

运行您的应用并输入不同的用户名和密码。您应该会看到两条不同的响应消息,一条表示成功,另一条表示密码失败(参见图 6-12 )。就是这样!您已经完成了移动登录客户端和服务器的编写。接下来,我们将讨论 web 上的安全性,以及在您的 web 应用中可能会遇到的各种攻击。

9781430240624_Fig06-12.jpg

图 6-12T3。登录失败

OWASP 和网页攻击

开放 web 应用安全项目(OWASP)【www.owasp.org】是一个为测试和保护 Web 应用提供大量知识、技术和指南的组织。OWASP 成立于 2001 年 12 月,并于 2004 年获得美国非营利慈善机构地位。它的核心目标是"成为一个蓬勃发展的全球社区,推动全球软件安全性的可见性和发展。“这是了解和修复 web 应用安全性的绝佳资源。

OWASP 十大项目自 2004 年以来一直是 OWASP 基金会 的子项目。在半定期的基础上,OWASP 十大漏洞列出了十个最重要的应用安全漏洞。这些漏洞被列为项目成员和全球安全专家在 web 应用中所经历的广泛共识。十大清单被大量商业组织使用和采纳,它已经成为 web 应用安全的事实标准。

在这本书出版的时候,2010 年 OWASP 十大仍然是最近更新的名单 。这里可以找到:【www.owasp.org/index.php/Top_10_2010】??。

下面列出了 2010 年 OWASP 十大主题:

  • A1:注射
  • A2:跨站点脚本(XSS)
  • A3:不完整的认证和会话管理
  • A4:不安全的直接对象引用
  • A5:跨站点请求伪造(CSRF)
  • A6:安全错误配置
  • A7:不安全的加密存储
  • A8:无法限制 URL 访问
  • A9:传输层保护不足
  • A10:未经验证的重定向和转发

较新的 OWASP 项目之一是移动十大,它是 OWASP 移动安全项目 的一部分。该项目仍在开发中,在撰写本文时还没有发布最终的清单。然而,网站上有一个实用技巧的列表,将证明对你这个移动开发者有巨大的帮助。本章涵盖的大多数主题都与移动十大共享许多技术和原则。以下是该列表涵盖的主题:

  • 识别和保护移动设备上的敏感数据。
  • 在设备上安全地处理密码凭证。
  • 确保敏感数据在传输过程中受到保护。
  • 正确实施用户认证/授权和会话管理。
  • 保持后端 API(服务)和平台(服务器)的安全。
  • 安全地执行与第三方服务/应用的数据集成。
  • 特别注意收集和存储同意收集和使用用户数据。
  • 实施控制以防止对付费资源(如钱包、短信和电话)的未授权访问。
  • 确保移动应用的安全分发/供应。
  • 仔细检查代码的任何运行时解释是否有错误。

认证技术

现在,让我们继续讨论保护“传输中的数据”的主题。我希望你对 web 应用的幕后工作有一个公平的理解,这就是为什么我在本章中讨论了与 web 应用相关的主题。如果你致力于成为一名移动应用开发人员,那么看看你的应用如何与你想与之交流的 web 应用进行通信是很有趣的。更好地理解应用还可以帮助您提高安全性和性能。如果像我一样,您从头到尾都在编写代码,包括 web 应用开发,那么您可能已经熟悉了我将要讨论的主题。不管怎样,既然您已经对 web 应用和安全性有了一个简短的回顾,那么让我们继续手头的主要任务。

身份验证是需要与远程 web 应用交互的移动应用的一个重要特性。几乎所有当今的应用都依赖某种形式的用户名和密码或 PIN 组合来授权对其数据的访问。用户名和密码存储在服务器上,每当最终用户希望通过应用进行身份验证时,就会进行比较。如果你重新看一下清单 6-1,你会发现我们正在这么做。以下几行包含 web 应用的用户名和密码:

nvPairs.add(new BasicNameValuePair("username", "sheran"));
nvPairs.add(new BasicNameValuePair("password", "s3kretc0dez"));

在这种情况下,信息是硬编码的,但它可以很容易地存储在设备上(当然是加密的!)并在用户想要登录时检索。但是如果我们的流量在传输过程中被拦截了呢?“啊哈!但是我们有 SSL!”你说。这是真的,但是我们似乎没有在我们的例子中使用它,因为我们的 POST 请求发送到一个非 SSL/TLS 端口:

HttpPost post = new HttpPost(" [`logindemo1.appspot.com/logindemo`](http://logindemo1.appspot.com/logindemo) ");

好吧,那是个卑鄙的手段。但是我们认真考虑一下,我们的 SSL 流量被攻破了。窃听我们与 web 应用对话的攻击者现在可以访问我们的凭据。她现在要做的就是直接在完整的网络应用或另一个移动设备上使用它们。如果她这样做,她将完全控制我们的用户资料。如果这是一个社交网站,那么我们可能不会太在意;然而,如果这是我们的网上银行应用,那么我们会非常担心。

到目前为止,我们知道在进行远程身份认证时面临的风险。尽管我们的数据可能会通过安全通道,但仍然容易受到攻击。而且不一定是像 DigiNotar 事件那样的严重攻击,攻击者可以颁发她自己的证书。例如,攻击可能像 SSL 中间人攻击一样平淡无奇。

因为我不止一次提到 DigiNotar 和不信任 SSL,所以我认为我概述一下我的理由是公平的。

你不能总是相信 SSL 。一般来说,最终用户认为 SSL 意味着他们是安全的。浏览器上的挂锁图标和地址栏变成绿色表示您正在浏览一个安全的网站。然而,这不一定是真的。我想花点时间回顾一下 SSL 的一些概念。

SSL(安全套接字层)是一种传输协议,它对两台计算机之间传输的数据进行加密。一个窃听者不可能不经过一番努力就截获加密数据——。因此,SSL 确保数据在客户端和服务器计算机之间保持私密。SSL 已经过时了。大多数人把客户端和服务器之间的加密 HTTP 数据传输称为 SSL 但实际上,较新的协议是 TLS(传输层安全)。SSL 和 TLS 不可或缺的一部分是 X.509 证书。X.509 是公钥基础设施(PKI)的标准,我在第五章中简单介绍过。通常,用户会将 X.509 服务器证书称为 SSL 证书。这是 SSL 的一个关键且非常重要的组件。图 6-13 显示了设置 SSL 会话的浏览器。

9781430240624_Fig06-13.jpg

图 6-13T3。设置 SSL/TLS 会话

TLS 和 SSL 结合使用加密技术来确保数据传输的安全性。现在让我们来看看这个会话设置。我不会给你外科手术的细节,因为你几乎永远不需要写自己的 TLS 协商算法。相反,本节将让您了解如何设置加密以及在 TLS 会话期间发生了什么。

首先,客户端或浏览器将联系 web 服务器并向其发送一些信息。该信息包含它可以支持的 TLS 版本的详细信息和加密算法列表。这些被称为密码套件 ,它们包含支持各种任务的算法,如密钥交换、认证和批量密码。

接下来,服务器在选择了它支持的特定密码套件以及客户机和服务器都支持的最高通用 TLS 版本后做出响应。然后,服务器还会向客户端发送其 SSL 证书。

然后,客户端使用服务器的公钥加密并交换一个预主密钥,一个生成主密钥的密钥。

交换预主密钥后,客户端和服务器将使用随机值和预主密钥来生成最终的主密钥。这个主密钥存储在客户端和服务器上。

然后,服务器和客户端切换到加密所有来回发送的数据。使用选定的密码套件,并在两端使用对称主密钥来加密和解密数据。图 6-14 显示了如果您能够捕获客户端和服务器之间的加密数据会话,您会看到什么。图 6-15 显示了使用 OpenSSL 查看时的握手和其他相关细节。只要看一眼就会立即告诉您,绝对没有可供攻击者使用的数据。那么,这对开发人员来说意味着什么呢?您应该使用 SSL,并且在客户机和服务器之间交换敏感数据时永远不用担心被窥探?我暂时不会接受你的回答。我们先来看几个细节,稍后再来回答你。

9781430240624_Fig06-14.jpg

图 6-14T3。SSL 会话的流量捕获

9781430240624_Fig06-15.jpg

图 6-15T3。使用 OpenSSL 的 s_client 选项查看 SSL 握手

SSL 与信任息息相关。实际上,X.509 是关于信任的。SSL 证书是根据特定标准颁发给个人或公司的。颁发机构,也称为 CA 或证书颁发机构,负责确定您是否是您所说的那个人。例如,你不能只申请一个 www.google.com 证书而不证明你以某种方式隶属于该公司,或者有能力代表该公司行事。这很重要,因为如果 CA 不检查这些凭证,那么任何人都可以申请 SSL 证书并将其安装在自己的 web 服务器上。

通过欺骗最终用户,让他们相信你的服务器是 google.com 服务器,你可以实施中间人攻击,拦截他的所有数据。我们很快会看到中间人攻击;但是首先,我想介绍另一个您可能知道的话题,即自签名证书。

注意CA 向客户端颁发 SSL 证书。颁发证书时,CA 还将使用自己的根证书对 SSL 证书进行签名。这个签名表明 CA 信任发布的 SSL 证书。浏览器可以通过首先查看 CA 签名并验证签名是否与根证书匹配来验证 SSL 证书。

世界各地都有许多知名的根 ca。通常,CA 根证书打包在您的 web 浏览器中。这允许浏览器验证由不同 ca 颁发的 SSL 证书。

例如,假设您向 VeriSign 申请了您的域名、example.com 的证书。VeriSign 首先确定您是该域的正确所有者,然后为您的 web 服务器颁发证书。它用自己的根证书签署该证书。收到 SSL 证书后,您可以将其安装在 web 服务器上并建立您的网站。现在当我访问您的网站时,我的浏览器首先查看您的 SSL 证书,然后尝试验证您的证书是否确实是由受信任的 CA 颁发的。为此,我的浏览器将查看其可信根证书的内部存储,以确定 VeriSign 根证书的签名是否与您的证书上的签名匹配。如果是的话,我可以继续浏览你的网站。但是,如果难以验证您的证书,我的浏览器会警告我无法验证证书。

请注意,在给证书开绿灯之前,您的浏览器将验证关于证书的许多其他细节。

自签名证书

在一些项目的开发和测试阶段,开发人员有时会在他们的网站上使用自签名证书。这种类型的证书在所有方面都与 CA 颁发的 SSL 证书相同。但是,主要的区别在于该证书上的签名不是来自可信的 CA。相反,开发人员自己签署证书。当浏览器使用自签名 SSL 证书连接到站点时,它无法验证谁签署了证书。这是因为签名者没有列在浏览器的内部可信证书库中。然后浏览器会向用户发出类似于图 6-16 中所示的警告。

9781430240624_Fig06-16.jpg

图 6-16T3。不可信或自签名证书的警告

发生在浏览器上的验证阶段非常重要。它的存在使得攻击者不能简单地给自己颁发一个属于的证书来欺骗用户。如果浏览器无法验证 SSL 证书,它将始终提醒用户。

中间人攻击

中间人(MitM) 攻击是一种攻击者可以窃听双方之间的网络流量或数据流动的方法。攻击者将自己定位成能够拦截来自发送方和接收方的流量,有效地将自己置于两者之间(见图 6-17 )。在这个位置上,他能够在双方之间截取和传递信息。如果执行正确,会话两端的用户都不会知道攻击者在中继和拦截他们的流量。

9781430240624_Fig06-17.jpg

图 6-17T3。艾丽丝和鲍勃中间的马洛里(维基百科提供)

下面是一个 MitM 攻击的例子,使用图 6-17 作为参考 ??:

Alice "Hi Bob, it's Alice. Give me your key"--> Mallory Bob
Alice Mallory "Hi Bob, it's Alice. Give me your key"--> Bob
Alice Mallory <--[Bob's_key] Bob
Alice <--[Mallory's_key] Mallory Bob
Alice "Meet me at the bus stop!"[encrypted with Mallory's key]--> Mallory Bob
Alice Mallory "Meet me in the windowless van at 22nd Ave!"encrypted with Bob's![image
 key]--> Bob

大多数时候,我们看到的攻击都集中在自签名证书上,或者诱骗浏览器相信攻击者拥有有效的证书。直到最近,攻击者对 CA 安全知之甚少,涉及 CA 的事件也少得多。不管怎么说,直到 2011 年 6 月之前都是如此。

理论上,攻击 CA 以获取合法签名的可信 SSL 证书也是一种选择。没有多少攻击者会考虑这一点,因为他们显然希望 CAs 具有高度的安全性。正确错了!2011 年 6 月,一个名为 DigiNotar 的 CA 遭到攻击。攻击者给自己颁发了 500 多个由 DigiNotar 签名的欺诈 SSL 证书。作为一个可信的 CA,DigiNotar 在所有现代浏览器中都有根证书。这意味着攻击者拥有合法的 SSL 证书,可以用来执行 MitM 攻击。由于浏览器已经信任 DigiNotar 根证书,它们将总是验证这些流氓 SSL 证书,最终用户永远不会知道攻击者正在拦截她的数据。

为什么会这样?DigiNotar 的基础设施安全控制非常松散。攻击者能够远程破坏其服务器,并访问负责颁发合法证书的系统。在这之后,对于攻击者来说,随时为自己颁发证书是一个相对简单的任务。一些有流氓证书的比较著名的网站 包括:

  • *.google.com (指【google.com】的任何子域,包括【mail.google.com】【docs.google.com】【plus.google.com】等等)
  • *.android.com
  • *.microsoft.com
  • *.mozilla.org
  • *.wordpress.org
  • www.facebook.com
  • www.mossad.gov.il
  • www.sis.gov.uk

所有的网页浏览器开发者都将 DigiNotar 的根证书列入黑名单,DigiNotar 开始系统地撤销所有的流氓证书。不幸的是,当这一切发生的时候,DigiNotar 已经失去了全球成千上万用户的信任。该公司于 2011 年 9 月宣布破产。

如果这么大的 CA 可以遭受这么大的安全漏洞,危及数百个 SSL 证书,那么我们真的可以一直依赖 SSL 吗?事实上,我们可以。像 DigiNotar 这样的事件很少发生,所以我会选择信任 SSL。然而,我也会选择在我的移动应用和服务器之间部署我自己的数据加密层。然后,如果 SSL 层被以任何方式破坏,攻击者将有另一层加密要处理。在大多数情况下,这一额外的层将作为一种威慑,攻击者可能会离开您的应用。

有没有一种方法可以防止攻击者在通过 SSL 传输时窥探我们的凭据?确实是的!让我们来看两种方法,即使我们的安全传输通道出现故障,我们也可以防止我们的凭据被破坏。一个是 OAuth ,一个是挑战/响应。

听觉

OAuth 协议允许被称为消费者 的第三方网站或应用使用被称为服务供应器 的网络应用上的最终用户数据。最终用户对他可以授予这些第三方的访问权限拥有最终控制权,并且这样做时不必泄露或存储他现有的 web 应用凭证。

以 Picasa 网络相册为例;照片编辑应用 Picnik(www.picnik.com)允许最终用户编辑他们的照片。Picnik 还允许终端用户从 Picasa 和 Flickr 等其他网站导入内容。在 OAuth 之前,用户必须登录 Picnik,还要输入他的 Picasa 或 Flickr 用户名和密码,这样 Picnik 就可以开始从这些网站导入照片。这种方法的问题是,现在用户已经用 Picnik 保存或使用了他的凭证。他的曝光度增加了,因为他在 Picasa Picnik 保存了自己的凭证。

如果用 OAuth 重现相同的场景,那么用户就不必在 Picnik 站点上再次输入凭证。相反,Picnik(消费者)会将他重定向到他的 Picasa(服务供应器)网站(见图 6-18 )并要求他允许或拒绝访问 Picasa 上存储的照片(见图 6-19 )。这样,用户的凭证更安全。

9781430240624_Fig06-18.jpg

图 6-18T3。Picnik 请求连接到 Picasa,这样它就可以请求一个访问令牌

9781430240624_Fig06-19.jpg

图 6-19T3。Picasa 请求授权让 Picnik 查看一些照片

OAuth 通过使用请求令牌来工作。想要访问 web 应用中的数据的站点需要从该应用获得一个令牌,然后才能开始访问这些数据。

让我们先来看看 OAuth 是如何为 Picasa 网络相册工作的。例如,假设您编写了一个列出用户 Picasa 相册的 Android 应用。您的 Android 应用需要访问用户的 Picasa 网络相册才能做到这一点。在这种情况下,参与者是您的 Android 应用(消费者)、Picasa(服务供应器)和您的最终用户。

OAuth 要求您首先在进行身份验证的站点上注册您的消费者应用。这是必要的,因为您将收到一个需要在代码中使用的应用标识符。要注册您的应用,您必须访问【http://code.google.com/apis/console】(参见图 6-20 ),创建一个项目,并创建一个 OAuth 客户端 ID(参见图 6-21 、 6-22 、 6-23 和 6-24 )。

9781430240624_Fig06-20.jpg

图 6-20T3。在 Google APIs 上创建一个新项目

9781430240624_Fig06-21.jpg

图 6-21T3。创建新的客户端 ID

9781430240624_Fig06-22.jpg

图 6-22T3。填写您的申请详情

9781430240624_Fig06-23.jpg

图 6-23T3。选择您的申请类型

9781430240624_Fig06-24.jpg

图 6-24T3。您的客户端 ID 和客户端密码现在已创建

现在你已经得到了 OAuth 客户端 ID,让我们来看看 OAuth 应用的认证流程 (见图 6-25 )

9781430240624_Fig06-25.jpg

图 6-25T3。OAuth 认证流程(由 Google 提供)

OAuth 是一个包含三个主要交互方的多阶段流程。消费者是希望从服务提供者那里访问数据的应用,这只有在用户明确授权消费者的情况下才会发生。让我们详细回顾一下这些步骤:

当最终用户打开您的 Android 应用 时,会启动以下场景:

  1. 流程 A: 消费者应用(您的 Android 应用)向服务供应器(Picasa)请求令牌。
  2. 流程 B: Picasa 告诉您的应用将最终用户重定向到 Picasa 的网页。然后,您的应用会打开一个浏览器页面,将最终用户指引到特定的 URL。
  3. 流程 C: 最终用户在该屏幕中输入她的凭证。请记住,她正在登录服务供应器(Picasa)网站,并授权访问您的应用。她将凭据发送到网站,而不是存储在设备上的任何地方。
  4. 流程 D: 一旦 Picasa 确认最终用户输入了正确的用户名和密码,并授予了对您的应用的访问权限,它会回复一个响应,指示请求令牌是否已被授权。此时,您的应用必须检测到这种响应并采取相应的行动。假设授权被授予,您的应用现在有一个授权的请求令牌。
  5. 流程 E: 使用这个授权的请求令牌,你的应用向服务供应器发出另一个请求。
  6. 流程 F: 然后,服务提供者将请求令牌交换为访问令牌,并在响应中将其发送回去。
  7. 你的应用现在使用这个访问令牌来访问任何受保护的资源(在这个例子中是用户的 Picasa 相册),直到令牌过期。

您的应用现已成功访问 Picasa,无需存储最终用户的凭据。如果用户的手机遭到破坏,攻击者复制了所有应用数据,他将无法在您的应用数据中找到 Picasa 用户名和密码。这样,你就确保了你的应用不会不必要地泄露敏感数据。

我在这里使用 Picasa 只是作为一个参考框架。我们的最终目标是为我们的后端应用创建一个 OAuth 认证系统。因此,您的后端 web 应用 将成为 OAuth 服务提供者,而不是 Picasa 作为服务提供者。您的最终用户必须通过 web 浏览器登录到您的应用,并明确授权它访问资源。接下来,您的移动应用和后端 web 应用将使用请求和访问令牌进行通信。最重要的是,你的移动应用不会保存你的网络应用的用户名和密码。

为了说明这些概念,我为 Picasa 创建了一个示例应用。我将在第八章向你展示如何在你的 web 应用中实现 OAuth。

用密码术挑战/响应

保护您的最终用户凭证不通过 Internet 的第二种机制是使用挑战/响应技术。这种技术在许多方面与 OAuth 相似,因为没有凭证通过网络。相反,一方请求另一方挑战。然后,另一方将根据特别选择的算法和密码功能对随机信息进行加密。用于加密这些数据的密钥是用户密码。这个加密的数据被发送到质询方,然后质询方使用存储在其末端的密码对同一条信息进行加密。然后比较密文;如果匹配,则允许用户访问。学习这种技术的最好方法是通过一个实际的例子。与 OAuth 一样,我在第八章中包含了源代码和应用示例。

总结

在这一章中,我们重点讲述了如何将我们的数据从移动应用安全地传输到 web 应用。我们还介绍了如何使用成熟的协议和机制来保护传输中的数据。与此同时,我们看到,在某些情况下,我们无法信任协议本身。在这种情况下,我们会考虑一些选项,帮助我们保护最终用户的凭据不被窃取或拦截。

我们还讨论了涉及 web 应用安全性的主题。考虑到大多数移动应用以某种形式与 web 应用通信,了解这方面的技术如何工作总是有好处的。最后,我们查看了一些有助于我们保护 web 应用的有用资源,以及一些在传输过程中保护用户凭证的具体示例。

七、企业安全

一直以来,我们都是站在个人开发者的角度来看待移动应用。尽管我相信个人开发者或较小的开发公司远远超过企业开发者,但我认为关注企业开发者和他可能面临的独特挑战是有益的。你可能想跳过这一章,因为你不属于“企业开发者”的范畴;然而,我会敦促你考虑这一点:现在大多数企业都在考虑外包他们的开发工作。

对于一个企业来说,拥有一个内部的移动开发团队可能是没有意义的,除非这是公司的核心业务。我见过许多企业将开发工作外包给个人或小公司,这样他们就不用担心管理内部移动开发团队。

如果有一天,一家公司雇佣你为它开发一个移动应用,那么在你开始开发之前,你可能需要考虑几个领域。在大多数方面,你的目标群体比你向公众发布你的应用要小得多。

然而,重要的一点是,就企业而言,您可能要处理的不仅仅是个人信息的丢失。例如,在企业环境中,您处理机密信息(例如,商业秘密、公司财务信息或敏感的服务器凭证)的可能性比您处理向公众发布的应用的可能性要高得多。此外,您的应用可能会更容易成为攻击目标,因为目前许多攻击者认为移动平台由于安全性较低而“容易得手”。让我们首先来看一下企业应用与公开发布的应用的一些主要区别。

连通性

最近,从远程位置连接到企业环境已经变得司空见惯。远程办公、远程支持和外包都导致企业技术团队允许授权用户进入他们组织的网络。这并不意味着网络管理员只是让防火墙对远程登录和远程桌面敞开大门;入站连接受到某些安全控制。为了确保最安全的路由,组织通常会使用 VPN 或虚拟专用网络(见图 7-1 ),以允许远程用户加入其网络。

9781430240624_Fig07-01.jpg

图 7-1 。虚拟专用网(VPN)(由维基百科提供)

VPN 通常是网络管理员通常会在其边界网络设备上创建的附加逻辑或虚拟网络。该网络充当公共网络(如互联网)和企业的私有内部网络之间的桥梁。用户可以通过这个公共网络连接到 VPN,并使用企业的内部资源(包括文件服务器、内部应用等),就像他们实际连接到内部网络一样。

VPN 也逐渐进入移动领域。黑莓、iPhone 和 Android 等设备现在能够连接到企业网络并安全地传输数据。在为企业设计时,请记住这一点。很有可能企业网络管理员会告诉你需要使用 VPN 但是如果她没有提到,你应该提出这个话题。这里的目标不是让一个企业向互联网公开更多它不应该公开的内容。

如果出于某种原因,你遇到一个没有或没有使用 VPN 的组织,那么你可能想花一点时间讨论一下 VPN 的优点。如果这是绝对不行的,那么你将不得不对应用和服务器之间的数据进行加密。但是,请记住,这样做的成本很高,尤其是在有大量数据交换的情况下。在这种情况下,您可能还需要考虑数据压缩。在这一章的后面我会给你一个数据压缩的例子。所有这些都增加了处理器的使用,但是,你需要考虑到,在几乎所有的情况下,这将耗尽你的终端用户的设备电池。

企业应用

那么,我一直在谈论的这些企业应用是什么呢?请放心,他们不像独角兽那样神秘;他们确实存在。如果你没有太多的机会在企业中工作,那么你可能不会马上认出一个企业系统。有许多不同的类型,但这里我们将重点放在企业资源规划(ERP) 应用上,主要是因为它们往往涵盖了企业中广泛的用途。典型的 ERP 应用通常涵盖以下领域之一:

  • 供应链管理
  • 客户关系管理
  • 制造业
  • 人力资源
  • 财务和会计
  • 项目管理

你将不得不使用的 ERP 应用很可能是成熟的和完善的。作为新的开发人员,您还可能需要编写自己的应用来与现有系统一起工作。这可能有点令人沮丧,尤其是当这意味着您必须在移动应用的某些功能上做出妥协的时候。在我看来,解决这个问题的最好方法之一是采用和使用某种形式的移动中间件 。

移动中间件

与其让自己的移动应用与遗留的企业应用一起工作,还不如花些时间开发自己的移动中间件平台。简单地说,移动中间件平台在您的移动应用与企业系统的通信中充当中间人的角色。目标 是让您的移动应用能够处理企业应用中的数据,而不会影响操作系统功能或移动设备上可用的有限资源。

我曾经测试过一个银行手机应用的安全性。移动应用开发人员在与一个非常专有的、封闭的、文档记录不充分的应用集成时,遵循了使用移动中间件的思想。开发人员以屏幕翻译器的形式创建了一个移动中间件组件。本质上,这是一个基于服务器的应用,它将从银行应用 获取网站,挖掘或复制特定页面上的所有文本,然后将这些页面转换为移动格式的文本。

看一下图 7-2 。它展示了移动应用如何连接到一个中间件系统,该中间件系统抽象了遗留应用的数据和用户界面。在某些情况下,移动客户端可以通过移动浏览器直接访问遗留应用,但在这种情况下,它不会提供理想的用户体验。因此,通过与移动中间件接口,应用的通信基础设施可以标准化。与遗留应用的大多数交互 将在更强大的硬件上完成。

9781430240624_Fig07-02.jpg

图 7-2 。移动中间件示例

考虑到这一点,当我们决定开发企业移动应用时,我们需要确定我们将会遇到的一些关键场景。在这一章中,我着眼于开发企业移动应用时被证明是一个挑战的两个领域:数据库访问 和数据表示。在移动企业应用开发过程中,这些特定的领域被证明是一个挑战。让我们从数据库访问开始。

数据库访问

Android 支持可以用来访问数据库服务器的 javax.sql 和 java.sql 包。让我们从一个非常简单但不安全的示例应用 — 开始,向您展示这种方法的不足之处。接下来,我们将看看一些更好的技术。您可能会奇怪,为什么我要浪费您的时间来详细研究一个不安全的解决方案。重点是看它为什么没有安全感;只有当你明白它是多么的不安全,你才会充分体会到正确方法的好处。随意向前跳——后果自负!

该应用将连接到一个 MySQL 数据库,并从名为 apress 的表中读取数据。为了正确执行,Android 设备和数据库服务器应该位于同一个网络上。我将把数据库的设置和创建留给您。确保您设置了数据库服务器来监听公共 IP 地址。你可以通过编辑 MySQL 安装中的 my.cnf 文件来实现。清单 7-1 包含了数据库模式。确保首先创建名为 android 的数据库。创建表格后,向其中输入一些测试数据,这样当您使用 Android 应用连接到表格时就可以检索到它。

清单 7-1。 一条 MySQL SQL 语句创建一个 apress 表

CREATE TABLE `apress` (
   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
   `name` varchar(50) NOT NULL DEFAULT '',
   `email` varchar(50) DEFAULT NULL,
   PRIMARY KEY (`id`)
) ENGINE = MyISAM AUTO_INCREMENT = 4 DEFAULT CHARSET = latin1;

现在让我们开始开发应用吧。创建一个名为 MySQLConnect 的新项目。在您的项目文件夹中,创建一个名为 lib 的新文件夹。现在从 www.mysql.com/products/connector/下载最新版本的 MySQL Connector/J。接下来,解压存档并复制。jar 文件到您的 lib 目录。该文件应该类似于 MySQL-connector-Java-5 . 1 . 15-bin . jar。如果您正在使用 Eclipse 进行开发,那么您的项目布局将类似于图 7-3 中的布局。在我的布局中,你可以看到我有几个版本的 MySQL 连接器,但是我使用的是最新的版本。

9781430240624_Fig07-03.jpg

图 7-3 。MySQLConnect 项目结构

在这个例子中,我们创建了一个列表视图布局。这为我们从数据库中检索的数据提供了一个很好的全屏列表。因为列表视图将包含单独的项目,我们必须告诉 Android 每个项目是什么。为此,我们创建一个名为 list_item.xml 的新 XML 文件,包含清单 7-2 中的文本,然后将其保存在布局文件夹下,如图 7-3 所示。

清单 7-2。list _ item . XML 文件内容

<?xml version  =  *"1.0"*encoding  =  *"utf-8"*?>
<TextView xmlns:android  =  "http://schemas.android.com/apk/res/android"
   android:layout_width  =  *"fill_parent"*
   android:layout_height  =  *"fill_parent"*
   android:padding  =  *"10dp"*
   android:textSize  =  *"16sp"*>
</TextView>

这告诉 Android 每个列表项都是文本类型的,并给它一些关于文本填充和字体大小的详细信息。接下来是 MySQLConnectActivity.java 文件的代码(见清单 7-3 )。记下将主机 IP 地址、用户名和密码更改为您创建的内容。

清单 7-3。【MySQLConnectActivity.java】源代码

package net.zenconsult.android;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.Hashtable;

import android.app.ListActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

public class MySQLConnectActivity extends ListActivity {
   /** Called when the activity is first created. */
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);

      Connection conn  =  null;
      String host  =  "192.168.3.105";
      int port  =  3306;
      String db  =  "android";

      String user  =  "sheran";
      String pass  =  "P@ssw0rd";

      String url  =  "jdbc:mysql://"  +  host  +  ":"  +  port  +  "/"  +  db  +  "?user = "
      + user  +  "&password = "  +  pass;
      String sql  =  "SELECT * FROM apress";

      try {
         Class.forName("com.mysql.jdbc.Driver").newInstance();
         conn  =  DriverManager.getConnection(url);

         PreparedStatement stmt  =  conn.prepareStatement(sql);
         ResultSet rs  =  stmt.executeQuery();
         Hashtable  < String, String  >  details  =  new Hashtable  < String, String  > ();
         while (rs.next()) {
           details.put(rs.getString("name"), rs.getString("email"));
         }
         String[] names  =  new String[details.keySet().size()];
         int x  =  0;
         for (Enumeration  < String  >  e  =  details.keys(); e.hasMoreElements();) {
           names[x]  =  e.nextElement();
           x++;
         }
         conn.close();
         this.setListAdapter(new ArrayAdapter  < String  > (this,
           R.layout.list_item, names));

         ListView lv  =  getListView();
         lv.setTextFilterEnabled(true);

         lv.setOnItemClickListener(new OnItemClickListener() {
           public void onItemClick(AdapterView  < ?  >  parent, View view,
           int position, long id) {
             Toast.makeText(getApplicationContext(),
             ((TextView) view).getText(), Toast.LENGTH_SHORT).show();
            }
         });

         } catch (ClassNotFoundException e) {
           Log.e("MYSQL", "Class not found!");
         } catch (SQLException e) {
           Log.e("MYSQL", "SQL Exception "  +  e.getMessage());
         } catch (InstantiationException e) {
           Log.e("MYSQL", "Instantiation error "  +  e.getMessage());
         } catch (IllegalAccessException e) {
           // TODO Auto-generated catch block
           e.printStackTrace();
         }

   }
}

因为我们正在访问网络,你必须确保你的应用具有在 AndroidManifest.xml 文件中设置的 Android . permission . internet 权限。

保存您的项目并在您的 Android 模拟器上运行它。您的应用应该启动,连接到数据库,检索数据,并以类似于图 7-4 所示的全屏列表视图显示数据。

9781430240624_Fig07-04.jpg

图 7-4 。应用正确执行时的输出

正如你所看到的,即使我们能够直接从数据库中读取数据,除了用我们的应用打包大型 JDBC 驱动程序库之外,似乎还有很多繁琐的代码需要我们编写。

在某些情况下,如果你不得不连接到一个没有纯 JDBC 驱动的数据库,那么你就被困住了。如果您考虑安全问题,那么您需要考虑您的数据库服务器必须暴露在互联网或 VPN 上,因为移动设备和数据库服务器都应该能够相互通信。最后,您可以看到数据库凭证存储在应用中。

请看下面这段代码:

Connection conn  =  null;
   String host  =  "192.168.3.105";
   int port  =  3306;
   String db  =  "android";

   String user  =  "sheran";
   String pass  =  "P@ssw0rd";
   String url  =  "jdbc:mysql://"  +  host  +  ":"  +  port  +  "/"  +  db  +  "?user = "
    + user  +  "&password = "  +  pass;
   String sql  =  "SELECT * FROM apress";

以 String user 和 String pass 开始的行显示了数据库凭证是如何在应用中被硬编码的。如果手机遭到破坏,攻击者可以从你的应用数据中读取数据库凭据,并使用它们从另一台计算机连接,直接攻击你的数据库。

因此,在你的 Android 应用中使用原生 JDBC 连接并不是最好的方法。最好编写一个移动中间件模块,让 app 以更方便、更安全的方式访问数据。

我们如何改进数据库访问过程?HTTP 是最简单也可能是最成熟的请求/响应机制之一。通过使用 HTTP ,我们当然可以简化和提高我们的数据访问方法的安全性。Android 已经内置了非常强大的 HTTP 客户端;我们有 SSL 来保护我们的数据;而且,如果需要,我们可以为来回传输的数据添加额外的加密层。你可能会说使用 HTTP 是不需要动脑筋的,所以我们就这么做吧。

但是我们应该如何使用 HTTP 从数据库中请求数据呢?我们可以使用 web 服务从后端获取数据。我们可以使用 REST(表述性状态转移)进行通信,而不是制作非常复杂的 web 服务。公开 RESTful API 将极大地简化我们的移动应用请求数据的方式。考虑这个例子:

 [`192.168.3.105/apress/members`](https://192.168.3.105/apress/members) 

通过发出这个 get 请求,我们可以获取与之前在 MySQLConnect 示例中获取的数据集相同的数据集。使用 HTTP 请求获取数据肯定要简单得多。当然,下一步是检索数据。因为我们选择了 HTTP 作为我们的传输机制,所以我们必须使用 HTTP 友好的响应机制。这就给我们带来了数据表示的问题。我们将在下一节中讨论这一点。

我希望您正在构建自己的库集,以便以后重用。这是一个很好的练习。我有几个不同的库,它们是我在开发时为不同的任务创建的。我有一个处理数据库连接的库,一个处理数据编码和解码的库,还有许多我在构建应用时使用的其他小工具库。它们加快了我的开发周期,并且通常使一切保持一致的状态。我现在提出这一点是因为,如果您打算开始构建自己的定制移动中间件的旅程,那么如果您将它设计成可以插入到尽可能多的部署场景中,您会更好。从那里,您可以调整配置设置,这样您就可以快速启动并运行。

注意自定义库

开发你自己的库是一个很好的实践。对我来说,编写自己的库意味着我永远不会忘记几个月前完成的一个特定的实现。我可以简单地调用我的共享库函数,并毫不费力地集成它。

但是,请记住,您所有的外部库函数都应该非常简单。这些基本功能以后可以串联起来执行一个复杂的功能。因此,您可以构建自己的库,并完全加快开发时间。

假设您花费了大量的时间和精力为您的客户编写一个电子商务应用。项目完成后,可能没有明确要求保留源代码。然而,如果你遇到另一个客户希望你建立一个类似的电子商务商店,这可能对你很重要。如果您对自己在早期应用中编写的代码拥有无可争议的所有权,您就可以重用它,从而大大减少准备新应用所需的时间。

数据表示

解决了这个问题之后,让我们来谈谈数据表示。通过数据表示,我指的是您的移动应用如何从后端 web 应用接收数据。在我们的案例中,我们正试图将我们的移动应用接收和处理数据的方式标准化。目前最常见的数据表示格式是 XML(可扩展标记语言)和 JSON (JavaScript 对象表示法)。因此,让我们致力于编写移动应用框架来接收和处理这种类型的数据。关于 XML 和 JSON 的快速入门,请参考附录。选择这种类型的数据表示的另一个原因是,有许多第三方的开源库,您可以根据自己的目的使用或修改。

回到我们的 RESTful API 请求,让我们看看我们可能从移动中间件得到的以下两个潜在响应:

XML
<?xml version = "1.0" encoding = "UTF-8"?>
<apress>
   <users>
    <user name = "Sheran" email =  "sheranapress@gmail.com" />
      <user name = "Kevin" email = "kevin@example.com" />
      <user name = "Scott" email = "scottm@example.com" />
   </users>
</apress>
JSON
{
   users:{
    user:[
    {
    name:'Sheran',
    email:'sheranapress@gmail.com'
    },
    {
    name:'Kevin',
    email:'kevin@example.com'
     },
    {
    name:'Scott',
    email:'scottm@example.com'
     }
     ]
  }
}

好的一面是,您不需要编写这么多代码来读取 XML 和 JSON 表示。Android 包括解析这两种格式的库。让我们看一些源代码。再次创建一个新项目,并将其命名为 RESTFetch 。像前面的例子一样创建 list_item.xml 文件,然后将 Android . permission . internet 权限分配给应用。清单 7-4 包含应用的代码,它将发出请求,处理 XML 响应,并在列表中呈现结果。图 7-5 包含输出。

清单 7-4。 使用 RESTful API 获取数据并处理 XML 输出

packagenet.zenconsult.android;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.URI;
import java.net.URISyntaxException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import android.app.ListActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

public class RESTFetchActivity extends ListActivity {
   @Override
   public voidonCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    BufferedReader in =  null;

    try{
    HttpClient client =  new DefaultHttpClient();
    HttpGet request =  new HttpGet();
    request.setURI(new URI("http://192.168.3.105/apress/members"));
    HttpResponse response =  client.execute(request);
    in =  new BufferedReader(new InputStreamReader(response.getEntity()
    .getContent()));
    StringBuffer sb =  new StringBuffer("");
    String line =  "";
    String newLine =  System.*getProperty*("line.separator");
    while ((line =  in.readLine()) ! =  null ) {
      sb.append(line  +  newLine);
    }
    in.close();

    Document doc =  null ;

    DocumentBuilderFactory dbf =  DocumentBuilderFactory.*newInstance*();

    DocumentBuilder db =  dbf.newDocumentBuilder();

    InputSource is =  new InputSource();
    is.setCharacterStream(new StringReader(sb.toString()));
    doc =  db.parse(is);

    NodeList nodes =  doc.getElementsByTagName("user");
    String[] names =  new String[nodes.getLength()];
    for (int k =  0; k  <  nodes.getLength(); ++k) {
      names[k] =  nodes.item(k).getAttributes().getNamedItem("name")
        .getNodeValue();
    }

    this .setListAdapter(new ArrayAdapter  < String  > (this ,
      R.layout.*list_item*, names));

    ListView lv =  getListView();
    lv.setTextFilterEnabled(true );

    lv.setOnItemClickListener(new OnItemClickListener() {
     public void onItemClick(AdapterView  < ?  >  parent, View view,
       int position,long id) {
        Toast.*makeText*(getApplicationContext(),
        ((TextView) view).getText(), Toast.*LENGTH_SHORT*)
        .show();
    }
    });

    }catch (IOException e) {
       Log.*e*("REST", "IOException "  +  e.getMessage());
    }catch (URISyntaxException e) {
       Log.*e*("REST", "Incorret URI Syntax "  +  e.getMessage());
    }catch (ParserConfigurationException e) {
       //TODO Auto-generated catch block
    e.printStackTrace();
    }catch (SAXException e) {
       //TODO Auto-generated catch block
       e.printStackTrace();
    }

   }
}

9781430240624_Fig07-05.jpg

图 7-5 。带有 XML 响应的 RESTful API 查询的输出

对于 JSON 请求/响应代码和输出,分别看一下清单 7-5 和图 7-6 。

清单 7-5。 使用 RESTful API 获取数据并处理 JSON 输出


package net.zenconsult.android;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.ListActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

public class RESTJSONActivity extends ListActivity {
   @Override
   public void onCreate(Bundle savedInstanceState) {
    super .onCreate(savedInstanceState);
    BufferedReader in =  null ;
    try {
        HttpClient client =  new DefaultHttpClient();
        HttpGet request =  new HttpGet();
        request.setURI(new URI("http://192.168.3.105/apress/members.json"));
        HttpResponse response =  client.execute(request);
        in =  new BufferedReader(new InputStreamReader(response.getEntity()
        .getContent()));
        StringBuffer sb =  new StringBuffer("");
        String line =  "";
        while ((line =  in.readLine()) ! =  null ) {
           sb.append(line);
        }
        in.close();
        JSONObject users =  new JSONObject(sb.toString())
          .getJSONObject("users");
        JSONArray jArray =  users.getJSONArray("user");
        String[] names =  new String[jArray.length()];
        for (int i =  0; i  <  jArray.length(); i++) {
           JSONObject jsonObject =  jArray.getJSONObject(i);
           names[i] =  jsonObject.getString("name");
        }
    this .setListAdapter(new ArrayAdapter  < String  > (this ,
        R.layout.*list_item*, names));

      ListView lv =  getListView();
      lv.setTextFilterEnabled(true );
      lv.setOnItemClickListener(new OnItemClickListener() {
      public void onItemClick(AdapterView  < ?  >  parent, View view,
      int position,long id) {
         Toast.*makeText*(getApplicationContext(),
        ((TextView) view).getText(), Toast.*LENGTH_SHORT*)
        .show();
       }
     });
        }catch (IOException e) {
        Log.*e*("RESTJSON", "IOException "  +  e.getMessage());
        }catch (URISyntaxException e) {
        Log.*e*("RESTJSON", "Incorret URI Syntax "  +  e.getMessage());
        }catch (JSONException e) {
        //TODO Auto-generated catch block
        e.printStackTrace();
        }
   }
}

9781430240624_Fig07-06.jpg

图 7-6 。带有 JSON 响应的 RESTful API 查询的输出

如果需要,可以将 XML 和 JSON 示例合并到一个类文件中。为了区分响应类型,通常可以在成员请求后附加一个文件扩展名。因此,对于 XML 响应,调用192 . 168 . 3 . 105/a press/members . XML; 并且,对于 JSON 响应,调用192 . 168 . 3 . 105/a press/members . JSON。同样,我们可以修改我们的示例,以便我们分析响应数据来自动发现结构。这将使我们能够根据某些关键字提取数据,而不管它们出现在哪里。然而,在大多数情况下,在代码中定义你的数据结构是无害的,因为毕竟,你的移动应用只会与你的移动中间件对话。

说到移动中间件,生成 XML 和 JSON 响应的服务器端代码到底在哪里?目前,这样的代码超出了本书的范围。但是为了让您更好地理解如何实现这种类型的移动中间件,请看附录中一个非常基本的例子,它也共享了部署说明。

摘要

如果让您开发一个与遗留企业系统一起工作的移动应用,我们快速地看了一下您将面临的两个问题。毫无疑问,当您涉足移动企业应用开发领域时,您可能会遇到不同的挑战。几乎在所有情况下,您都可以通过在移动中间件中构建翻译或桥接模块来克服这些问题。

就安全性而言,在本章的开始,我们讨论了向公众开放企业环境是一个坏主意。最好的方法是通过使用中间件来减少企业系统的暴露。我们决定使用 HTTP,不仅因为它的简单,还因为我们不需要做任何神奇的事情来保护它。可以应用与 SSL 相同的安全控制,而无需更改我们的任何代码。当然,我们也可以为我们的数据创建额外的加密和压缩层。

八、概念实战:第二部分

在这一章中,就像在第四章中一样,我们将更仔细地看看实现我们已经讨论过的一些理论概念的源代码和应用。这会让你对如何在实践中应用它们有更好的感觉。本章的代码示例将着重于设备上的安全认证和保护密码。回想一下,我们已经讨论了两种登录到后端应用而不在设备上存储凭证的机制。在这里,我们将探索与此相关的更详细的源代码。

oath〔??〕

让我们重温一下第六章中的 OAuth 登录示例。我们讨论了开发一个应用,该应用将与 Google Picasa 网络相册交互,以读取特定用户的相册列表。本章中的代码会做到这一点。查看这本书在 www.apress.comT3 的网站上的最新代码。首先,我们来看看图 8-1 中我们的项目结构。您将看到几个源文件。我们将讨论每个源文件的关键功能。

9781430240624_Fig08-01.jpg

图 8-1 。OAuth 示例的项目结构

正在检索令牌

你可以在图 8-1 中看到 OAuth 示例项目的结构。让我们从应用的入口点开始,它是 OAuthPicasaActivity.java 的,如清单 8-1 所示。

清单 8-1。 申请入口点

**package** net.zenconsult.android;

**import** android.app.ListActivity;
**import** android.content.Intent;
**import** android.os.Bundle;
**import** android.view.View;
**import** android.widget.AdapterView;
**import** android.widget.AdapterView.OnItemClickListener;
**import** android.widget.ArrayAdapter;
**import** android.widget.ListView;
**import** android.widget.TextView;
**import** android.widget.Toast;

**public class** OAuthPicasaActivity **extends** ListActivity {
  OAuthPicasaActivity act;

  /** Called when the activity is first created. */
  @Override
  **public void** onCreate(Bundle savedInstanceState) {
         **super**.onCreate(savedInstanceState);
         act = **this;**
         OAuth o = **new** OAuth**(this)**;
         Token t = o.getToken();

  if (!t.isValidForReq()) {
          Intent intent = **new** Intent(**this**, AuthActivity.**class**);
          **this**.startActivity(intent);
         }
         **if** (t.isExpired()) {
                 o.getRequestToken();
         }

         DataFetcher df = **new** DataFetcher(t);
         df.fetchAlbums("sheranapress");
         String[] names = **new** String[] {}; // Add bridge code here to parse XML
                                           // from DataFetcher and populate
                                           // your List

         **this**.setListAdapter(**new** ArrayAdapter  < String > (**this**, R.layout.*list_item*,
                           names));

         ListView lv = getListView();
         lv.setTextFilterEnabled(**true**);

         lv.setOnItemClickListener(**new** OnItemClickListener() {
                 **public void** onItemClick(AdapterView  <?>  parent, View view,
                                 **int** position, **long** id) {
                            Toast.*makeText* (getApplicationContext(),
                                              ((TextView) view).getText(),
 Toast. *LENGTH_SHORT*).show();
            }
       });

  }

}

您将看到这个文件正在做几件事情。首先,它实例化了 OAuth 类。接下来,它检索令牌对象,并测试该令牌是否有效,以便在 isValidForReq() 函数中发出请求。它还在 isExpired() 函数中测试令牌是否过期。如果令牌有效,则实例化 DataFetcher 对象,该对象向 Picasa 查询属于用户 sheranapress 的所有相册列表。这是在 df . fetchalbums(" sherana press ")行中完成的。

显然,这个应用第一次运行时,不会有有效的令牌对象。应用处理这种情况的方法是,首先获取一个授权代码,然后获取一个带有该授权代码的请求令牌(按照 Google 的 OAuth 2 规范)。接下来看看这是怎么做的。

处理授权

清单 8-2 显示了我们的应用中处理授权部分的源代码。如果您查看 doAuth() 函数,您将看到一个对 Google 的请求,应用在一个 WebView 对象中显示响应。WebView 对象是一个显示 HTML 内容的字段。你可以把它想象成一个简约的浏览器。这允许最终用户登录到她的谷歌帐户,并授予或拒绝我们的应用访问。用户将看到 Google 登录网页,并被要求使用她的凭证登录。这些凭证没有存储在我们应用的任何地方。如果他允许我们的应用使用她的 Picasa 流,那么 Google 会发回一个授权码。我们的应用将把这个授权码存储在令牌对象中。这是在 ClientHandler 对象中完成的(参见清单 8-3 )。

清单 8-2。Auth 活动获取授权码。

**package** net.zenconsult.android;

**import** java.net.URI;
**import** java.net.URISyntaxException;

**import** org.apache.http.message.BasicNameValuePair;

**import** android.app.Activity;
**import** android.content.Context;
**import** android.os.Bundle;
**import** android.util.Log;
**import** android.webkit.WebView;

**public class** AuthActivity **extends** Activity {
        **private** BasicNameValuePair clientId = new BasicNameValuePair("client_id",
                         "200744748489.apps.googleusercontent.com");
        **private** BasicNameValuePair clientSecret = new BasicNameValuePair(
                         "client_secret", "edxCTl_L8_SFl1rz2klZ4DbB");
        **private** BasicNameValuePair redirectURI = new BasicNameValuePair(
                         "redirect_uri", "urn:ietf:wg:oauth:2.0:oob");
        **private** String scope = "scope=[`picasaweb.google.com/data/`](https://picasaweb.google.com/data/)";
        **private** String oAuth = "[`accounts.google.com/o/oauth2/auth`](https://accounts.google.com/o/oauth2/auth) ?";
        **private** String httpReqPost = " [`accounts.google.com/o/oauth2/token`](https://accounts.google.com/o/oauth2/token) ";
        **private final** String FILENAME = ".oauth_settings";
        **private** URI uri;
        **private** WebView wv;
        **private** Context ctx;
        **private** Token token;

        @Override
        **public void** onCreate(Bundle savedInstanceState) {
               **super**.onCreate(savedInstanceState);
               setContentView(R.layout.auth);
               doAuth();
  }
  **public void** doAuth() {
          **try** {
                 uri = **new** URI(oAuth + clientId + "&" + redirectURI + "&" + scope
                              + "&response_type = code");
                 wv = (WebView) findViewById(R.id.webview);
                 wv.setWebChromeClient(**new** ClientHandler(**this**));
                 wv.setWebViewClient(**new** MWebClient());
                 wv.getSettings().setJavaScriptEnabled(**true**);
                 wv.loadUrl(uri.toASCIIString());
                 Log.v("OAUTH", "Calling " + uri.toASCIIString());
          }**catch** (URISyntaxException e) {
                 e.printStackTrace();
          }
    }
}

清单 8-3。client handler 将授权码写入令牌对象。

package net.zenconsult.android;

import android.app.Activity;
import android.util.Log;

import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.widget.Toast;

public class ClientHandler extends WebChromeClient {
       private Activity activity;
       private OAuth oAuth;

       public ClientHandler(Activity act) {
               activity = act;
               oAuth = new OAuth(activity);
       }

       @Override
        public void onReceivedTitle(WebView view, String title) {
               String code = "";
               if (title.contains("Success")) {
                      code = title.substring(title.indexOf(' = ') + 1, title.length());
                      setAuthCode(code);
                      Log.v("OAUTH", "Code is " + code);
                      oAuth.getRequestToken();
                      oAuth.writeToken(oAuth.getToken());
                      Toast toast = Toast.makeText(activity.getApplicationContext(),
                                      "Authorization Successful", Toast.LENGTH_SHORT);
                      toast.show();
                      activity.finish();
               } else if (title.contains("Denied")) {
                      code = title.substring(title.indexOf(' = ') + 1, title.length());
                      setAuthCode(code);
                      Log.v("OAUTH", "Denied, error was " + code);
                      Toast toast = Toast.makeText(activity.getApplicationContext(),
                                      "Authorization Failed", Toast.LENGTH_SHORT);
                      toast.show();
                      activity.finish();
               }
         }

  public String getAuthCode() {
          return oAuth.getToken().getAuthCode();
  }

  public void setAuthCode(String authCode) {
          oAuth.getToken().setAuthCode(authCode);
  }

  @Override
  public void onProgressChanged(WebView view, int progress) {

  }
}

把 ClientHandler 想象成一个观察者。它在每个 HTML 网页中寻找一个特定的字符串——“成功”——。如果它找到了这个词,那么我们就获得了正确的授权码,这意味着我们的最终用户已经批准了我们的访问。

将授权码写入内部存储后,您将需要获取一个请求令牌。在 Oauth 中,您将需要一个请求令牌来开始请求访问任何资源的过程。OAuth 流程请参见图 6-25。如果您再次查看我们的 ClientHandler 代码,您将会看到下面几行代码 oAuth.getRequestToken() 和 oauth . write token(oauth . gettoken())。这两行使用实例化的 OAuth 类(参见清单 8-4 )请求一个请求令牌,然后将其写入内存。 getRequestToken() 函数处理该部分。同样值得注意的是,每当我提到存储时,您都应该考虑使用加密。有关实施安全数据存储的更多信息,请参考第五章中的“Android 中的数据存储”部分。

清单 8-4。 如果授权码有效,OAuth 类从 Google 获取请求令牌。

**package** net.zenconsult.android;

**import** java.io.BufferedInputStream;
**import** java.io.BufferedOutputStream;
**import** java.io.File;
**import** java.io.FileInputStream;
**import** java.io.FileNotFoundException;
**import** java.io.FileOutputStream;
**import** java.io.IOException;
**import** java.io.ObjectInputStream;
**import** java.io.ObjectOutputStream;
**import** java.io.StreamCorruptedException;
**import** java.io.UnsupportedEncodingException;
**import** java.net.URI;
**import** java.util.ArrayList;
**import** java.util.List;

**import** org.apache.http.HttpEntity;
**import** org.apache.http.HttpResponse;
**import** org.apache.http.NameValuePair;
**import** org.apache.http.client.ClientProtocolException;
**import** org.apache.http.client.HttpClient;
**import** org.apache.http.client.entity.UrlEncodedFormEntity;
**import** org.apache.http.client.methods.HttpPost;
**import** org.apache.http.impl.client.DefaultHttpClient;
**import** org.apache.http.message.BasicNameValuePair;
**import** org.apache.http.util.EntityUtils;
**import** org.json.JSONException;
**import** org.json.JSONObject;

**import** android.app.Activity;
**import** android.content.Context;
**import** android.util.Log;
**import** android.webkit.WebView;
**import** android.widget.Toast;

**public class** OAuth {
        **private** BasicNameValuePair clientId = **new** BasicNameValuePair("client_id",
                        "200744748489.apps.googleusercontent.com");
        **private** BasicNameValuePair clientSecret = **new** BasicNameValuePair(
                        "client_secret", "edxCTl_L8_SFl1rz2klZ4DbB");
        **private** BasicNameValuePair redirectURI = **new** BasicNameValuePair(
                        "redirect_uri", "urn:ietf:wg:oauth:2.0:oob");
        **private** String scope = "scope=[`picasaweb.google.com/data/`](https://picasaweb.google.com/data/)";
        **private** String oAuth = "[`accounts.google.com/o/oauth2/auth`](https://accounts.google.com/o/oauth2/auth)?";
        **private** String httpReqPost = "[`accounts.google.com/o/oauth2/token`](https://accounts.google.com/o/oauth2/token)";
        **private** final String FILENAME = ".oauth_settings";
        **private** URI uri;
        **private** WebView wv;
        **private** Context ctx;
        **private** Activity activity;
        **private boolean** authenticated;
        **private** Token token;

        **public** OAuth(Activity act) {
                ctx = act.getApplicationContext();
                activity = act;
                token = readToken();

        }

        **public** Token readToken() {
                Token token = **null**;
                FileInputStream fis;
                **try** {
                        fis = ctx.openFileInput(FILENAME);
                        ObjectInputStream in = **new** ObjectInputStream(
                                       **new** BufferedInputStream(fis));
                        token = (Token) in.readObject();
                        **if** (token == **null**) {
                                token = **new** Token();
                                writeToken(token);
                        } 
                        in.close();
                        fis.close();
                } **catch** (FileNotFoundException e) {
                        writeToken(**new** Token());
                } **catch** (StreamCorruptedException e) {
                        // **TODO** Auto-generated catch block
                        e.printStackTrace();
                } **catch** (IOException e) {
                        // **TODO** Auto-generated catch block
                        e.printStackTrace();
                } **catch** (ClassNotFoundException e) {
                        // **TODO** Auto-generated catch block
                        e.printStackTrace();
                }
                **return** token;
                }

        **public void** writeToken(Token token) {
                **try** {
                        File f = **new** File(FILENAME);
                        **if** (f.exists()) {
                                f.delete();
                        }
                        FileOutputStream fos = ctx.openFileOutput(FILENAME,
                                       Context.*MODE_PRIVATE*);

                        ObjectOutputStream out = **new** ObjectOutputStream(
                                       **new** BufferedOutputStream(fos));
                        out.writeObject(token);
                        out.close();
                        fos.close();
                } **catch** (FileNotFoundException e1) {
                        Log.*e*("OAUTH", "Error creating settings file");
                } **catch** (IOException e2) {
                        // **TODO** Auto-generated catch block
                        e2.printStackTrace();
                }
        }

        **public void** getRequestToken() {
                HttpClient httpClient = **new** DefaultHttpClient();
                HttpPost post = **new** HttpPost(httpReqPost);
                List  <  NameValuePair  > nvPairs = **new** ArrayList  <  NameValuePair  >  ();
                nvPairs.add(clientId);
                nvPairs.add(clientSecret);
                nvPairs.add(**new** BasicNameValuePair("code", token.getAuthCode()));
                nvPairs.add(redirectURI);
                nvPairs.add(**new** BasicNameValuePair("grant_type", "authorization_code"));
                **try** {
                        post.setEntity(**new** UrlEncodedFormEntity(nvPairs));
                        HttpResponse response = httpClient.execute(post);
                        HttpEntity httpEntity = response.getEntity();
                        String line = EntityUtils.*toString*(httpEntity);
                        JSONObject jObj = **new** JSONObject(line);
                        token.buildToken(jObj);
                        writeToken(token);
                } **catch** (UnsupportedEncodingException e) {
                        // **TODO** Auto-generated catch block
                        e.printStackTrace();
                } **catch** (ClientProtocolException e) {
                        // **TODO** Auto-generated catch block
                        e.printStackTrace();
                } **catch** (IOException e) {
                        **if** (e.getMessage().equals("No peer certificate")) {
                                Toast toast = Toast.*makeText*![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
(activity.getApplicationContext(),
                                                "Possible HTC Error for Android 2.3.3",
                                                Toast.*LENGTH_SHORT*);
                            toast.show();
                        }
                        Log.*e*("OAUTH", "IOException " + e.getMessage());
                } **catch** (JSONException e) {
                        // **TODO** Auto-generated catch block
                        e.printStackTrace();
                }

        }

        **public** Token getToken() {
                **return** token;
        }

        **public void** setToken(Token token) {
                **this**.token = token;
        }
}

您可能已经注意到,该令牌被用作单例令牌。它被写入设备的内部存储器并从中读取。这允许应用的不同区域在身份验证过程的不同阶段对其进行读写。理想情况下,这应该是同步的,以确保读和写只发生在一个类中。

我已经为清单 8-5 中的令牌对象提供了源代码。该对象实现了可序列化的接口;因此,它可以完整地写入内部存储。确保通过数据存储加密器运行它,以增加安全性。除了检查自己的到期日期之外,令牌对象几乎不包含任何逻辑。

清单 8-5。 令牌对象

package net.zenconsult.android;

**import** java.io.Serializable;
**import** java.util.Calendar;
**import** org.json.JSONException;
**import** org.json.JSONObject;
**public class** Token **implements** Serializable {
        /**
        *
        */
        **private static final long** serialVersionUID = 6534067628631656760L;
        **private** String refreshToken;
        **private**  String accessToken;
        **private**  Calendar expiryDate;
        **private**  String authCode;
        **private**  String tokenType;
        **private**  String name;

        **public** Token() {
                setExpiryDate(0);
                setTokenType("");
                setAccessToken("");
                setRefreshToken("");
                setName("");
        }

        **public** Token(JSONObject response) {
                try {
                      setExpiryDate(response.getInt("expires_in"));
                } **catch** (JSONException e) {
                      setExpiryDate(0);
                }
                try {
                      setTokenType(response.getString("token_type"));
                } **catch** (JSONException e) {
                      setTokenType("");
                }
                try {
                      setAccessToken(response.getString("access_token"));
                } **catch** (JSONException e) {
                      setAccessToken("");
                }
                try {
                      setRefreshToken(response.getString("refresh_token"));
                } **catch** (JSONException e) {
                      setRefreshToken("");
                }
        }
        **public void** buildToken(JSONObject response) {
                try {
                      setExpiryDate(response.getInt("expires_in"));
                } catch (JSONException e) {
                      setExpiryDate(0);
                }
                try {
                      setTokenType(response.getString("token_type"));
                } catch (JSONException e) {
                      setTokenType("");
                }
                try {
                      setAccessToken(response.getString("access_token"));
                } catch (JSONException e) {
                      setAccessToken("");
                }
                try {
                      setRefreshToken(response.getString("refresh_token"));
                } catch (JSONException e) {
                      setRefreshToken("");
                }
        }

        **public boolean** isValidForReq() {
                if (getAccessToken() != null && !getAccessToken().equals("")) {
                      return true;
                } else {
                      return false;
                }
        }

        **public boolean**  isExpired() {
                Calendar now = Calendar.getInstance();
                if (now.after(getExpiryDate()))
                      return true;
                else
                      return false;
        }

        **public** String getRefreshToken() {
                      return refreshToken;
        }

        **public void** setRefreshToken(String refreshToken) {
                if (refreshToken == null)
                        refreshToken = "";
                        this.refreshToken = refreshToken;
        }

        **public** String getAccessToken() {
                      return accessToken;
        }

        **public void** setAccessToken(String accessToken) {
                if (accessToken == null)
                        accessToken = "";
                this.accessToken = accessToken;
        }

        **public** Calendar getExpiryDate() {
                      return expiryDate;
        }

        **public void** setExpiryDate(int seconds) {
                Calendar now = Calendar.getInstance();
                now.add(Calendar.SECOND, seconds);
                this.expiryDate = now;
        }

        **public** String getAuthCode() {
                      return authCode;
        }

        **public void** setAuthCode(String authCode) {
                if (authCode == null)
                authCode = "";
                this.authCode = authCode;
        }

        **public** String getTokenType() {
                      return tokenType;
        }

        **public void** setTokenType(String tokenType) {
                if (tokenType == null)
                tokenType = "";
                this.tokenType = tokenType;
        }

        **public** String getName() {
                      return name;
        }

        **public void** setName(String name) {
                this.name = name;
        }
}

最后,还有数据提取器类(见清单 8-6 )。您使用该类对 Picasa 进行所有受保护的查询。例如,您可以使用这个类来获取相册和照片,甚至上传照片。Picasa 以 XML 格式发回所有回复(注意,我省略了 XML 解析组件)。如果你想知道如何编写一个简单的 XML 解析器来读取 Picasa 的响应,那么看看这本书的附录。

清单 8-6。data etcher 类

**package** net.zenconsult.android;

**import** java.io.IOException;

**import** org.apache.http.HttpEntity;
**import** org.apache.http.HttpResponse;
**import** org.apache.http.client.ClientProtocolException;
**import** org.apache.http.client.HttpClient;
**import** org.apache.http.client.methods.HttpGet;
**import** org.apache.http.impl.client.DefaultHttpClient;
**import** org.apache.http.util.EntityUtils;

**public** class DataFetcher {
        **private** HttpClient httpClient;

        **private** Token token;
        **public** DataFetcher(Token t) {
                token = t;
                httpClient = **new** DefaultHttpClient();
        }
        **public void** fetchAlbums(String userId) {
                String url = " [`picasaweb.google.com/data/feed/api/user/`](https://picasaweb.google.com/data/feed/api/user/) "
                                  + userId;
                **try** {
                     HttpResponse resp = httpClient.execute(buildGet(
                                   token.getAccessToken(), url));
                     **if** (resp.getStatusLine().getStatusCode() == 200) {
                                          HttpEntity httpEntity = resp.getEntity();
                                          String line = EntityUtils. *toString*(httpEntity);
                                          // Do your XML Parsing here
                                          }
                } **catch** (ClientProtocolException e) {
                                          // TODO Auto-generated **catch** block
                                          e.printStackTrace();
                } **catch** (IOException e) {
                                          // TODO Auto-generated **catch** block
                                          e.printStackTrace();
        }
        }
        **public** HttpGet buildGet(String accessToken, String url) {
                HttpGet get = **new** HttpGet(url);
                get.addHeader("Authorization", "Bearer " + accessToken);
                return get;
        }
}

挑战响应

我们在第六章的中非常简要地讨论了基于挑战响应的认证。让我们仔细看看挑战-响应认证技术。以下是所需步骤 ?? 的简要概述,也显示在图 8-2 中。请记住,这只是服务器对客户端进行身份验证的单向身份验证:1。

  1. 客户端请求安全资源。
  2. 服务器发送一个质询字符串 c。
  3. 客户端生成一个随机字符串 r。
  4. 客户端根据 C、R 和用户密码生成一个散列。
  5. 客户端将 R 和散列发送回服务器。
  6. 服务器根据存储的用户密码和。
  7. 如果验证正确,服务器发回请求的资源;否则,会发回一条错误消息。

9781430240624_Fig08-02.jpg

图 8-2 。质询-响应会话期间客户端和服务器之间数据交换的图形表示

注意您还可以有一个相互认证的场景,其中客户端对服务器进行认证。

让我们编写一些简单的代码,帮助我们在应用中使用挑战-响应认证技术。您应该发展这些代码段以适应您自己的需要,然后在您的应用中使用它们。它们有助于减少终端用户的暴露,因为您不会在设备上存储任何凭据。我已经给出了客户端和服务器端代码的例子。服务器端的代码是用 Java 写的,可以打包成 Java Web 存档文件(WAR 文件)。要测试它,将它打包成一个 WAR 文件,并简单地将它放到 servlet 容器或应用服务器的部署目录中。

让我们从服务器端代码开始。我们将创建一个 Java servlet 来处理与客户端的 HTTP 通信。图 8-3 显示了项目结构。这个结构说明了我们有一个只有四个文件的相当简单的项目。

9781430240624_Fig08-03.jpg

图 8-3 。我们的挑战响应服务器端项目结构

其中之一,Hex.java 文件 ,是我用来将各种数据类型转换成十六进制字符串的工具类;另一个,【Constants.java】,保存用户名和密码。这些凭证将用于比较客户端输入的内容。

您还会注意到,我们使用 Apache Commons 编解码器库来帮助我们的 Base64 编码和解码。在本例中,我们采用 CRAM-MD5 身份验证方法,改为使用 SHA1 哈希。(CRAM 是挑战响应认证机制。)

我将首先展示代码,然后解释我们要做什么。让我们从我们的 servletLogin.java 开始,如清单 8-7 所示。该代码有两个主要分支:

  • 主分支 1 处理接收到没有“挑战”参数的请求的情况。
  • 主分支 2 处理接收到带有“挑战”参数的请求的情况。

清单 8-7。 登录类

**package** net.zenconsult.android;

**import** java.io.IOException;

**import** javax.servlet.ServletException;
**import** javax.servlet.annotation.WebServlet;
**import** javax.servlet.http.HttpServlet;
**import** javax.servlet.http.HttpServletRequest;
**import** javax.servlet.http.HttpServletResponse;
**import** javax.servlet.http.HttpSession;

/**
        * Servlet implementation class login
        */
@WebServlet(description = "Login Servlet", urlPatterns = { "/login" })
        **public class** Login **extends** HttpServlet {
                **private static final long** serialVersionUID = 1 L;

        /**
                * **@see** HttpServlet#HttpServlet()
                */
                **public** Login() {
                **super()**;
                **// TODO** Auto-generated constructor stub
                }

                /**
                * **@see** HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
                * response)
                */
        **protected void** doGet(HttpServletRequest request,
                        HttpServletResponse response) **throws** ServletException,![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
  IOException {
                HttpSession session = request.getSession();
                String param = request.getParameter("challenge");
                **if** (param != **null**) {
                CRAM c = (CRAM) session.getAttribute("challenge");
                **if** (c == **null**) {
                c = **new** CRAM();
                session.setAttribute("challenge", c);
                response.setHeader("Content-Type", "text/xml");
                response.getWriter().write(c.generate());
                } **else** {
                **if** (c.verifyChallenge(param.trim())) {
                response.setHeader("Content-Type", "text/xml");
                response.getWriter().write![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
(c.generateReply("Authorized"));
                session.invalidate();
                } **else** {
                response.setHeader("Content-Type", "text/xml");
                response.getWriter().write![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
(c.generateReply("Unauthorized"));
                session.invalidate();
                }
                }
                } **else** {
                CRAM c = **new** CRAM();
                session.setAttribute("challenge", c);
                response.setHeader("Content-Type", "text/xml");
                response.getWriter().write(c.generate());
                }

                }

                /**
                * **@see** HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
                * response)
                */
                **protected void** doPost(HttpServletRequest request,
                HttpServletResponse response) **throws** ServletException,![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
                IOException {
                **// TODO** Auto-generated method stub
                }
}

在每种情况下,我们都在创建一个 CRAM 对象。该对象将生成我们的质询字符串,并对用户响应进行比较。我们将 CRAM 对象与每个 HTTP 会话相关联,以便使用相同的挑战字节进行验证。

现在将是一个很好的时间来从协议层面看一下客户端和服务器之间发生了什么(见图 8-4 )。整个流程有四个步骤,非常简单:

  1. 客户端请求受保护的资源。
  2. 服务器回复一个询问。
  3. 客户端使用最终用户凭证来计算响应,并将其发送回服务器。
  4. 最后,服务器将计算相同的响应,进行比较,并决定用户是否被授权。

9781430240624_Fig08-04.jpg

图 8-4 。挑战响应消息流

所有这些都是在不通过 Web 发送用户凭证的情况下完成的。

CRAM 对象 的源代码如清单 8-8 所示。

清单 8-8。 补习班

**package** net.zenconsult.android;

**import** java.io.StringWriter;
**import** java.security.InvalidKeyException;
**import** java.security.NoSuchAlgorithmException;
**import** java.security.SecureRandom;
**import** javax.crypto.Mac;

**import** javax.crypto.SecretKey;
**import** javax.crypto.spec.SecretKeySpec;
**import** javax.xml.parsers.DocumentBuilder;
**import** javax.xml.parsers.DocumentBuilderFactory;
**import** javax.xml.parsers.ParserConfigurationException;
**import** javax.xml.transform.OutputKeys;
**import** javax.xml.transform.Transformer;
**import** javax.xml.transform.TransformerConfigurationException;
**import** javax.xml.transform.TransformerException;
**import** javax.xml.transform.TransformerFactory;
**import** javax.xml.transform.dom.DOMSource;
**import** javax.xml.transform.stream.StreamResult;

**import** org.apache.commons.codec.binary.Base64;
**import** org.w3c.dom.Document;
**import** org.w3c.dom.Element;
**import** org.w3c.dom.Text;

**public class** CRAM **implements** Constants {
  **private final byte[]** secret = **new byte[32];**

         **public** CRAM() {
                 SecureRandom sr = **new** SecureRandom();
                 sr.nextBytes(secret);
                 }

         **public** String generate() {
                 DocumentBuilderFactory dbFactory = DocumentBuilderFactory.*newInstance*();
                 DocumentBuilder dBuilder = **null**;
                 try {
                       dBuilder = dbFactory.newDocumentBuilder();
                 } **catch** (ParserConfigurationException e) {
                       // **TODO** Auto-generated catch block
                       e.printStackTrace();
                 }
                 Document doc = dBuilder.newDocument();

                 // Build Root
                 Element root = doc.createElement("ServerResponse");
                 doc.appendChild(root);

                 // Challenge Section
                 Element authChallenge = doc.createElement("AuthChallenge");
                 root.appendChild(authChallenge);

                 // The Challenge
                 Element challenge = doc.createElement("Challenge");
                 Text challengeText = doc.createTextNode(Base64
                               .*encodeBase64String*(secret));
                 challenge.appendChild(challengeText);
                 authChallenge.appendChild(challenge);

                 TransformerFactory tFactory = TransformerFactory.*newInstance*();
                 Transformer transformer = **null**;
                 **try** {
                       transformer = tFactory.newTransformer();
                 } **catch** (TransformerConfigurationException e) {
                       // **TODO** Auto-generated catch block
                       e.printStackTrace();
                 }
                 transformer.setOutputProperty(OutputKeys.*OMIT_XML_DECLARATION*, "yes");
                 transformer.setOutputProperty(OutputKeys.*INDENT*, "yes");
                 StringWriter sw = **new** StringWriter();
                 StreamResult res = **new** StreamResult(sw);
                 DOMSource source = **new** DOMSource(doc);
                 **try** {
                       transformer.transform(source, res);
                 } **catch** (TransformerException e) {
                       // **TODO** Auto-generated catch block
                       e.printStackTrace();
                 }
                 String xml = sw.toString();
                 **return** xml;
                 }
         **public boolean** verifyChallenge(String userResponse) {
                 String algo = "HmacSHA1";
                 Mac mac = **null**;
                 **try** {
                        mac = Mac.*getInstance*(algo);
                 } **catch** (NoSuchAlgorithmException e) {
                       // **TODO** Auto-generated catch block
                       e.printStackTrace();
                 }
                 SecretKey key = **new** SecretKeySpec(*PASSWORD*.getBytes(), algo);

                 **try** {
                 mac.init(key);
                 } **catch** (InvalidKeyException e) {
                       // **TODO** Auto-generated catch block
                       e.printStackTrace();
                 }
                 String tmpHash = *USERNAME* + " " + Hex.*toHex*(mac.doFinal(secret));
                 String hash = Base64.*encodeBase64String*(tmpHash.getBytes());
                 **return** hash.equals(userResponse);
                 }

                 public String generateReply(String response) {
                 DocumentBuilderFactory dbFactory = DocumentBuilderFactory.*newInstance*();
                 DocumentBuilder dBuilder = **null**;
                 **try** {
                 dBuilder = dbFactory.newDocumentBuilder();
                 } **catch** (ParserConfigurationException e) {
                       // **TODO** Auto-generated catch block
                       e.printStackTrace();
                 }
                 Document doc = dBuilder.newDocument();

                 // Build Root
                 Element root = doc.createElement("ServerResponse");
                 doc.appendChild(root);

                 // Challenge Section
                 Element authChallenge = doc.createElement("AuthChallenge");
                 root.appendChild(authChallenge);

                 // Reply
                 Element challenge = doc.createElement("Response");
                 Text challengeText = doc.createTextNode(response);
                 challenge.appendChild(challengeText);
                 authChallenge.appendChild(challenge);

                 TransformerFactory tFactory = TransformerFactory.*newInstance*();
                 Transformer transformer = **null**;
                 **try** {
                 transformer = tFactory.newTransformer();
                 } **catch** (TransformerConfigurationException e) {
                       // **TODO** Auto-generated catch block
                       e.printStackTrace();
                 }
                 transformer.setOutputProperty(OutputKeys.*OMIT_XML_DECLARATION*, "yes");
                 transformer.setOutputProperty(OutputKeys.*INDENT*, "yes");
                 StringWriter sw = **new** StringWriter();
                 StreamResult res = **new** StreamResult(sw);
                 DOMSource source = **new** DOMSource(doc);
                 **try** {
                 transformer.transform(source, res);
                 } **catch** (TransformerException e) {
                       // **TODO** Auto-generated catch block
                       e.printStackTrace();
                 }
                 String xml = sw.toString();
                 **return** xml;
         }

}

在实例化 CRAM 对象时,生成一个新的 32 字节随机数。这是一个领域,而且它与补习对象密切相关。这个随机字节串将用于进一步的质询生成和响应验证。

接下来是 generate() 函数 ,它只不过是为我们生成的随机字节创建一个 Base64 编码。然后,它创建一个 XML 响应和这个质询字符串,然后将它返回给 servlet,以便发送给最终用户。

下一个函数 verify challenge(String user response)是一个重要的函数。如果使用了正确的凭据,它将生成客户端应该生成的响应。使用存储的用户密码,通过 HMAC-SHA1 算法对原始随机字节序列进行哈希运算。然后用户名被添加到这个散列的前面,并进行 Base64 编码。接下来,将它与客户端响应进行比较,当然,如果用户名和密码输入正确,客户端响应应该是相同的 — 。

最后,generate reply(String response)函数将把响应变量中指定的单词作为 XML 文本发回。servlet 根据客户端响应 是否正确,使用以下任意一个词调用该函数:

  • 【授权】
  • “未授权”

您还可以设置一个特殊的授权 cookie 来表明会话已经过身份验证。有许多方法可以改进和构建这些代码。我在这里包含了基本代码,这样您可以更好地理解如何在您的前端和后端应用中实现挑战-响应身份验证机制。

现在我们已经看了服务器端代码 ,让我们为客户端写一些代码。我已经在图 8-5 中展示了项目结构。同样,skeletal 项目相当简单,只有三个文件,不包括十六进制函数类。我将带你浏览每个文件的功能,从入口点开始,【ChallengeResponseClientActivity.java】的(见清单 8-9 )。创建一个 Comms 对象(参见清单 8-10 )和一个 CRAM 对象(参见清单 8-11 )的代码相当简单。 Comms 对象处理客户端和服务器之间的所有网络通信,而 CRAM 对象处理哈希生成部分。在服务器端, CRAM 对象与 CRAM 对象非常相似。在这种情况下,没有验证组件,因为客户端不验证服务器。相反, CRAM 对象使用 HMAC-SHA1 来计算基于服务器挑战的散列。

清单 8-9。 切入点和主要活动

**package** net.zenconsult.android;

**import** android.app.Activity;
**import** android.os.Bundle;
**import** android.view.View;
**import** android.widget.Button;
**import** android.widget.Toast;

**public class** ChallengeResponseClientActivity **extends** Activity {
         /** Called when the activity is first created. */
         @Override
         **public void** onCreate(Bundle savedInstanceState) {
                 **super**.onCreate(savedInstanceState);
                 setContentView(R.layout. *main*); 
                 **final** Activity activity = **this;**

                 **final** Button button = (Button) findViewById(R.id.*button1*);
                 button.setOnClickListener(new View.OnClickListener() {
                         **public void** onClick(View v) {
                                 Comms c = new Comms(activity);
                                        String challenge = c.getChallenge();
                                        CRAM cram = new CRAM(activity);
                                        String hash = cram.generate(challenge);
                                        String reply = c.sendResponse(hash);
                                        **if** (c.authorized(reply)) {
                                                 Toast toast = Toast.*makeText*(
                                                         activity![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
.getApplicationContext(), "Login success",
         Toast.*LENGTH_LONG*);
                                            toast.show();
                                        } **else** {
                                                 Toast toast = Toast.*makeText*(
                                                         activity![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
.getApplicationContext(), "Login failed",
         Toast.*LENGTH_LONG*);
                                                 toast.show();
                                        }
                             }
                 });
         }
}

清单 8-10。Comms 类处理这个应用的所有 HTTP 请求。

package net.zenconsult.android;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;

public class Comms {
  private final String url = "[`192.168.3.117:8080/ChallengeResponse/login`](http://192.168.3.117:8080/ChallengeResponse/login)";
  private Context ctx;
  private DefaultHttpClient client;

  public Comms(Activity act) {
  ctx = act.getApplicationContext();
  client = new DefaultHttpClient();
  }

  public String sendResponse(String hash) {
  List  <  NameValuePair  >  params = new ArrayList  <  NameValuePair  >  ();
  params.add(new BasicNameValuePair("challenge", hash));
  String paramString = URLEncodedUtils.*format*(params, "utf-8");
  String cUrl = url + "?" + paramString;
  return doGetAsString(cUrl);
  }

  public boolean authorized(String response) {
  InputStream is = new ByteArrayInputStream(response.getBytes());
  DocumentBuilderFactory dbFactory = DocumentBuilderFactory.*newInstance*();
  DocumentBuilder db = null;
  Document doc = null;
  String reply = "";
  try {
  db = dbFactory.newDocumentBuilder();
  doc = db.parse(is);
  NodeList nl = doc.getElementsByTagName("Response");
  reply = nl.item(0).getTextContent();
  is.close();
  } catch (ParserConfigurationException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (SAXException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  }
  return reply.matches("Authorized");
  }

  public String getChallenge() {
  InputStream challengeText = doGetAsInputStream(url);
  DocumentBuilderFactory dbFactory = DocumentBuilderFactory.*newInstance*();
  DocumentBuilder db = null;
  Document doc = null;
  String challenge = "";
  try {
  db = dbFactory.newDocumentBuilder();
  doc = db.parse(challengeText);
  NodeList nl = doc.getElementsByTagName("Challenge");
  challenge = nl.item(0).getTextContent();
  challengeText.close();
  } catch (SAXException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (ParserConfigurationException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  }
  return challenge;
  }

  public String doGetAsString(String url) {
  HttpGet request = new HttpGet(url);
  String result = "";
  try {
  HttpResponse response = client.execute(request);
  int code = response.getStatusLine().getStatusCode();
  if (code == 200) {
  result = EntityUtils.*toString*(response.getEntity());
  } else {
  Toast toast = Toast.*makeText*(ctx, "Status Code " + code,
  Toast.*LENGTH_SHORT*);
  toast.show();
  }
  } catch (ClientProtocolException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  }
  return result;

  }

  public InputStream doGetAsInputStream(String url) {
  HttpGet request = new HttpGet(url);
  InputStream result = null;
  try {
  HttpResponse response = client.execute(request);
  int code = response.getStatusLine().getStatusCode();
  if (code == 200) {
  result = response.getEntity().getContent();
  } else {
  Toast toast = Toast.*makeText*(ctx, "Status Code " + code,
  Toast.*LENGTH_SHORT*);
  toast.show();
  }
  } catch (ClientProtocolException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  }
  return result;

  }
}

清单 8-11。 补习班

**package** net.zenconsult.android;

**import** java.security.InvalidKeyException;
**import** java.security.NoSuchAlgorithmException;

**import** javax.crypto.Mac;
**import** javax.crypto.SecretKey;
**import** javax.crypto.spec.SecretKeySpec;

**import** android.app.Activity;
**import** android.util.Base64;
**import** android.widget.TextView;

 **public class** CRAM {
        **private Activity** activity;

        **public** CRAM(Activity act) {
                 activity = act;
  }

        **public** String generate(String serverChallenge) {
                String algo = "HmacSHA1";
                TextView pass = (TextView) activity.findViewById(R.id.*editText2*);
                **byte[]** server = Base64.*decode*(serverChallenge, Base64.*DEFAULT*);

                Mac mac = null;
                **try** {
                        mac = Mac.*getInstance*(algo);
                } **catch** (NoSuchAlgorithmException e) {
                        // **TODO** Auto-generated catch block
                        e.printStackTrace();
                }
                String keyText = pass.getText().toString();
                SecretKey key = **new** SecretKeySpec(keyText.getBytes(), algo);
                **try** {
                        mac.init(key);
                } **catch** (InvalidKeyException e) {
                        // **TODO** Auto-generated catch block
                        e.printStackTrace();
                }
                **byte[]** tmpHash = mac.doFinal(server);
                TextView user = (TextView) activity.findViewById(R.id.*editText1*);
                String username = user.getText().toString();
                String concat = username + " " + Hex.*toHex*(tmpHash);
                String hash = Base64.*encodeToString*(concat.getBytes(), Base64.*URL_SAFE*);
                **return** hash;
        }
}

在客户端 ,如果一切按计划进行,你的 app 会弹出一条精彩的“登录成功”的消息来迎接你,如图图 8-5 所示。

9781430240624_Fig08-05.jpg

图 8-5 成功的挑战-响应身份验证

摘要

我希望这些例子能让您更好地理解如何在移动和后端 web 应用中实现替代的身份验证机制。通过减少对用户凭据存储的依赖,您可以显著提高应用的安全性。

在前端和后端代码中实现 OAuth 不会是最容易完成的事情。然而,花费一些初始努力并为将来的代码准备一组可重用的库是值得的。补习也是一样。由于所涉及的工作量,这些身份验证方法并不是许多开发人员首先要考虑的。但是,它可以确保您的应用比那些通过网络存储和转发用户凭据的应用更安全。

希望你会认为你到目前为止学到的东西是有用的。我的希望是,你会相信你没有浪费时间在这个新协议的缩写上,它被称为挑战响应认证协议。

九、发布和销售您的应用

你可能决定通过出售你花了无数时间开发的应用来赚钱。随着移动领域最近的发展,个人开发者现在比以往任何时候都更容易营销、销售其应用并从中赚取收入。苹果有 iTunes 应用商店,黑莓有 AppWorld,安卓有市场。销售应用的过程很简单:注册成为应用销售者,然后在在线商店上发布你的应用。一旦获得批准,您的应用将立即可供 Android 用户下载。在这一章中,我们将更详细地研究这个过程,我将讲述如何让你的应用在 Android Market 上上市的基础知识。在这个过程中,我会谈到从你决定你的应用运行良好开始,到你决定把它发布到网上为止,都涉及到哪些步骤。当谈到在线销售你的应用时,我还会谈到另一个要点:收入保护。如果你的应用在任何在线商店变得受欢迎,那么你很可能会吸引那些想要“破解”和盗版你的应用的人。除非你打算免费发布你的应用,否则这可能会影响你的收入。我将花一些时间在这个主题上,并探索如何编写好的许可证密钥和注册例程来阻止盗版。在这一节中,我还将阐明如果你的应用发现自己处于一个敌对的环境中,它可能不得不经历的一些事情。

开发者注册

你还记得我们之前写的 Proxim 应用吗?让我们在 Android 市场上免费发布。我将带您了解发布应用的基础知识。在这种情况下,我不会输入任何允许我收款的具体财务信息(例如,我的银行账号),因为我不打算出售该应用。此外,我不想花太多时间告诉你如何注册成为一名开发人员,因为谷歌已经有很多关于这方面的有用信息和一套关于如何开始的综合文章。

在发布你的应用之前,你需要做的第一件事就是注册成为一名开发者。您可以使用现有的 Gmail 帐户进行注册。导航到 http://market.android.com/publish 的 并登录(参见图 9-1 )。在本文发表时,注册开发者的费用是 25 美元。你通过 Google Checkout 支付这个金额,而且是一次性的注册费用(见图 9-2 )。费用的存在是为了确保你是一个认真的开发者。据谷歌称,这有助于减少可能进入市场的“垃圾产品”的数量。

9781430240624_Fig09-01.jpg

图 9-1 注册发布您的应用

9781430240624_Fig09-02.jpg

图 9-2 注册费的支付

您的应用——已公开

外面是一片丛林。谁知道你的应用最终会在哪里?嗯,这大概是夸张了;但是正如我在这一章开始时提到的,任何可以访问 Android Market 的人都可以下载你的应用。如果这些下载转化为收入,那就太好了;不幸的是,在某些情况下,盗版会让你损失收入。盗版不是什么新鲜事。自从桌面计算时代开始,它就存在了。盗版的正式术语是侵犯软件版权;它意味着在没有适当授权的情况下将一个软件从一个设备复制到另一个设备。在大多数情况下,这仅仅意味着复制你没有购买和支付的软件。如果一个朋友买了一些软件,并给了你一份你没有付钱的拷贝,那么你就拥有了你没有购买的软件。在成长的过程中,我记得我是如何迫不及待地走进我买第一台 8088 电脑的商店(一个巨大的笨重的、坚不可摧的金属怪兽),把我每周的零花钱花在最新的游戏上。当时,我从未想过我参与了协助盗版。据我所知,我付了现金,收到了一个游戏作为回报。我从来没有意识到我支付的是从原始开发者那里购买软件的十分之一。我也不知道我的钱从来没有到达最初的开发商;它留在了店里。

开发者仍然会因为软件盗版而损失收入。你的软件有多受欢迎,以及你如何分发它,将在你的软件被盗版的程度中起到关键作用。例如,如果你允许在七天内免费试用你的应用,但允许完全访问它的所有功能,那么很可能有人会试图绕过这七天的试用期。如果成功了,那么这个人就不需要付费下载你的应用的完整版本了。侵犯版权的另一种隐蔽形式是代码盗窃。当有人下载你的软件,对其进行逆向工程,并复制代码时,就会发生这种情况。然后,这个人将你的代码重新包装成新产品,并以较低的价格出售。证明这种侵犯版权的唯一方法是下载新的和类似的应用,对其进行逆向工程,并寻找与你自己相同的编码结构。但是,如果代码被修改,这将是一项艰巨的任务来证明,甚至更难在法庭上打,因为涉及的成本很高。作为一个个人开发者,你可能不会有太多的资源用于打击盗版。因此,最好决定你是否想要保护你的应用免受盗版 — 的侵害,如果是的话,如何保护。

在这一部分,我将讨论一些你在做决定时需要考虑的话题。然后,如果你确信你需要保护你的应用免受盗版,我会给你一些例子,告诉你如何使用 Android 的许可证验证库(LVL) 来阻止未来的盗版者非法复制和分发你的应用。让我们从你的应用放在 Android Market 上时会发生什么开始。

可供下载

当你的应用出现在 Android 市场时,最终用户可以下载它。如果你对应用收费,那么显然最终用户在下载之前必须先购买它。一旦应用安装到设备上,你就可以使用 Android 调试桥(adb)将其复制到电脑上。adb 允许您以不同的方式与您的 Android 设备进行交互。您可以安装软件,打开 Linux shell 以浏览设备文件系统,以及将文件复制到设备或从设备复制文件。我已经在清单 9-1 中给出了 adb 特性的完整列表。你可以在你的 Android SDK 的平台-工具目录下找到 adb。对我来说,这个位置是在/Users/sheran/Android-SDK-MAC _ x86/platform-tools。

清单 9-1 。 亚行命令及功能

Android Debug Bridge version 1.0.29

 -d - directs command to the only connected USB device
  returns an error if more than one USB device is![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 present.
 -e - directs command to the only running emulator.
  returns an error if more than one emulator is running.
 -s  <  serial number> - directs command to the USB device or emulator with
  the given serial number. Overrides ANDROID_SERIAL
  environment variable.
 -p  <  product name or path> - simple product name like 'sooner', or
  a relative/absolute path to a product
  out directory like 'out/target/product/sooner'.
  If -p is not specified, the ANDROID_PRODUCT_OUT
  environment variable is used, which must
  be an absolute path.
 devices    - list all connected devices
 connect  <  host  >  [:<port>]    - connect to a device via TCP/IP
  Port 5555 is used by default if no port number is![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 specified.
 disconnect [<host  >  [:<port>]] - disconnect from a TCP/IP device.
  Port 5555 is used by default if no port number is![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 specified.
  Using this command with no additional arguments
  will disconnect from all connected TCP/IP devices.

device commands:
  adb push  <  local  >  <remote>    -      copy file/dir to device
  adb pull  <  remote  >  [<local>]    -      copy file/dir from device
  adb sync [ <directory> ]    - copy host-  >  device only if changed
  (−l means list but don't copy)
  (see 'adb help all')
  adb shell    - run remote shell interactively
  adb shell  <  command> - run remote shell command
  adb emu  <  command> - run emulator console command
  adb logcat [ <filter-spec> ]    - View device log
  adb forward  <  local  >  <remote  >   − forward socket connections
     forward specs are one of:
  tcp:<port>
  localabstract:<unix domain socket name>
  localreserved:<unix domain socket name>
  localfilesystem:<unix domain socket name>
  dev:<character device name>
  jdwp:<process pid  >  (remote only)
  adb jdwp    - list PIDs of processes hosting a JDWP transport
  adb install [−l] [−r] [−s]  <  file  >  − push this package file to the device and install![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 it
  ('-l' means forward-lock the app)
  ('-r' means reinstall the app, keeping its data)
  ('-s' means install on SD card instead of internal![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 storage)
  adb uninstall [−k]  <  package  >  − remove this app package from the device
  ('-k' means keep the data and cache directories)
  adb bugreport    -     return all information from the device
  that should be included in a bug report.

  adb backup [−f  <  file>] [−apk|-noapk] [−shared|-noshared] [−all] [−system|-nosystem]![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 [<packages...>]
     -     write an archive of the device's data to  <  file  >  .
  If no -f option is supplied then the data is written
  to "backup.ab" in the current directory.
  (−apk|-noapk enable/disable backup of the .apks![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 themselves
  in the archive; the default is noapk.)
  (−shared|-noshared enable/disable backup of the![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 device's
  shared storage / SD card contents; the default is![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 noshared.)
  (−all means to back up all installed applications)
  (−system|-nosystem toggles whether -all automatically![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 includes
  system applications; the default is to include![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 system apps)
  (<packages...  >  is the list of applications to be![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 backed up. If
  the -all or -shared flags are passed, then the![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 package
  list is optional. Applications explicitly given![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 on the
  command line will be included even if –nosystem![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 would
  ordinarily cause them to be omitted.)

  adb restore  <  file> - restore device contents from the  <  file  >  backup archive

  adb help    -     show this help message
  adb version    -     show version num

scripting:
  adb wait-for-device    -     block until device is online
  adb start-server    -     ensure that there is a server running
  adb kill-server    -     kill the server if it is running
  adb get-state    -     prints: offline | bootloader | device
  adb get-serialno    -     prints: <serial-number>
  adb status-window    -     continuously print device status for a specified device
  adb remount    -     remounts the /system partition on the device read-write
  adb reboot [bootloader|recovery] - reboots the device, optionally into the![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 bootloader or recovery program
  adb reboot-bootloader    -     reboots the device into the bootloader
  adb root    -     restarts the adbd daemon with root permissions
  adb usb    -     restarts the adbd daemon listening on USB
  adb tcpip  <  port> -     restarts the adbd daemon listening on TCP on the![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 specified port
networking:
  adb ppp  <  tty  >  [parameters]    -     Run PPP over USB.
 Note: you should not automatically start a PPP connection.
 <tty  >  refers to the tty for PPP stream. Eg. dev:/dev/omap_csmi_tty1
 [parameters] - Eg. defaultroute debug dump local notty usepeerdns

adb sync notes: adb sync [ <directory> ]
  <localdir  >  can be interpreted in several ways:

  - If  <  directory  >  is not specified, both /system and /data partitions will be updated.

  - If it is "system" or "data", only the corresponding partition
  is updated.

environmental variables:
  ADB_TRACE - Print debug information. A comma separated list of![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 the following values
  1 or all, adb, sockets, packets, rwx, usb, sync,![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 sysdeps, transport, jdwp
  ANDROID_SERIAL - The serial number to connect to. -s takes priority![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 over this if given.
  ANDROID_LOG_TAGS - When used with the logcat option, only these debug![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 tags are printed.

对于希望将文件从安卓设备复制到自己电脑的人来说,拉和推命令非常有用。一般第三方 app 都存储在设备的 /data/app 目录下。首先,让我们看看应用目录 中有什么:

  1. 通过键入 adb shell 打开设备的外壳。
  2. 通过做 cd /data/app 将目录更改为 /data/app 。
  3. 使用 ls 列出内容。

您将看到类似于以下内容的输出:

$ ./adb shell
# cd /data/app
# ls
net.zenconsult.android.chucknorris-1.apk
test_limits_host
ApiDemos.apk
test_list_host
test_set_host
CubeLiveWallpapers.apk
test_iostream_host
test_iomanip_host
SoftKeyboard.apk
test_iterator_host
test_vector_host
test_algorithm_host
test_uninitialized_host
GestureBuilder.apk
test_sstream_host
test_char_traits_host
test_memory_host
test_ios_base_host
test_type_traits_host
test_ios_pos_types_host
test_streambuf_host
test_functional_host
test_string_host

再来看 net . Zen consult . Android . chuck Norris-1 . apk 包。我们可以抄下来看看。

要从设备复制包,您可以使用命令 adb pull 。就这么办吧。键入 exit 并按回车键,退出当前的亚行 shell 会话。接下来,键入以下内容:

adb pull /data/app/ net.zenconsult.android.chucknorris-1.apk.

这将把包复制到您的当前目录。如果您想要将文件复制到计算机上的其他位置,请用您选择的目录替换句点。您现在拥有了包文件的副本,就像它离开开发人员的计算机一样。我们可以进一步研究这个文件。

逆向工程

奇怪的排序不会仅仅停留在从设备复制包文件。他们会想更仔细地看看应用和代码。这就是逆向工程发挥作用的地方。逆向工程是获取编译后的二进制程序并生成等效的汇编或源代码以提高可读性的过程。在大多数情况下,获得源代码是最理想的情况,因为阅读源代码要比阅读汇编代码容易得多。将程序逆向工程成汇编代码的过程称为反汇编,从程序生成源代码称为反编译。你要明白,每个 CPU 都会有自己的汇编器,自己的汇编语言。这就是英特尔 x86 CPU 上的汇编代码与基于 ARM–的 CPU 上的汇编代码不同的原因。不过,我们不必达到如此低的水平。通常,工作到 Dalvik VM (DVM)级别就足够了。

DVM 还包含一个汇编器。出于解释的目的,假设 DVM 是 CPU。因此,必须使用这个汇编器来构建 Java 代码,以便在 DVM 上工作。当您使用 Android SDK 构建应用时,就会发生这种情况。将在 DVM 上运行的结果可执行文件被称为 Dalvik 可执行文件 (DEX ) 文件。您用 Java 编写代码,并使用标准的 Java 编译器( javac )将其编译成 Java 类文件。然后,要将这个类文件转换成 DEX 格式,可以使用名为 dx 的命令。您也可以在您的平台工具目录中找到这个工具。一旦生成了 DEX 文件,它就被打包成一个 APK 文件。你可能已经知道,APK 文件只不过是一个压缩文件。如果我想检查我的 APK 文件中的文件,我将如下提取该文件:

$ unzip net.zenconsult.android.chucknorris-1.apk
Archive: net.zenconsult.android.chucknorris-1.apk
  inflating: res/layout/main.xml
  inflating: AndroidManifest.xml
 extracting: resources.arsc
 extracting: res/drawable-hdpi/ic_launcher.png
 extracting: res/drawable-ldpi/ic_launcher.png
 extracting: res/drawable-mdpi/ic_launcher.png
  inflating: classes.dex
  inflating: META-INF/MANIFEST.MF
  inflating: META-INF/CERT.SF
  inflating: META-INF/CERT.RSA
$

请注意 DEX 文件。

幸运的是,Eclipse 将处理整个构建过程,并确保在我们的项目中插入、对齐和打包所有相关文件。我已经在图 9-3 中展示了整个构建过程。

9781430240624_Fig09-03.jpg

图 9-3 。Android 构建流程

现在,您已经对应用是如何构建的有了一个简单的概念,让我们看看如何将它们分开。正如我们在提取 APK 文件的内容时看到的,我们可以直接访问 classes.dex 文件。因为我们认为 DVM 是我们的 CPU,这是我们的二进制。就像 Win32 PE 文件或 Linux ELF 文件一样,这个 DEX 文件是我们的二进制文件,因为它运行在我们的 CPU (DVM)上。谷歌也为我们提供了名为 dexdump 的工具(也可以在你的平台工具目录中找到)。如果我在提取的 classes.dex 文件上运行 dexdump ,我将获得关于文件如何构建的大量信息,包括成员、调用等等。清单 9-2 显示了典型的 dexdump 反汇编的样子。

清单 9-2 。 输出

$ dexdump –d classes.dex
...
...
Virtual methods -
  #0 : (in Lnet/zenconsult/android/chucknorris/e;)
  name : 'a'
  type : '()Ljava/lang/String;'
  access : 0x0011 (PUBLIC FINAL)
  code -
  registers : 16
  ins : 1
  outs : 2
  insns size : 180 16-bit code units
0009d4: |[0009d4] net.zenconsult.android![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
.chucknorris.e.a:()Ljava/lang/String;
0009e4: 1202 |0000: const/4 v2, #int 0 // #0
0009e6: 1a00 5100 |0001: const-string v0,![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 "[`www.chucknorrisfacts.com/`](http://www.chucknorrisfacts.com/)" // string@0051
0009ea: 7020 2900 0f00 |0003: invoke-direct {v15, v0},![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 Lnet/zenconsult/android/chucknorris/e;.a:(Ljava/lang/String;)Ljava/io/InputStream;![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 // method@0029
0009f0: 0c05 |0006: move-result-object v5
0009f2: 7100 1600 0000 |0007: invoke-static {},![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 Ljavax/xml/parsers/DocumentBuilderFactory;.newInstance:()Ljavax/xml/![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
parsers/DocumentBuilderFactory; // method@0016
0009f8: 0c00 |000a: move-result-object v0
0009fa: 1a01 0000 |000b: const-string v1, "" // string@0000
0009fe: 2206 1100 |000d: new-instance v6,![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 Ljava/util/Vector; // type@0011
000a02: 7010 1000 0600 |000f: invoke-direct {v6},![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 Ljava/util/Vector;.  <  init>:()V // method@0010
000a08: 6e10 1500 0000 |0012: invoke-virtual {v0},![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
 Ljavax/xml/parsers/DocumentBuilderFactory;.newDocumentBuilder:()Ljavax/xml![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
/parsers/DocumentBuilder; // method@0015
000a0e: 0c00 |0015: move-result-object v0
000a10: 6e20 1400 5000 |0016: invoke-virtual {v0, v5},
...
...

我想你明白了。反汇编的 DEX 文件很难阅读,就像 Linux 或 Windows 上反汇编的代码一样。这不是不可能的;但对于门外汉来说,这似乎是压倒性的。

多亏了一些非常聪明的人,他们也认为反汇编的 DEX 文件很难阅读,我们现在有了反汇编器,可以生成更可读的输出。一个名叫 JesusFreke 的天才为 DEX 文件格式构建了一个全新的汇编器和反汇编器。他分别称这些斯马利和巴克斯马利;他在 http://code.google.com/p/smali/发布了开源软件。他的方法的妙处在于,你可以反汇编一个文件,修改反汇编代码,然后重新汇编成一个 DEX 文件。你可能想知道 smali 和 baksmali 有什么特别之处,所以我给你看一下 baksmali 反汇编的同一个文件的一些输出:

$ java -jar ∼/Downloads/baksmali-1.2.8.jar classes.dex
$ cd out/net/zenconsult/android/chucknorris/
$ ls
ChuckNorrisFactsActivity.smali b.smali d.smali
a.smali c.smali e.smali
$

这将文件分解成单个的文件,并且更容易检查。我们来看文件 b.smali 。清单 9-3 显示了反汇编代码。

清单 9-3 。 代码被 baks Mali反汇编

.class public final Lnet/zenconsult/android/chucknorris/b;
.super Ljava/lang/Thread;

# instance fields
.field private a:Lnet/zenconsult/android/chucknorris/a;

# direct methods
.method public constructor  <  init  >  (Lnet/zenconsult/android/chucknorris/a;)V
  .registers 2

  invoke-direct {p0}, Ljava/lang/Thread;-  >  <init  >  ()V

  iput-object p1, p0, Lnet/zenconsult/android/chucknorris/b;-  >  a:Lnet/zenconsult![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
/android/chucknorris/a;

  return-void
.end method

# virtual methods
.method public final run()V
  .registers 3

  new-instance v0, Lnet/zenconsult/android/chucknorris/e;

  invoke-direct {v0}, Lnet/zenconsult/android/chucknorris/e;-  >  <init  >  ()V

  iget-object v1, p0, Lnet/zenconsult/android/chucknorris/b;-  >  a:Lnet/zenconsult![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
/android/chucknorris/a;

  invoke-virtual {v0}, Lnet/zenconsult/android/chucknorris/e;-  >  a()Ljava/lang/String;

  move-result-object v0

  invoke-interface {v1, v0}, Lnet/zenconsult/android/chucknorris/a;-  >  a![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
(Ljava/lang/String;)V

  return-void
.end method

这并没有好到哪里去,但它明显更容易理解和遵循。另一个可以让你反汇编 DEX 文件的工具叫做 dedexer,它是由 Gabor Paller 编写的。你可以在 http://dedexer.sourceforge.net/找到。

一个明显更容易使用的工具是 dex2jar,你可以在code.google.com/p/dex2jar/找到它。这个工具帮助你解构安卓。dex 文件直接转换成 Java JAR 文件。生成 JAR 文件后,可以使用任何标准的 Java 反编译器来检索 Java 源代码。我用的是 JD-,或者 Java 反编译器,你可以在java.decompiler.free.fr/找到。

要运行 dex2jar ,只需从给定的 URL 下载并解压存档文件,然后运行。蝙蝠或。sh 文件,如图 9-4 所示。这将生成一个。jar 文件,除了它以 _dex2jar.jar 结尾之外,它的名字听起来很相似。如果在 JD-GUI 中打开这个文件,就可以看到重构后的 Java 源代码。在大多数情况下,反编译的代码可以在您的开发环境中重新编译,比如 Eclipse。

9781430240624_Fig09-04.jpg

图 9-4 。在 classes.dex 文件上运行 dex 2 jarT5

图 9-5 向你展示了 JD-GUI 中反编译后的源代码是什么样子。JD-GUI 有一个简单直观的界面来浏览 JAR 文件源代码,甚至可以将源代码导出到 Java 文件中。

9781430240624_Fig09-05.jpg

图 9-5 使用 JD-GUI 反编译 JAR 文件

有了这样不断发展的工具,坚定的用户下载、修改和重新打包你的应用就容易多了。如果你计划编写自己的保护机制来防止盗版,那么你有了一个好的开始。但这是你应该考虑的事情吗?我将在下一节中简要介绍这一点。

你应该许可吗?

这个问题是我看到开发者问的常见问题。你真的想花和开发你的应用一样多的时间去写一个许可程序吗?答案很主观,真的要看你的 app 是做什么的。如果你的应用有独特的或者比其他应用高效几倍的功能;或者,如果它展示了一种独特的感觉,可以确保它卖得很好,那么它可能值得考虑开发一个许可 例程。然而,请注意,当我说许可时,这并不意味着收费。你仍然可以为你的应用向用户收费;只是,如果你的应用没有监控许可的方法,那么最终用户将可以自由地复制和分发应用。

你可能会考虑开发许可程序的另一个原因是,如果你计划在未来开发更多的应用,并且你也想许可它们。在这种情况下,您可以简单地使用您已经创建的一个许可库。然而,有一点需要注意的是,你需要稍微改变每个应用的算法或许可证检查程序。因此,如果你的一个应用是盗版的,那么同样的技术将不会在其他应用上工作。

安卓许可证验证库

谷歌已经提供了 Android LVL 来帮助开发者保护他们的应用不被任意分发。将 LVL 添加到应用构建路径中,并使用其中的 API 来检查和验证用户许可。LVL 与 Android Market 应用(见图 9-6 )连接,然后与 Google market 服务器核对。根据您收到的响应,您可以选择是允许还是拒绝进一步使用应用。了解 LVL 的最好方法是在一个示例应用中使用它,我们就这么做吧。但是,在您继续之前,您需要注册成为应用发布者。不过,你现在不需要这么做。让我们从编写一个非常基本的应用开始,用它来测试我们的许可程序。清单 9-4 到 9-7 展示了这个基本应用的代码。

9781430240624_Fig09-06.jpg

图 9-6 LVL 库与市场应用接口,然后与市场服务器接口

这个应用本身非常简单。它涉及到 Chuck Norris(正如你从前面的提取和逆向工程部分已经猜到的。)我们都知道也害怕查克·诺里斯。他的回旋踢是传奇性的,人们报道说它们经常是许多自然灾害的原因。为了向这位伟人致敬,我将创建一个应用,从一个名为查克·诺里斯事实( 、www.chucknorrisfacts.com/)的热门网站获取最新的查克·诺里斯事实。该应用将从该网站获取所有报价,并在我们的应用屏幕的文本区域显示随机报价。只需点击按钮获取另一个事实。我依赖于该网站报价的随机性,以确保每次都有新的报价出现。像往常一样,这个应用仅仅是一个例子,说明了你需要添加 LVL 检查的方式和位置。几乎没有错误检查,应用的功能也很少。说到这里,我不知道为什么我需要为自己辩护;这是查克·诺里斯的应用。仅此一点就足够了。你可能会注意到应用中有几个你可以改进的地方。请随意这样做。

清单 9-4 。

package net.zenconsult.android.chucknorris;

**import** android.app.Activity;
**import** android.os.Bundle;
**import** android.view.View;
**import** android.widget.Button;
**import** android.widget.TextView;

**public class** ChuckNorrisFactsActivity **extends** Activity **implements** CommsEvent {
        **private** Activity activity;
        **private** TextView view;
        **private** CommsEvent event;

        /** Called when the activity is first created. */
        @Override
        **public void** onCreate(Bundle savedInstanceState) {
                **super**.onCreate(savedInstanceState);
                setContentView(R.layout. *main* );
                activity = **this**;
                event = **this**;
                view = (TextView) findViewById(R.id.*editText1*);

                // Click Button
                **final** Button button = (Button) findViewById(R.id.*button1*);
                button.setOnClickListener(**new** View.OnClickListener() {
                        **public void** onClick(View v) {
                                view.setText("Fetching fact...");
                                CommsNotifier c = **new** CommsNotifier(event);
                                c.start();
                        }
                });
        }

        @Override
        **public void** onTextReceived(**final** String text) {
                runOnUiThread(**new** Runnable() {
                        **public void** run() {
                                view.setText(text);
                        }
                });

        }
}

清单 9-5。

**package** net.zenconsult.android.chucknorris;

**public interface** CommsEvent {
        **public void** onTextReceived(String text);
}

清单 9-6。【CommsNotifier.java】

**package** net.zenconsult.android.chucknorris;

**public class** CommsNotifier **extends** Thread {
        **private** CommsEvent event;

        **public** CommsNotifier(CommsEvent evt) {
        event = evt;
        }

        **public void** run() {
        Comms c = **new** Comms();
        event.onTextReceived(c.get());
        }
}

清单 9-7 。【Comms.java】文件

**package** net.zenconsult.android.chucknorris;

**import** java.io.IOException;
**import** java.io.InputStream;
**import** java.util.Random;
**import** java.util.Vector;

**import** javax.xml.parsers.DocumentBuilder;
**import** javax.xml.parsers.DocumentBuilderFactory;
**import** javax.xml.parsers.ParserConfigurationException;

**import** org.apache.http.HttpResponse;
**import** org.apache.http.client.ClientProtocolException;
**import** org.apache.http.client.methods.HttpGet;
**import** org.apache.http.impl.client.DefaultHttpClient;
**import** org.apache.http.util.EntityUtils;
**import** org.w3c.dom.Document;
**import** org.w3c.dom.NamedNodeMap;
**import** org.w3c.dom.Node;
**import** org.w3c.dom.NodeList;
**import** org.xml.sax.SAXException;

**import** android.app.Activity;
**import** android.content.Context;
**import** android.util.Log;
**import** android.widget.Toast;

**public class** Comms {
        **private final** String url = "[`www.chucknorrisfacts.com/`](http://www.chucknorrisfacts.com/)";

        **private** DefaultHttpClient client;

        **public** Comms() {

        client = **new** DefaultHttpClient();
        }

        **public** String get() {
        InputStream pageStream = doGetAsInputStream(url);
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.*newInstance*();
        DocumentBuilder db = **null**;
        Document doc = **null;**
        String pageText = "";
        Vector        < String >        quotes = **new** Vector        < String >        ();
        **try** {
        db = dbFactory.newDocumentBuilder();
        doc = db.parse(pageStream);
        NodeList nl = doc.getElementsByTagName("div");
        **for** (**int** x = 0; x < nl.getLength(); ++x) {
        Node node = nl.item(x);
        NamedNodeMap attributes = node.getAttributes();
        **for** (**int** y = 0; y        <        attributes.getLength(); ++y) {
        **if** (attributes.getNamedItem("class") ! = null) {
        Node attribute =![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
       attributes.getNamedItem("class");
        **if** (attribute.getNodeValue()
        .equals("views-![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
field-title")) {
        NodeList children =![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
       node.getChildNodes();
        **for** (**int** z = 0; z <![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
       children.getLength(); ++z) {
        Node child =![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
       children.item(z);
        **if** (child.getNodeName()
        ![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
.equalsIgnoreCase("span"))
        quotes.add![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
(child.getTextContent());
        }
        }
        }

        }
        }
        Random r = **new** Random();
        pageText = quotes.get(r.nextInt(quotes.size() - 1));
        pageStream.close();
        } **catch** (SAXException e) {
              // **TODO** Auto-generated **catch** block
        e.printStackTrace();
        } **catch** (IOException e) {
              // **TODO** Auto-generated catch block
        e.printStackTrace();
        } **catch** (ParserConfigurationException e) {
              // **TODO** Auto-generated catch block
        e.printStackTrace();
        }
        **return** pageText;
        }

        **public** String doGetAsString(String url) {
        HttpGet request = **new** HttpGet(url);
        String result = "";
        **try** {
        HttpResponse response = client.execute(request);
        **int** code = response.getStatusLine().getStatusCode();
        **if** (code == 200) {
        result = EntityUtils.*toString*(response.getEntity());
        } **else** {
        Log.*e*("CN", "Non 200 Status Code "        +        code);
        }
        } **catch** (ClientProtocolException e) {
              // **TODO** Auto-generated catch block
        e.printStackTrace();
        } **catch** (IOException e) {
              // **TODO** Auto-generated catch block
        e.printStackTrace();
        }
        **return** result;

        }

        **public** InputStream doGetAsInputStream(String url) {
        HttpGet request = **new** HttpGet(url);
        InputStream result = **null**;
        **try** {
        HttpResponse response = client.execute(request);
        **int** code = response.getStatusLine().getStatusCode();
        **if** (code == 200) {
        result = response.getEntity().getContent();
        } **else** {
        Log.*e*("CN", "Non 200 Status Code "        +        code);
        }
        } **catch** (ClientProtocolException e) {
              // **TODO** Auto-generated catch block
        e.printStackTrace();
        } **catch** (IOException e) {
              // **TODO** Auto-generated catch block
        e.printStackTrace();
        }
        **return** result;
        }
}

从主活动开始,您可以看到有一个按钮和一个文本视图,我们将使用它进行用户交互。当用户点击我们的按钮时,我们启动我们的 CommNotifier 线程。这个线程将执行我们的 Comms 文件中的 HTTP get 请求,并返回一个从网站上收集的 Chuck Norris 事实列表中随机选取的引用。 CommNotifier 然后触发 onTextReceived(字符串文本)函数。我们的主活动实现了 CommEvent 接口。因此,每当这个方法被触发时,我们需要访问文本参数来接收我们的报价。当我们执行应用并点击按钮时,我们会看到类似于图 9-7 所示的输出。查克·诺里斯确实很吓人。

现在我们已经有了自己的应用,让我们看看如何使用 LVL 来保护它。

9781430240624_Fig09-07.jpg

图 9-7 查克·诺里斯事实应用 ?? 在行动

我将在 Android 模拟器上运行这个演示。这涉及到一个额外的步骤,因为 Android 模拟器没有预先打包到 Android Market 应用中。我需要下载 Google API 插件 平台,它提供了 Android Market 的基本后台实现。它实现了我们测试 LVL 所需的许可服务。不过,我有点言过其实了。让我们从准备我们的开发环境开始。我将假设您使用 Eclipse 进行开发,并且您已经下载并安装了 API 级别至少为 8 的 Android SDK。我们出发了!

下载谷歌 API 插件

如果您使用 Eclipse,我将描述获得 Google API 附加组件所需的步骤。首先,打开 Android SDK 管理器。选择窗口 image Android SDK 管理器。接下来,导航到您计划使用的 API 级别,并勾选谷歌公司的谷歌 API(参见图 9-8 )。在点击安装按钮之前,再次导航到 Extras 文件夹并勾选 Google Market 许可包(参见图 9-9 )。现在点击安装按钮。对于这个应用,我使用 2.3.3 版本的 Android API level 10,所以这是我选择的。

9781430240624_Fig09-08.jpg

图 9-8 为 Android 版本 2.3.3 安装 Google APIs

9781430240624_Fig09-09.jpg

图 9-9 安装市场许可包

就是这样。Eclipse 会将您的 API 下载并安装到 SDK 目录中。要找到 LVL 源代码,请从 Android SDK 目录导航到/extras/Google/market _ licensing/library。在这里,你会看到一个类似于图 9-10 所示的目录结构。让我们进入下一组步骤,即导入、修改和构建 LVL。

9781430240624_Fig09-10.jpg

图 9-10T3。LVL 来源

将 LVL 源文件复制到单独的目录下

现在我们已经有了 LVL 源代码,让我们把它移到另一个工作目录中。这样做的主要原因是,如果我们继续从原始的源目录开始工作,每当我们进行更新时,我们所有的更改都可能被覆盖。因此,我们需要将我们的 LVL 源文件保存在一个单独的目录中,这样就不会被覆盖。这很简单。将库目录以及所有子目录和文件复制到您的开发目录中。

导入 LVL 源作为库项目

我们现在将建立 LVL 图书馆。为此,我们必须创建一个新的 Eclipse Android 项目,并将该项目标记为库项目。库项目没有活动,也不直接与最终用户交互。相反,它的存在是为了让其他应用可以在它们的代码中使用它的功能。创建一个新的 Eclipse 项目,选择文件 image 新建 image 其他,打开 Android 文件夹,选择 Android 项目(参见图 9-11 )。命名您的项目(参见图 9-12 ,并选择您计划开发项目的正确 API 版本(参见图 9-13 )。您需要将您的包命名为与 LVL 源代码相同的名称,即 com . Android . vending . licensing(参见图 9-14 )。

9781430240624_Fig09-11.jpg

图 9-11T3。Android 项目

9781430240624_Fig09-12.jpg

图 9-12T3。命名您的项目

9781430240624_Fig09-13.jpg

图 9-13T3。选择 API 版本

9781430240624_Fig09-14.jpg

图 9-14T3。指定包名。它应该与 LVL 源代码包相同

完成后,让我们将 LVL 源代码导入到我们的项目中。但在此之前,我们先把项目设定为库项目。在项目资源管理器窗口中右键单击项目名称,然后选择 Properties。在左侧窗格中选择 Android 选项,在右侧窗格的下半部分,您会看到一个标记为 Is Library 的勾选框。勾选此选项并点击确定按钮(参见图 9-15 )。

9781430240624_Fig09-15.jpg

图 9-15 。将项目标记为库

现在我们可以导入我们的源代码了。在“项目资源管理器”窗口中右键单击项目名称,然后选择“导入”。在出现的窗口中,选择文件系统(见图 9-16 )并点击下一步按钮。在下一个窗口中,单击 Browse 按钮并导航到作为 Android LVL 源代码一部分的 library 文件夹。在左中窗格中,您应该会看到目录出现。勾选库目录并点击完成按钮,将 LVL 源文件导入到您的项目中(参见图 9-17 )。如果要求您覆盖 AndroidManifest.xml 文件,选择 Yes。您的 LVL 源现在是项目的一部分。

9781430240624_Fig09-16.jpg

图 9-16T3。导入文件系统

9781430240624_Fig09-17.jpg

图 9-17T3。找到并导入源代码

在我们的 app 中建立并包含 LVL

**我们先把 Google 提供的基础版 LVL 集成到我们的 app 中。在此之后,我将解释一些可能的地方,您可以修改 LVL 源代码,使之成为您自己的。我强烈推荐这种方法,因为正如我前面提到的,您编写的 LVL 修改过的源代码不会广为人知,因此攻击者会花更长的时间来破坏您的许可模块。

要在您的应用中包含 LVL,请在 Eclipse 的 Project Explorer 视图中导航到您的应用名称,右键单击并选择 Properties。从左侧窗口窗格中选择 Android 选项,然后在右下方的窗口页面中,单击“Add”按钮。然后提示您选择一个库项目(参见图 9-18 )。选择我们刚刚创建的 Android LVL 库项目。完成后,你会看到库项目包含在你的应用的项目中(见图 9-19 )。

9781430240624_Fig09-18.jpg

图 9-18 。选择 Android LVL 库项目

9781430240624_Fig09-19.jpg

图 9-19T3。LVL 库项目包含在 app 项目中

现在让我们把我们的 ChuckNorrisFactsActivity.java 文件改成清单 9-8 所示的文件。你可以看到我们添加了一个新的私有类,叫做 LicCallBack 。这实现了 LVL 的 LicenseCheckerCallBack 类。当许可证检查完成时,以及当许可证服务器有肯定或否定的响应时,调用此类。分别调用 allow() 或 don 牛油()方法。

清单 9-8 。 修改后的 ChuckNorrisFactsActivity.java 文件

**package** net.zenconsult.android.chucknorris;

**import** java.util.UUID;

**import** com.android.vending.licensing.AESObfuscator;
**import** com.android.vending.licensing.LicenseChecker;
**import** com.android.vending.licensing.LicenseCheckerCallback;
**import** com.android.vending.licensing.ServerManagedPolicy;

**import** android.app.Activity;
**import** android.content.Context;
**import** android.os.Build;
**import** android.os.Bundle;
**import** android.provider.Settings.Secure;
**import** android.view.View;
**import** android.view.Window;
**import** android.widget.Button;
**import** android.widget.TextView;
**import** android.widget.Toast;

**public class** ChuckNorrisFactsActivity **extends** Activity **implements** CommsEvent {
        **private** Button button;
        **private** TextView view;
        **private** Activity activity;
        **private** CommsEvent event;
        **private** LicCallBack lcb;
        **private static** final String*PUB_KEY* = "MIIBI...";// Add your Base64 Public
        // key here
        **private** staticfinal byte[]*SALT* = **new byte**[] { −118, -112, 38, 124, 15,
        -121, 59, 93, 35, -55, 14, -15, -52, 67, -53, 54, 111, -28,![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
       -87, 12 };

        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
        **super**.onCreate(savedInstanceState);
        requestWindowFeature(Window.*FEATURE_INDETERMINATE_PROGRESS*);
        setContentView(R.layout.*main*);
        event = **this**;
        activity = **this**;
        view = (TextView) findViewById(R.id.*editText1*);

        // Click Button
        button = (Button) findViewById(R.id.*button1*);
        button.setOnClickListener(**new** View.OnClickListener() {
        public void onClick(View v) {
        // Do License Check before allowing click

        // Generate a Unique ID
        String deviceId = Secure.*getString*(getContentResolver(),
        Secure.*ANDROID_ID*);
        String serialId = Build.*SERIAL*;
        UUID uuid = **new** UUID(deviceId.hashCode(),![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
       serialId.hashCode());
        String identity = uuid.toString();
        Context ctx = activity.getApplicationContext();

        // Create an Obfuscatorand a Policy
        AESObfuscator obf = **new** AESObfuscator(*SALT*,![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
       get**Package**Name(),
        identity);
        ServerManagedPolicy policy = **new**![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
       ServerManagedPolicy(ctx, obf);

        // Create the LicenseChecker
        LicenseChecker lCheck = **new** LicenseChecker(ctx,![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
       policy,*PUB_KEY*);

        // Do the license check
        lcb = **new** LicCallBack();
        lCheck.checkAccess(lcb);
        }
        });
        }

        @Override
        public void onTextReceived(final String text) {
        runOnUiThread(**new** Runnable() {
        public void run() {
        setProgressBarIndeterminateVisibility(**false**);
        view.setText(text);
        button.setEnabled(**true**);

        }
        });

        }

        **public class** LicCallBack implements LicenseCheckerCallback {

        @Override
        public void allow() {
        if (isFinishing()) {
        return;
        }

        Toast toast = Toast.*makeText*(getApplicationContext(),![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
       "Licensed!",
        Toast.*LENGTH_LONG*);
        toast.show();
        button.setEnabled(**false**);
        setProgressBarIndeterminateVisibility(**true**);
        view.setText("Fetching fact...");
        CommsNotifier c = **new** CommsNotifier(event);
        c.start();
        }

        @Override
        public void dontAllow() {
        if (isFinishing()) {
        return;
        }
        Toast toast = Toast.*makeText*(getApplicationContext(),
        "Unlicensed!", Toast.*LENGTH_LONG*);
        toast.show();
        }

        @Override
        public void applicationError(ApplicationErrorCode errorCode) {
        // TODO Auto-generated method stub

        }

        }

}

下一件你会注意到的事情是,我们没有在我们的按钮点击上做任何活动。相反,我们做执照检查。这意味着我们将我们的报价获取活动移到了 LicenseCallBack 类的 allow() 部分。要使用来自 LVL 的许可证检查,您必须调用 LicenseChecker 类的 checkAccess() 方法。您必须使用以下参数构建 LicenseChecker:

  1. 应用上下文
  2. 许可政策
  3. 您的公钥

对于应用上下文,您可以使用当前的应用上下文。如果您的 LicenseChecker 在另一个类中被实例化,那么您需要将应用上下文对象传递给这个类。您的 Base 64 编码公钥将位于您的在线发布者配置文件页面中。要访问它,登录 https://market.android.com/publish/Home 的,点击编辑个人资料,然后向下滚动到名为许可&应用内计费的部分。名为公钥的文本区域保存您的密钥(参见图 9-20 )。将此复制并粘贴到您的应用中。许可策略需要更多的解释,所以我将在下一节描述它。

同样,请看下面的代码:

AESObfuscator obf = new AESObfuscator(*SALT*, get**Package**Name(),identity);

当你的应用收到来自 Android 许可服务器的响应时,它需要在本地设备上存储关于这个响应的信息。将响应数据保持为纯文本形式只会意味着攻击者可以读取和篡改这些信息。为了防止这种情况发生,LVL 允许我们在将信息存储在设备上之前对其进行模糊处理。 AESObfuscator 类就是这样做的。它需要一个 salt 值(只是一个随机的 20 字节)和一个唯一的设备标识。唯一标识确保只能从具有该匹配标识的设备读取数据。在您自己的代码中,您将希望从尽可能多的信息来源中构建这个标识字符串。在这种情况下,我使用的是 ANDROID_ID 和 OS 构建序列号。

另请注意,您的应用必须请求新的权限。为了能够通过 Android Market 验证许可证,请确保将以下权限添加到 AndroidManifest.xml 文件中:

<用途-权限 Android:name =【com . Android . vending . check _ LICENSE】>

9781430240624_Fig09-20.jpg

图 9-20T3。Base64 编码的公钥

您的 publisher 仪表板有一个标有测试响应的下拉菜单(参见图 9-20 )。您可以通过将该值设置为许可或非许可来测试您的应用。Google API 和 LVL 将联系 Android Market 服务器,并向您的应用提供此响应。将测试响应值设置为 NOT_LICENSED 可让您看到未经授权的用户试图使用您的应用时,您的应用会如何运行(参见图 9-21 )。相应地,您可以进行更改,或者显示一条消息(我用一个词来表示该应用是否获得许可),或者将用户重定向到 Android Market,以便她可以购买您的应用。

9781430240624_Fig09-21.jpg

图 9-21T3。一个未经许可的用户收到一个否定的回应,应用无法运行

许可政策

可以用来定制许可过程的一个关键机制是许可策略。Android LVL 附带两个默认策略 :

  • 严格的政策
  • 服务器管理策略

Google 建议您使用 ServerManagedPolicy,因为它还处理服务器响应的缓存。这通常很有用,因为 Google 对应用查询其服务器的次数进行了限制。StrictPolicy 将始终查询服务器;虽然这可以防止本地设备数据被篡改,从而更加安全,但如果谷歌服务器因为您达到了极限而拒绝给您响应,它可能会将您的最终用户锁定在外。

这两个策略对象都提供了您将会关注的两个基本方法: allowAccess() 和 processServerResponse() 。 allowAccess() 方法必须返回一个布尔值。当被调用时,如果选择允许访问,则返回 true;否则,返回假。请看清单 9-9 中的示例实现。

清单 9-9 。server managed policy 对象中的 allowAccess()方法 (由 Google 提供)

public boolean allowAccess() {
        long ts = System.currentTimeMillis();
        if (mLastResponse == LicenseResponse.LICENSED) {
        // Check if the LICENSED response occurred within the validity timeout.
        if (ts        <        = mValidityTimestamp) {
        // Cached LICENSED response is still valid.
        return true;
        }
        } else if (mLastResponse == LicenseResponse.RETRY &&
        ts < mLastResponseTime        +        MILLIS_PER_MINUTE) {
        // Only allow access if we are within the retry period or we haven't![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
       used up our
        // max retries.
        return (ts        <        = mRetryUntil || mRetryCount        <        = mMaxRetries);
        }
        return false;
        }

您可以看到,如果该函数接收到 LicenseResponse,它将返回 true 。许可作为其回应。该函数首先检查收到的最后一个响应是否表明该应用已获得许可。然后,它检查该日期是否仍在有效期内。如果是,那么它返回真。如果日期大于有效期,则返回假。该函数还检查服务器是否要求我们继续重试,并且它在合理的重试限制和时间间隔内这样做。响应对象 mLastResponse 是从 processserveresponse()方法 中派生出来的,如清单 9-10 所示。您可以看到这个函数检查三个响应:

  • 许可证响应。瑞莉
  • 许可证响应。得到许可的
  • 许可证响应。未获得许可

相应地,它随后设置参数, allowAccess() 方法可以读取这些参数。你会注意到另一件事。 processServerResponse() 对象中的最后一行是一个 commit() 操作。这是一种缓存功能,在该功能中,响应被混淆,然后存储在设备的共享首选项中。StrictPolicy 中不存在此部分,因为没有缓存任何数据。

清单 9-10 。ServerManagedPolicy 对象中的 processServerResponse()方法

**public void** processServerResponse(LicenseResponse response, ResponseData rawData) {

        // Update retry counter
        if (response != LicenseResponse.*RETRY*) {
        setRetryCount(0);
     } **else** {
        setRetryCount(mRetryCount        +        1);
     }

     **if** (response == LicenseResponse.*LICENSED*) {
        // Update server policy data
        Map        < String, String >        extras = decodeExtras(rawData.extra);
        mLastResponse = response;
        setValidityTimestamp(extras.get("VT"));
        setRetryUntil(extras.get("GT"));
        setMaxRetries(extras.get("GR"));
     } **else if** (response == LicenseResponse.*NOT_LICENSED*) {
        // Clear out stale policy data
        setValidityTimestamp(*DEFAULT_VALIDITY_TIMESTAMP*);
        setRetryUntil(*DEFAULT_RETRY_UNTIL*);
        setMaxRetries(*DEFAULT_MAX_RETRIES*);
     }

     setLastResponse(response);
     mPreferences.commit();
 }

```***  *****LVL 的有效使用**

如果您修改了 LVL 源代码(即您的策略),使其成为您的应用独有的东西,那么这一努力是值得的。您可能犯的一个错误是使用 LVL 的普通实现,每个人都知道它的源代码。这使得有人更容易修补你的应用,绕过你的许可证检查程序。Justin Case 已经在 Android Police 网站上展示了这个漏洞。可以在[www . androidpolice . com/2010/08/23/exclusive-report-Google-Android-market-license-verification-easy-boxed-will-not-stop-pirates/](http://www.androidpolice.com/2010/08/23/exclusive-report-googles-android-market-license-verification-easily-circumvented-will-not-stop-pirates/)找到文章。诚然,这是一篇老文章,但它仍然展示了这样一个原则,即如果您知道源代码是什么样子,就可以很容易地理解和修改逆向工程代码。在这种情况下,Justin 在一个演示和一个实际的商业应用中演示了如何修补和绕过 LVL 检查。

Android 开发者博客的 Trevor Johns 为我们提供了另一套很好的指南。这篇文章非常值得一读,并列出了一些更有效地使用 LVL 的技巧。有一段代码非常有趣。看图 9-22 。Trevor 告诉我们,攻击者可以猜测许可和未 _ 许可常量值的响应,然后交换它们,以便未经许可的用户可以完全使用该应用。为了防止这种情况,Trevor 向我们展示了一些代码,这些代码将对响应运行 CRC32 检查;不检查常量,而是检查常量的 CRC32 检查结果。我想在这个问题上多谈一点。想象一下,如果您不运行固定值的检查,而是执行另一个 HTTP fetch 来从您自己的服务器检索响应。

![9781430240624_Fig09-22.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/9781430240624_Fig09-22.jpg)

图 9-22 。备用响应验证思路

考虑清单 9-11 中的代码。您向服务器发出一个额外的请求,并从那里检索响应代码,而不是直接与一个数字进行比较。这样做的一个好处是,你可以以任何你喜欢的方式设计你的 ServerVerifier 对象。您甚至可以设置它,使响应代码每次都发生变化。您甚至可以考虑在代码中使用质询响应来改变每次的响应。

***清单 9-11 。*** *修改验证功能*

```java
public void verify(PublicKey publicKey, int responseCode, String signedData, String![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
       signature) {
        // ... Response validation code omitted for brevity ...

        // Compute a derivative version of the response code
        // Rather than comparing to a static value, why not retrieve the value from![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
       a server that you control?

        java.util.zip.CRC32 crc32 = new java.util.zip.CRC32();
        crc32.update(responseCode);
        int transformedResponseCode = crc32.getValue();

        ServerVerifier sv = new ServerVerifier(); // This class will make an![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
       HTTP request to your server to fetch the code.
        int serverResponse = sv.retrieveLicensedCode(); // There is no limit![image](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/andr-app-sec/img/backarrow.jpg)
       to how you can create this routine.

        // ... put unrelated application code here ...
        // crc32(LICENSED) == 3523407757 But this part is calculated on your server.
        if (transformedResponse == serverResponse) {
        LicenseResponse limiterResponse = mDeviceLimiter.isDeviceAllowed(userId);
        handleResponse(limiterResponse, data);
        }

        ...
        ...
        ...

或者,这也可以发生在策略中(与检查硬编码的服务器响应相反):

**if** (response == LicenseResponse.*LICENSED*)

您可以通过以下方式从您信任的服务器之一检索响应来检查响应:

ServerVerifier sv = new ServerVerifier();
**if** (response == sv.getLicensedResponse())
```***  *****混淆视听**

混淆 是你需要考虑的另一个要点。它适用于软件盗版,以及知识产权盗窃。模糊处理是将源代码中的所有类名、变量名和方法名更改为随机的、不相关的名称的过程。你可能想知道为什么我的反编译应用在目录列表中有像 a.smali 、 b.smali 、 c.smali 等文件。当我使用 BakSmali 反编译我的应用时,我是在二进制文件的模糊版本上运行它的。代码混淆器确保将我的类名(如 Comms 、 CommsEvent 、 CommsNotifier 等)更改为不主动提供它们所做工作的信息的类名。此外,如果您查看这些文件,您会发现方法名和成员名都被混淆了。这对于试图对代码进行逆向工程的人来说是非常令人沮丧的,并且它可以作为对知识产权或代码盗窃的极好的威慑。

模糊处理不能保证你的代码不会被窃取或盗版。它只是让逆向工程的任务变得更加困难。Android SDK 附带了一个名为 ProGuard 的混淆器。您可以使用 ProGuard 来混淆您的任何 Java 代码。你可以在[`proguard.sourceforge.net/;`](http://proguard.sourceforge.net/;)下载它,它是免费的开源软件。Android 开发者文档强烈建议你在打包应用发布时对代码进行模糊处理。如果您使用 Eclipse,那么这是一项简单的任务。在您的项目中找到您的 project.properties 文件(参见图 9-23 )并添加这一行(参见图 9-24 ):

```java
proguard.config = proguard.cfg

注意,这一行假设您没有将 proguard.cfg 文件的位置从其默认位置移走。

9781430240624_Fig09-23.jpg

图 9-23T3。项目属性文件

9781430240624_Fig09-24.jpg

图 9-24T3。添加 proguard.config 属性

要导出已签名或未签名的 APK 文件,右键单击您的项目名称,选择 Android Tools,然后选择导出未签名的应用或导出已签名的应用(参见图 9-25 )。

9781430240624_Fig09-25.jpg

图 9-25T3。导出混淆后的

ProGuard 是一个免费的开源 Java 代码混淆器。除了混淆之外,ProGuard 还试图缩小、优化和预先验证您提供给它的代码。就缩短执行时间而言,预验证对于移动应用非常重要。预验证阶段确保 Java 类的注释方式允许 VM 更快地读取和执行一些运行时检查。在大多数情况下,使用默认的 proguard.cfg 文件就足够了。图 9-26 显示了反编译、混淆的类文件的输出。正如您所看到的,由于重命名的、看起来很神秘的类名和变量名,代码本身很难阅读。混淆并不意味着停止逆向工程;相反,它更像是一种威慑,因为重新构建被重命名的变量和类名可能需要很长时间。一些商业 Java 混淆器甚至混淆了类文件中的字符串。这使得代码更加难以逆向工程。

9781430240624_Fig09-26.jpg

图 9-26T3。反编译、混淆的类文件*** ***总结

这一章专门讨论了你在应用货币化时将会面临的一些重要问题。虽然像苹果应用商店、黑莓应用世界和安卓市场这样的网站让你很容易获得收入,但你无疑将不得不面对软件盗版和知识产权盗窃等问题。你应该记住,本章讨论的主题不是灵丹妙药。它们不会完全保护您,但是它们会为您提供优势,使您的代码变得更难被攻击。在最好的情况下,攻击者会不动你的应用,因为他不想花力气对它进行逆向工程。

在这一章中,我们看了如果你的应用发现自己处于一个敌对的环境中,它会受到什么影响。我们研究了如何对你的应用进行逆向工程,并在修改后重新构建。我们展示了如何混淆你的源代码,使得攻击者更难读懂你的代码,即使是在逆向工程之后。然后,我们研究了如何检查应用中的许可,以确保最终用户不会盗版你的应用。我们通过使用 Android LVL 做到了这一点。要记住的一件事是,总是在许可检查库中编写自己的例程。这确保了你的代码是新鲜的,新的,不为人所知的。这使得逆向工程更加困难。

在发布应用之前,请记住这几个步骤。你可以在网上的developer . Android . com/guide/publishing/preparating . html找到它们的完整描述。

  1. 选好名。它将在应用的整个生命周期中起作用。
  2. 关闭调试和日志记录。确保搜索调试跟踪并禁用它。
  3. 清除项目目录中的备份文件或开发和测试过程中创建的其他不必要的文件。
  4. 检查您的清单文件,并确保所有必需的权限都存在。确保设置了标签和图标值以及正确的版本代码和版本名称属性。
  5. 检查并优化您的应用,以获得正确的 Android 版本。确保您的应用适合在不同规格的设备上运行。
  6. 在你的应用中更新你的网址。这意味着删除任何本地 IP 地址和测试服务器。将它们更改为正确的生产 IP 地址。
  7. 在您的应用中实施许可。*****

十、恶意软件和间谍软件

像个人电脑一样,移动智能手机也容易受到各种恶意软件的攻击。在本章中,我将把恶意软件和间谍软件统称为恶意软件。尽管我这样做了,但了解这些类型的恶意应用之间的区别是非常重要的。

恶意软件被定义为 驻留在用户电脑或智能手机上的任何恶意软件,其唯一任务是破坏数据、窃取个人信息或访问系统资源,以获得对其所在设备的完全控制。编写恶意软件的唯一目的是造成伤害;通常,恶意软件作者会编写恶意软件来针对操作系统或平台中的特定弱点。通常,恶意软件作者会希望最大化其恶意软件的传播,并寻求实现一种机制,使其软件能够将自身复制到其他类似的设备上。

间谍软件 是一个术语,用来指从设备中访问和窃取个人或私人信息的恶意软件。例如,在手机恶意软件的情况下,应用可能会跟踪最终用户的电子邮件、联系人列表、SMS 消息,甚至照片。间谍软件通常需要隐蔽并长时间驻留在设备上。因此,间谍软件作者的目标是在设备上执行很少或没有破坏性的活动,以便最终用户不知道她的数据被盗。几乎任何人都可以使用恶意软件;不再要求您知道如何自己编写恶意软件代码。

许多公司向个人、大公司甚至政府出售恶意软件(参见本章后面的案例研究)。我见过两种出售恶意软件的公司:一种卖给大型组织或政府,另一种卖给个人零售消费者。正如我们将在本章后面回顾的那样,一家大型中东电信供应器被发现在监视其整个黑莓用户群。有助于做到这一点的软件是由一家著名的专门从事合法监听的美国公司出售的。原来,源代码完全是从头开始开发的,它的唯一目的是从受感染的设备上捕获和泄露电子邮件。

另一方面,你会发现恶意软件或间谍软件被打包出售给任何愿意监视她认识的人。在大多数情况下,销售这类软件的公司会宣称“抓住出轨的配偶!”显然,这对一些人来说很有吸引力!我还将更详细地看一下这些版本的零售恶意软件。

恶意软件的四个阶段

我们可以将恶意软件操作分为四个不同但截然不同的阶段。虽然不是正式的,但这些阶段在大多数在设备上发现恶意软件的情况下都是可见的。

感染

这是恶意软件被引入设备的阶段。感染的圣杯是不涉及终端用户交互的圣杯。当恶意软件可以通过一些无害的方式复制到设备上时,就会发生这种情况,如向用户发送 SMS 消息或在无线网络上损害设备。

第二种感染方式是通过部分辅助行为。用户被要求单击恶意网站中的链接。一旦他这样做了,恶意软件就会把自己复制到设备上。攻击者通过 SMS 或电子邮件将此链接发送给用户。虽然有效,但这需要用户干预;在大多数情况下,勤奋的用户总是对点击发送给他们的随机链接持怀疑态度。

最后一种感染形式是攻击者通过 USB 端口或浏览网站,将恶意软件物理复制到设备上。这发生在攻击者和终端用户彼此认识,或者攻击者可以物理访问终端用户的设备的情况下。如果用户对其设备进行了密码保护,并且需要密码才能使用设备或在设备上安装应用,则此技术无效。

妥协

大多数情况下,感染和妥协是相伴而生的。在这种情况下,我使用单词 compromise 来描述恶意软件如何能够获得对设备的超级用户访问。因此,恶意软件可以以它选择的任何方式对设备配置进行更改 — ,而不需要设备所有者的交互。

正如我们在前面章节中看到的,运行在 Android 上的程序需要用户明确授权才能访问互联网或阅读电子邮件。在危害阶段,恶意软件将利用操作系统中的弱点来规避权限授予过程,从而允许它在用户不知情的情况下执行任何功能。

传播

除非专门针对个人,否则恶意软件作者通常会想要感染大量用户。他可能想要控制一大批设备,或者只是从许多不同的人那里获取私人信息。Zeus 特洛伊木马(在个人计算机平台上发现)将利用操作系统中的弱点进行传播。它的唯一目的是收集用户的按键,并收集银行和社交网站的凭据。

最近,另一种流行的传播甚至感染机制是使用谷歌 Android 市场(作者可以在这里出售或免费分发他们的应用)。恶意软件作者可以将游戏或社交网络互动工具等看起来无害的应用上传到 Android Market。当最终用户购买或下载该应用时,她的设备就会被感染。

渗出

恶意软件通常以个人或机密信息为目标。它可能会记录击键,试图获取网上银行和电子邮件等网站的用户名或密码。然而,仅仅收集这些信息是不够的。攻击者需要访问这些信息,因此恶意软件会找到一种“呼叫总部”或与远程服务器通信的方式,要么接收新的指令,要么上传捕获的信息。这个阶段叫做渗出。让我们来看一个案例研究,说明这是如何工作的。

案例研究 1:政府批准的恶意软件

2009 年 7 月,阿拉伯联合酋长国(UAE)的电信供应器 Etisalat 向其所有黑莓手机用户发送了一条短信,要求下载并安装系统补丁。该补丁旨在提高手机 3G 功能的性能。原来,这个“补丁”只不过是一个恶意软件,旨在读取每个用户的外发电子邮件。

直到今天,该公司仍坚称该补丁旨在提高性能。大多数检查过该恶意软件的研究人员,包括我自己和 Research In Motion (RIM ),都可以看到它没有任何性能上的好处。相反,检查代码会发现有人故意试图捕获设备所有者的所有外发电子邮件,并将其副本发送到供应器的服务器进行检查。

这个案件的标题,政府批准的恶意软件,可能有点强,特别是当你考虑到没有确凿的证据已经从这个案件的调查具体化。我选择这个标题是基于我在阿联酋工作的 11 年(其中 5 年为 Etisalat 工作)、最近的媒体事件,以及我对政府和监管机构控制该国媒体和通信基础设施的密切程度的了解。

我提到的媒体事件发生在 2010 年 8 月左右,当时阿联酋政府宣布,如果 RIM 公司不提供监控用户信息的手段,包括电子邮件和 BlackBerry Messenger(允许黑莓用户相互发送信息的本地消息平台),它将关闭该国境内的所有黑莓服务。由于我不是在写间谍小说,我将为我的下一本书搁置我所有的理论,而是带你了解恶意软件本身的一些更真实的方面。在本案例研究中,我们将尝试唯一确定恶意软件感染的阶段。

感染

Etisalat 通过使用简单的 WAP-push 消息将恶意软件引入其用户的设备。这是一条出现在设备的 SMS 收件箱中的消息,它包含文本和 URL。WAP-push 消息的文本如下:

亲爱的 Etisalat BlackBerry 客户:

Etisalat 始终热衷于为您提供最佳的黑莓服务和终极体验,为此,我们将向您发送一个性能增强补丁,您需要将其安装在您的设备上。如需更多信息,请拨打 101

借助 Etisalat 的黑莓和移动解决方案提升您的业务

一旦用户点击附带的 URL,设备就会下载并安装一个名为 Registration 的应用。该设备将提示终端用户授予应用特定的权限。由于 WAP-push 消息来自看似合法的来源,大多数用户没有理由不信任该请求,并且通常授予应用完全权限。

妥协

在这种情况下,恶意软件没有依靠操作系统中的弱点来获取个人信息。用户认为应用是合法的,因此在安装阶段授予了所有必要的权限。

传播

Etisalat 发布的恶意软件旨在保留在设备上并收集信息。它不是为传播到其他设备而设计的。恶意软件依赖于 WAP-push 消息,而不是传播。安装将一次性完成,此后不会扩散。

渗出

这是 Etisalat 恶意软件最重要的阶段。它被设计成将自己附加到用户发送的电子邮件中,并向 Etisalat 内部的服务器发送每条外发邮件的副本。这由内置的 BlackBerry API 调用来完成。

图 10-1 中描述了一条真实的消息(恶意软件用来向服务器登记)。这是一条每小时发送到服务器的消息。然后,恶意软件系统的操作员可以看到哪些设备被恶意软件感染,包括哪些设备定期检查。

9781430240624_Fig10-01.jpg

图 10-1T3。注册恶意软件使用的捕获的“心跳”消息

检测

这种特定的恶意软件之所以被检测出来,是因为它写得很糟糕。恶意软件一发布,本应接收泄漏数据的服务器就被信息淹没了。无法承受负载,服务器崩溃。这导致设备上的恶意软件不断重试连接到无响应的服务器。这种持续的连接尝试增加了设备本身的处理器使用率。

此时,最终用户开始注意到他们的设备性能缓慢,电池过早耗尽。一些用户甚至注意到他们的设备过热。这促使几名研究人员调查注册应用,于是他们发现这实际上是恶意软件。图 10-2 显示了恶意软件安装在设备上时如何运行的流程图。以下是注册恶意软件特征的详细列表:

  • 它检查它是否在 BlackBerry 安装的应用中被列为可见。
  • 如果它是可见的,它会隐藏自己,不让订阅者看到。这可以防止用户找到并删除它。
  • 它遍历手持设备上的所有邮件帐户,并将自己附加到每个帐户上,查找收到的电子邮件和 PIN 消息。
  • 它截取并监控手持设备的状态,以发现发生的网络事件。当这些事件发生时,它通知服务供应器的服务器。
  • 它监听通过电子邮件或 BlackBerry PIN 从特定地址收到的消息。这些控制消息可以启用或禁用对用户消息的拦截。
  • 它定期向预定义的服务供应器服务器报告。
  • 如果启用,应用会将订户发出的电子邮件的副本转发到服务供应器服务器。

9781430240624_Fig10-02.jpg

图 10-2T3。Etisalat 恶意软件操作流程图

案例研究 2:零售恶意软件—FlexiSPY

现在让我们看看第二个恶意软件应用:FlexiSPY,一种零售恶意软件。当攻击者在目标设备上安装 FlexiSPY 时,它会窃听所有通信。最新版本的 FlexiSPY Omni 为 Android 用户提供了以下功能:

  • 捕获短信和电子邮件
  • 捕获通话记录
  • 通过 GPS 和手机信号塔信息发现 GPS 位置
  • 把手机变成监听设备
  • 拦截电话
  • SIM 卡更换通知

对于监视任何人来说,这似乎已经足够了,我发现这个特性列表非常有趣,足以获得一个副本并对其进行分析。

注意本着全面披露的精神,我要提一下,在我评估 FlexiSPY 的时候,我查看了黑莓版本,因为那是我的主要手机。激活和启用设备的协议都是基于网络的,所以它们或多或少保持相同,不管支持的设备平台是什么(当然包括 Android)。

一旦买家支付了 349 美元,她就会收到一本用户手册,上面提供了如何在目标客户的手机上安装该应用的信息。当浏览用户手册时,首先映入我眼帘的是它提供了。。。明确指示 将黑莓手持设备的默认权限设置为允许所有

这意味着,不仅仅是 FlexiSPY,目标安装在手机上的每一个应用都可以获得对手持设备的完全控制(在编程接口或 API 的范围内)。显然,在这种情况下,用户保护并不是最重要的。类似地,查看 FlexiSPY 的 Android 手册,在您可以成功地在设备上安装恶意软件之前,设备本身必须是根。该网站以超级一键点击的形式提供了一个根设备的解决方案。除了这段文字,该网站没有提供直接链接。找到漏洞是客户的责任。

FlexiSPY 需要激活才能开始监视目标。为此,用户必须拨打号码 *#900900900 ,这将激活一个隐藏的屏幕。在此屏幕上,系统会提示用户输入激活码。从来没有一个离开家没有我最喜欢的网络数据包嗅探器,Wireshark,我嗅探了激活过程中通过的流量。以下是通过网络传递的信息:

  • 邮件/t4l-mcli/cmd/productactivate?模式=【0】&【查看】【0302】&【PID =【FSP _ bb _ v 4.2】&【act code】【启用代码】&散列=)

此请求是向具有下列二级域的服务器发出的:

aabackup.info

它解析为与之前列出的主机 djp.cc 相同的 IP 地址。正如你所看到的,手机的 IMEI 被发送回 FlexiSPY 总部。还可以看到激活码,它返回一个哈希值。看起来手机计算了一个类似的算法,并等待一个匹配的散列。一旦收到正确的散列,应用就被激活。

从这一点上说,这是一个配置应用拦截短信,电子邮件,通话记录,等等。该应用有一个通过短信的命令通道。因此,您有一个包含八个命令的列表,它们执行以下操作:

  • 开始捕获:开始捕获事件,如电子邮件、SMS、位置等。
  • 停止捕获:停止已经开始的捕获。
  • 立即发送:将所有收集的事件发送到中央日志记录主机。
  • 发送诊断信息:发送诊断信息。
  • 启动 SIM 卡监视器:观察任何改变 SIM 卡的企图。
  • 停止 SIM 卡监控:停止监控 SIM 卡。
  • 启动麦克风监听:等待触发号码的来电。
  • 停止麦克风监听:停止监听来自该触发号码呼叫。

有趣的是,命令频道 SMS 消息不能被删除,所以手册建议用户选择像“早上好”或类似的短语来开始捕捉信息。措辞应该选择得不会引起目标的怀疑。

请记住,我在 BlackBerry 版本的 FlexiSPY 上执行了前面的检查。考虑到运行 Java 的每个平台的相似性,Android 也会以相似的方式运行。

反取证

目前,最广泛使用的检测机制 是基于签名的。这意味着任何反恶意软件公司编写删除或检测功能需要事先了解恶意软件。如果它遇到它,那么它可以删除它。因此,不太可能检测到新的恶意软件。这是不幸的,因为如果反恶意软件公司无法跟上恶意软件的发展,那么从恶意软件发布到发现并解决它总是有一个滞后。在此滞后期间,所有用户都面临风险。

*作为开发人员,您无法直接控制用户是否选择安装反恶意软件应用。您的责任在于确保您的应用安全地处理其数据。我们在前面的章节中已经介绍了这些技术中的大部分,但是我想强调另一个可用的、非正统的选项:反取证。

反取证是一种 技术,用于通过降低可收集信息的质量来挫败对计算机或移动设备的取证分析。法医分析包括检查这种设备的证据。大多数情况下,需要收集的证据非常脆弱。反取证试图通过使用定期运行的自动化工具来销毁这些信息。然后,当进行法医分析时,调查人员只会发现乱码或无用的数据。这大大降低了可检索信息的质量。我们可以使用类似的技术来阻止恶意软件的行为。

我将从一个简单的例子开始:假设您的应用读写设备的消息存储。既然可以接触到这些数据,就可以人为生成邮件信息,随意删除。假设在设备上安装了等待复制进入收件箱的消息的恶意软件应用。通过生成许多虚假消息,然后定期删除它们,您正在为恶意软件提供低质量、无用的数据。如果操作正确,这个过程会使恶意软件作者提取有效信息变得非常繁琐。这个概念在图 10-3 和图 10-4 中进行了说明。

9781430240624_Fig10-03.jpg

图 10-3T3。恶意软件拦截邮件信息

9781430240624_Fig10-04.jpg

图 10-4T3。生成假消息

这种技术可以被认为有点咄咄逼人;显然,最终用户应该同意您的应用的这种行为。我在这里提到它是作为另一种要考虑的技术。至于如何想出击败恶意软件的额外机制,我将留给你的想象力。然而,除非你的主要目标是开发这样的反恶意软件应用,否则你可以选择跳过它们。

摘要

在本章中,我们看了恶意软件和间谍软件以及它们是什么。我们还研究了恶意软件的各个阶段,以及我们如何将它们分成几大类。我们了解到恶意软件可以被任何人使用,并且有许多商业实体向个人和公司消费者提供恶意软件。我们的案例研究涉及 2009 年发生的真实世界恶意软件感染,当时阿联酋的电信供应器之一 Etisalat 将其整个黑莓用户群置于间谍软件应用之下。

我们已经看到,作为一名应用开发人员,您通常在控制什么样的恶意软件被引入设备方面能力有限。相反,您的目标是安全地处理应用的数据(和最终用户数据)。我们非常简要地讨论了一个主题,即如何使用一些反取证技术有目的地向恶意软件提供无用的数据,从而迫使恶意软件作者费力地通过这些消息找到真正的消息。虽然这绝不是一个万无一失的解决方案,但这种技术主要是作为一种威慑。除非恶意软件作者专门针对你,否则他不太可能浪费时间筛选无用的数据。相反,他会把注意力转移到下一个被他感染的人身上。*

十一、附录一:Android 权限常量

出于参考目的,本附录提供了 Android 权限常量 的完整列表。许可和它们的使用在整本书中都有讨论,尤其是在第三章。

许可常数 描述
访问签入属性 允许对签入数据库中的属性表进行读/写访问,从而能够更改上传的值
访问 _ 粗略 _ 位置 允许应用访问粗略位置(例如,蜂窝 ID、WiFi)
访问 _ 精细 _ 位置 允许应用访问精确位置(例如 GPS)
访问位置额外命令 允许应用访问额外的位置提供程序命令
访问模拟位置 允许应用创建模拟位置提供程序进行测试
访问网络状态 允许应用访问网络信息
ACCESS_SURFACE_FLINGER 允许应用使用 SurfaceFlinger 的底层特性
访问 _ WIFI _ 状态 允许应用访问有关 Wi-Fi 网络的信息
客户 _ 经理 允许应用调用帐户授权码
身份验证 _ 帐户 允许应用充当 AccountManager 的帐户验证者
电池状态 允许应用收集电池统计数据
BIND_APPWIDGET 允许应用告诉 AppWidget 服务哪个应用可以访问 AppWidget 的数据
绑定设备管理 设备管理接收器必须要求,以确保只有系统才能与之交互
绑定输入方法 必须是 InputMethodService 所要求的,以确保只有系统可以绑定到它
绑定 _ 远程视图 必须是 RemoteViewsService 所要求的,以确保只有系统可以绑定到它
绑定 _ 壁纸 必须是壁纸服务所要求的,以确保只有系统可以绑定到它
蓝牙技术 允许应用连接到配对的蓝牙设备
蓝牙 _ 管理 允许应用发现和配对蓝牙设备
要求能够禁用设备(非常危险!)
广播 _ 包 _ 已删除 允许应用广播应用包已被删除的通知
广播 _ 短信 允许应用广播短信回执通知
广播 _ 粘性 允许应用广播粘性意图
广播 _ WAP _ 推送 允许应用广播 WAP 服务信息回执通知
呼叫电话 允许应用发起电话呼叫,而无需通过拨号器用户界面让用户确认正在进行的呼叫
通话特权 允许应用呼叫任何电话号码,包括紧急号码,而无需通过拨号器用户界面让用户确认正在进行的呼叫
照相机 需要能够访问相机设备
更改组件启用状态 允许应用更改是否启用应用组件(而不是它自己的组件)
更改配置 允许应用修改当前配置,如区域设置
改变网络状态 允许应用更改网络连接状态
更改 WIFI 多播状态 允许应用进入 Wi-Fi 多播模式
更改 WIFI 状态 允许应用更改 Wi-Fi 连接状态
清除应用缓存 允许应用清除设备上所有已安装应用的缓存
清除应用用户数据 允许应用清除用户数据
控制 _ 位置 _ 更新 允许从无线电启用/禁用位置更新通知
删除缓存文件 允许应用删除缓存文件
删除 _ 包 允许应用删除包
设备 _ 电源 允许对电源管理进行低级访问
诊断的 允许应用读写诊断资源
禁用 _ 键盘守卫 允许应用禁用键盘守卫
倾销 允许应用从系统服务中检索状态转储信息
展开状态栏 允许应用展开或折叠状态栏
工厂测试 作为制造商测试应用运行,作为根用户运行
手电筒 允许使用手电筒
强制返回 允许应用在顶层活动上强制执行 BACK 操作
获取 _ 帐户 允许访问帐户服务中的帐户列表
获取 _ 包 _ 大小 允许应用找出任何包使用的空间
获取 _ 任务 允许应用获取关于当前或最近运行的任务的信息:任务的缩略图、其中正在运行的活动等等
全局 _ 搜索 可用于内容供应器,以允许全球搜索系统访问他们的数据
硬件 _ 测试 允许访问硬件外围设备
注入 _ 事件 允许应用将用户事件(例如,按键、触摸和轨迹球)注入到事件流中,并将它们传递给任何窗口
安装位置供应器 允许应用将位置提供程序安装到位置管理器中
安装软件包 允许应用安装软件包
内部系统窗口 允许应用打开供部分系统用户界面使用的窗口
因特网 允许应用打开网络套接字
终止 _ 后台 _ 进程 允许应用调用 killBackgroundProcesses(String)
管理 _ 账户 允许应用管理帐户管理器中的帐户列表
管理应用令牌 允许应用在窗口管理器中管理(例如,创建、销毁和 Z 顺序)应用令牌
主机 _ 清除
修改 _ 音频 _ 设置 允许应用修改全局音频设置
修改电话状态 允许修改电话状态—开机、人机界面等
挂载格式文件系统 允许格式化可移动存储的文件系统
挂载卸载文件系统 允许安装和卸载可移动存储的文件系统
国家足球联盟 允许应用通过 NFC 执行 I/O 操作
持久 _ 活动 此常数已被否决。将来会删除此功能;请不要使用它。允许应用保持其活动的持久性。
处理 _ 呼出 _ 呼叫 允许应用监控、修改或中止呼出
阅读 _ 日历 允许应用读取用户的日历数据
阅读 _ 联系人 允许应用读取用户的联系人数据
读取帧缓冲区 允许应用获取屏幕截图,并且更一般地访问帧缓冲区数据
阅读 _ 历史 _ 书签 允许应用读取(但不写入)用户的浏览历史和书签
读取输入状态 允许应用检索按键和开关的当前状态
读取日志 允许应用读取低级系统日志文件
读取电话状态 允许对电话状态进行只读访问
阅读 _ 短信 允许应用读取短信
读取同步设置 允许应用读取同步设置
读取同步统计数据 允许应用读取同步统计数据
重新启动 需要能够重新启动设备
接收 _ 引导 _ 完成 允许应用接收系统完成引导后广播的 ACTION_BOOT_COMPLETED
接收 _ 彩信 允许应用监控收到的彩信,并记录或执行处理
接收 _ 短信 允许应用监控传入的 SMS 消息,并记录或处理它们
接收 _WAP_PUSH 允许应用监控传入的 WAP 服务信息
录音 _ 音频 允许应用录制音频
重新排序 _ 任务 允许应用更改任务的 Z 顺序
重启 _ 包 此常数已被否决。不再支持 restart package(String)API
发送 _ 短信 允许应用发送短信
设置活动观察器 允许应用观察和控制活动如何在系统中全局启动
设置 _ 报警 允许应用广播为用户设置警报的意图
设置 _ 总是 _ 完成 允许应用控制活动是否在后台立即完成
设置 _ 动画 _ 缩放 修改全局动画比例因子
SET_DEBUG_APP 为调试配置应用
设置方向 允许设置屏幕方向(实际上是旋转)的低级访问
设定 _ 指针 _ 速度 允许设置指针速度的低级访问
设置 _ 首选 _ 应用 此常量已被弃用,不再有用;详见 addpackagetoppreferred(String)
集合 _ 进程 _ 限制 允许应用设置可以运行的(不需要的)应用进程的最大数量
设置定时器 允许应用设置系统时间
设置时区 允许应用设置系统时区
设置 _ 壁纸 允许应用设置壁纸
设置 _ 壁纸 _ 提示 允许应用设置壁纸提示
信号 _ 持久 _ 进程 允许应用请求向所有持久进程发送信号
状态栏 允许应用打开、关闭或禁用状态栏及其图标
订阅 _ 订阅源 _ 阅读 允许应用允许访问订阅的提要内容提供者
订阅 _ 订阅源 _ 写入
系统警报窗口 允许应用使用类型 TYPE_SYSTEM_ALERT 打开窗口,显示在所有其他应用的顶部
更新设备状态 允许应用更新设备统计数据
使用凭据 允许应用从 AccountManager 请求 authtokens
使用 _SIP 允许应用使用 SIP 服务
颤动 允许接触振动器
唤醒 _ 锁定 允许使用电源管理器唤醒锁来防止处理器休眠或屏幕变暗
写 _ APN _ 设置 允许应用写入 apn 设置
写日历 允许应用写入(但不读取)用户的日历数据
写联系人 允许应用写入(但不读取)用户的联系人数据
写 _ 外部 _ 存储 允许应用写入外部存储
WRITE _ 服务 允许应用修改谷歌服务地图
写 _ 历史 _ 书签 允许应用写入(但不读取)用户的浏览历史和书签
写入 _ 安全 _ 设置 允许应用读取或写入安全系统设置
写入设置 允许应用读取或写入系统设置
写短信 允许应用编写 SMS 消息

内容供应器类别

类别名 描述
闹钟响了 AlarmClock 提供程序包含一个意向动作和附加动作,可用于启动一个活动,在闹钟应用中设置一个新的闹钟
浏览器
浏览器。书签栏 在书签 _URI 提供的混合书签和历史项目的列定义
浏览器。搜索列 搜索历史表的列定义,可从搜索 _URI 获得
呼叫日志 呼叫日志提供程序包含有关发出和接收呼叫的信息
通话记录。打电话 包含最近的通话
联系人联系人 联系人提供者和应用之间的合同
联系我们。聚合 xceptions 联系人汇总例外表的常数,该表包含覆盖自动汇总所用规则的汇总规则
联系合同。通用数据类型 存储在 ContactsContract 中的通用数据类型定义的容器。数据表
联系合同。CommonDataKinds.Email 代表电子邮件地址的数据类型
联系人联系人。CommonDataKinds.Event 表示事件的数据类型
联系人联系人。common data kinds . group membership 组成员关系
ContactsContract.CommonDataKinds.Im 表示 IM 地址的数据类型
您可以使用为 ContactsContract 定义的所有列。数据,以及以下别名
联系合同。CommonDataKinds .昵称 代表联系人昵称的数据类型
联系人联系人。CommonDataKinds。注意 关于联系人的注释
联系人联系人。通用数据类型.组织 代表组织的数据类型
联系合同。CommonDataKinds.Phone 代表电话号码的数据类型
联系合同。CommonDataKinds.Photo 代表联系人照片的数据类型
联系人联系人。CommonDataKinds.Relation 表示关系的数据类型
联系人联系人。CommonDataKinds.SipAddress 代表联系人的 SIP 地址的数据类型
联系人联系人。CommonDataKinds.StructuredName 表示联系人正确姓名的数据类型
联系合同。common data kinds . structured postal 代表邮政地址的数据类型
联系人联系人。CommonDataKinds .网站 表示与联系人相关的网站的数据种类
联系我们。连络人 contacts 表的常数,该表包含代表同一个人的每个原始联系人聚合的记录
联系合同。联系人。聚合建议 包含所有聚合建议(例如,其他联系人)的单个联系人聚合的只读子目录
联系人联系人。联系人.数据 单个联系人的子目录,包含所有组成的 raw contactContactsContract。数据行
联系合同。联系人.实体 联系人的子目录,包含其所有的 contacts contact。原始联系人,以及联系人。数据行
联系我们。联系人,照片 包含联系人主要照片的单个联系人的只读子目录
联系我们。日期 数据表的常数,其中包含与原始联系人相关的数据点
联系人联系人。目录 目录代表联系人语料库
联系人联系人。组 组表的常数
联系我们。试试看 包含用于创建或管理涉及联系人的意图的助手类
联系人联系人。意图。插入 包含用于创建联系意图的字符串常量的便利类
联系我们。PhoneLookup(电话查找) 表示查找电话号码(例如,查找呼叫者 ID)的结果的表
联系我们。QuickContact(快速联系) 帮助器方法显示 QuickContact 对话框,允许用户在特定的联系人条目上旋转
联系我们。拉瓦联系人 原始联系人表的常量,该表包含每个同步帐户中每个人的一行联系人信息
联系我们。RawContacts 日期 单个原始联系人的子目录,包含其所有的 contacts contact。数据行
联系我们。RawContacts.Entity .实体 单个原始联系人的子目录,包含其所有的 contacts contact。数据行
联系我们。rawcontactsentity(联系人信息) 原始 contacts 实体表的常量,可以认为是数据表的 raw_contacts 表的外部连接
联系合同。设置 各种帐户的联系人特定设置
联系我们。状态更新 状态更新链接到一个 ContactsContract。Data row 并通过相应的源捕获用户的最新状态更新
联系我们。SyncState 为同步适配器提供的用于存储专用同步状态数据的表
实时文件夹 LiveFolder 是一个特殊的文件夹,其内容由 ContentProvider 提供
媒体库 媒体提供程序包含内部和外部存储设备上所有可用媒体的元数据
媒体商店。声音的 所有音频内容的容器
媒体商店。音频.相册 包含音频文件的艺术家
媒体商店。音频。艺术家 包含音频文件的艺术家
MediaStore。音频,艺术家,专辑 每个艺术家的子目录,包含出现该艺术家歌曲的所有专辑
媒体商店。音频类型 包含所有类型的音频文件
媒体商店。音频.类型.成员 包含所有成员的每个流派的子目录
媒体商店。音频媒体
媒体商店。音频.播放列表 包含音频文件的播放列表
媒体商店。音频.播放列表.成员 包含所有成员的每个播放列表的子目录
媒体商店。文件 包含媒体存储器中所有文件(包括非媒体文件)索引的媒体提供者表
媒体商店。形象 包含所有可用图像的元数据
媒体商店。图像.媒体
媒体商店。图像.缩略图 这个类允许开发者查询并获取两种缩略图:MINI_KIND: 512 × 384 缩略图和 MICRO_KIND: 96 × 96 缩略图
媒体商店。录像
MediaStore。视频。媒体
媒体商店。视频.缩略图 这个类允许开发者查询并获取两种缩略图:MINI_KIND: 512 × 384 缩略图和 MICRO_KIND: 96 × 96 缩略图
SearchRecentSuggestions 这是一个工具类,提供对 SearchRecentSuggestionsProvider 的访问
设置 设置提供程序包含全局系统级设备首选项
设置。名称值表 名称/值设置表的公共库
设置。安全的 安全系统设置,包含应用可以读取但不允许写入的系统偏好设置
设置。系统 系统设置,包含各种系统偏好设置
SyncStateContract 用于将数据与任何数据数组帐户相关联的 ContentProvider 协定
SyncStateContract(同步状态合同)。常数值
SyncStateContract。助手
用户词典 用于输入法的用户定义单词的提供者,用于预测文本输入
用户词典。话 包含用户定义的单词