Decrypting Android M adopted storage

One of the new features Android M introduces is adoptable storage. This feature allows external storage devices such as SD cards or USB drives to be ‘adopted’ and used in the same manner as internal storage. What this means in practice is that both apps and their private data can be moved to the adopted storage device. In other words, this is another take on everyone’s (except for widget authors…) favorite 2010 feature — AppsOnSD. There are, of course, a few differences, the major one being that while AppsOnSD (just like app Android 4.1 app encryption) creates per-app encrypted containers, adoptable storage encrypts the whole device. This short post will look at how adoptable storage encryption is implemented, and show how to decrypt and use adopted drives on any Linux machine.

Adopting an USB drive

In order to enable adoptable storage for devices connected via USB you need to execute the following command in the Android shell (presumably, this is not needed if your device has an internal SD card slot; however there are no such devices that run Android M at present):

$ adb shell sm set-force-adoptable true

Now, if you connect a USB drive to the device’s micro USB slot (you can also use an USB OTG cable), Android will give you an option to set it up as ‘internal’ storage, which requires reformatting and encryption. ‘Portable’ storage is formatted using VFAT, as before.

After the drive is formatted, it shows up under Device storage in the Storage screen of system Settings. You can now migrate media and application data to the newly added drive, but it appears that there is no option in the system UI that allows you to move applications (APKs).

Adopted devices are mounted via Linux’s device-mapper under /mnt/expand/ as can be seen below, and can be directly accessed only by system apps.

$ mount
rootfs / rootfs ro,seclabel,relatime 0 0
...
/dev/block/dm-1 /mnt/expand/a16653c3-... ext4 rw,dirsync,seclabel,nosuid,nodev,noatime 0 0
/dev/block/dm-2 /mnt/expand/0fd7f1a0-... ext4 rw,dirsync,seclabel,nosuid,nodev,noatime 0 0

You can safely eject an adopted drive by tapping on it in the Storage screen, and the choosing Eject from the overflow menu. Android will show a persistent notification that prompts you to reinsert the device once it’s removed. Alternatively, you also can ‘forget’ the drive, which removes it from the system, and should presumably delete the associated encryption key (which doesn’t seem to be the case in the current preview build).

Inspecting the drive

Once you’ve ejected the drive, you can connect it to any Linux box in order to inspect it. Somewhat surprisingly, the drive will be automatically mounted on most modern Linux distributions, which suggests that there is at least one readable partition. If you look at the partition table with fdisk or a similar tool, you may see something like this:

# fdisk /dev/sdb
Disk /dev/sdb: 7811 MB, 7811891200 bytes, 15257600 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: gpt


# Start End Size Type Name
1 2048 34815 16M unknown android_meta
2 34816 15257566 7.3G unknown android_expand

As you can see, there is a tiny android_meta partition, but the bulk of the device has been assigned to the android_expand partition. Unfortunately, the full source code of Android M is not available, so we cannot be sure how exactly this partition table is created, or what the contents of each partition is. However, we know that most of Android’s storage management functionality is implemented in the vold system daemon, so we check if there is any mention of android_expand inside vold with the following command:

$ strings vold|grep -i expand
--change-name=0:android_expand
%s/expand_%s.key
/mnt/expand/%s

Here expand_%s.key suspiciously looks like a key filename template, and we already know that adopted drives are encrypted, so our next step is to look for any similar files in the device’s /data partition (you’ll need a custom recovery or root access for this). Unsurprisingly, there is a matching file in /data/misc/vold which looks like this:

# ls /data/misc/vold
bench
expand_8838e738a18746b6e435bb0d04c15ccd.key

# ls -l expand_8838e738a18746b6e435bb0d04c15ccd.key

-rw------- 1 root root 16 expand_8838e738a18746b6e435bb0d04c15ccd.key


# od -t x1 expand_8838e738a18746b6e435bb0d04c15ccd.key
0000000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
0000020

Decrypting the drive

That’s exactly 16 bytes, enough for a 128-bit key. As we know, Android’s FDE implementation uses an AES 128-bit key, so it’s a good bet that adoptable storage uses a similar (or identical) implementation. Looking at the start and end of our android_expand partition doesn’t reveal any readable info, nor is it similar to Android’s crypto footer, or LUKS‘s header. Therefore, we need to guess the encryption mode and/or any related parameters. Looking once again at Android’s FDE implementation (which is based on the dm-crypt target of Linux’s device-mapper), we see that the encryption mode used is aes-cbc-essiv:sha256. After consulting dm-crypt’s mapping table reference, we see that the remaining parameters we need are the IV offset and the starting offset of encrypted data. Since the IV offset is usually zero, and most probably the entire android_expand partition (offset 0) is encrypted, the command we need to map the encrypted partition becomes the following:

# dmsetup create crypt1 --table "0 `blockdev --getsize /dev/sdb2` crypt 
aes-cbc-essiv:sha256 00010203040506070809010a0b0c0d0e0f 0 /dev/sdb2 0"

It completes with error, so we can now try to mount the mapped device, again guessing that the file system is most probably ext4 (or you can inspect the mapped device and find the superblock first, if you want to be extra diligent).

# mount -t ext4 /dev/mapper/crypt1 /mnt/1/
# cd /mnt/1
# find ./ -type d
./
./lost+found
./app
./user
./media
./local
./local/tmp
./misc
./misc/vold
./misc/vold/bench

This reveals a very familiar Android /data layout, and you should see any media and app data you’ve moved to the adopted device. If you copy any files to the mounted device, they should be visible when you mount the drive again in Android.

Storage manager commands

Back in Android, you can use the sm command (probably short for ‘storage manager’) we showed in the first section to list disks and volumes as shown below:

$ sm list-disks
disk:8,16
disk:8,0

$ sm list-volumes
emulated:8,2 unmounted null
private mounted null
private:8,18 mounted 0fd7f1a0-2d27-48f9-8702-a484cb894a92
emulated:8,18 mounted null
emulated unmounted null
private:8,2 mounted a16653c3-6f5e-455c-bb03-70c8a74b109e

If you have root access, you can also partition, format, mount, unmount and forget disks/volumes. The full list of supported commands is shown in the following listing.

$ sm
usage: sm list-disks
sm list-volumes [public|private|emulated|all]
sm has-adoptable
sm get-primary-storage-uuid
sm set-force-adoptable [true|false]

sm partition DISK [public|private|mixed] [ratio]
sm mount VOLUME
sm unmount VOLUME
sm format VOLUME

sm forget [UUID|all]

Most features are also available from the system UI, but sm allows you to customize the ratio of the android_meta and android_expand partitions, as well as to create ‘mixed’ volumes.

Summary

Android M allows for adoptable storage, which is implemented similarly to internal storage FDE — using dm-crypt with a per-volume, static 128-bit AES key, stored in /data/misc/vold/. Once the key is extracted from the device, adopted storage can be mounted and read/written on any Linux machine. Adoptable storage encryption is done purely in software (at least in the current preview build), so its performance is likely comparable to encrypted internal storage on devices that don’t support hardware-accelerated FDE.