All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.crypto.tink.integration.android.AndroidKeystore Maven / Gradle / Ivy

// Copyright 2024 Google LLC
//
// 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 androidx.annotation.ChecksSdkIntAtLeast;
import androidx.annotation.RequiresApi;
import com.google.crypto.tink.Aead;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;

/**
 * API to store and use AEAD keys in the Android Keystore.
 *
 * 

Android Keystore is only supported on Android M (API level 23) or newer. * *

Warning: This API is not thread-safe. */ public final class AndroidKeystore { /** * Generates a new 256-bit AES-GCM key in Android Keystore, with the given {@code alias}. * *

Warning: Existing keys with the same {@code alias} will be overwritten. */ @RequiresApi(23) public static void generateNewAes256GcmKey(String alias) throws GeneralSecurityException { KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder( alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setKeySize(256) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .build(); generateNewKeyWithSpec(spec); } /** * Generates a new key in Android Keystore with the given {@link KeyGenParameterSpec}. * *

This can be used to generate keys with Android Keystore specific properties. It is the * user's responsibility to ensure that the values in the {@link KeyGenParameterSpec} are * correctly set. * *

Warning: Existing keys with the same {@code alias} will be overwritten. */ @RequiresApi(23) public static void generateNewKeyWithSpec(KeyGenParameterSpec spec) throws GeneralSecurityException { KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); keyGenerator.init(spec); keyGenerator.generateKey(); } /** * Returns an {@link Aead} backed by a key in Android Keystore specified by {@code alias}. * *

Warning: Android Keystore can only handle a limited number of requests in parallel. If too * many calls are made at the same time, both encrypt or decrypt may fail with a {@link * GeneralSecurityException}. But if you avoid calling keystore from many threads at the same * time, then such failures are unlikely and retrying is not necessary. * *

See KeyStore2 for more information. * *

If decryption throws a {@link BadPaddingException} (which includes {@link * AEADBadTagException}), then the ciphertext is not decryptable and retrying will not help. */ public static Aead getAead(String alias) throws GeneralSecurityException { return new AeadImpl(alias, getAndroidKeyStore()); } /** Deletes a key in Android Keystore if it exists. */ public static void deleteKey(String alias) throws GeneralSecurityException { KeyStore keyStore = getAndroidKeyStore(); keyStore.deleteEntry(alias); } /** Returns true if there is a key in Android Keystore. */ public static boolean hasKey(String alias) throws GeneralSecurityException { KeyStore keyStore = getAndroidKeyStore(); return keyStore.containsAlias(alias); } private static KeyStore getAndroidKeyStore() throws GeneralSecurityException { if (!isAtLeastM()) { throw new IllegalStateException("Need Android Keystore on Android M or newer"); } try { KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(/* param= */ null); return keyStore; } catch (IOException ex) { throw new GeneralSecurityException(ex); } } @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.M) private static boolean isAtLeastM() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; } private static final class AeadImpl implements Aead { // All instances of this class use a 12-byte IV and a 16-byte tag. private static final int IV_SIZE_IN_BYTES = 12; private static final int TAG_SIZE_IN_BYTES = 16; private final SecretKey key; public AeadImpl(String alias, KeyStore keyStore) throws GeneralSecurityException { key = (SecretKey) keyStore.getKey(alias, /* password= */ null); if (key == null) { throw new InvalidKeyException("Keystore cannot load the key with ID: " + alias); } } @Override public byte[] encrypt(final byte[] plaintext, final byte[] associatedData) throws GeneralSecurityException { // Check that ciphertext is not longer than the max size of a Java array. if (plaintext.length > Integer.MAX_VALUE - IV_SIZE_IN_BYTES - TAG_SIZE_IN_BYTES) { throw new GeneralSecurityException("plaintext too long"); } // ciphertext gets prefixed with the IV of size IV_SIZE_IN_BYTES. byte[] ciphertext = new byte[IV_SIZE_IN_BYTES + plaintext.length + TAG_SIZE_IN_BYTES]; Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, key); cipher.updateAAD(associatedData); int unusedWritten = cipher.doFinal(plaintext, 0, plaintext.length, ciphertext, IV_SIZE_IN_BYTES); byte[] iv = cipher.getIV(); if (iv.length != IV_SIZE_IN_BYTES) { throw new GeneralSecurityException("IV has unexpected length"); } System.arraycopy(iv, 0, ciphertext, 0, IV_SIZE_IN_BYTES); return ciphertext; } @Override public byte[] decrypt(final byte[] ciphertext, final byte[] associatedData) throws GeneralSecurityException { if (ciphertext.length < IV_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES) { throw new BadPaddingException("ciphertext too short"); } // The first IV_SIZE_IN_BYTES bytes of ciphertext are the IV. GCMParameterSpec params = new GCMParameterSpec(8 * TAG_SIZE_IN_BYTES, ciphertext, 0, IV_SIZE_IN_BYTES); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, key, params); cipher.updateAAD(associatedData); return cipher.doFinal(ciphertext, IV_SIZE_IN_BYTES, ciphertext.length - IV_SIZE_IN_BYTES); } } private AndroidKeystore() {} }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy