【译】在 Jelly Bean 中使用应用加密

翻译自:Using app encryption in Jelly Bean

关键词 : adb install -l


最新的 Android 4.1(Jelly Bean)版本在上周的 Google I / O 大会上发布了,它有一大堆新功能和改进。 其中一个有趣的功能是应用程序加密,除了简短的说明还没有任何细节:“从 Jelly Bean 版本起,Google Play 中的付费应用程序在分发和存储之前,会使用设备特定的密钥加密”。缺乏细节当然会引起猜测,有些人甚至担心,当他们使用一个新的设备时必须回购他们的付费应用程序。 在本文中,我们将介绍如何在操作系统中实施应用加密,展示如何在不通过 Google Play 的情况下安装加密的应用,并了解 Google Play 如何提供加密的应用。

操作系统对加密应用程序的支持

本文之前的版本是基于 Eclipse 框架和二进制的系统镜像,并且缺少一部分。由于 Jelly Bean 已经开源,下面的讨论已经修改,现在是基于 AOSP 代码(4.1.1_r1)。如果你回来重读本文,重点放在第二部分。

Android 上的应用可以通过几种不同的方式安装:

  • 通过应用商店(例如 Google Play Store,也称为 Android Market)
  • 直接在手机上打开应用文件或电子邮件附件(如果启用“位置来源”选项)
  • 使用通过 USB 连接的计算机的 adb install SDK 命令

前两个不提供任何选项或者特定视角的底层实现,让我们探索第三个。看看使用 adb 命令的输出,可以看到 install 命令在最新的 SDK 版本中获得了几个新选项:

1
2
$ adb install [-l] [-r] [-s] [--algo <algorithm name> --key <hex-encoded key> 
--iv <hex-encoded iv>] <file>

显然 —algo—key—iv 参数与加密应用程序有关,所以在进入详细信息之前,首先尝试安装一个加密的 APK。使用 **OpenSSL enc ** 命令加密文件相当容易,通常它已经在大多数 Linux 系统上安装。我们使用 CBC 模式下的 AES 算法,配合一个128 bit 的 Key(下文可以看到,这一个不是很安全的密钥),为使事情更简单,这里使用与密钥相同的初始化向量(initialization vector, IV)。

1
2
$ openssl enc -aes-128-cbc -K 000102030405060708090A0B0C0D0E0F 
-iv 000102030405060708090A0B0C0D0E0F -in my-app.apk -out my-app-enc.apk

让我们尝试安装它来检查 Android 是否喜欢我们新加密的应用程序:

1
2
3
4
$ adb install --algo 'AES/CBC/PKCS5Padding' --key 000102030405060708090A0B0C0D0E0F 
--iv 000102030405060708090A0B0C0D0E0F my-app-enc.apk
pkg: /data/local/tmp/my-app-enc.apk
Success

“Success” 的输出结果看起来似乎很有前途,然后确认应用的图标显示在系统面板中并且启动时无错误。实际的 APK 文件像往常一样被复制在 /data/app 中,将其哈希值与我们加密的 APK 进行比较,发现它实际上是一个不同的文件。安装后的文件的哈希值和原始(未加密) APK 的完全相同,因此我们可以得出结论,APK 在安装是使用我们提供的加密参数(算法、密钥和 IV)进行解密。让我们看看这是如何实现的。

adb install 命令根本上是调用 pm Android 命令行实用程序,它允许我们列出、安装和删除包(应用程序)。 通常 PackageManagerService 是负责在 Android 上安装应用程序的组件,pm 只是一个方便的前端。 应用程序通常通过 PackageManager 类来访问包服务。 浏览其代码并检查加密相关方法,我们发现:

1
2
3
4
public abstract void installPackageWithVerification(Uri packageURI,
IPackageInstallObserver observer, int flags, String installerPackageName,
Uri verificationURI, ManifestDigest manifestDigest,
ContainerEncryptionParams encryptionParams);

ContainerEncryptionParams 类看起来特别有可能,所以让我们看看里面:

