Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 178a5024 authored by Al Sutton's avatar Al Sutton
Browse files

Import EncryptedFullBackupDataProcessor

Bug: 111386661
Test: make RunBackupEncryptionRoboIntegTests
Change-Id: I5b9f828663157df13e55f7ed7c8eceef99fa5899
parent 4238f749
Loading
Loading
Loading
Loading
+28 −0
Original line number Diff line number Diff line
@@ -18,9 +18,13 @@ package com.android.server.backup.encryption;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/** Utility methods for dealing with Streams */
public class StreamUtils {
    private static final int MAX_COPY_BUFFER_SIZE = 1024; // 1k copy buffer size.

    /**
     * Close a Closeable and silently ignore any IOExceptions.
     *
@@ -33,4 +37,28 @@ public class StreamUtils {
            // Silently ignore
        }
    }

    /**
     * Copy data from an InputStream to an OutputStream upto a given number of bytes.
     *
     * @param in The source InputStream
     * @param out The destination OutputStream
     * @param limit The maximum number of bytes to copy
     * @throws IOException Thrown if there is a problem performing the copy.
     */
    public static void copyStream(InputStream in, OutputStream out, int limit) throws IOException {
        int bufferSize = Math.min(MAX_COPY_BUFFER_SIZE, limit);
        byte[] buffer = new byte[bufferSize];

        int copied = 0;
        while (copied < limit) {
            int maxReadSize = Math.min(bufferSize, limit - copied);
            int read = in.read(buffer, 0, maxReadSize);
            if (read < 0) {
                return; // Reached the stream end before the limit
            }
            out.write(buffer, 0, read);
            copied += read;
        }
    }
}
+210 −0
Original line number Diff line number Diff line
/*
 * 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.tasks;

import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.Preconditions.checkState;

import android.annotation.Nullable;
import android.app.backup.BackupTransport;
import android.content.Context;
import android.util.Slog;

import com.android.server.backup.encryption.FullBackupDataProcessor;
import com.android.server.backup.encryption.StreamUtils;
import com.android.server.backup.encryption.client.CryptoBackupServer;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;

import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.security.SecureRandom;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

/**
 * Accepts backup data from a {@link InputStream} and passes it to the encrypted full data backup
 * path.
 */
public class EncryptedFullBackupDataProcessor implements FullBackupDataProcessor {

    private static final String TAG = "EncryptedFullBackupDP";

    private final Context mContext;
    private final ExecutorService mExecutorService;
    private final CryptoBackupServer mCryptoBackupServer;
    private final SecureRandom mSecureRandom;
    private final RecoverableKeyStoreSecondaryKey mSecondaryKey;
    private final String mPackageName;

    @Nullable private InputStream mInputStream;
    @Nullable private PipedOutputStream mOutputStream;
    @Nullable private EncryptedFullBackupTask mBackupTask;
    @Nullable private Future<Void> mBackupTaskFuture;
    @Nullable private FullBackupCallbacks mFullBackupCallbacks;

    public EncryptedFullBackupDataProcessor(
            Context context,
            ExecutorService executorService,
            CryptoBackupServer cryptoBackupServer,
            SecureRandom secureRandom,
            RecoverableKeyStoreSecondaryKey secondaryKey,
            String packageName) {
        mContext = checkNotNull(context);
        mExecutorService = checkNotNull(executorService);
        mCryptoBackupServer = checkNotNull(cryptoBackupServer);
        mSecureRandom = checkNotNull(secureRandom);
        mSecondaryKey = checkNotNull(secondaryKey);
        mPackageName = checkNotNull(packageName);
    }

    @Override
    public boolean initiate(InputStream inputStream) throws IOException {
        checkState(mBackupTask == null, "initiate() twice");

        this.mInputStream = inputStream;
        mOutputStream = new PipedOutputStream();

        mBackupTask =
                EncryptedFullBackupTask.newInstance(
                        mContext,
                        mCryptoBackupServer,
                        mSecureRandom,
                        mSecondaryKey,
                        mPackageName,
                        new PipedInputStream(mOutputStream));

        return true;
    }

    @Override
    public void start() {
        checkState(mBackupTask != null, "start() before initiate()");
        mBackupTaskFuture = mExecutorService.submit(mBackupTask);
    }

    @Override
    public int pushData(int numBytes) {
        checkState(
                mBackupTaskFuture != null && mInputStream != null && mOutputStream != null,
                "pushData() before start()");

        // If the upload has failed then stop without pushing any more bytes.
        if (mBackupTaskFuture.isDone()) {
            Optional<Exception> exception = getTaskException();
            Slog.e(TAG, "Encrypted upload failed", exception.orElse(null));
            if (exception.isPresent()) {
                reportNetworkFailureIfNecessary(exception.get());

                if (exception.get().getCause() instanceof SizeQuotaExceededException) {
                    return BackupTransport.TRANSPORT_QUOTA_EXCEEDED;
                }
            }

            return BackupTransport.TRANSPORT_ERROR;
        }

        try {
            StreamUtils.copyStream(mInputStream, mOutputStream, numBytes);
        } catch (IOException e) {
            Slog.e(TAG, "IOException when processing backup", e);
            return BackupTransport.TRANSPORT_ERROR;
        }

        return BackupTransport.TRANSPORT_OK;
    }

    @Override
    public void cancel() {
        checkState(mBackupTaskFuture != null && mBackupTask != null, "cancel() before start()");
        mBackupTask.cancel();
        closeStreams();
    }

