com.google.crypto.tink.integration.android.AndroidKeystoreKmsClient Maven / Gradle / Ivy
Show all versions of tink-android Show documentation
// 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.os.Build;
import android.util.Log;
import androidx.annotation.ChecksSdkIntAtLeast;
import androidx.annotation.RequiresApi;
import com.google.crypto.tink.Aead;
import com.google.crypto.tink.KmsClient;
import com.google.crypto.tink.subtle.Random;
import com.google.crypto.tink.subtle.Validators;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStoreException;
import java.util.Arrays;
import java.util.Locale;
/**
* An implementation of {@link KmsClient} for Android Keystore.
*
* We don't recommend using this class. Instead, directly use {@link AndroidKeystore}.
*
*
This class requires Android M (API level 23) or newer.
*
* @since 1.0.0
*/
public final class AndroidKeystoreKmsClient implements KmsClient {
private static final Object keystoreLock = new Object();
private static final String TAG = AndroidKeystoreKmsClient.class.getSimpleName();
private static final int MAX_WAIT_TIME_MILLISECONDS_BEFORE_RETRY = 40;
/** The prefix of all keys stored in Android Keystore. */
public static final String PREFIX = "android-keystore://";
private final String keyUri;
@RequiresApi(23)
public AndroidKeystoreKmsClient() throws GeneralSecurityException {
this(new Builder());
}
/**
* Constructs an {@link AndroidKeystoreKmsClient} that is bound to a single key identified by
* {@code uri}.
*
* @deprecated use {@link AndroidKeystoreKmsClient.Builder}.
*/
@RequiresApi(23)
@Deprecated
public AndroidKeystoreKmsClient(String uri) {
this(new Builder().setKeyUri(uri));
}
private AndroidKeystoreKmsClient(Builder builder) {
this.keyUri = builder.keyUri;
}
/** Builder for AndroidKeystoreKmsClient */
public static final class Builder {
String keyUri = null;
@RequiresApi(23)
public Builder() {
if (!isAtLeastM()) {
throw new IllegalStateException("need Android Keystore on Android M or newer");
}
}
@CanIgnoreReturnValue
@RequiresApi(23)
public Builder setKeyUri(String val) {
if (val == null || !val.toLowerCase(Locale.US).startsWith(PREFIX)) {
throw new IllegalArgumentException("val must start with " + PREFIX);
}
this.keyUri = val;
return this;
}
public AndroidKeystoreKmsClient build() {
return new AndroidKeystoreKmsClient(this);
}
}
/**
* @return true either if {@link AndroidKeystoreKmsClient#keyUri} is not null and equal to {@code
* uri}, or {@link AndroidKeystoreKmsClient#keyUri} is null and {@code uri} starts with {@link
* AndroidKeystoreKmsClient#PREFIX}.
*/
@Override
@RequiresApi(23)
public boolean doesSupport(String uri) {
if (this.keyUri != null && this.keyUri.equals(uri)) {
return true;
}
return this.keyUri == null && uri.toLowerCase(Locale.US).startsWith(PREFIX);
}
/**
* Initializes a {@link KmsClient} for Android Keystore.
*
*
Note that Android Keystore doesn't need credentials, thus the credential path is unused.
*/
@Override
@RequiresApi(23)
public KmsClient withCredentials(String unused) throws GeneralSecurityException {
return new AndroidKeystoreKmsClient();
}
/**
* Initializes a {@code KmsClient} for Android Keystore.
*
*
Note that Android Keystore does not use credentials.
*/
@Override
@RequiresApi(23)
public KmsClient withDefaultCredentials() throws GeneralSecurityException {
return new AndroidKeystoreKmsClient();
}
/**
* Returns an {@link Aead} backed by a key in Android Keystore specified by {@code uri}.
*
*
Since Android Keystore is somewhat unreliable, a self-test is done against the key. This
* will incur a small performance penalty.
*/
@Override
public Aead getAead(String uri) throws GeneralSecurityException {
if (this.keyUri != null && !this.keyUri.equals(uri)) {
throw new GeneralSecurityException(
String.format(
"this client is bound to %s, cannot load keys bound to %s", this.keyUri, uri));
}
try {
synchronized (keystoreLock) {
Aead aead =
new AndroidKeystoreAesGcm(Validators.validateKmsKeyUriAndRemovePrefix(PREFIX, uri));
return validateAead(aead);
}
} catch (IOException ex) {
throw new GeneralSecurityException(ex);
}
}
/** Deletes a key in Android Keystore. */
public void deleteKey(String keyUri) throws GeneralSecurityException {
String keyId = Validators.validateKmsKeyUriAndRemovePrefix(PREFIX, keyUri);
synchronized (keystoreLock) {
AndroidKeystore.deleteKey(keyId);
}
}
/** Returns whether a key exists in Android Keystore. */
boolean hasKey(String keyUri) throws GeneralSecurityException {
String keyId = Validators.validateKmsKeyUriAndRemovePrefix(PREFIX, keyUri);
try {
synchronized (keystoreLock) {
return AndroidKeystore.hasKey(keyId);
}
} catch (NullPointerException ex1) {
Log.w(TAG, "Keystore is temporarily unavailable, wait, reinitialize Keystore and try again.");
sleepRandomAmount();
synchronized (keystoreLock) {
return AndroidKeystore.hasKey(keyId);
}
}
}
private static void sleepRandomAmount() {
int waitTimeMillis = (int) (Math.random() * MAX_WAIT_TIME_MILLISECONDS_BEFORE_RETRY);
try {
Thread.sleep(waitTimeMillis);
} catch (InterruptedException ex) {
// Ignored.
}
}
/**
* Generates a new key in Android Keystore, if it doesn't exist.
*
*
Generates AES256-GCM keys.
*/
@RequiresApi(Build.VERSION_CODES.M)
public static Aead getOrGenerateNewAeadKey(String keyUri)
throws GeneralSecurityException, IOException {
String keyId = Validators.validateKmsKeyUriAndRemovePrefix(PREFIX, keyUri);
synchronized (keystoreLock) {
if (!AndroidKeystore.hasKey(keyId)) {
AndroidKeystore.generateNewAes256GcmKey(keyId);
}
Aead aead = new AndroidKeystoreAesGcm(keyId);
return validateAead(aead);
}
}
/**
* Generates a new key in Android Keystore.
*
*
Generates AES256-GCM keys.
*/
@RequiresApi(Build.VERSION_CODES.M)
public static void generateNewAeadKey(String keyUri) throws GeneralSecurityException {
synchronized (keystoreLock) {
String keyId = Validators.validateKmsKeyUriAndRemovePrefix(PREFIX, keyUri);
if (AndroidKeystore.hasKey(keyId)) {
throw new IllegalArgumentException(
String.format(
"cannot generate a new key %s because it already exists; please delete it with"
+ " deleteKey() and try again",
keyUri));
}
AndroidKeystore.generateNewAes256GcmKey(keyId);
}
}
/**
* Checks if the key exists, and generates a new one if it does not yet exist.
*
*
Returns true if a new key was generated.
*/
@RequiresApi(Build.VERSION_CODES.M)
static boolean generateKeyIfNotExist(String keyUri) throws GeneralSecurityException {
synchronized (keystoreLock) {
String keyId = Validators.validateKmsKeyUriAndRemovePrefix(PREFIX, keyUri);
if (!AndroidKeystore.hasKey(keyId)) {
AndroidKeystore.generateNewAes256GcmKey(keyId);
return true;
}
return false;
}
}
/** Does a self-test to verify whether we can rely on Android Keystore */
@CanIgnoreReturnValue
private static Aead validateAead(Aead aead) throws GeneralSecurityException {
// Non-empty message and empty aad.
// This is a combination that usually fails.
byte[] message = Random.randBytes(10);
byte[] aad = new byte[0];
byte[] ciphertext = aead.encrypt(message, aad);
byte[] decrypted = aead.decrypt(ciphertext, aad);
if (!Arrays.equals(message, decrypted)) {
throw new KeyStoreException(
"cannot use Android Keystore: encryption/decryption of non-empty message and empty"
+ " aad returns an incorrect result");
}
return aead;
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.M)
private static boolean isAtLeastM() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
}