安卓应用安全指南(一)

原文: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 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

Android 架构的组件

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 。 一个视频播放器演示(由谷歌公司提供)

/*

*版权所有©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 所示的屏幕提示。值得注意的是,当应用执行时,不会出现权限提示。相反,需要在安装时授予权限。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 所示的屏幕。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

信息分类

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

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

表 2-1 。数据分类表

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

如果你仔细看一下表 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 ),任何不经意的窥探者或蓄意攻击者都无法读取。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

fileoutput stream output stream = new file output stream外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

(输出文件);

字节[] key =加密。generate key外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

(“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 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

(null)、location . geti identifier();

fileoutput stream output stream = new file output stream外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

(输出文件);

字节[] key =加密。generate key外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

(“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 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 进程的核心库。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 中的所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

清单 3-1 。 最富阶层

net . Zen consult . libs;

公共类 Mofest {

public Mofest(){

}

公共类权限{

public permission(){

最终字符串清除 _ 数据库= 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

”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**外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传**

。清除 _ 数据库/>

" Android . permission . set _ WALLPAPER "/>

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

“.禅藏馆活动”

Android:permission ="net . Zen consult . libs . mofest . permission**外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传**

。清除 _ 数据库

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

《Android . intent . action . main》/>

" Android . intent . category . launcher "/>

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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-5 。权限请求屏幕

摘要

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

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

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

四、概念实战:第一部分

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

Proxim 应用

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 )不会让他轻易窃取敏感的用户数据。相反,他有一个额外的层,他必须攻击。这一原则类似于美国国家安全局制定的深度防御的信息保证原则。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 算法运行这最后一块,它会失败。为了应对这种情况,有几种不同的填充选项可用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

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

分组密码的操作模式

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

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

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

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

表 5-4。的机制将的数据存储在 Android| 存储方法 | 描述 | 数据保密 |
| --- | --- | --- |
| 共享偏好设置 | 允许您存储原始数据类型(例如, intBooleanfloatlongString ),这些数据类型将在整个设备会话中保持不变。即使您的应用没有运行,您的数据也将持续存在,直到设备重新启动。 | 可以设置四种隐私模式: MODE_PRIVATEMODE_WORLD_READABLEMODE_WORLD_WRITABLEMODE_MULTI_PROCESS 。默认模式是 MODE_PRIVATE |
| 内部存储器 | 允许您将数据存储在设备的内部存储器中。通常,其他应用甚至最终用户都无法访问这些数据。这是一个私人数据存储区。存储在此处的数据即使在设备重新启动后仍将存在。当最终用户删除你的应用时,Android 也会删除你的数据。 | 可以设置三种隐私模式: MODE_PRIVATEMODE_WORLD_READABLEMODE_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());
        }

}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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;
        }

}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

SQLite 数据库

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

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

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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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;
        }
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 类解密任何数据。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

摘要

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值