Route EncryptedLocalTransport KV backup/restore through encryption code
Bug: 142227548
Test: Verify the device boots successfully
Verify EncryptedLocalTransport APK is present
Verify manual backup/restore using bmgr for LocalTransport and EncryptedLocalTransport
For LocalTransport (unencrypted) and EncryptedLocalTransport:
atest CtsBackupTestCases
atest CtsBackupHostTestCases
atest GtsBackupTestCases
atest GtsBackupHostTestCases
Change-Id: Iac3a8a50d7f761442a4b784cfba3a980e900dd7f
diff --git a/packages/BackupEncryption/Android.bp b/packages/BackupEncryption/Android.bp
index 342d796..68e937c 100644
--- a/packages/BackupEncryption/Android.bp
+++ b/packages/BackupEncryption/Android.bp
@@ -17,8 +17,7 @@
android_app {
name: "BackupEncryption",
srcs: ["src/**/*.java"],
- libs: ["backup-encryption-protos"],
- static_libs: ["backuplib"],
+ static_libs: ["backup-encryption-protos", "backuplib"],
optimize: { enabled: false },
platform_apis: true,
certificate: "platform",
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/EncryptionKeyHelper.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/EncryptionKeyHelper.java
new file mode 100644
index 0000000..2035b66
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/EncryptionKeyHelper.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.encryption;
+
+import android.content.Context;
+import android.security.keystore.recovery.InternalRecoveryServiceException;
+import android.security.keystore.recovery.RecoveryController;
+
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
+import com.android.server.backup.encryption.keys.TertiaryKeyManager;
+import com.android.server.backup.encryption.keys.TertiaryKeyRotationScheduler;
+
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+
+class EncryptionKeyHelper {
+ private static SecureRandom sSecureRandom = new SecureRandom();
+
+ private final Context mContext;
+ private final RecoverableKeyStoreSecondaryKeyManager
+ .RecoverableKeyStoreSecondaryKeyManagerProvider
+ mSecondaryKeyManagerProvider;
+
+ EncryptionKeyHelper(Context context) {
+ mContext = context;
+ mSecondaryKeyManagerProvider =
+ () ->
+ new RecoverableKeyStoreSecondaryKeyManager(
+ RecoveryController.getInstance(mContext), sSecureRandom);
+ }
+
+ RecoverableKeyStoreSecondaryKeyManager
+ .RecoverableKeyStoreSecondaryKeyManagerProvider getKeyManagerProvider() {
+ return mSecondaryKeyManagerProvider;
+ }
+
+ RecoverableKeyStoreSecondaryKey getActiveSecondaryKey()
+ throws UnrecoverableKeyException, InternalRecoveryServiceException {
+ String keyAlias = CryptoSettings.getInstance(mContext).getActiveSecondaryKeyAlias().get();
+ return mSecondaryKeyManagerProvider.get().get(keyAlias).get();
+ }
+
+ SecretKey getTertiaryKey(
+ String packageName,
+ RecoverableKeyStoreSecondaryKey secondaryKey)
+ throws IllegalBlockSizeException, InvalidAlgorithmParameterException,
+ NoSuchAlgorithmException, IOException, NoSuchPaddingException,
+ InvalidKeyException {
+ TertiaryKeyManager tertiaryKeyManager =
+ new TertiaryKeyManager(
+ mContext,
+ sSecureRandom,
+ TertiaryKeyRotationScheduler.getInstance(mContext),
+ secondaryKey,
+ packageName);
+ return tertiaryKeyManager.getKey();
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/KeyValueEncrypter.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/KeyValueEncrypter.java
new file mode 100644
index 0000000..1d841b4
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/KeyValueEncrypter.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.encryption;
+
+import static com.android.server.backup.encryption.BackupEncryptionService.TAG;
+
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import com.android.server.backup.encryption.client.CryptoBackupServer;
+import com.android.server.backup.encryption.keys.KeyWrapUtils;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+import com.android.server.backup.encryption.tasks.EncryptedKvBackupTask;
+import com.android.server.backup.encryption.tasks.EncryptedKvRestoreTask;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.SecureRandom;
+import java.util.Map;
+
+public class KeyValueEncrypter {
+ private final Context mContext;
+ private final EncryptionKeyHelper mKeyHelper;
+
+ public KeyValueEncrypter(Context context) {
+ mContext = context;
+ mKeyHelper = new EncryptionKeyHelper(mContext);
+ }
+
+ public void encryptKeyValueData(
+ String packageName, ParcelFileDescriptor inputFd, OutputStream outputStream)
+ throws Exception {
+ EncryptedKvBackupTask.EncryptedKvBackupTaskFactory backupTaskFactory =
+ new EncryptedKvBackupTask.EncryptedKvBackupTaskFactory();
+ EncryptedKvBackupTask backupTask =
+ backupTaskFactory.newInstance(
+ mContext,
+ new SecureRandom(),
+ new FileBackupServer(outputStream),
+ CryptoSettings.getInstance(mContext),
+ mKeyHelper.getKeyManagerProvider(),
+ inputFd,
+ packageName);
+ backupTask.performBackup(/* incremental */ false);
+ }
+
+ public void decryptKeyValueData(String packageName,
+ InputStream encryptedInputStream, ParcelFileDescriptor outputFd) throws Exception {
+ RecoverableKeyStoreSecondaryKey secondaryKey = mKeyHelper.getActiveSecondaryKey();
+
+ EncryptedKvRestoreTask.EncryptedKvRestoreTaskFactory restoreTaskFactory =
+ new EncryptedKvRestoreTask.EncryptedKvRestoreTaskFactory();
+ EncryptedKvRestoreTask restoreTask =
+ restoreTaskFactory.newInstance(
+ mContext,
+ mKeyHelper.getKeyManagerProvider(),
+ new InputStreamFullRestoreDownloader(encryptedInputStream),
+ secondaryKey.getAlias(),
+ KeyWrapUtils.wrap(
+ secondaryKey.getSecretKey(),
+ mKeyHelper.getTertiaryKey(packageName, secondaryKey)));
+
+ restoreTask.getRestoreData(outputFd);
+ }
+
+ // TODO(b/142455725): Extract into a commong class.
+ private static class FileBackupServer implements CryptoBackupServer {
+ private static final String EMPTY_DOC_ID = "";
+
+ private final OutputStream mOutputStream;
+
+ FileBackupServer(OutputStream outputStream) {
+ mOutputStream = outputStream;
+ }
+
+ @Override
+ public String uploadIncrementalBackup(
+ String packageName,
+ String oldDocId,
+ byte[] diffScript,
+ WrappedKeyProto.WrappedKey tertiaryKey) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String uploadNonIncrementalBackup(
+ String packageName, byte[] data, WrappedKeyProto.WrappedKey tertiaryKey) {
+ try {
+ mOutputStream.write(data);
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to write encrypted data to file: ", e);
+ }
+
+ return EMPTY_DOC_ID;
+ }
+
+ @Override
+ public void setActiveSecondaryKeyAlias(
+ String keyAlias, Map<String, WrappedKeyProto.WrappedKey> tertiaryKeys) {
+ // Do nothing.
+ }
+ }
+
+ // TODO(b/142455725): Extract into a commong class.
+ private static class InputStreamFullRestoreDownloader extends FullRestoreDownloader {
+ private final InputStream mInputStream;
+
+ InputStreamFullRestoreDownloader(InputStream inputStream) {
+ mInputStream = inputStream;
+ }
+
+ @Override
+ public int readNextChunk(byte[] buffer) throws IOException {
+ return mInputStream.read(buffer);
+ }
+
+ @Override
+ public void finish(FinishType finishType) {
+ try {
+ mInputStream.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Error while reading restore data");
+ }
+ }
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/transport/IntermediateEncryptingTransport.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/transport/IntermediateEncryptingTransport.java
index 1d0224d..c3cb335 100644
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/transport/IntermediateEncryptingTransport.java
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/transport/IntermediateEncryptingTransport.java
@@ -18,27 +18,58 @@
import static com.android.server.backup.encryption.BackupEncryptionService.TAG;
+import android.app.backup.BackupTransport;
+import android.app.backup.RestoreDescription;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.encryption.KeyValueEncrypter;
import com.android.server.backup.transport.DelegatingTransport;
import com.android.server.backup.transport.TransportClient;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.atomic.AtomicReference;
+
/**
* This is an implementation of {@link IBackupTransport} that encrypts (or decrypts) the data when
* sending it (or receiving it) from the {@link IBackupTransport} returned by {@link
* TransportClient.connect(String)}.
*/
public class IntermediateEncryptingTransport extends DelegatingTransport {
+ private static final String BACKUP_TEMP_DIR = "backup";
+ private static final String RESTORE_TEMP_DIR = "restore";
+
private final TransportClient mTransportClient;
private final Object mConnectLock = new Object();
+ private final Context mContext;
private volatile IBackupTransport mRealTransport;
+ private AtomicReference<String> mNextRestorePackage = new AtomicReference<>();
+ private final KeyValueEncrypter mKeyValueEncrypter;
+ private final boolean mShouldEncrypt;
+
+ IntermediateEncryptingTransport(
+ TransportClient transportClient, Context context, boolean shouldEncrypt) {
+ this(transportClient, context, new KeyValueEncrypter(context), shouldEncrypt);
+ }
@VisibleForTesting
- IntermediateEncryptingTransport(TransportClient transportClient) {
+ IntermediateEncryptingTransport(
+ TransportClient transportClient, Context context, KeyValueEncrypter keyValueEncrypter,
+ boolean shouldEncrypt) {
mTransportClient = transportClient;
+ mContext = context;
+ mKeyValueEncrypter = keyValueEncrypter;
+ mShouldEncrypt = shouldEncrypt;
}
@Override
@@ -46,9 +77,116 @@
if (mRealTransport == null) {
connect();
}
+ Log.d(TAG, "real transport = " + mRealTransport.name());
return mRealTransport;
}
+ @Override
+ public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor inFd, int flags)
+ throws RemoteException {
+ if (!mShouldEncrypt) {
+ return super.performBackup(packageInfo, inFd, flags);
+ }
+
+ File encryptedStorageFile = getBackupTempStorage(packageInfo.packageName);
+ if (encryptedStorageFile == null) {
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+
+ // Encrypt the backup data and write it into a temp file.
+ try (OutputStream encryptedOutput = new FileOutputStream(encryptedStorageFile)) {
+ mKeyValueEncrypter.encryptKeyValueData(packageInfo.packageName, inFd,
+ encryptedOutput);
+ } catch (Throwable e) {
+ Log.e(TAG, "Failed to encrypt backup data: ", e);
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+
+ // Pass the temp file to the real transport for backup.
+ try (FileInputStream encryptedInput = new FileInputStream(encryptedStorageFile)) {
+ return super.performBackup(
+ packageInfo, ParcelFileDescriptor.dup(encryptedInput.getFD()), flags);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to read encrypted data from temp storage: ", e);
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+ }
+
+ @Override
+ public int getRestoreData(ParcelFileDescriptor outFd) throws RemoteException {
+ if (!mShouldEncrypt) {
+ return super.getRestoreData(outFd);
+ }
+
+ String nextRestorePackage = mNextRestorePackage.get();
+ if (nextRestorePackage == null) {
+ Log.e(TAG, "No next restore package set");
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+
+ File encryptedStorageFile = getRestoreTempStorage(nextRestorePackage);
+ if (encryptedStorageFile == null) {
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+
+ // Get encrypted restore data from the real transport and write it into a temp file.
+ try (FileOutputStream outputStream = new FileOutputStream(encryptedStorageFile)) {
+ int status = super.getRestoreData(ParcelFileDescriptor.dup(outputStream.getFD()));
+ if (status != BackupTransport.TRANSPORT_OK) {
+ Log.e(TAG, "Failed to read restore data from transport, status = " + status);
+ return status;
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to write encrypted data to temp storage: ", e);
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+
+ // Decrypt the data and write it into the fd given by the real transport.
+ try (InputStream inputStream = new FileInputStream(encryptedStorageFile)) {
+ mKeyValueEncrypter.decryptKeyValueData(nextRestorePackage, inputStream, outFd);
+ encryptedStorageFile.delete();
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to decrypt restored data: ", e);
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+
+ return BackupTransport.TRANSPORT_OK;
+ }
+
+ @Override
+ public RestoreDescription nextRestorePackage() throws RemoteException {
+ if (!mShouldEncrypt) {
+ return super.nextRestorePackage();
+ }
+
+ RestoreDescription restoreDescription = super.nextRestorePackage();
+ mNextRestorePackage.set(restoreDescription.getPackageName());
+
+ return restoreDescription;
+ }
+
+ @VisibleForTesting
+ protected File getBackupTempStorage(String packageName) {
+ return getTempStorage(packageName, BACKUP_TEMP_DIR);
+ }
+
+ @VisibleForTesting
+ protected File getRestoreTempStorage(String packageName) {
+ return getTempStorage(packageName, RESTORE_TEMP_DIR);
+ }
+
+ private File getTempStorage(String packageName, String operationType) {
+ File encryptedDir = new File(mContext.getFilesDir(), operationType);
+ encryptedDir.mkdir();
+ File encryptedFile = new File(encryptedDir, packageName);
+ try {
+ encryptedFile.createNewFile();
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to create temp file for encrypted data: ", e);
+ }
+ return encryptedFile;
+ }
+
private void connect() throws RemoteException {
Log.i(TAG, "connecting " + mTransportClient);
synchronized (mConnectLock) {
@@ -65,4 +203,9 @@
TransportClient getClient() {
return mTransportClient;
}
+
+ @VisibleForTesting
+ void setNextRestorePackage(String nextRestorePackage) {
+ mNextRestorePackage.set(nextRestorePackage);
+ }
}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportManager.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportManager.java
index 6e6d571..7c4082c 100644
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportManager.java
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportManager.java
@@ -26,20 +26,20 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.backup.IBackupTransport;
+import com.android.internal.widget.LockPatternUtils;
import com.android.server.backup.transport.TransportClientManager;
import com.android.server.backup.transport.TransportStats;
import java.util.HashMap;
import java.util.Map;
-/**
- * Handles creation and cleanup of {@link IntermediateEncryptingTransport} instances.
- */
+/** Handles creation and cleanup of {@link IntermediateEncryptingTransport} instances. */
public class IntermediateEncryptingTransportManager {
private static final String CALLER = "IntermediateEncryptingTransportManager";
private final TransportClientManager mTransportClientManager;
private final Object mTransportsLock = new Object();
private final Map<ComponentName, IntermediateEncryptingTransport> mTransports = new HashMap<>();
+ private Context mContext;
@VisibleForTesting
IntermediateEncryptingTransportManager(TransportClientManager transportClientManager) {
@@ -48,6 +48,7 @@
public IntermediateEncryptingTransportManager(Context context) {
this(new TransportClientManager(UserHandle.myUserId(), context, new TransportStats()));
+ mContext = context;
}
/**
@@ -55,31 +56,42 @@
* provide a {@link IntermediateEncryptingTransport} which is an implementation of {@link
* IBackupTransport} that encrypts (or decrypts) the data when sending it (or receiving it) from
* the real {@link IBackupTransport}.
+ *
* @param intent {@link Intent} created with a call to {@link
- * TransportClientManager.getEncryptingTransportIntent(ComponentName)}.
+ * TransportClientManager.getEncryptingTransportIntent(ComponentName)}.
* @return
*/
public IntermediateEncryptingTransport get(Intent intent) {
Intent transportIntent = TransportClientManager.getRealTransportIntent(intent);
Log.i(TAG, "get: intent:" + intent + " transportIntent:" + transportIntent);
synchronized (mTransportsLock) {
- return mTransports.computeIfAbsent(transportIntent.getComponent(),
- c -> create(transportIntent));
+ return mTransports.computeIfAbsent(
+ transportIntent.getComponent(), c -> create(transportIntent));
}
}
- /**
- * Create an instance of {@link IntermediateEncryptingTransport}.
- */
+ /** Create an instance of {@link IntermediateEncryptingTransport}. */
private IntermediateEncryptingTransport create(Intent realTransportIntent) {
Log.d(TAG, "create: intent:" + realTransportIntent);
- return new IntermediateEncryptingTransport(mTransportClientManager.getTransportClient(
- realTransportIntent.getComponent(), realTransportIntent.getExtras(), CALLER));
+
+ LockPatternUtils patternUtils = new LockPatternUtils(mContext);
+ boolean shouldEncrypt =
+ realTransportIntent.getComponent().getClassName().contains("EncryptedLocalTransport")
+ && (patternUtils.isLockPatternEnabled(UserHandle.myUserId())
+ || patternUtils.isLockPasswordEnabled(UserHandle.myUserId()));
+
+ return new IntermediateEncryptingTransport(
+ mTransportClientManager.getTransportClient(
+ realTransportIntent.getComponent(),
+ realTransportIntent.getExtras(),
+ CALLER),
+ mContext,
+ shouldEncrypt);
}
/**
- * Cleanup the {@link IntermediateEncryptingTransport} which was created by a call to
- * {@link #get(Intent)} with this {@link Intent}.
+ * Cleanup the {@link IntermediateEncryptingTransport} which was created by a call to {@link
+ * #get(Intent)} with this {@link Intent}.
*/
public void cleanup(Intent intent) {
Intent transportIntent = TransportClientManager.getRealTransportIntent(intent);
diff --git a/packages/BackupEncryption/test/unittest/src/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportTest.java b/packages/BackupEncryption/test/unittest/src/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportTest.java
index cc4b0ab..a85b2e4 100644
--- a/packages/BackupEncryption/test/unittest/src/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportTest.java
+++ b/packages/BackupEncryption/test/unittest/src/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportTest.java
@@ -18,43 +18,71 @@
import static junit.framework.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
+import android.app.backup.BackupTransport;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.os.ParcelFileDescriptor;
import android.platform.test.annotations.Presubmit;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.encryption.KeyValueEncrypter;
import com.android.server.backup.transport.TransportClient;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.io.File;
+
@Presubmit
@RunWith(AndroidJUnit4.class)
public class IntermediateEncryptingTransportTest {
- @Mock private IBackupTransport mRealTransport;
- @Mock private TransportClient mTransportClient;
+ private static final String TEST_PACKAGE_NAME = "test_package";
private IntermediateEncryptingTransport mIntermediateEncryptingTransport;
+ private final PackageInfo mTestPackage = new PackageInfo();
+
+ @Mock private IBackupTransport mRealTransport;
+ @Mock private TransportClient mTransportClient;
+ @Mock private ParcelFileDescriptor mParcelFileDescriptor;
+ @Mock private KeyValueEncrypter mKeyValueEncrypter;
+ @Mock private Context mContext;
+
+ @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ private File mTempFile;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mIntermediateEncryptingTransport = new IntermediateEncryptingTransport(mTransportClient);
+
+ mIntermediateEncryptingTransport =
+ new IntermediateEncryptingTransport(
+ mTransportClient, mContext, mKeyValueEncrypter, true);
+ mTestPackage.packageName = TEST_PACKAGE_NAME;
+ mTempFile = mTemporaryFolder.newFile();
+
+ when(mTransportClient.connect(anyString())).thenReturn(mRealTransport);
+ when(mRealTransport.getRestoreData(any())).thenReturn(BackupTransport.TRANSPORT_OK);
}
@Test
public void testGetDelegate_callsConnect() throws Exception {
- when(mTransportClient.connect(anyString())).thenReturn(mRealTransport);
-
IBackupTransport ret = mIntermediateEncryptingTransport.getDelegate();
assertEquals(mRealTransport, ret);
@@ -74,4 +102,79 @@
verify(mTransportClient, times(1)).connect(anyString());
verifyNoMoreInteractions(mTransportClient);
}
+
+ @Test
+ public void testPerformBackup_shouldEncryptTrue_encryptsDataAndPassesToDelegate()
+ throws Exception {
+ mIntermediateEncryptingTransport =
+ new TestIntermediateTransport(mTransportClient, mContext, mKeyValueEncrypter, true);
+
+ mIntermediateEncryptingTransport.performBackup(mTestPackage, mParcelFileDescriptor, 0);
+
+ verify(mKeyValueEncrypter, times(1))
+ .encryptKeyValueData(eq(TEST_PACKAGE_NAME), eq(mParcelFileDescriptor), any());
+ verify(mRealTransport, times(1)).performBackup(eq(mTestPackage), any(), eq(0));
+ }
+
+ @Test
+ public void testPerformBackup_shouldEncryptFalse_doesntEncryptDataAndPassedToDelegate()
+ throws Exception {
+ mIntermediateEncryptingTransport =
+ new TestIntermediateTransport(
+ mTransportClient, mContext, mKeyValueEncrypter, false);
+
+ mIntermediateEncryptingTransport.performBackup(mTestPackage, mParcelFileDescriptor, 0);
+
+ verifyZeroInteractions(mKeyValueEncrypter);
+ verify(mRealTransport, times(1))
+ .performBackup(eq(mTestPackage), eq(mParcelFileDescriptor), eq(0));
+ }
+
+ @Test
+ public void testGetRestoreData_shouldEncryptTrue_decryptsDataAndPassesToDelegate()
+ throws Exception {
+ mIntermediateEncryptingTransport =
+ new TestIntermediateTransport(mTransportClient, mContext, mKeyValueEncrypter, true);
+ mIntermediateEncryptingTransport.setNextRestorePackage(TEST_PACKAGE_NAME);
+
+ mIntermediateEncryptingTransport.getRestoreData(mParcelFileDescriptor);
+
+ verify(mKeyValueEncrypter, times(1))
+ .decryptKeyValueData(eq(TEST_PACKAGE_NAME), any(), eq(mParcelFileDescriptor));
+ verify(mRealTransport, times(1)).getRestoreData(any());
+ }
+
+ @Test
+ public void testGetRestoreData_shouldEncryptFalse_doesntDecryptDataAndPassesToDelegate()
+ throws Exception {
+ mIntermediateEncryptingTransport =
+ new TestIntermediateTransport(
+ mTransportClient, mContext, mKeyValueEncrypter, false);
+ mIntermediateEncryptingTransport.setNextRestorePackage(TEST_PACKAGE_NAME);
+
+ mIntermediateEncryptingTransport.getRestoreData(mParcelFileDescriptor);
+
+ verifyZeroInteractions(mKeyValueEncrypter);
+ verify(mRealTransport, times(1)).getRestoreData(eq(mParcelFileDescriptor));
+ }
+
+ private final class TestIntermediateTransport extends IntermediateEncryptingTransport {
+ TestIntermediateTransport(
+ TransportClient transportClient,
+ Context context,
+ KeyValueEncrypter keyValueEncrypter,
+ boolean shouldEncrypt) {
+ super(transportClient, context, keyValueEncrypter, shouldEncrypt);
+ }
+
+ @Override
+ protected File getBackupTempStorage(String packageName) {
+ return mTempFile;
+ }
+
+ @Override
+ protected File getRestoreTempStorage(String packageName) {
+ return mTempFile;
+ }
+ }
}
diff --git a/packages/EncryptedLocalTransport/Android.bp b/packages/EncryptedLocalTransport/Android.bp
new file mode 100644
index 0000000..dd30ad1
--- /dev/null
+++ b/packages/EncryptedLocalTransport/Android.bp
@@ -0,0 +1,27 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+android_app {
+ name: "EncryptedLocalTransport",
+ srcs: ["src/**/*.java"],
+ optimize: {
+ proguard_flags_files: ["proguard.flags"],
+ },
+ static_libs: ["LocalTransport"],
+ platform_apis: true,
+ certificate: "platform",
+ privileged: true,
+}
diff --git a/packages/EncryptedLocalTransport/AndroidManifest.xml b/packages/EncryptedLocalTransport/AndroidManifest.xml
new file mode 100644
index 0000000..dc3617f
--- /dev/null
+++ b/packages/EncryptedLocalTransport/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (c) 2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.encryptedlocaltransport"
+ android:sharedUserId="android.uid.system" >
+
+
+ <application android:allowBackup="false" >
+ <!-- This service does not need to be exported because it shares uid with the system server
+ which is the only client. -->
+ <service android:name=".EncryptedLocalTransportService"
+ android:permission="android.permission.CONFIRM_FULL_BACKUP"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="android.backup.TRANSPORT_HOST" />
+ </intent-filter>
+ </service>
+
+ </application>
+</manifest>
diff --git a/packages/EncryptedLocalTransport/proguard.flags b/packages/EncryptedLocalTransport/proguard.flags
new file mode 100644
index 0000000..e4ce3c5
--- /dev/null
+++ b/packages/EncryptedLocalTransport/proguard.flags
@@ -0,0 +1,2 @@
+-keep class com.android.localTransport.EncryptedLocalTransport
+-keep class com.android.localTransport.EncryptedLocalTransportService
diff --git a/packages/EncryptedLocalTransport/src/com/android/encryptedlocaltransport/EncryptedLocalTransport.java b/packages/EncryptedLocalTransport/src/com/android/encryptedlocaltransport/EncryptedLocalTransport.java
new file mode 100644
index 0000000..3dd453e
--- /dev/null
+++ b/packages/EncryptedLocalTransport/src/com/android/encryptedlocaltransport/EncryptedLocalTransport.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.encryptedlocaltransport;
+
+import android.app.backup.BackupTransport;
+import android.app.backup.RestoreDescription;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructStat;
+import android.util.Log;
+
+import com.android.localtransport.LocalTransport;
+import com.android.localtransport.LocalTransportParameters;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+
+public class EncryptedLocalTransport extends LocalTransport {
+ private static final String TAG = "EncryptedLocalTransport";
+ private static final int BACKUP_BUFFER_SIZE = 32 * 1024; // 32 KB.
+
+ public EncryptedLocalTransport(Context context,
+ LocalTransportParameters parameters) {
+ super(context, parameters);
+ }
+
+ @Override
+ public int performBackup(
+ PackageInfo packageInfo, ParcelFileDescriptor data, int flags) {
+ File packageFile;
+ try {
+ StructStat stat = Os.fstat(data.getFileDescriptor());
+ if (stat.st_size > KEY_VALUE_BACKUP_SIZE_QUOTA) {
+ Log.w(TAG, "New datastore size " + stat.st_size
+ + " exceeds quota " + KEY_VALUE_BACKUP_SIZE_QUOTA);
+ return TRANSPORT_QUOTA_EXCEEDED;
+ }
+ } catch (ErrnoException e) {
+ Log.w(TAG, "Failed to stat the backup input file: ", e);
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+
+ clearBackupData(packageInfo);
+
+ try (InputStream in = new FileInputStream(data.getFileDescriptor())) {
+ packageFile = new File(mCurrentSetIncrementalDir, packageInfo.packageName);
+ Files.copy(in, packageFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to save backup data to file: ", e);
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+
+ return TRANSPORT_OK;
+ }
+
+ @Override
+ public int getRestoreData(ParcelFileDescriptor outFd) {
+ if (mRestorePackages == null) {
+ throw new IllegalStateException("startRestore not called");
+ }
+ if (mRestorePackage < 0) {
+ throw new IllegalStateException("nextRestorePackage not called");
+ }
+ if (mRestoreType != RestoreDescription.TYPE_KEY_VALUE) {
+ throw new IllegalStateException("getRestoreData(fd) for non-key/value dataset");
+ }
+
+ try(OutputStream out = new FileOutputStream(outFd.getFileDescriptor())) {
+ File packageFile = new File(mRestoreSetIncrementalDir,
+ mRestorePackages[mRestorePackage].packageName);
+ Files.copy(packageFile.toPath(), out);
+ } catch (IOException e) {
+ Log.d(TAG, "Failed to transfer restore data: " + e);
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+
+ return BackupTransport.TRANSPORT_OK;
+ }
+
+ @Override
+ protected boolean hasRestoreDataForPackage(String packageName) {
+ File contents = (new File(mRestoreSetIncrementalDir, packageName));
+ return contents.exists() && contents.length() != 0;
+
+ }
+}
diff --git a/packages/EncryptedLocalTransport/src/com/android/encryptedlocaltransport/EncryptedLocalTransportService.java b/packages/EncryptedLocalTransport/src/com/android/encryptedlocaltransport/EncryptedLocalTransportService.java
new file mode 100644
index 0000000..952f90d
--- /dev/null
+++ b/packages/EncryptedLocalTransport/src/com/android/encryptedlocaltransport/EncryptedLocalTransportService.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.encryptedlocaltransport;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+import com.android.localtransport.LocalTransportParameters;
+
+public class EncryptedLocalTransportService extends Service {
+ private static EncryptedLocalTransport sTransport = null;
+
+ @Override
+ public void onCreate() {
+ if (sTransport == null) {
+ LocalTransportParameters parameters =
+ new LocalTransportParameters(getMainThreadHandler(), getContentResolver());
+ sTransport = new EncryptedLocalTransport(this, parameters);
+ }
+ sTransport.getParameters().start();
+ }
+
+ @Override
+ public void onDestroy() {
+ sTransport.getParameters().stop();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return sTransport.getBinder();
+ }
+}
diff --git a/packages/LocalTransport/src/com/android/localtransport/LocalTransport.java b/packages/LocalTransport/src/com/android/localtransport/LocalTransport.java
index 4408ef5..42cd500 100644
--- a/packages/LocalTransport/src/com/android/localtransport/LocalTransport.java
+++ b/packages/LocalTransport/src/com/android/localtransport/LocalTransport.java
@@ -71,19 +71,19 @@
// Size quotas at reasonable values, similar to the current cloud-storage limits
private static final long FULL_BACKUP_SIZE_QUOTA = 25 * 1024 * 1024;
- private static final long KEY_VALUE_BACKUP_SIZE_QUOTA = 5 * 1024 * 1024;
+ protected static final long KEY_VALUE_BACKUP_SIZE_QUOTA = 5 * 1024 * 1024;
private Context mContext;
private File mDataDir;
private File mCurrentSetDir;
- private File mCurrentSetIncrementalDir;
+ protected File mCurrentSetIncrementalDir;
private File mCurrentSetFullDir;
- private PackageInfo[] mRestorePackages = null;
- private int mRestorePackage = -1; // Index into mRestorePackages
- private int mRestoreType;
+ protected PackageInfo[] mRestorePackages = null;
+ protected int mRestorePackage = -1; // Index into mRestorePackages
+ protected int mRestoreType;
private File mRestoreSetDir;
- private File mRestoreSetIncrementalDir;
+ protected File mRestoreSetIncrementalDir;
private File mRestoreSetFullDir;
// Additional bookkeeping for full backup
@@ -115,7 +115,7 @@
makeDataDirs();
}
- LocalTransportParameters getParameters() {
+ public LocalTransportParameters getParameters() {
return mParameters;
}
@@ -537,14 +537,14 @@
int bytesLeft = numBytes;
while (bytesLeft > 0) {
try {
- int nRead = mSocketInputStream.read(mFullBackupBuffer, 0, bytesLeft);
- if (nRead < 0) {
- // Something went wrong if we expect data but saw EOD
- Log.w(TAG, "Unexpected EOD; failing backup");
- return TRANSPORT_ERROR;
- }
- mFullBackupOutputStream.write(mFullBackupBuffer, 0, nRead);
- bytesLeft -= nRead;
+ int nRead = mSocketInputStream.read(mFullBackupBuffer, 0, bytesLeft);
+ if (nRead < 0) {
+ // Something went wrong if we expect data but saw EOD
+ Log.w(TAG, "Unexpected EOD; failing backup");
+ return TRANSPORT_ERROR;
+ }
+ mFullBackupOutputStream.write(mFullBackupBuffer, 0, nRead);
+ bytesLeft -= nRead;
} catch (IOException e) {
Log.e(TAG, "Error handling backup data for " + mFullTargetPackage);
return TRANSPORT_ERROR;
@@ -620,20 +620,15 @@
}
if (mRestorePackages == null) throw new IllegalStateException("startRestore not called");
- boolean found = false;
+ boolean found;
while (++mRestorePackage < mRestorePackages.length) {
String name = mRestorePackages[mRestorePackage].packageName;
// If we have key/value data for this package, deliver that
// skip packages where we have a data dir but no actual contents
- String[] contents = (new File(mRestoreSetIncrementalDir, name)).list();
- if (contents != null && contents.length > 0) {
- if (DEBUG) {
- Log.v(TAG, " nextRestorePackage(TYPE_KEY_VALUE) @ "
- + mRestorePackage + " = " + name);
- }
+ found = hasRestoreDataForPackage(name);
+ if (found) {
mRestoreType = RestoreDescription.TYPE_KEY_VALUE;
- found = true;
}
if (!found) {
@@ -664,6 +659,18 @@
return RestoreDescription.NO_MORE_PACKAGES;
}
+ protected boolean hasRestoreDataForPackage(String packageName) {
+ String[] contents = (new File(mRestoreSetIncrementalDir, packageName)).list();
+ if (contents != null && contents.length > 0) {
+ if (DEBUG) {
+ Log.v(TAG, " nextRestorePackage(TYPE_KEY_VALUE) @ "
+ + mRestorePackage + " = " + packageName);
+ }
+ return true;
+ }
+ return false;
+ }
+
@Override
public int getRestoreData(ParcelFileDescriptor outFd) {
if (mRestorePackages == null) throw new IllegalStateException("startRestore not called");
diff --git a/packages/LocalTransport/src/com/android/localtransport/LocalTransportParameters.java b/packages/LocalTransport/src/com/android/localtransport/LocalTransportParameters.java
index 784be22..8b4db92 100644
--- a/packages/LocalTransport/src/com/android/localtransport/LocalTransportParameters.java
+++ b/packages/LocalTransport/src/com/android/localtransport/LocalTransportParameters.java
@@ -22,7 +22,7 @@
import android.provider.Settings;
import android.util.KeyValueListParser;
-class LocalTransportParameters extends KeyValueSettingObserver {
+public class LocalTransportParameters extends KeyValueSettingObserver {
private static final String TAG = "LocalTransportParams";
private static final String SETTING = Settings.Secure.BACKUP_LOCAL_TRANSPORT_PARAMETERS;
private static final String KEY_FAKE_ENCRYPTION_FLAG = "fake_encryption_flag";
@@ -31,7 +31,7 @@
private boolean mFakeEncryptionFlag;
private boolean mIsNonIncrementalOnly;
- LocalTransportParameters(Handler handler, ContentResolver resolver) {
+ public LocalTransportParameters(Handler handler, ContentResolver resolver) {
super(handler, resolver, Settings.Secure.getUriFor(SETTING));
}