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

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

Merge "Import EncryptedBackupTask"

parents 5f6c76dd 75eda037
Loading
Loading
Loading
Loading
+67 −0
Original line number Original line 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.client;

import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;

import java.util.Map;

/**
 * Contains methods for communicating with the parts of the backup server relevant to encryption.
 */
public interface CryptoBackupServer {
    /**
     * Uploads an incremental backup to the server.
     *
     * <p>Handles setting up and tearing down the connection.
     *
     * @param packageName the package to associate the data with
     * @param oldDocId the id of the previous backup doc in Drive
     * @param diffScript containing the actual backup data
     * @param tertiaryKey the wrapped key used to encrypt this backup
     * @return the id of the new backup doc in Drive.
     */
    String uploadIncrementalBackup(
            String packageName,
            String oldDocId,
            byte[] diffScript,
            WrappedKeyProto.WrappedKey tertiaryKey);

    /**
     * Uploads non-incremental backup to the server.
     *
     * <p>Handles setting up and tearing down the connection.
     *
     * @param packageName the package to associate the data with
     * @param data the actual backup data
     * @param tertiaryKey the wrapped key used to encrypt this backup
     * @return the id of the new backup doc in Drive.
     */
    String uploadNonIncrementalBackup(
            String packageName, byte[] data, WrappedKeyProto.WrappedKey tertiaryKey);

    /**
     * Sets the alias of the active secondary key. This is the alias used to refer to the key in the
     * {@link java.security.KeyStore}. It is also used to key storage for tertiary keys on the
     * backup server. Also has to upload all existing tertiary keys, wrapped with the new key.
     *
     * @param keyAlias The ID of the secondary key.
     * @param tertiaryKeys The tertiary keys, wrapped with the new secondary key.
     */
    void setActiveSecondaryKeyAlias(
            String keyAlias, Map<String, WrappedKeyProto.WrappedKey> tertiaryKeys);
}
+243 −0
Original line number Original line 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.annotation.Nullable;
import android.annotation.TargetApi;
import android.os.Build.VERSION_CODES;
import android.util.Slog;

import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.chunking.BackupFileBuilder;
import com.android.server.backup.encryption.chunking.EncryptedChunk;
import com.android.server.backup.encryption.client.CryptoBackupServer;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.GCMParameterSpec;

/**
 * Task which reads encrypted chunks from a {@link BackupEncrypter}, builds a backup file and
 * uploads it to the server.
 */
@TargetApi(VERSION_CODES.P)
public class EncryptedBackupTask {
    private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
    private static final int GCM_NONCE_LENGTH_BYTES = 12;
    private static final int GCM_TAG_LENGTH_BYTES = 16;
    private static final int BITS_PER_BYTE = 8;

    private static final String TAG = "EncryptedBackupTask";

    private final CryptoBackupServer mCryptoBackupServer;
    private final SecureRandom mSecureRandom;
    private final String mPackageName;
    private final ByteArrayOutputStream mBackupDataOutput;
    private final BackupEncrypter mBackupEncrypter;
    private final AtomicBoolean mCancelled;

    /** Creates a new instance which reads data from the given input stream. */
    public EncryptedBackupTask(
            CryptoBackupServer cryptoBackupServer,
            SecureRandom secureRandom,
            String packageName,
            BackupEncrypter backupEncrypter) {
        mCryptoBackupServer = cryptoBackupServer;
        mSecureRandom = secureRandom;
        mPackageName = packageName;
        mBackupEncrypter = backupEncrypter;

        mBackupDataOutput = new ByteArrayOutputStream();
        mCancelled = new AtomicBoolean(false);
    }

