/* * 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.util.Slog; import com.android.server.backup.encryption.chunk.ChunkHash; import com.android.server.backup.encryption.chunking.ChunkEncryptor; import com.android.server.backup.encryption.chunking.ChunkHasher; import com.android.server.backup.encryption.chunking.EncryptedChunk; import com.android.server.backup.encryption.chunking.cdc.ContentDefinedChunker; import com.android.server.backup.encryption.chunking.cdc.FingerprintMixer; import com.android.server.backup.encryption.chunking.cdc.IsChunkBreakpoint; import com.android.server.backup.encryption.chunking.cdc.RabinFingerprint64; import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.crypto.SecretKey; /** * Splits backup data into variable-sized chunks using content-defined chunking, then encrypts the * chunks. Given a hash of the SHA-256s of existing chunks, performs an incremental backup (i.e., * only encrypts new chunks). */ public class BackupStreamEncrypter implements BackupEncrypter { private static final String TAG = "BackupStreamEncryptor"; private final InputStream mData; private final int mMinChunkSizeBytes; private final int mMaxChunkSizeBytes; private final int mAverageChunkSizeBytes; /** * A new instance over the given distribution of chunk sizes. * * @param data The data to be backed up. * @param minChunkSizeBytes The minimum chunk size. No chunk will be smaller than this. * @param maxChunkSizeBytes The maximum chunk size. No chunk will be larger than this. * @param averageChunkSizeBytes The average chunk size. The mean size of chunks will be roughly * this (with a few tens of bytes of overhead for the initialization vector and message * authentication code). */ public BackupStreamEncrypter( InputStream data, int minChunkSizeBytes, int maxChunkSizeBytes, int averageChunkSizeBytes) { this.mData = data; this.mMinChunkSizeBytes = minChunkSizeBytes; this.mMaxChunkSizeBytes = maxChunkSizeBytes; this.mAverageChunkSizeBytes = averageChunkSizeBytes; } @Override public Result backup( SecretKey secretKey, byte[] fingerprintMixerSalt, Set existingChunks) throws IOException, GeneralSecurityException { MessageDigest messageDigest = MessageDigest.getInstance(BackupEncrypter.MESSAGE_DIGEST_ALGORITHM); RabinFingerprint64 rabinFingerprint64 = new RabinFingerprint64(); FingerprintMixer fingerprintMixer = new FingerprintMixer(secretKey, fingerprintMixerSalt); IsChunkBreakpoint isChunkBreakpoint = new IsChunkBreakpoint(mAverageChunkSizeBytes - mMinChunkSizeBytes); ContentDefinedChunker chunker = new ContentDefinedChunker( mMinChunkSizeBytes, mMaxChunkSizeBytes, rabinFingerprint64, fingerprintMixer, isChunkBreakpoint); ChunkHasher chunkHasher = new ChunkHasher(secretKey); ChunkEncryptor encryptor = new ChunkEncryptor(secretKey, new SecureRandom()); Set includedChunks = new HashSet<>(); // New chunks will be added only once to this list, even if they occur multiple times. List newChunks = new ArrayList<>(); // All chunks (including multiple occurrences) will be added to the chunkListing. List chunkListing = new ArrayList<>(); includedChunks.addAll(existingChunks); chunker.chunkify( mData, chunk -> { messageDigest.update(chunk); ChunkHash key = chunkHasher.computeHash(chunk); if (!includedChunks.contains(key)) { newChunks.add(encryptor.encrypt(key, chunk)); includedChunks.add(key); } chunkListing.add(key); }); Slog.i( TAG, String.format( "Chunks: %d total, %d unique, %d new", chunkListing.size(), new HashSet<>(chunkListing).size(), newChunks.size())); return new Result( Collections.unmodifiableList(chunkListing), Collections.unmodifiableList(newChunks), messageDigest.digest()); } }