blob: 45798d32885a1fc3eb89b433fedc6716047a7cb9 [file] [log] [blame]
/*
* 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<ChunkHash> 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<ChunkHash> includedChunks = new HashSet<>();
// New chunks will be added only once to this list, even if they occur multiple times.
List<EncryptedChunk> newChunks = new ArrayList<>();
// All chunks (including multiple occurrences) will be added to the chunkListing.
List<ChunkHash> 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());
}
}