Using app encryption in Jelly Bean

The latest Android version, 4.1 (Jelly Bean) was announced last week at Google I/O with a bunch of new features and improvements. One of the more interesting features is app encryption, but there haven't been any details besides the short announcement: 'From Jelly Bean and forward, paid apps in Google Play are encrypted with a device-specific key before they are delivered and stored on the device.'. The lack of details is of course giving rise to guesses and speculations, some people even fear that they will have to repurchase their paid apps when they get a new device. In this article we will look at how app encryption is implemented in the OS,  show how you can install encrypted apps without going through Google Play, and take a peak at how Google Play delivers encrypted apps.

OS support for encrypted apps

The previous version of this article was based on Eclipse framework source packages and binary system images, and was missing a few pieces. As Jelly Bean source has now been open sourced, the discussion below has been revised and is now based on the AOSP code (4.1.1_r1). If you are coming back you might want to re-read this post, focusing on the second part.

Apps on Android can be installed in a few different ways:
  • via an application store (e.g., the Google Play Store, aka Android Market)
  • directly on the phone by opening app files or email attachments (if the 'Unknown sources' options is enabled)
  • from a computer connected through USB by using the adb install SDK command
The first two don't provide any options or particular insight into the underlying implementation, so let's explore the third one. Looking at the adb usage output, we see that the install command has gained a few new options in the latest SDK release:

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

The --algo, --key and --iv parameters obviously have to do with encrypted apps, so before going into details lets first try to install an encrypted APK. Encrypting a file is quite easy to do using the enc OpenSSL commands, usually already installed on most Linux systems. We'll use AES in CBC mode with a 128 bit key (a not very secure one, as you can see below), and specify an initialization vector (IV) which is the same as the key to make things simpler:

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

Let's check if Android likes our newly encrypted app by trying to install it:

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

The 'Success' output seems promising, and sure enough the app's icon is in the system tray and it starts without errors. The actual apk file is copied in /data/app as usual, and comparing its hash value with our encrypted APK reveals that it's in fact a different file. The hash value is exactly the same as that of the original (unencrytped) APK though, so we can conclude that the APK is being decrytped at install time using the encryption parameters (algorithm, key and IV) we have provided. Let's look into how this is actually implemented. 

The adb install command simply calls the pm Android command line utility which lets us list, install and delete packages (apps). The component responsible for installing apps on Android has traditionally been the PackageManagerService and the pm is just a convenient frontend for it. Apps usually access the package service through the facade class PackageManager. Browsing through its   code and checking for encryption related methods we find this:

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

The ContainerEncryptionParams class looks especially promising, so let's peek inside:

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;

The adb install parameters we used above neatly correspond to the first three fields of the class. In addition to that, the class also stores MAC related parameters, so it's safe to assume that Android can now check the integrity of application binaries. Unfortunately, the pm command doesn't have any MAC-related parameters (it does actually, but for some reason those are disabled in the current build), so to try out the MAC support we need to call the installPackageWithVerification method directly.

The method is hidden from SDK applications, so the only way to call it from an app is to use reflection. It turns out that most of its parameter classes (IPackageInstallObserver, ManifestDigest and ContainerEncryptionParams) are also hidden, but that's only a minor snag. Android pre-loads framework classes, so even if you app bundles a framework class, the system copy will always be used at runtime. This means that all we have to do to get a handle for the installPackageWithVerification method is add the required classes to the package in our app. Once we have a method handle, we just need to instantiate the ContainerEncryptionParams class, providing all the encryption and MAC related parameters. One thing to note is that since our entire file is encrypted, and the MAC is calculated over all of its contents (see below), we specify 0 for both the encrypted and authenticated data start, and the file size as the data end (see sample code). To calculate the MAC value (tag) we once again use OpenSSL:

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

Note that the dgst command doesn't support specifying the HMAC key using hexadecimal or Base64, so you are limited to ASCII characters. This may not be a good idea for production use, so consider using a real key and calculating the MAC in some other way (using JCE, etc.).

Our app is mostly ready now, but installing apps requires the INSTALL_PACKAGES permission, which is defined with protection level signatureOrSystem. Thus it is granted only to apps signed with the system (ROM) key, or apps installed in the /system partition. Building a Jelly Bean ROM is an interesting excercise, but for now, we'll simply copy our app to /system/app in order to get the necessary permission to install packages (on the emulator or a rooted device). Once this is done, we can install an encrypted app via the PackageManager and Android will both decrypt the APK and verify that the package hasn't been tampered with by comparing the specified MAC tag with value calculated based on the actual file contents. You can test that using the sample application by slightly changing the encryption and MAC parameters. This should result in an install error.

The package has some more classes of interest, such as MacAuthenticatedInputStream and ManifestDigest, but the actual APK encryption and MAC verification is done by the DefaultContainerService$ApkContainer, part of the DefaultContainerService (aka, 'Package Access Helper').

Forward locking

