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

Commit e2d322ef authored by Al Sutton's avatar Al Sutton Committed by Android (Google) Code Review
Browse files

Merge changes Idbfec4ff,I135f561a

* changes:
  Add backup data round trip test
  Import EncryptedFull???Task
parents 4902d437 fde3621d
Loading
Loading
Loading
Loading
+109 −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 android.app.backup.BackupTransport;

import java.io.IOException;
import java.io.InputStream;

/** Accepts the full backup data stream and sends it to the server. */
public interface FullBackupDataProcessor {
    /**
     * Prepares the upload.
     *
     * <p>After this, call {@link #start()} to establish the connection.
     *
     * @param inputStream to read the backup data from, calling {@link #finish} or {@link #cancel}
     *     will close the stream
     * @return {@code true} if the connection was set up successfully, otherwise {@code false}
     */
    boolean initiate(InputStream inputStream) throws IOException;

    /**
     * Starts the upload, establishing the connection to the server.
     *
     * <p>After this, call {@link #pushData(int)} to request that the processor reads data from the
     * socket, and uploads it to the server.
     *
     * <p>After this you must call one of {@link #cancel()}, {@link #finish()}, {@link
     * #handleCheckSizeRejectionZeroBytes()}, {@link #handleCheckSizeRejectionQuotaExceeded()} or
     * {@link #handleSendBytesQuotaExceeded()} to close the upload.
     */
    void start();

    /**
     * Requests that the processor read {@code numBytes} from the input stream passed in {@link
     * #initiate(InputStream)} and upload them to the server.
     *
     * @return {@link BackupTransport#TRANSPORT_OK} if the upload succeeds, or {@link
     *     BackupTransport#TRANSPORT_QUOTA_EXCEEDED} if the upload exceeded the server-side app size
     *     quota, or {@link BackupTransport#TRANSPORT_PACKAGE_REJECTED} for other errors.
     */
    int pushData(int numBytes);

    /** Cancels the upload and tears down the connection. */
    void cancel();

    /**
     * Finish the upload and tear down the connection.
     *
     * <p>Call this after there is no more data to push with {@link #pushData(int)}.
     *
     * @return One of {@link BackupTransport#TRANSPORT_OK} if the app upload succeeds, {@link
     *     BackupTransport#TRANSPORT_QUOTA_EXCEEDED} if the upload exceeded the server-side app size
     *     quota, {@link BackupTransport#TRANSPORT_ERROR} for server 500s, or {@link
     *     BackupTransport#TRANSPORT_PACKAGE_REJECTED} for other errors.
     */
    int finish();

    /**
     * Notifies the processor that the current upload should be terminated because the estimated
     * size is zero.
     */
    void handleCheckSizeRejectionZeroBytes();

    /**
     * Notifies the processor that the current upload should be terminated because the estimated
     * size exceeds the quota.
     */
    void handleCheckSizeRejectionQuotaExceeded();

    /**
     * Notifies this class that the current upload should be terminated because the quota was
     * exceeded during upload.
     */
    void handleSendBytesQuotaExceeded();

    /**
     * Attaches {@link FullBackupCallbacks} which the processor will notify when the backup
     * succeeds.
     */
    void attachCallbacks(FullBackupCallbacks fullBackupCallbacks);

    /**
     * Implemented by the caller of the processor to receive notification of when the backup
     * succeeds.
     */
    interface FullBackupCallbacks {
        /** The processor calls this to indicate that the current backup has succeeded. */
        void onSuccess();

        /** The processor calls this if the upload failed for a non-transient reason. */
        void onTransferFailed();
    }
}
+51 −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 java.io.IOException;

/**
 * Retrieves the data during a full restore, decrypting it if necessary.
 *
 * <p>Use {@link FullRestoreDataProcessorFactory} to construct the encrypted or unencrypted
 * processor as appropriate during restore.
 */
public interface FullRestoreDataProcessor {
    /** Return value of {@link #readNextChunk} when there is no more data to download. */
    int END_OF_STREAM = -1;

