使能全盘加密后的Android ota升级

1. 背景知识

关于Android全盘加密功能的实现可以参考https://source.android.google.cn/security/encryption/full-disk。data分区的加密原理是基于块设备层的dm-crypt,实现是通过在mount data分区时添加forceencrypt fstab属性。

目前android版本的ota升级包都是存放在data分区中,重启后recovery会去data分区取升级包后进行升级操作。但是使能forceencrypt后,recovery系统无法正常挂在data分区,按照https://source.android.google.cn/security/encryption/full-disk的原理,data分区会被挂载成tmpfs,那么此时recovery怎么实现升级功能的呢?

2. 解密流程

文中涉及的代码引用自aosp Android7.1。

在installPackage之前processPackage会被调用到,在function的最后rs.uncrypt(filename, progressListener)会被调用到以处理/data目录内的升级包文件。因此,我们看到有一个针对升级文件的解密过程在reboot之前进行,recovery只需要处理解密后的内容即可。
frameworks/base/core/java/android/os/RecoverySystem.java

    public static void processPackage(Context context,
                                      File packageFile,
                                      final ProgressListener listener,
                                      final Handler handler)
            throws IOException {
 ...

        if (!rs.uncrypt(filename, progressListener)) {
            throw new IOException("process package failed");
        }
    }

uncrypt函数会通过binder调用uncrypt service。

    private boolean uncrypt(String packageFile, IRecoverySystemProgressListener listener) {
        try {
            return mService.uncrypt(packageFile, listener);
        } catch (RemoteException unused) {
        }
        return false;
    }

uncrypt service定义在uncrypt.rc中,通过init进程解析启动的。
/system/etc/init/uncrypt.rc

service uncrypt /system/bin/uncrypt
    class main
    socket uncrypt stream 600 system system
    disabled
    oneshot

读者有兴趣可以跟进uncrypt.cpp中看下几个调用函数,流程是
main --> uncrypt_wrapper --> uncrpt --> produce_block_map

此处直接进入解密的核心函数produce_block_map进行解析,代码实现的功能在注释中有详述。
bootable/recovery/uncrypt/uncrypt.cpp