1
2
3
4
5
6
7
8
9
10
11
public class ContainerEncryptionParams implements Parcelable {
private final String mEncryptionAlgorithm;
private final IvParameterSpec mEncryptionSpec;
private final SecretKey mEncryptionKey;
private final String mMacAlgorithm;
private final AlgorithmParameterSpec mMacSpec;
private final SecretKey mMacKey;
private final byte[] mMacTag;
private final long mAuthenticatedDataStart;
private final long mEncryptedDataStart;
}

我们使用的 adb install 参数完全对应于类的前三个字段。 除此之外,该类还存储 MAC 相关参数,因此可以安全地假设 Android 现在可以检查应用程序二进制文件的完整性。 不幸的是,pm 命令没有任何 MAC 相关的参数(它实际上有,但由于某些原因,在当前生成中禁用),所以为了尝试 MAC 支持,我们需要直接调用 installPackageWithVerification 方法。

该方法对于 SDK 应用程序是隐藏的,因此从应用程序里调用它的唯一方法是使用反射。事实证明,它的大多数参数类( IPackageInstallObserver,ManifestDigest 和 ContainerEncryptionParams )也被隐藏,但这只是一个小陷阱。 对于 Android 预加载框架类,即使你的应用程序捆绑了一个框架类,系统副本仍旧会在运行时使用它。这意味着我们必须做的是为 installPackageWithVerification 方法获取一个句柄,将所需的类添加到我们的应用程序中的 andorid.content.pm 包中。一旦我们有一个方法句柄,我们只需要实例化 ContainerEncryptionParams 类,然后提供所有的加密和 MAC 相关的参数。需要注意的是,由于我们的整个文件是加密的,并且 MAC 是在其所有内容上计算的(见下文),我们为加密和认证数据的开头指定为 0,将文件大小指定为数据的结尾(查看示例代码)。为了计算 MAC 值(标签),我们再次使用 OpenSSL:

1
2
$ openssl dgst -hmac 'hmac_key_1' -sha1 -hex my-app-enc.apk
HMAC-SHA1(my-app-enc.apk)= 0dc53c04d33658ce554ade37de8013b2cff0a6a5

请注意,dgst 命令不支持使用十六进制或 Base64 的 HMAC 密钥,因此您只能使用 ASCII 字符。 这对于生产使用可能不是一个好主意,因此请考虑使用真正的密钥并以某种其他方式(使用 JCE 等)来计算 MAC。

我们的应用程序现在大部分已准备就绪,但安装应用程序需要 INSTALL_PACKAGES 权限,该权限与保护级别 signatureOrSystem 一起定义。 因此,它只被授予使用系统(ROM)密钥签名的应用程序或安装在 /system 分区中的应用程序。 构建 Jelly Bean ROM 是一个有趣的练习,但现在,我们只需将我们的应用程序复制到 /system/app,以获得安装软件包(在模拟器或 Root 过的设备上)的必要权限。 一旦完成,我们可以通过 PackageManager 安装加密的应用程序,Android 将通过比较指定的 MAC 标记与基于实际文件内容计算的值来解密 APK 并验证包没有被篡改。 您可以通过稍微更改加密和 MAC 参数来使用示例应用程序进行测试,这将导致安装错误。

jb-app-encryption

android.content.pm 包有一些更有趣的类,例如 MacAuthenticatedInputStreamManifestDigest ,但实际的 APK 加密和 MAC 验证是由 DefaultContainerService$ApkContainer 做的,它是 DefaultContainerService(aka,Package Access Helper)的一部分。

前向锁定(Forward locking)

