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

Commit 7c698614 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Import EncryptedFullBackupDataProcessor"

parents 4cba33d4 178a5024
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();
    }
}