'Forward locking' popped up around the time ringtones, wallpalers and other digital 'goods' started selling on mobile (feature) phones. The name comes from the intention -- stop users from forwarding files they have bought to their friends and family. The main digital content on Android were originally apps, and as paid apps gained popularity, sharing (and later re-selling those) was becoming a problem. Application packages (APKs) have been traditionally world readable on Android, which made extracting apps from even a production device relatively easy. While world-readable app files might sound like a bad idea, it's rooted in Android's open and extensible nature -- third party launchers, widget containers and utility apps can easily inspect APKs to extract icons, widget definitions available intents, etc. In an attempt to lock down paid apps without losing any of the OS flexibility, Android introduced forward locking (aka, 'copy protection'). The idea was to split app packages into two parts -- a world-readable part, containing resources and the manifest (in /data/app), and a package readable only by the system user, containing executable code (in /data/app-private). The code package was protected by file system permissions, and while this made it inaccessible to users on most consumer devices, one only needed to gain root access to be able to extract it. This approach was quickly deprecated, and online Android Licensing (LVL) was introduced as a replacement. This, however, shifted app protection implementation from the OS to app developers, and has had mixed results.

In Jelly Bean, the forward locking implementation has been re-designed and now offers the ability to store APKs in an encrypted container that requires a device-specific key to be mounted at runtime. Let's look into the implementation in a bit more detail.

Jelly Bean implementation

While encrypted app containers as a forward locking mechanism are new to JB, the encrypted container idea has been around since Froyo. At the time (May 2010) most Android devices came with limited internal storage and a fairly large (a few GB) external storage, usually in the form of a micro SD card. To make file sharing easier, external storage was formatted using the FAT filesystem, which lacks file permissions. As a result, files on the SD card could be read and written by anyone (any app). To prevent users from simply copying paid apps off the SD card, Froyo created an encrypted filesystem image file and stored the APK in it when you opted to move the app to external storage. The image was then mounted at runtime using Linux's device-mapper and the system would load the app files from the newly created mount point (one per app). Building on this, JB makes the container EXT4, which allows for permissions. A typical forward locked app's mount point now looks like this:

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

Here the holds app resources and is world-readable, while the pkg.apk file which hold the full APK is only readable by the system and the app's dedicated user (u0_a96). The actual app containers are stored in /data/app-asec with filenames in the form ASEC container management (creating/deleting and mounting/unmounting) is implemented int the system volume daemon (vold) and framework services talk to it by sending commands via a local socket. We can use the vdc utility to manage forward locked apps from the shell:

# 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

All commands take a namespace ID (based on the package name in practice) as a parameter, and for the mount command you need to specify the encryption key and the mount point's owner UID (1000 is system) as well. That about covers how apps are stored and used, what's left is to find out the actual encryption algorithm and the key. Both are unchanged from the original Froyo apps-to-SD implementation: Twofish with a 128-bit key stored in /data/misc/systemkeys:

shell@android:/data/misc/systemkeys # ls
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

Forward locking an application is triggered by specifying the -l option of the pm install command or specifying the INSTALL_FORWARD_LOCK flag to PackageManager's installPackage* methods (see sample app).

Encrypted apps and Google Play

All of this is quite interesting, but as we have seen, installing apps, encrypted or otherwise, requires system permissions, so it can only be used by custom carrier Android firmware and probably the next version of your friendly CyanogenMod ROM. Currently the only app that takes advantage of the new encrypted apps and forward locking infrastructure is the Play Store (who comes up with those names, really?) Android client. Describing exactly how the Google Play client works would require detailed knowledge of the underlying protocol (which is always a moving target), but a casual look at the newest Android client does reveal a few useful pieces of information. Google Play servers send quite a bit of metadata about the app you are about to download and install, such as download URL, APK file size, version code and refund window. New among those are the EncryptionParams which look very similar to the ContainerEncryptionParams shown above:

class AndroidAppDelivery$EncryptionParams {
private int cachedSize;
private String encryptionKey;
private String hmacKey;
private int version;

The encryption algorithm and the HMAC algorithm are always set to 'AES/CBC/PKCS5Padding' and 'HMACSHA1', respectively. The IV and the MAC tag are bundled with the encrypted APK in a single blob. Once all parameters are read and verified, they are essentially converted to a ContainerEncryptionParams instance, and the app is installed using the familiar PackageManager.installPackageWithVerification() method. As might be expected, the INSTALL_FORWARD_LOCK flag is set when installing a paid app. The OS takes it from here, and the process is the same as described in the previous section: free apps are decrypted and the APKs end up in /data/app, while an encrypted container in /data/app-asec is created and mounted under /mnt/asec/ for paid apps.

So what does all this mean in practice? Google Play can now claim that paid apps are always transferred and stored in encrypted form, and so can your own app distribution channel if you decide to implement it using the app encryption facilities Jelly Bean provides. The apps have to be made available to the OS at some point though, so if you have root access to a running Android device, extracting a forward-locked APK or the container encryption key is still possible, but that is true for all software-based solutions.

Update: while forward locking is making it harder to copy paid apps, it seems its integration with other services still has some issues. As reported by multiple developers and users here, it currently breaks apps that register their own account manager implementation, as well as most paid widgets. This is due to some services being initialized before /mnt/asec is mounted, and thus not being able to access it. A fix is said to be available (no Gerrit link though), and should be released in a Jelly Bean maintenance release.

Update 2: It seems that the latest version of the Google Play client, 3.7.15, installs paid apps with widgets and possibly ones that manage accounts in /data/app as a (temporary?) workaround. The downloaded APK is still encrypted for transfer. For example:

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

That's about it for now. Hopefully, more detailed information both about the app encryption OS implementation and design and its usage by Google's Play Store will be available from official sources soon. Until then, get the sample project, fire up OpenSSL and give it a try.