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:
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
Let's check if Android likes our newly encrypted app by trying to install it:
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
--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
Success
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
The
The
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 (
Note that the
Our app is mostly ready now, but installing apps requires the
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 andorid.content.pm
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
android.content.pm
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.
Here the
All commands take a namespace ID (based on the package name in practice) as a parameter, and for the
Forward locking an application is triggered by specifying the
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'sdevice-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 res.zip
Here the
res.zip
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 pacakge.name-1.asec
. 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
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
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/package.name
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.
Post a Comment