    @Override
    public int finish() {
        checkState(mBackupTaskFuture != null, "finish() before start()");

        // getTaskException() waits for the task to finish. We must close the streams first, which
        // causes the task to finish, otherwise it will block forever.
        closeStreams();
        Optional<Exception> exception = getTaskException();

        if (exception.isPresent()) {
            Slog.e(TAG, "Exception during encrypted full backup", exception.get());
            reportNetworkFailureIfNecessary(exception.get());

            if (exception.get().getCause() instanceof SizeQuotaExceededException) {
                return BackupTransport.TRANSPORT_QUOTA_EXCEEDED;
            }
            return BackupTransport.TRANSPORT_ERROR;

        } else {
            if (mFullBackupCallbacks != null) {
                mFullBackupCallbacks.onSuccess();
            }

            return BackupTransport.TRANSPORT_OK;
        }
    }

    private void closeStreams() {
        StreamUtils.closeQuietly(mInputStream);
        StreamUtils.closeQuietly(mOutputStream);
    }

    @Override
    public void handleCheckSizeRejectionZeroBytes() {
        cancel();
    }

    @Override
    public void handleCheckSizeRejectionQuotaExceeded() {
        cancel();
    }

    @Override
    public void handleSendBytesQuotaExceeded() {
        cancel();
    }

    @Override
    public void attachCallbacks(FullBackupCallbacks fullBackupCallbacks) {
        this.mFullBackupCallbacks = fullBackupCallbacks;
    }

    private void reportNetworkFailureIfNecessary(Exception exception) {
        if (!(exception.getCause() instanceof SizeQuotaExceededException)
                && mFullBackupCallbacks != null) {
            mFullBackupCallbacks.onTransferFailed();
        }
    }

    private Optional<Exception> getTaskException() {
        if (mBackupTaskFuture != null) {
            try {
                mBackupTaskFuture.get();
            } catch (InterruptedException | ExecutionException e) {
                return Optional.of(e);
            }
        }
        return Optional.empty();
    }
}
+75 −0
Original line number Diff line number Diff line
/*
 * 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.google.common.truth.Truth.assertThat;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

@RunWith(RobolectricTestRunner.class)
public class StreamUtilsTest {
    private static final int SOURCE_DATA_SIZE = 64;

    private byte[] mSourceData;

    private InputStream mSource;
    private ByteArrayOutputStream mDestination;

    @Before
    public void setUp() {
        mSourceData = new byte[SOURCE_DATA_SIZE];
        for (byte i = 0; i < SOURCE_DATA_SIZE; i++) {
            mSourceData[i] = i;
        }
        mSource = new ByteArrayInputStream(mSourceData);
        mDestination = new ByteArrayOutputStream();
    }

    @Test
    public void copyStream_copiesAllBytesIfAsked() throws IOException {
        StreamUtils.copyStream(mSource, mDestination, mSourceData.length);
        assertOutputHasBytes(mSourceData.length);
    }

    @Test
    public void copyStream_stopsShortIfAsked() throws IOException {
        StreamUtils.copyStream(mSource, mDestination, mSourceData.length - 10);
        assertOutputHasBytes(mSourceData.length - 10);
    }

    @Test
    public void copyStream_stopsShortIfAskedToCopyMoreThanAvailable() throws IOException {
        StreamUtils.copyStream(mSource, mDestination, mSourceData.length + 10);
        assertOutputHasBytes(mSourceData.length);
    }

    private void assertOutputHasBytes(int count) {
        byte[] output = mDestination.toByteArray();
        assertThat(output.length).isEqualTo(count);
        for (int i = 0; i < count; i++) {
            assertThat(output[i]).isEqualTo(mSourceData[i]);
        }
    }
}
+387 −0

File added.

Preview size limit exceeded, changes collapsed.

+83 −0
Original line number Diff line number Diff line
/*
 * 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.testing;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * ExecutorService which needs to be stepped through the jobs in its' queue.
 *
 * <p>This is a deliberately simple implementation because it's only used in testing. The queued
 * jobs are run on the main thread to eliminate any race condition bugs.
 */
public class QueuingNonAutomaticExecutorService extends AbstractExecutorService {

    private List<Runnable> mWaitingJobs = new ArrayList<>();
    private int mWaitingJobCount = 0;

    @Override
    public void shutdown() {
        mWaitingJobCount = mWaitingJobs.size();
        mWaitingJobs = null; // This will force an error if jobs are submitted after shutdown
    }

    @Override
    public List<Runnable> shutdownNow() {
        List<Runnable> queuedJobs = mWaitingJobs;
        shutdown();
        return queuedJobs;
    }

    @Override
    public boolean isShutdown() {
        return mWaitingJobs == null;
    }

    @Override
    public boolean isTerminated() {
        return mWaitingJobs == null && mWaitingJobCount == 0;
    }

    @Override
    public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
        long expiry = System.currentTimeMillis() + unit.toMillis(timeout);
        for (Runnable job : mWaitingJobs) {
            if (System.currentTimeMillis() > expiry) {
                return false;
            }

            job.run();
        }
        return true;
    }

    @Override
    public void execute(Runnable command) {
        mWaitingJobs.add(command);
    }

    public void runNext() {
        if (mWaitingJobs.isEmpty()) {
            throw new IllegalStateException("Attempted to run jobs on an empty paused executor");
        }

        mWaitingJobs.remove(0).run();
    }
}