diff options
15 files changed, 762 insertions, 46 deletions
diff --git a/data/etc/framework-sysconfig.xml b/data/etc/framework-sysconfig.xml index 987c3b439db8..7296cfdfbec7 100644 --- a/data/etc/framework-sysconfig.xml +++ b/data/etc/framework-sysconfig.xml @@ -29,6 +29,8 @@ 'service' attribute here is a flattened ComponentName string. --> <backup-transport-whitelisted-service service="com.android.localtransport/.LocalTransportService" /> + <backup-transport-whitelisted-service + service="com.android.encryptedlocaltransport/.EncryptedLocalTransportService" /> <!-- Whitelist Shell to use the bugreport API --> <bugreport-whitelisted package="com.android.shell" /> diff --git a/packages/BackupEncryption/Android.bp b/packages/BackupEncryption/Android.bp index 342d796de402..68e937c24a89 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 000000000000..2035b6605559 --- /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 000000000000..1d841b4a2c8f --- /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 1d0224d49be7..c3cb335db89e 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 @@ package com.android.server.backup.encryption.transport; 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 @@ public class IntermediateEncryptingTransport extends DelegatingTransport { 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 @@ public class IntermediateEncryptingTransport extends DelegatingTransport { 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 6e6d571aa3c7..7c4082c2a54d 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 android.util.Log; 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 class IntermediateEncryptingTransportManager { public IntermediateEncryptingTransportManager(Context context) { this(new TransportClientManager(UserHandle.myUserId(), context, new TransportStats())); + mContext = context; } /** @@ -55,31 +56,42 @@ public class IntermediateEncryptingTransportManager { * 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 cc4b0ab1bb36..a85b2e4498a6 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 @@ package com.android.server.backup.encryption.transport; 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 { + 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; - private IntermediateEncryptingTransport mIntermediateEncryptingTransport; + @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 @@ public class IntermediateEncryptingTransportTest { 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 000000000000..dd30ad177d69 --- /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 000000000000..dc3617fa6d4e --- /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 000000000000..e4ce3c524e35 --- /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 000000000000..3dd453e83ab8 --- /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 000000000000..952f90d8b11f --- /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 4408ef5a2c75..42cd50003f7a 100644 --- a/packages/LocalTransport/src/com/android/localtransport/LocalTransport.java +++ b/packages/LocalTransport/src/com/android/localtransport/LocalTransport.java @@ -71,19 +71,19 @@ public class LocalTransport extends BackupTransport { // 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 @@ public class LocalTransport extends BackupTransport { makeDataDirs(); } - LocalTransportParameters getParameters() { + public LocalTransportParameters getParameters() { return mParameters; } @@ -537,14 +537,14 @@ public class LocalTransport extends BackupTransport { 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 @@ public class LocalTransport extends BackupTransport { } 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 @@ public class LocalTransport extends BackupTransport { 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 784be224f367..8b4db92910f5 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.os.Handler; 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 @@ class LocalTransportParameters extends KeyValueSettingObserver { private boolean mFakeEncryptionFlag; private boolean mIsNonIncrementalOnly; - LocalTransportParameters(Handler handler, ContentResolver resolver) { + public LocalTransportParameters(Handler handler, ContentResolver resolver) { super(handler, resolver, Settings.Secure.getUriFor(SETTING)); } diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java index e5e11ea2253e..ac006df7f475 100644 --- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java +++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java @@ -239,7 +239,6 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { private final KeyValueBackupReporter mReporter; private final OnTaskFinishedListener mTaskFinishedListener; private final boolean mUserInitiated; - private final boolean mNonIncremental; private final int mCurrentOpToken; private final int mUserId; private final File mStateDirectory; @@ -264,6 +263,7 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { // and at least one of the packages had data. Used to avoid updating current token for // empty backups. private boolean mHasDataToBackup; + private boolean mNonIncremental; /** * This {@link ConditionVariable} is used to signal that the cancel operation has been @@ -412,6 +412,11 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { try { IBackupTransport transport = mTransportClient.connectOrThrow("KVBT.startTask()"); String transportName = transport.name(); + if (transportName.contains("EncryptedLocalTransport")) { + // Temporary code for EiTF POC. Only supports non-incremental backups. + mNonIncremental = true; + } + mReporter.onTransportReady(transportName); // If we haven't stored PM metadata yet, we must initialize the transport. |