    /**
     * Creates a non-incremental backup file and uploads it to the server.
     *
     * @param fingerprintMixerSalt Fingerprint mixer salt used for content-defined chunking during a
     *     full backup. May be {@code null} for a key-value backup.
     */
    public ChunksMetadataProto.ChunkListing performNonIncrementalBackup(
            SecretKey tertiaryKey,
            WrappedKeyProto.WrappedKey wrappedTertiaryKey,
            @Nullable byte[] fingerprintMixerSalt)
            throws IOException, GeneralSecurityException {

        ChunksMetadataProto.ChunkListing newChunkListing =
                performBackup(
                        tertiaryKey,
                        fingerprintMixerSalt,
                        BackupFileBuilder.createForNonIncremental(mBackupDataOutput),
                        new HashSet<>());

        throwIfCancelled();

        newChunkListing.documentId =
                mCryptoBackupServer.uploadNonIncrementalBackup(
                        mPackageName, mBackupDataOutput.toByteArray(), wrappedTertiaryKey);

        return newChunkListing;
    }

    /** Creates an incremental backup file and uploads it to the server. */
    public ChunksMetadataProto.ChunkListing performIncrementalBackup(
            SecretKey tertiaryKey,
            WrappedKeyProto.WrappedKey wrappedTertiaryKey,
            ChunksMetadataProto.ChunkListing oldChunkListing)
            throws IOException, GeneralSecurityException {

        ChunksMetadataProto.ChunkListing newChunkListing =
                performBackup(
                        tertiaryKey,
                        oldChunkListing.fingerprintMixerSalt,
                        BackupFileBuilder.createForIncremental(mBackupDataOutput, oldChunkListing),
                        getChunkHashes(oldChunkListing));

        throwIfCancelled();

        String oldDocumentId = oldChunkListing.documentId;
        Slog.v(TAG, "Old doc id: " + oldDocumentId);

        newChunkListing.documentId =
                mCryptoBackupServer.uploadIncrementalBackup(
                        mPackageName,
                        oldDocumentId,
                        mBackupDataOutput.toByteArray(),
                        wrappedTertiaryKey);
        return newChunkListing;
    }

    /**
     * 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.
     */
    public void cancel() {
        mCancelled.getAndSet(true);
    }

    private void throwIfCancelled() {
        if (mCancelled.get()) {
            throw new CancellationException("EncryptedBackupTask was cancelled");
        }
    }

    private ChunksMetadataProto.ChunkListing performBackup(
            SecretKey tertiaryKey,
            @Nullable byte[] fingerprintMixerSalt,
            BackupFileBuilder backupFileBuilder,
            Set<ChunkHash> existingChunkHashes)
            throws IOException, GeneralSecurityException {
        BackupEncrypter.Result result =
                mBackupEncrypter.backup(tertiaryKey, fingerprintMixerSalt, existingChunkHashes);
        backupFileBuilder.writeChunks(result.getAllChunks(), buildChunkMap(result.getNewChunks()));

        ChunksMetadataProto.ChunkOrdering chunkOrdering =
                backupFileBuilder.getNewChunkOrdering(result.getDigest());
        backupFileBuilder.finish(buildMetadata(tertiaryKey, chunkOrdering));

        return backupFileBuilder.getNewChunkListing(fingerprintMixerSalt);
    }

    /** Returns a set containing the hashes of every chunk in the given listing. */
    private static Set<ChunkHash> getChunkHashes(ChunksMetadataProto.ChunkListing chunkListing) {
        Set<ChunkHash> hashes = new HashSet<>();
        for (ChunksMetadataProto.Chunk chunk : chunkListing.chunks) {
            hashes.add(new ChunkHash(chunk.hash));
        }
        return hashes;
    }

    /** Returns a map from chunk hash to chunk containing every chunk in the given list. */
    private static Map<ChunkHash, EncryptedChunk> buildChunkMap(List<EncryptedChunk> chunks) {
        Map<ChunkHash, EncryptedChunk> chunkMap = new HashMap<>();
        for (EncryptedChunk chunk : chunks) {
            chunkMap.put(chunk.key(), chunk);
        }
        return chunkMap;
    }

    private ChunksMetadataProto.ChunksMetadata buildMetadata(
            SecretKey tertiaryKey, ChunksMetadataProto.ChunkOrdering chunkOrdering)
            throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
                    InvalidAlgorithmParameterException, NoSuchAlgorithmException,
                    ShortBufferException, NoSuchPaddingException {
        ChunksMetadataProto.ChunksMetadata metaData = new ChunksMetadataProto.ChunksMetadata();
        metaData.cipherType = ChunksMetadataProto.AES_256_GCM;
        metaData.checksumType = ChunksMetadataProto.SHA_256;
        metaData.chunkOrdering = encryptChunkOrdering(tertiaryKey, chunkOrdering);
        return metaData;
    }