在移动(功能)手机上,通常当铃声、壁纸或者其他数字商品开始进行销售时,Forward locking 会弹出。它的名称来自它的意图:阻止用户转发他们已经购买的文件给自己的朋友和家人。Android 上的主要数字内容是原生应用程序,随着付费应用越来越受欢迎,共享(二次销售)成为一个问题。应用程序包(APKs)在 Android 上是公共可读的,这使得即使是在生产设备中提取应用程序也相对容易。虽然公共可读的应用程序文件可能听起来像是一个坏主意,它根植于 Android 的开放和可扩展性—第三方启动器、Widget 容器和应用程序可以轻松的提取 APKs 的 Icon、Widget 的可定义 Intents等等。为了锁定付费应用程序而不失去任何操作系统的灵活性,Android 引入了 Forward locking(又称“拷贝保护”)。它的想法是把应用程序包分为两部分:一个公共可读的部分,包括资源文件和 manifest (保存在 /data/app );一个系统用户可读的包含可执行代码的包(保存在 /data/app-private )。代码包受文件系统权限保护,虽然这使得大多数消费者设备上的用户无法访问它,但是只需要获得 root 访问权限即可提取它。这种方法很快就被启用,引入了在线 Android 授权(LVL)作为替代品。然而,这将应用程序保护实施从操作系统转移到应用程序开发人员,并取得了不同的结果。

在 Jelly Bean 中,Forward locking 的实现已经重新设计,现在提供了将 APK 存储在加密容器中的能力,该加密容器需要在运行时安装设备特定密钥。让我们更详细地研究一下实现。

Jelly Bean 实现

虽然对于 JB(Jelly Bean),把加密的应用程序容器作为一个前向锁定机制是新加的,但加密容器的想法开始于 Froyo。 当时(2010年5月),大多数Android 设备都配备有有限的内部存储空间和相当大(几GB)的外部存储设备,通常采用 micro SD 卡的形式。 为了使文件共享更容易,外部存储使用 FAT 文件系统进行了格式化,该文件系统缺少文件权限。 因此,任何人(任何应用程序)都可以读取和写入 SD 卡上的文件。 为了防止用户简单地将付费应用程序复制到 SD 卡上,Froyo 创建了一个加密的文件系统映像文件,并在您选择将应用移动到外部存储时将 APK 存储在其中。 然后使用 Linux 的设备映射程序在运行时安装映像,系统将从新创建的安装点(每个应用程序一个)加载应用程序文件。 基于此,JB 的容器使用 EXT4 文件系统,它允许权限。 典型的前向锁定应用程序的挂载点现在如下所示:

1
2
3
4
5
6
shell@android:/mnt/asec/org.mypackage-1 # ls -l
ls -l
drwxr-xr-x system system 2012-07-16 15:07 lib
drwx------ root root 1970-01-01 09:00 lost+found
-rw-r----- system u0_a96 1319057 2012-07-16 15:07 pkg.apk
-rw-r--r-- system system 526091 2012-07-16 15:07 res.zip

这里 res.zip 拥有应用程序资源,是公共可读的,而保存完整 APK 的 pkg.apk 文件只能由系统和应用程序的专用用户(u0_a96)读取。 实际的应用程序容器存储在/data/app-asec 中,文件名为 pacakge.name-1.asec 。 ASEC 容器管理(创建/删除和挂载/卸载)在系统卷守护程序(vold)中实现,框架服务通过通过本地 socket 发送命令与之通信。 我们可以使用 vdc 实用程序从 shell 管理转发锁定的应用程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# vdc asec list
vdc asec list
111 0 com.mypackage-1
111 0 org.foopackage-1
200 0 asec operation succeeded

# vdc asec unmount org.foopackage-1
200 0 asec operation succeeded

# vdc asec mount org.foopackage-1 000102030405060708090a0b0c0d0e0f 1000
org.foopackage-1 000102030405060708090a0b0c0d0e0f 1000
200 0 asec operation succeeded

# vdc asec path org.foopackage-1
vdc asec path org.foopackage-1
211 0 /mnt/asec/org.foopackage-1

所有命令都将命名空间 ID(实际上基于包名称)作为参数,对于 mount 命令,您需要指定加密密钥和装载点的所有者 UID(1000是系统)。 那就是如何存储和使用应用程序,剩下的是找出实际的加密算法和密钥。 两者从原始的 Froyo 应用程序到 SD 实现没有变化:Twofish 与128位密钥存储在 /data/misc/systemkeys

