/
AndroidKeysetManager.java
574 lines (532 loc) · 22.6 KB
/
AndroidKeysetManager.java
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
// Copyright 2017 Google Inc.
//
// 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.google.crypto.tink.integration.android;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import android.util.Log;
import androidx.annotation.ChecksSdkIntAtLeast;
import androidx.annotation.Nullable;
import com.google.crypto.tink.Aead;
import com.google.crypto.tink.BinaryKeysetReader;
import com.google.crypto.tink.CleartextKeysetHandle;
import com.google.crypto.tink.KeyTemplate;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.KeysetManager;
import com.google.crypto.tink.KeysetWriter;
import com.google.crypto.tink.proto.OutputPrefixType;
import com.google.crypto.tink.subtle.Hex;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.InlineMe;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.CharConversionException;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStoreException;
import java.security.ProviderException;
import javax.annotation.concurrent.GuardedBy;
/**
* A wrapper of {@link KeysetManager} that supports reading/writing {@link
* com.google.crypto.tink.proto.Keyset} to/from private shared preferences on Android.
*
* <h3>Warning</h3>
*
* <p>This class reads and writes to shared preferences, thus is best not to run on the UI thread.
*
* <h3>Usage</h3>
*
* <pre>{@code
* // One-time operations, should be done when the application is starting up.
* // Instead of repeatedly instantiating these crypto objects, instantiate them once and save for
* // later use.
* AndroidKeysetManager manager = AndroidKeysetManager.Builder()
* .withSharedPref(getApplicationContext(), "my_keyset_name", "my_pref_file_name")
* .withKeyTemplate(KeyTemplates.get("AES128_GCM_HKDF_4KB"))
* .build();
* StreamingAead streamingAead = manager.getKeysetHandle().getPrimitive(StreamingAead.class);
* }</pre>
*
* <p>This will read a keyset stored in the {@code my_keyset_name} preference of the {@code
* my_pref_file_name} preferences file. If the preference file name is null, it uses the default
* preferences file.
*
* <ul>
* <li>If a keyset is found, but it is invalid, an {@link IOException} is thrown. The most common
* cause is when you decrypted a keyset with a wrong master key. In this case, an {@link
* InvalidProtocolBufferException} would be thrown. This is an irrecoverable error. You'd have
* to delete the keyset in Shared Preferences and all existing data encrypted with it.
* <li>If a keyset is not found, and a {@link KeyTemplate} is set with {@link
* AndroidKeysetManager.Builder#withKeyTemplate(com.google.crypto.tink.KeyTemplate)}, a fresh
* keyset is generated and is written to the {@code my_keyset_name} preference of the {@code
* my_pref_file_name} shared preferences file.
* </ul>
*
* <h3>Adding a new key</h3>
*
* <p>The resulting manager supports all operations supported by {@link KeysetManager}. For example
* to add a key to the keyset, you can do:
*
* <pre>{@code
* manager.add(KeyTemplates.get("AES128_GCM_HKDF_4KB"));
* }</pre>
*
* <p>All operations that manipulate the keyset would automatically persist the new keyset to
* permanent storage.
*
* <h3>Opportunistic keyset encryption with Android Keystore</h3>
*
* <b>Warning:</b> because Android Keystore is unreliable, we strongly recommend disabling it by not
* setting any master key URI.
*
* <p>If a master key URI is set with {@link AndroidKeysetManager.Builder#withMasterKeyUri}, the
* keyset may be encrypted with a key generated and stored in <a
* href="https://developer.android.com/training/articles/keystore.html">Android Keystore</a>.
*
* <p>Android Keystore is only available on Android M or newer. Since it has been found that Android
* Keystore is unreliable on certain devices. Tink runs a self-test to detect such problems and
* disables Android Keystore accordingly, even if a master key URI is set. You can check whether
* Android Keystore is in use with {@link #isUsingKeystore}.
*
* <p>When Android Keystore is disabled or otherwise unavailable, keysets will be stored in
* cleartext. This is not as bad as it sounds because keysets remain inaccessible to any other apps
* running on the same device. Moreover, as of July 2020, most active Android devices support either
* full-disk encryption or file-based encryption, which provide strong security protection against
* key theft even from attackers with physical access to the device. Android Keystore is only useful
* when you want to <a
* href="https://developer.android.com/training/articles/keystore#UserAuthentication">require user
* authentication for key use</a>, which should be done if and only if you're absolutely sure that
* Android Keystore is working properly on your target devices.
*
* <p>The master key URI must start with {@code android-keystore://}. The remaining of the URI is
* used as a key ID when calling Android Keystore. If the master key doesn't exist, a fresh one is
* generated. If the master key already exists but is unusable, a {@link KeyStoreException} is
* thrown.
*
* <p>This class is thread-safe.
*
* @since 1.0.0
*/
public final class AndroidKeysetManager {
private static final Object lock = new Object();
private static final String TAG = AndroidKeysetManager.class.getSimpleName();
private final KeysetWriter writer;
private final Aead masterAead;
@GuardedBy("this")
private KeysetManager keysetManager;
private AndroidKeysetManager(Builder builder) {
writer = new SharedPrefKeysetWriter(builder.context, builder.keysetName, builder.prefFileName);
masterAead = builder.masterAead;
keysetManager = builder.keysetManager;
}
/**
* A builder for {@link AndroidKeysetManager}.
*
* <p>This class is thread-safe.
*/
public static final class Builder {
private Context context = null;
private String keysetName = null;
private String prefFileName = null;
private String masterKeyUri = null;
private Aead masterAead = null;
private boolean useKeystore = true;
private KeyTemplate keyTemplate = null;
@GuardedBy("this")
private KeysetManager keysetManager;
public Builder() {}
/** Reads and writes the keyset from shared preferences. */
@CanIgnoreReturnValue
public Builder withSharedPref(Context context, String keysetName, String prefFileName)
throws IOException {
if (context == null) {
throw new IllegalArgumentException("need an Android context");
}
if (keysetName == null) {
throw new IllegalArgumentException("need a keyset name");
}
this.context = context;
this.keysetName = keysetName;
this.prefFileName = prefFileName;
return this;
}
/**
* Sets the master key URI.
*
* <p>Only master keys stored in Android Keystore is supported. The URI must start with {@code
* android-keystore://}.
*/
@CanIgnoreReturnValue
public Builder withMasterKeyUri(String val) {
if (!val.startsWith(AndroidKeystoreKmsClient.PREFIX)) {
throw new IllegalArgumentException(
"key URI must start with " + AndroidKeystoreKmsClient.PREFIX);
}
if (!useKeystore) {
throw new IllegalArgumentException(
"cannot call withMasterKeyUri() after calling doNotUseKeystore()");
}
this.masterKeyUri = val;
return this;
}
/**
* If the keyset is not found or valid, generates a new one using {@code val}.
*
* @deprecated This method takes a KeyTemplate proto, which is an internal implementation
* detail. Please use the withKeyTemplate method that takes a {@link KeyTemplate} POJO.
*/
@CanIgnoreReturnValue
@Deprecated /* Deprecation under consideration */
public Builder withKeyTemplate(com.google.crypto.tink.proto.KeyTemplate val) {
keyTemplate =
KeyTemplate.create(
val.getTypeUrl(), val.getValue().toByteArray(), fromProto(val.getOutputPrefixType()));
return this;
}
/** If the keyset is not found or valid, generates a new one using {@code val}. */
@CanIgnoreReturnValue
public Builder withKeyTemplate(KeyTemplate val) {
keyTemplate = val;
return this;
}
/**
* Does not use Android Keystore which might not work well in some phones.
*
* <p><b>Warning:</b> When Android Keystore is disabled, keys are stored in cleartext. This
* should be safe because they are stored in private preferences.
*
* @deprecated Android Keystore can be disabled by not setting a master key URI.
*/
@CanIgnoreReturnValue
@Deprecated /* Deprecation under consideration */
public Builder doNotUseKeystore() {
masterKeyUri = null;
useKeystore = false;
return this;
}
/** Returns the serialized keyset if it exist or null. */
@Nullable
@SuppressWarnings("UnusedException")
private static byte[] readKeysetFromPrefs(
Context context, String keysetName, String prefFileName) throws IOException {
if (keysetName == null) {
throw new IllegalArgumentException("keysetName cannot be null");
}
Context appContext = context.getApplicationContext();
SharedPreferences sharedPreferences;
if (prefFileName == null) {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext);
} else {
sharedPreferences = appContext.getSharedPreferences(prefFileName, Context.MODE_PRIVATE);
}
try {
String keysetHex = sharedPreferences.getString(keysetName, /* defValue= */ null);
if (keysetHex == null) {
return null;
}
return Hex.decode(keysetHex);
} catch (ClassCastException | IllegalArgumentException ex) {
// The original exception is swallowed to prevent leaked key material.
throw new CharConversionException(
String.format(
"can't read keyset; the pref value %s is not a valid hex string", keysetName));
}
}
private KeysetManager readKeysetInCleartext(byte[] serializedKeyset)
throws GeneralSecurityException, IOException {
return KeysetManager.withKeysetHandle(
CleartextKeysetHandle.read(BinaryKeysetReader.withBytes(serializedKeyset)));
}
/**
* Builds and returns a new {@link AndroidKeysetManager} with the specified options.
*
* @throws IOException If a keyset is found but unusable.
* @throws KeystoreException If a master key is found but unusable.
* @throws GeneralSecurityException If cannot read an existing keyset or generate a new one.
*/
public synchronized AndroidKeysetManager build() throws GeneralSecurityException, IOException {
if (keysetName == null) {
throw new IllegalArgumentException("keysetName cannot be null");
}
// readKeysetFromPrefs(), readOrGenerateNewMasterKey() and generateNewKeyset() involve shared
// pref filesystem operations. To control access to this global state in multi-threaded
// contexts we need to ensure mutual exclusion of these functions.
synchronized (lock) {
byte[] serializedKeyset = readKeysetFromPrefs(context, keysetName, prefFileName);
if (serializedKeyset == null) {
if (masterKeyUri != null) {
masterAead = readOrGenerateNewMasterKey();
}
this.keysetManager = generateKeysetAndWriteToPrefs();
} else {
if (masterKeyUri == null || !isAtLeastM()) {
this.keysetManager = readKeysetInCleartext(serializedKeyset);
} else {
this.keysetManager = readMasterkeyDecryptAndParseKeyset(serializedKeyset);
}
}
return new AndroidKeysetManager(this);
}
}
@Nullable
private Aead readOrGenerateNewMasterKey() throws GeneralSecurityException {
if (!isAtLeastM()) {
Log.w(TAG, "Android Keystore requires at least Android M");
return null;
}
AndroidKeystoreKmsClient client = new AndroidKeystoreKmsClient();
boolean generated;
try {
// Note that this function does not use the keyStore instance set with withKeyStore.
generated = AndroidKeystoreKmsClient.generateKeyIfNotExist(masterKeyUri);
} catch (GeneralSecurityException | ProviderException ex) {
Log.w(TAG, "cannot use Android Keystore, it'll be disabled", ex);
return null;
}
try {
return client.getAead(masterKeyUri);
} catch (GeneralSecurityException | ProviderException ex) {
// Throw the exception if the key exists but is unusable. We can't recover by generating a
// new key because there might be existing encrypted data under the unusable key.
// Users can provide a master key that is stored in StrongBox (see
// https://developer.android.com/about/versions/pie/android-9.0#hardware-security-module),
// which may throw a ProviderException if there's any problem with it.
if (!generated) {
throw new KeyStoreException(
String.format("the master key %s exists but is unusable", masterKeyUri), ex);
}
// Otherwise swallow the exception if the key doesn't exist yet. We can recover by disabling
// Keystore.
Log.w(TAG, "cannot use Android Keystore, it'll be disabled", ex);
}
return null;
}
private KeysetManager generateKeysetAndWriteToPrefs()
throws GeneralSecurityException, IOException {
if (keyTemplate == null) {
throw new GeneralSecurityException("cannot read or generate keyset");
}
KeysetManager manager = KeysetManager.withEmptyKeyset().add(keyTemplate);
int keyId = manager.getKeysetHandle().getKeysetInfo().getKeyInfo(0).getKeyId();
manager = manager.setPrimary(keyId);
KeysetWriter writer = new SharedPrefKeysetWriter(context, keysetName, prefFileName);
if (masterAead != null) {
manager.getKeysetHandle().write(writer, masterAead);
} else {
CleartextKeysetHandle.write(manager.getKeysetHandle(), writer);
}
return manager;
}
@SuppressWarnings("UnusedException")
private KeysetManager readMasterkeyDecryptAndParseKeyset(byte[] serializedKeyset)
throws GeneralSecurityException, IOException {
// We expect that the keyset is encrypted. Try to get masterAead.
try {
masterAead = new AndroidKeystoreKmsClient().getAead(masterKeyUri);
} catch (GeneralSecurityException | ProviderException keystoreException) {
// Getting masterAead failed. Attempt to read the keyset in cleartext.
try {
KeysetManager manager = readKeysetInCleartext(serializedKeyset);
Log.w(TAG, "cannot use Android Keystore, it'll be disabled", keystoreException);
return manager;
} catch (IOException unused) {
// Keyset is encrypted, throw error about master key encryption
throw keystoreException;
}
}
// Got masterAead successfully.
try {
// Decrypt and parse the keyset using masterAead.
return KeysetManager.withKeysetHandle(
KeysetHandle.read(BinaryKeysetReader.withBytes(serializedKeyset), masterAead));
} catch (IOException | GeneralSecurityException ex) {
// Attempt to read the keyset in cleartext.
// This edge case may happen when either
// - the keyset was generated on a pre M phone which was upgraded to M or newer, or
// - the keyset was generated with Keystore being disabled, then Keystore is enabled.
// By ignoring the security failure here, an adversary with write access to private
// preferences can replace an encrypted keyset (that it cannot read or write) with a
// cleartext value that it controls. This does not introduce new security risks because to
// overwrite the encrypted keyset in private preferences of an app, said adversaries
// must have the same privilege as the app, thus they can call Android Keystore to read or
// write the encrypted keyset in the first place.
try {
return readKeysetInCleartext(serializedKeyset);
} catch (IOException unused) {
// Parsing failed because the keyset is encrypted but we were not able to decrypt it.
throw ex;
}
}
}
}
/** Returns a {@link KeysetHandle} of the managed keyset. */
public synchronized KeysetHandle getKeysetHandle() throws GeneralSecurityException {
return keysetManager.getKeysetHandle();
}
/**
* Generates and adds a fresh key generated using {@code keyTemplate}, and sets the new key as the
* primary key.
*
* @throws GeneralSecurityException if cannot find any {@link KeyManager} that can handle {@code
* keyTemplate}
* @deprecated Please use {@link #add}. This method adds a new key and immediately promotes it to
* primary. However, when you do keyset rotation, you almost never want to make the new key
* primary, because old binaries don't know the new key yet.
*/
@CanIgnoreReturnValue
@Deprecated /* Deprecation under consideration */
public synchronized AndroidKeysetManager rotate(
com.google.crypto.tink.proto.KeyTemplate keyTemplate) throws GeneralSecurityException {
keysetManager = keysetManager.rotate(keyTemplate);
write(keysetManager);
return this;
}
/**
* Generates and adds a fresh key generated using {@code keyTemplate}.
*
* @throws GeneralSecurityException if cannot find any {@link KeyManager} that can handle {@code
* keyTemplate}
* @deprecated This method takes a KeyTemplate proto, which is an internal implementation detail.
* Please use the add method that takes a {@link KeyTemplate} POJO.
*/
@CanIgnoreReturnValue
@GuardedBy("this")
@Deprecated /* Deprecation under consideration */
public synchronized AndroidKeysetManager add(com.google.crypto.tink.proto.KeyTemplate keyTemplate)
throws GeneralSecurityException {
keysetManager = keysetManager.add(keyTemplate);
write(keysetManager);
return this;
}
/**
* Generates and adds a fresh key generated using {@code keyTemplate}.
*
* @throws GeneralSecurityException if cannot find any {@link KeyManager} that can handle {@code
* keyTemplate}
*/
@CanIgnoreReturnValue
@GuardedBy("this")
public synchronized AndroidKeysetManager add(KeyTemplate keyTemplate)
throws GeneralSecurityException {
keysetManager = keysetManager.add(keyTemplate);
write(keysetManager);
return this;
}
/**
* Sets the key with {@code keyId} as primary.
*
* @throws GeneralSecurityException if the key is not found or not enabled
*/
@CanIgnoreReturnValue
public synchronized AndroidKeysetManager setPrimary(int keyId) throws GeneralSecurityException {
keysetManager = keysetManager.setPrimary(keyId);
write(keysetManager);
return this;
}
/**
* Sets the key with {@code keyId} as primary.
*
* @throws GeneralSecurityException if the key is not found or not enabled
* @deprecated use {@link setPrimary}
*/
@InlineMe(replacement = "this.setPrimary(keyId)")
@CanIgnoreReturnValue
@Deprecated /* Deprecation under consideration */
public synchronized AndroidKeysetManager promote(int keyId) throws GeneralSecurityException {
return setPrimary(keyId);
}
/**
* Enables the key with {@code keyId}.
*
* @throws GeneralSecurityException if the key is not found
*/
@CanIgnoreReturnValue
public synchronized AndroidKeysetManager enable(int keyId) throws GeneralSecurityException {
keysetManager = keysetManager.enable(keyId);
write(keysetManager);
return this;
}
/**
* Disables the key with {@code keyId}.
*
* @throws GeneralSecurityException if the key is not found or it is the primary key
*/
@CanIgnoreReturnValue
public synchronized AndroidKeysetManager disable(int keyId) throws GeneralSecurityException {
keysetManager = keysetManager.disable(keyId);
write(keysetManager);
return this;
}
/**
* Deletes the key with {@code keyId}.
*
* @throws GeneralSecurityException if the key is not found or it is the primary key
*/
@CanIgnoreReturnValue
public synchronized AndroidKeysetManager delete(int keyId) throws GeneralSecurityException {
keysetManager = keysetManager.delete(keyId);
write(keysetManager);
return this;
}
/**
* Destroys the key material associated with the {@code keyId}.
*
* @throws GeneralSecurityException if the key is not found or it is the primary key
*/
@CanIgnoreReturnValue
public synchronized AndroidKeysetManager destroy(int keyId) throws GeneralSecurityException {
keysetManager = keysetManager.destroy(keyId);
write(keysetManager);
return this;
}
/** Returns whether Android Keystore is being used to wrap Tink keysets. */
public synchronized boolean isUsingKeystore() {
return shouldUseKeystore();
}
private void write(KeysetManager manager) throws GeneralSecurityException {
try {
if (shouldUseKeystore()) {
manager.getKeysetHandle().write(writer, masterAead);
} else {
CleartextKeysetHandle.write(manager.getKeysetHandle(), writer);
}
} catch (IOException e) {
throw new GeneralSecurityException(e);
}
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.M)
private boolean shouldUseKeystore() {
return masterAead != null && isAtLeastM();
}
private static KeyTemplate.OutputPrefixType fromProto(OutputPrefixType outputPrefixType) {
switch (outputPrefixType) {
case TINK:
return KeyTemplate.OutputPrefixType.TINK;
case LEGACY:
return KeyTemplate.OutputPrefixType.LEGACY;
case RAW:
return KeyTemplate.OutputPrefixType.RAW;
case CRUNCHY:
return KeyTemplate.OutputPrefixType.CRUNCHY;
default:
throw new IllegalArgumentException("Unknown output prefix type");
}
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.M)
private static boolean isAtLeastM() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
}