1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
|
/*
* 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.checkState;
import android.annotation.Nullable;
import android.app.backup.BackupDataInput;
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.kv.KeyValueListingBuilder;
import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;
/**
* Reads key value backup data from an input, converts each pair into a chunk and encrypts the
* chunks.
*
* <p>The caller should pass in the key value listing from the previous backup, if there is one.
* This class emits chunks for both existing and new pairs, using the provided listing to
* determine the hashes of pairs that already exist. During the backup it computes the new listing,
* which the caller should store on disk and pass in at the start of the next backup.
*
* <p>Also computes the message digest, which is {@code SHA-256(chunk hashes sorted
* lexicographically)}.
*/
public class KvBackupEncrypter implements BackupEncrypter {
private final BackupDataInput mBackupDataInput;
private KeyValueListingProto.KeyValueListing mOldKeyValueListing;
@Nullable private KeyValueListingBuilder mNewKeyValueListing;
/**
* Constructs a new instance which reads data from the given input.
*
* <p>By default this performs non-incremental backup, call {@link #setOldKeyValueListing} to
* perform incremental backup.
*/
public KvBackupEncrypter(BackupDataInput backupDataInput) {
mBackupDataInput = backupDataInput;
mOldKeyValueListing = KeyValueListingBuilder.emptyListing();
}
/** Sets the old listing to perform incremental backup against. */
public void setOldKeyValueListing(KeyValueListingProto.KeyValueListing oldKeyValueListing) {
mOldKeyValueListing = oldKeyValueListing;
}
@Override
public Result backup(
SecretKey secretKey,
@Nullable byte[] unusedFingerprintMixerSalt,
Set<ChunkHash> unusedExistingChunks)
throws IOException, GeneralSecurityException {
ChunkHasher chunkHasher = new ChunkHasher(secretKey);
ChunkEncryptor chunkEncryptor = new ChunkEncryptor(secretKey, new SecureRandom());
mNewKeyValueListing = new KeyValueListingBuilder();
List<ChunkHash> allChunks = new ArrayList<>();
List<EncryptedChunk> newChunks = new ArrayList<>();
Map<String, ChunkHash> existingChunksToReuse = buildPairMap(mOldKeyValueListing);
while (mBackupDataInput.readNextHeader()) {
String key = mBackupDataInput.getKey();
Optional<byte[]> value = readEntireValue(mBackupDataInput);
// As this pair exists in the new backup, we don't need to add it from the previous
// backup.
existingChunksToReuse.remove(key);
// If the value is not present then this key has been deleted.
if (value.isPresent()) {
EncryptedChunk newChunk =
createEncryptedChunk(chunkHasher, chunkEncryptor, key, value.get());
allChunks.add(newChunk.key());
newChunks.add(newChunk);
mNewKeyValueListing.addPair(key, newChunk.key());
}
}
allChunks.addAll(existingChunksToReuse.values());
mNewKeyValueListing.addAll(existingChunksToReuse);
return new Result(allChunks, newChunks, createMessageDigest(allChunks));
}
/**
* Returns a listing containing the pairs in the new backup.
*
* <p>You must call {@link #backup} first.
*/
public KeyValueListingProto.KeyValueListing getNewKeyValueListing() {
checkState(mNewKeyValueListing != null, "Must call backup() first");
return mNewKeyValueListing.build();
}
private static Map<String, ChunkHash> buildPairMap(
KeyValueListingProto.KeyValueListing listing) {
Map<String, ChunkHash> map = new HashMap<>();
for (KeyValueListingProto.KeyValueEntry entry : listing.entries) {
map.put(entry.key, new ChunkHash(entry.hash));
}
return map;
}
private EncryptedChunk createEncryptedChunk(
ChunkHasher chunkHasher, ChunkEncryptor chunkEncryptor, String key, byte[] value)
throws InvalidKeyException, IllegalBlockSizeException {
KeyValuePairProto.KeyValuePair pair = new KeyValuePairProto.KeyValuePair();
pair.key = key;
pair.value = Arrays.copyOf(value, value.length);
byte[] plaintext = KeyValuePairProto.KeyValuePair.toByteArray(pair);
return chunkEncryptor.encrypt(chunkHasher.computeHash(plaintext), plaintext);
}
private static byte[] createMessageDigest(List<ChunkHash> allChunks)
throws NoSuchAlgorithmException {
MessageDigest messageDigest =
MessageDigest.getInstance(BackupEncrypter.MESSAGE_DIGEST_ALGORITHM);
// TODO:b/141531271 Extract sorted chunks code to utility class
List<ChunkHash> sortedChunks = new ArrayList<>(allChunks);
Collections.sort(sortedChunks);
for (ChunkHash hash : sortedChunks) {
messageDigest.update(hash.getHash());
}
return messageDigest.digest();
}
private static Optional<byte[]> readEntireValue(BackupDataInput input) throws IOException {
// A negative data size indicates that this key should be deleted.
if (input.getDataSize() < 0) {
return Optional.empty();
}
byte[] value = new byte[input.getDataSize()];
int bytesRead = 0;
while (bytesRead < value.length) {
bytesRead += input.readEntityData(value, bytesRead, value.length - bytesRead);
}
return Optional.of(value);
}
}
|