blob: 31924bad7507c2dda8b26c3a2de93b4c5bd96301 [file] [log] [blame]
// 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.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Log;
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 java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.util.Arrays;
import java.util.Locale;
import javax.crypto.KeyGenerator;
/**
* An implementation of {@link KmsClient} for <a
* href="https://developer.android.com/training/articles/keystore.html">Android Keystore</a>.
*
* <p>This class requires Android M (API level 23) or newer.
*
* @since 1.0.0
*/
public final class AndroidKeystoreKmsClient implements KmsClient {
private static final String TAG = AndroidKeystoreKmsClient.class.getSimpleName();
/** The prefix of all keys stored in Android Keystore. */
public static final String PREFIX = "android-keystore://";
private final String keyUri;
private final KeyStore keyStore;
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}.
*/
@Deprecated
public AndroidKeystoreKmsClient(String uri) {
this(new Builder().setKeyUri(uri));
}
private AndroidKeystoreKmsClient(Builder builder) {
this.keyUri = builder.keyUri;
this.keyStore = builder.keyStore;
}
/** Builder for AndroidKeystoreKmsClient */
public static final class Builder {
String keyUri = null;
KeyStore keyStore = null;
public Builder() {
if (!isAtLeastM()) {
throw new IllegalStateException("need Android Keystore on Android M or newer");
}
try {
this.keyStore = KeyStore.getInstance("AndroidKeyStore");
this.keyStore.load(null /* param */);
} catch (GeneralSecurityException | IOException ex) {
throw new IllegalStateException(ex);
}
}
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 Builder setKeyStore(KeyStore val) {
if (val == null) {
throw new IllegalArgumentException("val cannot be null");
}
this.keyStore = 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
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.
*
* <p>Note that Android Keystore doesn't need credentials, thus the credential path is unused.
*/
@Override
public KmsClient withCredentials(String unused) throws GeneralSecurityException {
return new AndroidKeystoreKmsClient();
}
/**
* Initializes a {@code KmsClient} for Android Keystore.
*
* <p>Note that Android Keystore does not use credentials.
*/
@Override
public KmsClient withDefaultCredentials() throws GeneralSecurityException {
return new AndroidKeystoreKmsClient();
}
/**
* Returns an {@link Aead} backed by a key in Android Keystore specified by {@code uri}.
*
* <p>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));
}
Aead aead =
new AndroidKeystoreAesGcm(
Validators.validateKmsKeyUriAndRemovePrefix(PREFIX, uri), keyStore);
return validateAead(aead);
}
/** Deletes a key in Android Keystore. */
public void deleteKey(String keyUri) throws GeneralSecurityException {
String keyId = Validators.validateKmsKeyUriAndRemovePrefix(PREFIX, keyUri);
keyStore.deleteEntry(keyId);
}
/** Returns whether a key exists in Android Keystore. */
boolean hasKey(String keyUri) throws GeneralSecurityException {
String keyId = Validators.validateKmsKeyUriAndRemovePrefix(PREFIX, keyUri);
return keyStore.containsAlias(keyId);
}
/**
* Generates a new key in Android Keystore, if it doesn't exist.
*
* <p>At the moment it can generate only AES256-GCM keys.
*/
public static Aead getOrGenerateNewAeadKey(String keyUri)
throws GeneralSecurityException, IOException {
AndroidKeystoreKmsClient client = new AndroidKeystoreKmsClient();
if (!client.hasKey(keyUri)) {
Log.w(TAG, String.format("key URI %s doesn't exist, generating a new one", keyUri));
generateNewAeadKey(keyUri);
}
return client.getAead(keyUri);
}
/**
* Generates a new key in Android Keystore.
*
* <p>At the moment it can generate only AES256-GCM keys.
*/
public static void generateNewAeadKey(String keyUri)
throws GeneralSecurityException {
AndroidKeystoreKmsClient client = new AndroidKeystoreKmsClient();
if (client.hasKey(keyUri)) {
throw new IllegalArgumentException(
String.format(
"cannot generate a new key %s because it already exists; please delete it with"
+ " deleteKey() and try again",
keyUri));
}
String keyId = Validators.validateKmsKeyUriAndRemovePrefix(PREFIX, keyUri);
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
KeyGenParameterSpec spec =
new KeyGenParameterSpec.Builder(keyId,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setKeySize(256)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build();
keyGenerator.init(spec);
keyGenerator.generateKey();
}
/** Does a self-test to verify whether we can rely on Android Keystore */
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;
}
private static boolean isAtLeastM() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
}