    /**
     * Reads the next chunk of restore data and writes it to the given buffer.
     *
     * <p>Where necessary, will open the connection to the server and/or decrypt the backup file.
     *
     * <p>The implementation may retry various errors. If the retries fail it will throw the
     * relevant exception.
     *
     * @return the number of bytes read, or {@link #END_OF_STREAM} if there is no more data
     * @throws IOException when downloading from the network or writing to disk
     */
    int readNextChunk(byte[] buffer) throws IOException;

    /**
     * Closes the connection to the server, deletes any temporary files and optionally sends a log
     * with the given finish type.
     *
     * @param finishType one of {@link FullRestoreDownloader.FinishType}
     */
    void finish(FullRestoreDownloader.FinishType finishType);
}
+36 −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 java.io.Closeable;
import java.io.IOException;

/** Utility methods for dealing with Streams */
public class StreamUtils {
    /**
     * Close a Closeable and silently ignore any IOExceptions.
     *
     * @param closeable The closeable to close
     */
    public static void closeQuietly(Closeable closeable) {
        try {
            closeable.close();
        } catch (IOException ioe) {
            // Silently ignore
        }
    }
}
+197 −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 android.content.Context;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.backup.encryption.StreamUtils;
import com.android.server.backup.encryption.chunking.ProtoStore;
import com.android.server.backup.encryption.chunking.cdc.FingerprintMixer;
import com.android.server.backup.encryption.client.CryptoBackupServer;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
import com.android.server.backup.encryption.keys.TertiaryKeyManager;
import com.android.server.backup.encryption.keys.TertiaryKeyRotationScheduler;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkListing;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;

import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.Callable;

import javax.crypto.SecretKey;

/**
 * Task which reads a stream of plaintext full backup data, chunks it, encrypts it and uploads it to
 * the server.
 *
 * <p>Once the backup completes or fails, closes the input stream.
 */
public class EncryptedFullBackupTask implements Callable<Void> {
    private static final String TAG = "EncryptedFullBackupTask";

    private static final int MIN_CHUNK_SIZE_BYTES = 2 * 1024;
    private static final int MAX_CHUNK_SIZE_BYTES = 64 * 1024;
    private static final int AVERAGE_CHUNK_SIZE_BYTES = 4 * 1024;

    // TODO(b/69350270): Remove this hard-coded salt and related logic once we feel confident that
    // incremental backup has happened at least once for all existing packages/users since we moved
    // to
    // using a randomly generated salt.
    //
    // The hard-coded fingerprint mixer salt was used for a short time period before replaced by one
    // that is randomly generated on initial non-incremental backup and stored in ChunkListing to be
    // reused for succeeding incremental backups. If an old ChunkListing does not have a
    // fingerprint_mixer_salt, we assume that it was last backed up before a randomly generated salt
    // is used so we use the hardcoded salt and set ChunkListing#fingerprint_mixer_salt to this
    // value.
    // Eventually all backup ChunkListings will have this field set and then we can remove the
    // default
    // value in the code.
    static final byte[] DEFAULT_FINGERPRINT_MIXER_SALT =
            Arrays.copyOf(new byte[] {20, 23}, FingerprintMixer.SALT_LENGTH_BYTES);

    private final ProtoStore<ChunkListing> mChunkListingStore;
    private final TertiaryKeyManager mTertiaryKeyManager;
    private final InputStream mInputStream;
    private final EncryptedBackupTask mTask;
    private final String mPackageName;
    private final SecureRandom mSecureRandom;

