blob: 9bf148ddc901b3992e6d5c39499f1242b4283c34 [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 android.util.SparseIntArray;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkOrdering;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunksMetadata;
import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Locale;
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;
/**
* A backup file consists of, in order:
*
* <ul>
* <li>A randomly ordered sequence of encrypted chunks
* <li>A plaintext {@link ChunksMetadata} proto, containing the bytes of an encrypted {@link
* ChunkOrdering} proto.
* <li>A 64-bit long denoting the offset of the file at which the ChunkOrdering proto starts.
* </ul>
*
* <p>This task decrypts such a blob and writes the plaintext to another file.
*
* <p>The backup file has two formats to indicate the boundaries of the chunks in the encrypted
* file. In {@link ChunksMetadataProto#EXPLICIT_STARTS} mode the chunk ordering contains the start
* positions of each chunk and the decryptor outputs the chunks in the order they appeared in the
* plaintext file. In {@link ChunksMetadataProto#INLINE_LENGTHS} mode the length of each encrypted
* chunk is prepended to the chunk in the file and the decryptor outputs the chunks in no specific
* order.
*
* <p>{@link ChunksMetadataProto#EXPLICIT_STARTS} is for use with full backup (Currently used for
* all backups as b/77188289 is not implemented yet), {@link ChunksMetadataProto#INLINE_LENGTHS}
* will be used for kv backup (once b/77188289 is implemented) to avoid re-uploading the chunk
* ordering (see b/70782620).
*/
public class BackupFileDecryptorTask {
private static final String TAG = "BackupFileDecryptorTask";
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 READ_MODE = "r";
private static final int BYTES_PER_LONG = 64 / BITS_PER_BYTE;
private final Cipher mCipher;
private final SecretKey mSecretKey;
/**
* A new instance.
*
* @param secretKey The tertiary key used to encrypt the backup blob.
*/
public BackupFileDecryptorTask(SecretKey secretKey)
throws NoSuchPaddingException, NoSuchAlgorithmException {
this.mCipher = Cipher.getInstance(CIPHER_ALGORITHM);
this.mSecretKey = secretKey;
}
/**
* Runs the task, reading the encrypted data from {@code input} and writing the plaintext data
* to {@code output}.
*
* @param inputFile The encrypted backup file.
* @param decryptedChunkOutput Unopened output to write the plaintext to, which this class will
* open and close during decryption.
* @throws IOException if an error occurred reading the encrypted file or writing the plaintext,
* or if one of the protos could not be deserialized.
*/
public void decryptFile(File inputFile, DecryptedChunkOutput decryptedChunkOutput)
throws IOException, EncryptedRestoreException, IllegalBlockSizeException,
BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException,
ShortBufferException, NoSuchAlgorithmException {
RandomAccessFile input = new RandomAccessFile(inputFile, READ_MODE);
long metadataOffset = getChunksMetadataOffset(input);
ChunksMetadataProto.ChunksMetadata chunksMetadata =
getChunksMetadata(input, metadataOffset);
ChunkOrdering chunkOrdering = decryptChunkOrdering(chunksMetadata);
if (chunksMetadata.chunkOrderingType == ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED
|| chunksMetadata.chunkOrderingType == ChunksMetadataProto.EXPLICIT_STARTS) {
Slog.d(TAG, "Using explicit starts");
decryptFileWithExplicitStarts(
input, decryptedChunkOutput, chunkOrdering, metadataOffset);
} else if (chunksMetadata.chunkOrderingType == ChunksMetadataProto.INLINE_LENGTHS) {
Slog.d(TAG, "Using inline lengths");
decryptFileWithInlineLengths(input, decryptedChunkOutput, metadataOffset);
} else {
throw new UnsupportedEncryptedFileException(
"Unknown chunk ordering type:" + chunksMetadata.chunkOrderingType);
}
if (!Arrays.equals(decryptedChunkOutput.getDigest(), chunkOrdering.checksum)) {
throw new MessageDigestMismatchException("Checksums did not match");
}
}
private void decryptFileWithExplicitStarts(
RandomAccessFile input,
DecryptedChunkOutput decryptedChunkOutput,
ChunkOrdering chunkOrdering,
long metadataOffset)
throws IOException, InvalidKeyException, IllegalBlockSizeException,
InvalidAlgorithmParameterException, ShortBufferException, BadPaddingException,
NoSuchAlgorithmException {
SparseIntArray chunkLengthsByPosition =
getChunkLengths(chunkOrdering.starts, (int) metadataOffset);
int largestChunkLength = getLargestChunkLength(chunkLengthsByPosition);
byte[] encryptedChunkBuffer = new byte[largestChunkLength];
// largestChunkLength is 0 if the backup file contains zero chunks e.g. 0 kv pairs.
int plaintextBufferLength =
Math.max(0, largestChunkLength - GCM_NONCE_LENGTH_BYTES - GCM_TAG_LENGTH_BYTES);
byte[] plaintextChunkBuffer = new byte[plaintextBufferLength];
try (DecryptedChunkOutput output = decryptedChunkOutput.open()) {
for (int start : chunkOrdering.starts) {
int length = chunkLengthsByPosition.get(start);
input.seek(start);
input.readFully(encryptedChunkBuffer, 0, length);
int plaintextLength =
decryptChunk(encryptedChunkBuffer, length, plaintextChunkBuffer);
outputChunk(output, plaintextChunkBuffer, plaintextLength);
}
}
}
private void decryptFileWithInlineLengths(
RandomAccessFile input, DecryptedChunkOutput decryptedChunkOutput, long metadataOffset)
throws MalformedEncryptedFileException, IOException, IllegalBlockSizeException,
BadPaddingException, InvalidAlgorithmParameterException, ShortBufferException,
InvalidKeyException, NoSuchAlgorithmException {
input.seek(0);
try (DecryptedChunkOutput output = decryptedChunkOutput.open()) {
while (input.getFilePointer() < metadataOffset) {
long start = input.getFilePointer();
int encryptedChunkLength = input.readInt();
if (encryptedChunkLength <= 0) {
// If the length of the encrypted chunk is not positive we will not make
// progress reading the file and so will loop forever.
throw new MalformedEncryptedFileException(
"Encrypted chunk length not positive:" + encryptedChunkLength);
}
if (start + encryptedChunkLength > metadataOffset) {
throw new MalformedEncryptedFileException(
String.format(
Locale.US,
"Encrypted chunk longer (%d) than file (%d)",
encryptedChunkLength,
metadataOffset));
}
byte[] plaintextChunk = new byte[encryptedChunkLength];
byte[] plaintext =
new byte
[encryptedChunkLength
- GCM_NONCE_LENGTH_BYTES
- GCM_TAG_LENGTH_BYTES];
input.readFully(plaintextChunk);
int plaintextChunkLength =
decryptChunk(plaintextChunk, encryptedChunkLength, plaintext);
outputChunk(output, plaintext, plaintextChunkLength);
}
}
}
private void outputChunk(
DecryptedChunkOutput output, byte[] plaintextChunkBuffer, int plaintextLength)
throws IOException, InvalidKeyException, NoSuchAlgorithmException {
output.processChunk(plaintextChunkBuffer, plaintextLength);
}
/**
* Decrypts chunk and returns the length of the plaintext.
*
* @param encryptedChunkBuffer The encrypted data, prefixed by the nonce.
* @param encryptedChunkBufferLength The length of the encrypted chunk (including nonce).
* @param plaintextChunkBuffer The buffer into which to write the plaintext chunk.
* @return The length of the plaintext chunk.
*/
private int decryptChunk(
byte[] encryptedChunkBuffer,
int encryptedChunkBufferLength,
byte[] plaintextChunkBuffer)
throws InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException,
ShortBufferException, IllegalBlockSizeException {
mCipher.init(
Cipher.DECRYPT_MODE,
mSecretKey,
new GCMParameterSpec(
GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE,
encryptedChunkBuffer,
0,
GCM_NONCE_LENGTH_BYTES));
return mCipher.doFinal(
encryptedChunkBuffer,
GCM_NONCE_LENGTH_BYTES,
encryptedChunkBufferLength - GCM_NONCE_LENGTH_BYTES,
plaintextChunkBuffer);
}
/** Given all the lengths, returns the largest length. */
private int getLargestChunkLength(SparseIntArray lengths) {
int maxSeen = 0;
for (int i = 0; i < lengths.size(); i++) {
maxSeen = Math.max(maxSeen, lengths.valueAt(i));
}
return maxSeen;
}
/**
* From a list of the starting position of each chunk in the correct order of the backup data,
* calculates a mapping from start position to length of that chunk.
*
* @param starts The start positions of chunks, in order.
* @param chunkOrderingPosition Where the {@link ChunkOrdering} proto starts, used to calculate
* the length of the last chunk.
* @return The mapping.
*/
private SparseIntArray getChunkLengths(int[] starts, int chunkOrderingPosition) {
int[] boundaries = Arrays.copyOf(starts, starts.length + 1);
boundaries[boundaries.length - 1] = chunkOrderingPosition;
Arrays.sort(boundaries);
SparseIntArray lengths = new SparseIntArray();
for (int i = 0; i < boundaries.length - 1; i++) {
lengths.put(boundaries[i], boundaries[i + 1] - boundaries[i]);
}
return lengths;
}
/**
* Reads and decrypts the {@link ChunkOrdering} from the {@link ChunksMetadata}.
*
* @param metadata The metadata.
* @return The ordering.
* @throws InvalidProtocolBufferNanoException if there is an issue deserializing the proto.
*/
private ChunkOrdering decryptChunkOrdering(ChunksMetadata metadata)
throws InvalidProtocolBufferNanoException, InvalidAlgorithmParameterException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException,
UnsupportedEncryptedFileException {
assertCryptoSupported(metadata);
mCipher.init(
Cipher.DECRYPT_MODE,
mSecretKey,
new GCMParameterSpec(
GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE,
metadata.chunkOrdering,
0,
GCM_NONCE_LENGTH_BYTES));
byte[] decrypted =
mCipher.doFinal(
metadata.chunkOrdering,
GCM_NONCE_LENGTH_BYTES,
metadata.chunkOrdering.length - GCM_NONCE_LENGTH_BYTES);
return ChunkOrdering.parseFrom(decrypted);
}
/**
* Asserts that the Cipher and MessageDigest algorithms in the backup metadata are supported.
* For now we only support SHA-256 for checksum and 256-bit AES/GCM/NoPadding for the Cipher.
*
* @param chunksMetadata The file metadata.
* @throws UnsupportedEncryptedFileException if any algorithm is unsupported.
*/
private void assertCryptoSupported(ChunksMetadata chunksMetadata)
throws UnsupportedEncryptedFileException {
if (chunksMetadata.checksumType != ChunksMetadataProto.SHA_256) {
// For now we only support SHA-256.
throw new UnsupportedEncryptedFileException(
"Unrecognized checksum type for backup (this version of backup only supports"
+ " SHA-256): "
+ chunksMetadata.checksumType);
}
if (chunksMetadata.cipherType != ChunksMetadataProto.AES_256_GCM) {
throw new UnsupportedEncryptedFileException(
"Unrecognized cipher type for backup (this version of backup only supports"
+ " AES-256-GCM: "
+ chunksMetadata.cipherType);
}
}
/**
* Reads the offset of the {@link ChunksMetadata} proto from the end of the file.
*
* @return The offset.
* @throws IOException if there is an error reading.
*/
private long getChunksMetadataOffset(RandomAccessFile input) throws IOException {
input.seek(input.length() - BYTES_PER_LONG);
return input.readLong();
}
/**
* Reads the {@link ChunksMetadata} proto from the given position in the file.
*
* @param input The encrypted file.
* @param position The position where the proto starts.
* @return The proto.
* @throws IOException if there is an issue reading the file or deserializing the proto.
*/
private ChunksMetadata getChunksMetadata(RandomAccessFile input, long position)
throws IOException, MalformedEncryptedFileException {
long length = input.length();
if (position >= length || position < 0) {
throw new MalformedEncryptedFileException(
String.format(
Locale.US,
"%d is not valid position for chunks metadata in file of %d bytes",
position,
length));
}
// Read chunk ordering bytes
input.seek(position);
long chunksMetadataLength = input.length() - BYTES_PER_LONG - position;
byte[] chunksMetadataBytes = new byte[(int) chunksMetadataLength];
input.readFully(chunksMetadataBytes);
try {
return ChunksMetadata.parseFrom(chunksMetadataBytes);
} catch (InvalidProtocolBufferNanoException e) {
throw new MalformedEncryptedFileException(
String.format(
Locale.US,
"Could not read chunks metadata at position %d of file of %d bytes",
position,
length));
}
}
}