/*
path, 输入, 如/data/update.zip
map_file, 输出,如/cache/recovery/block.map
blk_dev, 输入,如/dev/block/bootdevice/by-name/userdata
encrypted,输入,是否加密
socket,输入,通信用socket
*/
static int produce_block_map(const char* path, const char* map_file, const char* blk_dev,
                             bool encrypted, int socket) {
    std::string err;
//删除/cache/recovery/block.map, 新建/cache/recovery/block.map.tmp文件
    if (!android::base::RemoveFileIfExists(map_file, &err)) { 
        ALOGE("failed to remove the existing map file %s: %s", map_file, err.c_str());
        return kUncryptFileRemoveError;
    }
    std::string tmp_map_file = std::string(map_file) + ".tmp";
    unique_fd mapfd(open(tmp_map_file.c_str(), O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR));
    if (!mapfd) {
        ALOGE("failed to open %s: %s\n", tmp_map_file.c_str(), strerror(errno));
        return kUncryptFileOpenError;
    }
//确认对socket句柄对应的/dev/socket/uncrypt 有写的权限
    // Make sure we can write to the socket.
    if (!write_status_to_socket(0, socket)) {
        ALOGE("failed to write to socket %d\n", socket);
        return kUncryptSocketWriteError;
    }
//确认升级包存在,读取stat信息
    struct stat sb;
    if (stat(path, &sb) != 0) {
        ALOGE("failed to stat %s", path);
        return kUncryptFileStatError;
    }

    ALOGI(" block size: %ld bytes", static_cast<long>(sb.st_blksize)); //block size: 4096 bytes

    int blocks = ((sb.st_size-1) / sb.st_blksize) + 1;
    ALOGI("  file size: %" PRId64 " bytes, %d blocks", sb.st_size, blocks); //file size: 1220620 bytes, 299 blocks

    std::vector<int> ranges;

    std::string s = android::base::StringPrintf("%s\n%" PRId64 " %ld\n",
                       blk_dev, sb.st_size, static_cast<long>(sb.st_blksize));
    if (!android::base::WriteStringToFd(s, mapfd.get())) {
        ALOGE("failed to write %s: %s", tmp_map_file.c_str(), strerror(errno));
        return kUncryptWriteError;
    }
//申请 5个block size大小的buffer空间
    std::vector<std::vector<unsigned char>> buffers;
    if (encrypted) {
        buffers.resize(WINDOW_SIZE, std::vector<unsigned char>(sb.st_blksize));
    }
    int head_block = 0;
    int head = 0, tail = 0;
//打开升级包(/data/update.zip),句柄为fd, 打开device /dev/block/data,句柄为wfd。
    unique_fd fd(open(path, O_RDONLY));
    if (!fd) {
        ALOGE("failed to open %s for reading: %s", path, strerror(errno));
        return kUncryptFileOpenError;
    }

    unique_fd wfd(-1);
    if (encrypted) {
        wfd = open(blk_dev, O_WRONLY);
        if (!wfd) {
            ALOGE("failed to open fd for writing: %s", strerror(errno));
            return kUncryptBlockOpenError;
        }
    }
//循环按照block size大小,通过偏移指定的block,获取每个block数据在device的实际block索引,保存升级包的实际存储block的稀疏列表。 如果data分区是加密的,那么每次获取每个block实际索引时,读取解密后的block数据到buffer, 每当有5个block数据时,然后把buffer数据写到实际的对应的索引block里。这样实际索引的block里存储的就是解密后的数据。
//解密即是绕过文件系统层直接操作块设备层,文件系统层对应的是逻辑block,块设备层对应的是物理block
    off64_t pos = 0;
    int last_progress = 0;
    while (pos < sb.st_size) {
        // Update the status file, progress must be between [0, 99].
        int progress = static_cast<int>(100 * (double(pos) / double(sb.st_size)));
        if (progress > last_progress) {
            last_progress = progress;
            write_status_to_socket(progress, socket); //更新status
        }

        if ((tail+1) % WINDOW_SIZE == head) {
            // write out head buffer
            int block = head_block;
            if (ioctl(fd.get(), FIBMAP, &block) != 0) { //获取/data/update.zip所在block,通过逻辑block获取物理block,block既是输入又是输出
                ALOGE("failed to find block %d", head_block);
                return kUncryptIoctlError;
            }
            add_block_to_ranges(ranges, block);
            if (encrypted) {
                if (write_at_offset(buffers[head].data(), sb.st_blksize, wfd.get(),  //buffer数据写入block,每WINDOW_SIZE个block写
                        static_cast<off64_t>(sb.st_blksize) * block) != 0) {
                    return kUncryptWriteError;
                }
            }
            head = (head + 1) % WINDOW_SIZE;
            ++head_block;
        }

        // read next block to tail
        if (encrypted) {
            size_t to_read = static_cast<size_t>(
                    std::min(static_cast<off64_t>(sb.st_blksize), sb.st_size - pos));
            if (!android::base::ReadFully(fd.get(), buffers[tail].data(), to_read)) { //读/data/update.zip到buffer,每次读block大小
                ALOGE("failed to read: %s", strerror(errno));
                return kUncryptReadError;
            }
            pos += to_read;
        } else {
            // If we're not encrypting; we don't need to actually read
            // anything, just skip pos forward as if we'd read a
            // block.
            pos += sb.st_blksize;
        }
        tail = (tail+1) % WINDOW_SIZE;
    }
//最后把不足5个block数据的buffer写到对应的实际的block中去,这样稀疏列表包含的block中保存的就是解密后的升级包数据
    while (head != tail) {
        // write out head buffer
        int block = head_block;
        if (ioctl(fd.get(), FIBMAP, &block) != 0) {
            ALOGE("failed to find block %d", head_block);
            return kUncryptIoctlError;
        }
        add_block_to_ranges(ranges, block);
        if (encrypted) {
            if (write_at_offset(buffers[head].data(), sb.st_blksize, wfd.get(),
                    static_cast<off64_t>(sb.st_blksize) * block) != 0) {
                return kUncryptWriteError;
            }
        }
        head = (head + 1) % WINDOW_SIZE;
        ++head_block;
    }
//把稀疏列表写到/cache/recovery/block.map.tmp
    if (!android::base::WriteStringToFd(
            android::base::StringPrintf("%zu\n", ranges.size() / 2), mapfd.get())) {
        ALOGE("failed to write %s: %s", tmp_map_file.c_str(), strerror(errno));
        return kUncryptWriteError;
    }
    for (size_t i = 0; i < ranges.size(); i += 2) {
        if (!android::base::WriteStringToFd(
                android::base::StringPrintf("%d %d\n", ranges[i], ranges[i+1]), mapfd.get())) {
            ALOGE("failed to write %s: %s", tmp_map_file.c_str(), strerror(errno));
            return kUncryptWriteError;
        }
    }

    if (fsync(mapfd.get()) == -1) {
        ALOGE("failed to fsync \"%s\": %s", tmp_map_file.c_str(), strerror(errno));
        return kUncryptFileSyncError;
    }
    if (close(mapfd.get()) == -1) {
        ALOGE("failed to close %s: %s", tmp_map_file.c_str(), strerror(errno));
        return kUncryptFileCloseError;
    }
    mapfd = -1;
//关闭相关所有的句柄,/cache/recovery/block.map.tmp重名为/cache/recovery/block.map,fsync数据同步到磁盘。
    if (encrypted) {
        if (fsync(wfd.get()) == -1) {
            ALOGE("failed to fsync \"%s\": %s", blk_dev, strerror(errno));
            return kUncryptFileSyncError;
        }
        if (close(wfd.get()) == -1) {
            ALOGE("failed to close %s: %s", blk_dev, strerror(errno));
            return kUncryptFileCloseError;
        }
        wfd = -1;
    }

    if (rename(tmp_map_file.c_str(), map_file) == -1) {
        ALOGE("failed to rename %s to %s: %s", tmp_map_file.c_str(), map_file, strerror(errno));
        return kUncryptFileRenameError;
    }
    // Sync dir to make rename() result written to disk.
    std::string file_name = map_file;
    std::string dir_name = dirname(&file_name[0]);
    unique_fd dfd(open(dir_name.c_str(), O_RDONLY | O_DIRECTORY));
    if (!dfd) {
        ALOGE("failed to open dir %s: %s", dir_name.c_str(), strerror(errno));
        return kUncryptFileOpenError;
    }
    if (fsync(dfd.get()) == -1) {
        ALOGE("failed to fsync %s: %s", dir_name.c_str(), strerror(errno));
        return kUncryptFileSyncError;
    }
    if (close(dfd.get()) == -1) {
        ALOGE("failed to close %s: %s", dir_name.c_str(), strerror(errno));
        return kUncryptFileCloseError;
    }
    dfd = -1;
    return 0;
}