    /** Creates a new instance with the default min, max and average chunk sizes. */
    public static EncryptedFullBackupTask newInstance(
            Context context,
            CryptoBackupServer cryptoBackupServer,
            SecureRandom secureRandom,
            RecoverableKeyStoreSecondaryKey secondaryKey,
            String packageName,
            InputStream inputStream)
            throws IOException {
        EncryptedBackupTask encryptedBackupTask =
                new EncryptedBackupTask(
                        cryptoBackupServer,
                        secureRandom,
                        packageName,
                        new BackupStreamEncrypter(
                                inputStream,
                                MIN_CHUNK_SIZE_BYTES,
                                MAX_CHUNK_SIZE_BYTES,
                                AVERAGE_CHUNK_SIZE_BYTES));
        TertiaryKeyManager tertiaryKeyManager =
                new TertiaryKeyManager(
                        context,
                        secureRandom,
                        TertiaryKeyRotationScheduler.getInstance(context),
                        secondaryKey,
                        packageName);

        return new EncryptedFullBackupTask(
                ProtoStore.createChunkListingStore(context),
                tertiaryKeyManager,
                encryptedBackupTask,
                inputStream,
                packageName,
                new SecureRandom());
    }

    @VisibleForTesting
    EncryptedFullBackupTask(
            ProtoStore<ChunkListing> chunkListingStore,
            TertiaryKeyManager tertiaryKeyManager,
            EncryptedBackupTask task,
            InputStream inputStream,
            String packageName,
            SecureRandom secureRandom) {
        mChunkListingStore = chunkListingStore;
        mTertiaryKeyManager = tertiaryKeyManager;
        mInputStream = inputStream;
        mTask = task;
        mPackageName = packageName;
        mSecureRandom = secureRandom;
    }

    @Override
    public Void call() throws Exception {
        try {
            Optional<ChunkListing> maybeOldChunkListing =
                    mChunkListingStore.loadProto(mPackageName);

            if (maybeOldChunkListing.isPresent()) {
                Slog.i(TAG, "Found previous chunk listing for " + mPackageName);
            }

            // If the key has been rotated then we must re-encrypt all of the backup data.
            if (mTertiaryKeyManager.wasKeyRotated()) {
                Slog.i(
                        TAG,
                        "Key was rotated or newly generated for "
                                + mPackageName
                                + ", so performing a full backup.");
                maybeOldChunkListing = Optional.empty();
                mChunkListingStore.deleteProto(mPackageName);
            }

            SecretKey tertiaryKey = mTertiaryKeyManager.getKey();
            WrappedKeyProto.WrappedKey wrappedTertiaryKey = mTertiaryKeyManager.getWrappedKey();

            ChunkListing newChunkListing;
            if (!maybeOldChunkListing.isPresent()) {
                byte[] fingerprintMixerSalt = new byte[FingerprintMixer.SALT_LENGTH_BYTES];
                mSecureRandom.nextBytes(fingerprintMixerSalt);
                newChunkListing =
                        mTask.performNonIncrementalBackup(
                                tertiaryKey, wrappedTertiaryKey, fingerprintMixerSalt);
            } else {
                ChunkListing oldChunkListing = maybeOldChunkListing.get();

                if (oldChunkListing.fingerprintMixerSalt == null
                        || oldChunkListing.fingerprintMixerSalt.length == 0) {
                    oldChunkListing.fingerprintMixerSalt = DEFAULT_FINGERPRINT_MIXER_SALT;
                }

                newChunkListing =
                        mTask.performIncrementalBackup(
                                tertiaryKey, wrappedTertiaryKey, oldChunkListing);
            }

            mChunkListingStore.saveProto(mPackageName, newChunkListing);
            Slog.v(TAG, "Saved chunk listing for " + mPackageName);
        } catch (IOException e) {
            Slog.e(TAG, "Storage exception, wiping state");
            mChunkListingStore.deleteProto(mPackageName);
            throw e;
        } finally {
            StreamUtils.closeQuietly(mInputStream);
        }

        return null;
    }

    /**
     * Signals to the task that the backup has been cancelled. If the upload has not yet started
     * then the task will not upload any data to the server or save the new chunk listing.
     *
     * <p>You must then terminate the input stream.
     */
    public void cancel() {
        mTask.cancel();
    }
}
+137 −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.checkArgument;

import android.annotation.Nullable;
import android.content.Context;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.backup.encryption.FullRestoreDataProcessor;
import com.android.server.backup.encryption.FullRestoreDownloader;
import com.android.server.backup.encryption.StreamUtils;
import com.android.server.backup.encryption.chunking.DecryptedChunkFileOutput;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;

