Expose encryptedstore feature to system apps
This patch adds 2 api functions: set/get EncryptedStorageKib &
isEncryptedStorageEnabled() which can be used to specify the size of
storage required. VirtualMachine, on being initialized, will create the
backing (empty) file if storageSize > 0.
The (storage) allocation happens when VirtualMachine.create() is called:
by using VS' initializeWritablePartition() called over binder.
Test that EncryptedStore is available & accessible -
encryptedStorageAvailable() is a device test which uses these system
apis to enable encrypted storage in VM. It then verifies that the native
api(getEncryptedStoragePath()) inside VM indeed returns the right mount
point, hence verifying both the ends of the encryptedstore infra :)
Test: atest MicrodroidTests#encryptedStorageAvailable
Bug: 254454175
Bug: 260084116
Change-Id: I4842ebac6af795beaf250525252087545895b231
diff --git a/javalib/api/system-current.txt b/javalib/api/system-current.txt
index f38d8fd..a907f7d 100644
--- a/javalib/api/system-current.txt
+++ b/javalib/api/system-current.txt
@@ -58,10 +58,12 @@
public final class VirtualMachineConfig {
method @NonNull public String getApkPath();
method @NonNull public int getDebugLevel();
+ method @IntRange(from=0) public long getEncryptedStorageKib();
method @IntRange(from=0) public int getMemoryMib();
method @IntRange(from=1) public int getNumCpus();
method @Nullable public String getPayloadBinaryPath();
method public boolean isCompatibleWith(@NonNull android.system.virtualmachine.VirtualMachineConfig);
+ method public boolean isEncryptedStorageEnabled();
method public boolean isProtectedVm();
field public static final int DEBUG_LEVEL_APP_ONLY = 1; // 0x1
field public static final int DEBUG_LEVEL_FULL = 2; // 0x2
@@ -73,6 +75,7 @@
method @NonNull public android.system.virtualmachine.VirtualMachineConfig build();
method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setApkPath(@NonNull String);
method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setDebugLevel(int);
+ method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setEncryptedStorageKib(@IntRange(from=0) long);
method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setMemoryMib(@IntRange(from=0) int);
method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setNumCpus(@IntRange(from=1) int);
method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadBinaryPath(@NonNull String);
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 63b5628..1330afb 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -168,6 +168,9 @@
/** Size of the instance image. 10 MB. */
private static final long INSTANCE_FILE_SIZE = 10 * 1024 * 1024;
+ /** Name of the file backing the encrypted storage */
+ private static final String ENCRYPTED_STORE_FILE = "storage.img";
+
/** The package which owns this VM. */
@NonNull private final String mPackageName;
@@ -190,6 +193,9 @@
/** Path to the idsig file for this VM. */
@NonNull private final File mIdsigFilePath;
+ /** File that backs the encrypted storage - Will be null if not enabled. */
+ @Nullable private final File mEncryptedStoreFilePath;
+
/**
* Unmodifiable list of extra apks. Apks are specified by the vm config, and corresponding
* idsigs are to be generated.
@@ -323,6 +329,10 @@
mExtraApks = setupExtraApks(context, config, thisVmDir);
mMemoryManagementCallbacks = new MemoryManagementCallbacks();
mContext = context;
+ mEncryptedStoreFilePath =
+ (config.isEncryptedStorageEnabled())
+ ? new File(thisVmDir, ENCRYPTED_STORE_FILE)
+ : null;
}
/**
@@ -385,6 +395,14 @@
} catch (IOException e) {
throw new VirtualMachineException("failed to create instance image", e);
}
+ if (config.isEncryptedStorageEnabled()) {
+ try {
+ vm.mEncryptedStoreFilePath.createNewFile();
+ } catch (IOException e) {
+ throw new VirtualMachineException(
+ "failed to create encrypted storage image", e);
+ }
+ }
IVirtualizationService service =
IVirtualizationService.Stub.asInterface(
@@ -402,6 +420,22 @@
} catch (ServiceSpecificException | IllegalArgumentException e) {
throw new VirtualMachineException("failed to create instance partition", e);
}
+
+ if (config.isEncryptedStorageEnabled()) {
+ try {
+ service.initializeWritablePartition(
+ ParcelFileDescriptor.open(vm.mEncryptedStoreFilePath, MODE_READ_WRITE),
+ config.getEncryptedStorageKib() * 1024L,
+ PartitionType.ENCRYPTEDSTORE);
+ } catch (FileNotFoundException e) {
+ throw new VirtualMachineException("encrypted storage image missing", e);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ } catch (ServiceSpecificException | IllegalArgumentException e) {
+ throw new VirtualMachineException(
+ "failed to create encrypted storage partition", e);
+ }
+ }
return vm;
} catch (VirtualMachineException | RuntimeException e) {
// If anything goes wrong, delete any files created so far and the VM's directory
@@ -431,7 +465,9 @@
if (!vm.mInstanceFilePath.exists()) {
throw new VirtualMachineException("instance image missing");
}
-
+ if (config.isEncryptedStorageEnabled() && !vm.mEncryptedStoreFilePath.exists()) {
+ throw new VirtualMachineException("Storage image missing");
+ }
return vm;
}
@@ -680,8 +716,12 @@
// Re-open idsig file in read-only mode
appConfig.idsig = ParcelFileDescriptor.open(mIdsigFilePath, MODE_READ_ONLY);
- appConfig.instanceImage = ParcelFileDescriptor.open(mInstanceFilePath,
- MODE_READ_WRITE);
+ appConfig.instanceImage =
+ ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_WRITE);
+ if (mEncryptedStoreFilePath != null) {
+ appConfig.encryptedStorageImage =
+ ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_WRITE);
+ }
List<ParcelFileDescriptor> extraIdsigs = new ArrayList<>();
for (ExtraApkSpec extraApk : mExtraApks) {
extraIdsigs.add(ParcelFileDescriptor.open(extraApk.idsig, MODE_READ_ONLY));
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index f9f29a1..dee8fbb 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -65,6 +65,7 @@
private static final String KEY_PROTECTED_VM = "protectedVm";
private static final String KEY_MEMORY_MIB = "memoryMib";
private static final String KEY_NUM_CPUS = "numCpus";
+ private static final String KEY_ENCRYPTED_STORAGE_KIB = "encryptedStorageKib";
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@@ -129,6 +130,9 @@
*/
@Nullable private final String mPayloadBinaryPath;
+ /** The size of storage in KB. 0 indicates that encryptedStorage is not required */
+ private final long mEncryptedStorageKib;
+
private VirtualMachineConfig(
@NonNull String apkPath,
@Nullable String payloadConfigPath,
@@ -136,7 +140,8 @@
@DebugLevel int debugLevel,
boolean protectedVm,
int memoryMib,
- int numCpus) {
+ int numCpus,
+ long encryptedStorageKib) {
requireNonNull(apkPath);
if (!apkPath.startsWith("/")) {
throw new IllegalArgumentException("APK path must be an absolute path");
@@ -177,6 +182,9 @@
throw new UnsupportedOperationException(
"Unprotected VMs are not supported on this device.");
}
+ if (encryptedStorageKib < 0) {
+ throw new IllegalArgumentException("Encrypted Storage size cannot be negative");
+ }
mApkPath = apkPath;
mPayloadConfigPath = payloadConfigPath;
@@ -185,6 +193,7 @@
mProtectedVm = protectedVm;
mMemoryMib = memoryMib;
mNumCpus = numCpus;
+ mEncryptedStorageKib = encryptedStorageKib;
}
/** Loads a config from a file. */
@@ -237,9 +246,17 @@
boolean protectedVm = b.getBoolean(KEY_PROTECTED_VM);
int memoryMib = b.getInt(KEY_MEMORY_MIB);
int numCpus = b.getInt(KEY_NUM_CPUS);
+ long encryptedStorageKib = b.getLong(KEY_ENCRYPTED_STORAGE_KIB);
- return new VirtualMachineConfig(apkPath, payloadConfigPath, payloadBinaryPath, debugLevel,
- protectedVm, memoryMib, numCpus);
+ return new VirtualMachineConfig(
+ apkPath,
+ payloadConfigPath,
+ payloadBinaryPath,
+ debugLevel,
+ protectedVm,
+ memoryMib,
+ numCpus,
+ encryptedStorageKib);
}
/** Persists this config to a file. */
@@ -264,6 +281,9 @@
if (mMemoryMib > 0) {
b.putInt(KEY_MEMORY_MIB, mMemoryMib);
}
+ if (mEncryptedStorageKib > 0) {
+ b.putLong(KEY_ENCRYPTED_STORAGE_KIB, mEncryptedStorageKib);
+ }
b.writeToStream(output);
}
@@ -348,6 +368,28 @@
}
/**
+ * Returns whether encrypted storage is enabled or not.
+ *
+ * @hide
+ */
+ @SystemApi
+ public boolean isEncryptedStorageEnabled() {
+ return mEncryptedStorageKib > 0;
+ }
+
+ /**
+ * Returns the size of encrypted storage (in KB) available in the VM, or 0 if encrypted storage
+ * is not enabled
+ *
+ * @hide
+ */
+ @SystemApi
+ @IntRange(from = 0)
+ public long getEncryptedStorageKib() {
+ return mEncryptedStorageKib;
+ }
+
+ /**
* Tests if this config is compatible with other config. Being compatible means that the configs
* can be interchangeably used for the same virtual machine. Compatible changes includes the
* number of CPUs and the size of the RAM. All other changes (e.g. using a different payload,
@@ -357,6 +399,7 @@
*/
@SystemApi
public boolean isCompatibleWith(@NonNull VirtualMachineConfig other) {
+ // TODO(b/254454175): mEncryptedStorageKib being equal is also required for compatibility.
return this.mDebugLevel == other.mDebugLevel
&& this.mProtectedVm == other.mProtectedVm
&& Objects.equals(this.mPayloadConfigPath, other.mPayloadConfigPath)
@@ -419,6 +462,7 @@
private boolean mProtectedVmSet;
private int mMemoryMib;
private int mNumCpus;
+ private long mEncryptedStorageKib;
/**
* Creates a builder for the given context.
@@ -430,6 +474,7 @@
mContext = requireNonNull(context, "context must not be null");
mDebugLevel = DEBUG_LEVEL_NONE;
mNumCpus = 1;
+ mEncryptedStorageKib = 0;
}
/**
@@ -447,8 +492,14 @@
}
return new VirtualMachineConfig(
- apkPath, mPayloadConfigPath, mPayloadBinaryPath, mDebugLevel, mProtectedVm,
- mMemoryMib, mNumCpus);
+ apkPath,
+ mPayloadConfigPath,
+ mPayloadBinaryPath,
+ mDebugLevel,
+ mProtectedVm,
+ mMemoryMib,
+ mNumCpus,
+ mEncryptedStorageKib);
}
/**
@@ -544,5 +595,27 @@
mNumCpus = num;
return this;
}
+
+ /**
+ * Sets the size (in KB) of encrypted storage available to the VM.
+ *
+ * <p>The storage is encrypted with a key deterministically derived from the VM identity
+ *
+ * <p>The encrypted storage is persistent across VM reboots as well as device reboots. The
+ * backing file (containing encrypted data) is stored in the app's private data directory.
+ *
+ * <p>Note - There is no integrity guarantee or rollback protection on the storage in case
+ * the encrypted data is modified.
+ *
+ * <p>Deleting the VM will delete the encrypted data - there is no way to recover that data.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public Builder setEncryptedStorageKib(@IntRange(from = 0) long encryptedStorageKib) {
+ mEncryptedStorageKib = encryptedStorageKib;
+ return this;
+ }
}
}
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 35b9e61..0bf65b3 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -875,6 +875,24 @@
assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
}
+ @Test
+ @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+ public void encryptedStorageAvailable() throws Exception {
+ assumeSupportedKernel();
+
+ VirtualMachineConfig config =
+ newVmConfigBuilder()
+ .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+ .setMemoryMib(minMemoryRequired())
+ .setEncryptedStorageKib(4096)
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .build();
+ VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
+
+ TestResults testResults = runVmTestService(vm);
+ assertThat(testResults.mEncryptedStoragePath).isEqualTo("/mnt/encryptedstore");
+ }
+
private void assertFileContentsAreEqualInTwoVms(String fileName, String vmName1, String vmName2)
throws IOException {
File file1 = getVmFile(vmName1, fileName);
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 7d24a32..3e4323d 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -881,8 +881,9 @@
// Return whether a partition is exempt from selinux label checks, because we know that it does
// not contain code and is likely to be generated in an app-writable directory.
fn is_safe_app_partition(label: &str) -> bool {
- // See make_payload_disk in payload.rs.
+ // See add_microdroid_system_images & add_microdroid_payload_images in payload.rs.
label == "vm-instance"
+ || label == "encryptedstore"
|| label == "microdroid-apk-idsig"
|| label == "payload-metadata"
|| label.starts_with("extra-idsig-")
@@ -898,7 +899,7 @@
match ctx.selinux_type()? {
| "system_file" // immutable dm-verity protected partition
| "apk_data_file" // APKs of an installed app
- | "staging_data_file" // updated/staged APEX imagess
+ | "staging_data_file" // updated/staged APEX images
| "shell_data_file" // test files created via adb shell
=> Ok(()),
_ => bail!("Label {} is not allowed", ctx),