代码中的(ioctl(fd.get(), FIBMAP, &block)是一个根据逻辑block找到物理block的过程,下面是从https://github.com/prashants/c/blob/master/fibmap/fibmap.c找的一个关于此函数的demo,也有助于理解此函数的功能。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/fs.h>
#include <assert.h>

int main(int argc, char **argv)
{
	int fd, i, block, blocksize, blkcnt;
	struct stat st;

	assert(argv[1] != NULL);

	fd = open(argv[1], O_RDONLY);
	if (fd <= 0) {
		perror("error opening file");
		goto end;
	}

	if (ioctl(fd, FIGETBSZ, &blocksize)) {
		perror("FIBMAP ioctl failed");
		goto end;
	}

	if (fstat(fd, &st)) {
		perror("fstat error");
		goto end;
	}

	blkcnt = (st.st_size + blocksize - 1) / blocksize;
	printf("File %s size %d blocks %d blocksize %d\n",
			argv[1], (int)st.st_size, blkcnt, blocksize);

	for (i = 0; i < blkcnt; i++) {
		block = i;
		if (ioctl(fd, FIBMAP, &block)) {
			perror("FIBMAP ioctl failed");
		}
		printf("%3d %10d\n", i, block);
	}

end:
	close(fd);
	return 0;
}

通过uncrypt处理后,data分区升级包占用的block已经进行了解密。
installPackage再进行最后的处理,将升级包名字,如/data/update.zip,写到/cache/recovery/uncrypt_file,–[email protected]/cache/recovery/block.map写到/cache/recovery/command。之后reboot重启即可。

    public static void installPackage(Context context, File packageFile, boolean processed)
            throws IOException {
        synchronized (sRequestLock) {
            LOG_FILE.delete();
            // Must delete the file in case it was created by system server.
            UNCRYPT_PACKAGE_FILE.delete();

            String filename = packageFile.getCanonicalPath();
            Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");

            // If the package name ends with "_s.zip", it's a security update.
            boolean securityUpdate = filename.endsWith("_s.zip");

            // If the package is on the /data partition, the package needs to
            // be processed (i.e. uncrypt'd). The caller specifies if that has
            // been done in 'processed' parameter.
            if (filename.startsWith("/data/")) {
                if (processed) {
                    if (!BLOCK_MAP_FILE.exists()) {
                        Log.e(TAG, "Package claimed to have been processed but failed to find "
                                + "the block map file.");
                        throw new IOException("Failed to find block map file");
                    }
                } else {
                    FileWriter uncryptFile = new FileWriter(UNCRYPT_PACKAGE_FILE);
                    try {
                        uncryptFile.write(filename + "\n");
                    } finally {
                        uncryptFile.close();
                    }
                    // UNCRYPT_PACKAGE_FILE needs to be readable and writable
                    // by system server.
                    if (!UNCRYPT_PACKAGE_FILE.setReadable(true, false)
                            || !UNCRYPT_PACKAGE_FILE.setWritable(true, false)) {
                        Log.e(TAG, "Error setting permission for " + UNCRYPT_PACKAGE_FILE);
                    }

                    BLOCK_MAP_FILE.delete();
                }

                // If the package is on the /data partition, use the block map
                // file as the package name instead.
                filename = "@/cache/recovery/block.map";
            }

            final String filenameArg = "--update_package=" + filename + "\n";
            final String localeArg = "--locale=" + Locale.getDefault().toString() + "\n";
            final String securityArg = "--security\n";

            String command = filenameArg + localeArg;
            if (securityUpdate) {
                command += securityArg;
            }

            RecoverySystem rs = (RecoverySystem) context.getSystemService(
                    Context.RECOVERY_SERVICE);
            if (!rs.setupBcb(command)) {
                throw new IOException("Setup BCB failed");
            }

            // Having set up the BCB (bootloader control block), go ahead and reboot
            PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
            pm.reboot(PowerManager.REBOOT_RECOVERY_UPDATE);

            throw new IOException("Reboot failed (no permissions?)");
        }
    }

3. 原理

关于produce_block_map为什么能够进行解密的原理需要结合文件系统与设备驱动关系的图进行下解释。
使能全盘加密后的Android ota升级
在文章开始提到过全盘加密是基于在块设备层运行的dm-crypt实现的,并且需要文件系统的支持,YAFFS 就是因为直接与原始 NAND 闪存芯片交互,所以无法进行全盘加密。解密过程是通过文件系统层的逻辑block找到块设备层的物理block,读出文件系统层的存储内容写入块设备层,也就是相当于绕过了整个加密的层,所以才能实现真正的解密。