/** Downloads the encrypted backup file, decrypts it and passes the data to backup manager. */
public class EncryptedFullRestoreTask implements FullRestoreDataProcessor {
    private static final String DEFAULT_TEMPORARY_FOLDER = "encrypted_restore_temp";
    private static final String ENCRYPTED_FILE_NAME = "encrypted_restore";
    private static final String DECRYPTED_FILE_NAME = "decrypted_restore";

    private final FullRestoreToFileTask mFullRestoreToFileTask;
    private final BackupFileDecryptorTask mBackupFileDecryptorTask;
    private final File mEncryptedFile;
    private final File mDecryptedFile;
    @Nullable private InputStream mDecryptedFileInputStream;

    /**
     * Creates a new task which stores temporary files in the files directory.
     *
     * @param fullRestoreDownloader which will download the backup file
     * @param tertiaryKey which the backup file is encrypted with
     */
    public static EncryptedFullRestoreTask newInstance(
            Context context, FullRestoreDownloader fullRestoreDownloader, SecretKey tertiaryKey)
            throws NoSuchAlgorithmException, NoSuchPaddingException {
        File temporaryFolder = new File(context.getFilesDir(), DEFAULT_TEMPORARY_FOLDER);
        temporaryFolder.mkdirs();
        return new EncryptedFullRestoreTask(
                temporaryFolder, fullRestoreDownloader, new BackupFileDecryptorTask(tertiaryKey));
    }

    @VisibleForTesting
    EncryptedFullRestoreTask(
            File temporaryFolder,
            FullRestoreDownloader fullRestoreDownloader,
            BackupFileDecryptorTask backupFileDecryptorTask) {
        checkArgument(temporaryFolder.isDirectory(), "Temporary folder must be existing directory");

        mEncryptedFile = new File(temporaryFolder, ENCRYPTED_FILE_NAME);
        mDecryptedFile = new File(temporaryFolder, DECRYPTED_FILE_NAME);

        mFullRestoreToFileTask = new FullRestoreToFileTask(fullRestoreDownloader);
        mBackupFileDecryptorTask = backupFileDecryptorTask;
    }

    /**
     * Reads the next decrypted bytes into the given buffer.
     *
     * <p>During the first call this method will download the backup file from the server, decrypt
     * it and save it to disk. It will then read the bytes from the file on disk.
     *
     * <p>Once this method has read all the bytes of the file, the caller must call {@link #finish}
     * to clean up.
     *
     * @return the number of bytes read, or {@code -1} on reaching the end of the file
     */
    @Override
    public int readNextChunk(byte[] buffer) throws IOException {
        if (mDecryptedFileInputStream == null) {
            try {
                mDecryptedFileInputStream = downloadAndDecryptBackup();
            } catch (BadPaddingException
                    | InvalidKeyException
                    | NoSuchAlgorithmException
                    | IllegalBlockSizeException
                    | ShortBufferException
                    | EncryptedRestoreException
                    | InvalidAlgorithmParameterException e) {
                throw new IOException("Encryption issue", e);
            }
        }

        return mDecryptedFileInputStream.read(buffer);
    }

    private InputStream downloadAndDecryptBackup()
            throws IOException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException,
                    IllegalBlockSizeException, ShortBufferException, EncryptedRestoreException,
                    InvalidAlgorithmParameterException {
        mFullRestoreToFileTask.restoreToFile(mEncryptedFile);
        mBackupFileDecryptorTask.decryptFile(
                mEncryptedFile, new DecryptedChunkFileOutput(mDecryptedFile));
        mEncryptedFile.delete();
        return new BufferedInputStream(new FileInputStream(mDecryptedFile));
    }

    /** Cleans up temporary files. */
    @Override
    public void finish(FullRestoreDownloader.FinishType unusedFinishType) {
        // The download is finished and log sent during RestoreToFileTask#restoreToFile(), so we
        // don't need to do either of those things here.

        StreamUtils.closeQuietly(mDecryptedFileInputStream);
        mEncryptedFile.delete();
        mDecryptedFile.delete();
    }
}
Loading