    private byte[] encryptChunkOrdering(
            SecretKey tertiaryKey, ChunksMetadataProto.ChunkOrdering chunkOrdering)
            throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
                    NoSuchPaddingException, NoSuchAlgorithmException,
                    InvalidAlgorithmParameterException, ShortBufferException {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);

        byte[] nonce = generateNonce();

        cipher.init(
                Cipher.ENCRYPT_MODE,
                tertiaryKey,
                new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE, nonce));

        byte[] orderingBytes = ChunksMetadataProto.ChunkOrdering.toByteArray(chunkOrdering);
        // We prepend the nonce to the ordering.
        byte[] output =
                Arrays.copyOf(
                        nonce,
                        GCM_NONCE_LENGTH_BYTES + orderingBytes.length + GCM_TAG_LENGTH_BYTES);

        cipher.doFinal(
                orderingBytes,
                /*inputOffset=*/ 0,
                /*inputLen=*/ orderingBytes.length,
                output,
                /*outputOffset=*/ GCM_NONCE_LENGTH_BYTES);

        return output;
    }

    private byte[] generateNonce() {
        byte[] nonce = new byte[GCM_NONCE_LENGTH_BYTES];
        mSecureRandom.nextBytes(nonce);
        return nonce;
    }
}
+397 −0

File added.

Preview size limit exceeded, changes collapsed.

+57 −0
Original line number Original line Diff line number Diff line
@@ -57,4 +57,61 @@ public class CryptoTestUtils {
        newChunk.length = length;
        newChunk.length = length;
        return newChunk;
        return newChunk;
    }
    }

    public static ChunksMetadataProto.ChunkListing newChunkListing(
            String docId,
            byte[] fingerprintSalt,
            int cipherType,
            int orderingType,
            ChunksMetadataProto.Chunk... chunks) {
        ChunksMetadataProto.ChunkListing chunkListing =
                newChunkListingWithoutDocId(fingerprintSalt, cipherType, orderingType, chunks);
        chunkListing.documentId = docId;
        return chunkListing;
    }

    public static ChunksMetadataProto.ChunkListing newChunkListingWithoutDocId(
            byte[] fingerprintSalt,
            int cipherType,
            int orderingType,
            ChunksMetadataProto.Chunk... chunks) {
        ChunksMetadataProto.ChunkListing chunkListing = new ChunksMetadataProto.ChunkListing();
        chunkListing.fingerprintMixerSalt = Arrays.copyOf(fingerprintSalt, fingerprintSalt.length);
        chunkListing.cipherType = cipherType;
        chunkListing.chunkOrderingType = orderingType;
        chunkListing.chunks = chunks;
        return chunkListing;
    }

    public static ChunksMetadataProto.ChunkOrdering newChunkOrdering(
            int[] starts, byte[] checksum) {
        ChunksMetadataProto.ChunkOrdering chunkOrdering = new ChunksMetadataProto.ChunkOrdering();
        chunkOrdering.starts = Arrays.copyOf(starts, starts.length);
        chunkOrdering.checksum = Arrays.copyOf(checksum, checksum.length);
        return chunkOrdering;
    }

    public static ChunksMetadataProto.ChunkListing clone(
            ChunksMetadataProto.ChunkListing original) {
        ChunksMetadataProto.Chunk[] clonedChunks;
        if (original.chunks == null) {
            clonedChunks = null;
        } else {
            clonedChunks = new ChunksMetadataProto.Chunk[original.chunks.length];
            for (int i = 0; i < original.chunks.length; i++) {
                clonedChunks[i] = clone(original.chunks[i]);
            }
        }

        return newChunkListing(
                original.documentId,
                original.fingerprintMixerSalt,
                original.cipherType,
                original.chunkOrderingType,
                clonedChunks);
    }

    public static ChunksMetadataProto.Chunk clone(ChunksMetadataProto.Chunk original) {
        return newChunk(original.hash, original.length);
    }
}
}