1
2
3
4
5
6
7
shell@android:/data/misc/systemkeys $ ls
ls
AppsOnSD.sks
shell@android:/data/misc/systemkeys $ od -t x1 AppsOnSD.sks
od -t x1 AppsOnSD.sks
0000000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
0000020

通过指定 pm install 命令的 -l 选项或为 PackageManagerinstallPackage* 方法指定 INSTALL_FORWARD_LOCK 标志(请参阅示例应用程序)来触发转发锁定应用程序。

加密应用程序和 Google Play

所有这一切都很有趣,但正如我们所见,安装应用程序、加密或其他都需要系统权限,因此它只能由自定义运营商Android 固件和下一个版本友好的 CyanogenMod ROM 使用。 目前唯一利用了新的加密应用程序和前向锁定基础设施的应用程序是 Play Store(谁出来了这些名字,真的?)Android 客户端。 要详细了解 Google Play 客户端的工作原理,需要详细了解底层协议(这始终是一个移动目标),但随意查看最新的 Android 客户端确实显示了一些有用的信息。 Google Play 服务器会发送相当多的有关您即将下载和安装的应用的元数据,例如下载网址,APK 文件大小,版本代码和退款窗口。 其中新增的是 EncryptionParams ,它看起来非常类似于上面显示的ContainerEncryptionParams

1
2
3
4
5
6
class AndroidAppDelivery$EncryptionParams {
private int cachedSize;
private String encryptionKey;
private String hmacKey;
private int version;
}

加密算法和 HMAC 算法总是分别设置为 'AES / CBC / PKCS5Padding''HMACSHA1' 。 IV 和 MAC 标签与加密的APK 捆绑在一个 blob 中。 一旦读取和验证所有参数,它们基本上将转换为 ContainerEncryptionParams 实例,并使用熟悉的 PackageManager.installPackageWithVerification() 方法安装应用程序。 如可能预期的,安装付费应用程序时,将设置 INSTALL_FORWARD_LOCK 标志。 操作系统从这里获取它,并且过程与上一节中描述的相同:免费应用程序被解密,APK最终在 /data/app 中,而在 /data/app-asec 中的加密容器被创建和装载在付费应用的 /mnt/asec/package.name 下。

那么在实践中这是什么意思? Google Play 现在声称,付费应用程序始终以加密形式传输和存储,因此,如果您决定使用 Jelly Bean 提供的应用程序加密设施实现它,则您自己的应用程序分发渠道也会如此。 应用程序必须在某些时候可用于操作系统,所以如果你有 root 用户访问正在运行的 Android 设备,仍然可以提取前向锁定 APK 或容器加密密钥,但这确实是所有软件的解决方案。

更新:虽然正向锁定使得复制付费应用程序更难,似乎它与其他服务的集成仍然有一些问题。 根据这里的多个开发人员和用户的报告,它目前打破了应用程序注册自己账户管理的实现,包含大多数付费小部件的应用程序。 这是由于一些服务在 /mnt/asec 被挂载之前被初始化,因此不能访问它。 据说有一个可用的修复(没有Gerrit链接),并应在 Jelly Bean 维护版中发布。

更新2:似乎最新版本的 Google Play 客户端(3.7.15)安装了带有小部件的付费应用程序,并且可能还会在 /data/app 中管理帐户(临时?)解决方法。 下载的 APK 仍会加密传输。 例如:

1
2
3
shell@android:/data/app # ls -l|grep -i beautiful
ls -l|grep -i beautiful
-rw-r--r-- system system 6046274 2012-08-06 10:45 com.levelup.beautifulwidgets-1.apk

这就是现在的情况。 希望,很快就会从官方来源获得有关应用加密操作系统实施和设计以及 Google Play 商店的使用情况的更详细信息。 在那之前,获取示例项目,启动 OpenSSL 并尝试。

Nikolay Elenkov 发布于 2012 年 7 月 6 日。