diff options
29 files changed, 8178 insertions, 0 deletions
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStore3DESCipherSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStore3DESCipherSpi.java new file mode 100644 index 000000000000..275dcef8a78c --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStore3DESCipherSpi.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2018 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 android.security.keystore2; + +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.ArrayUtils; +import android.security.keystore.KeyProperties; + +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.ProviderException; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidParameterSpecException; +import java.util.Arrays; + +import javax.crypto.CipherSpi; +import javax.crypto.spec.IvParameterSpec; + +/** + * Base class for Android Keystore 3DES {@link CipherSpi} implementations. + * + * @hide + */ +public class AndroidKeyStore3DESCipherSpi extends AndroidKeyStoreCipherSpiBase { + + private static final int BLOCK_SIZE_BYTES = 8; + + private final int mKeymasterBlockMode; + private final int mKeymasterPadding; + /** Whether this transformation requires an IV. */ + private final boolean mIvRequired; + + private byte[] mIv; + + /** Whether the current {@code #mIv} has been used by the underlying crypto operation. */ + private boolean mIvHasBeenUsed; + + AndroidKeyStore3DESCipherSpi( + int keymasterBlockMode, + int keymasterPadding, + boolean ivRequired) { + mKeymasterBlockMode = keymasterBlockMode; + mKeymasterPadding = keymasterPadding; + mIvRequired = ivRequired; + } + + abstract static class ECB extends AndroidKeyStore3DESCipherSpi { + protected ECB(int keymasterPadding) { + super(KeymasterDefs.KM_MODE_ECB, keymasterPadding, false); + } + + public static class NoPadding extends + AndroidKeyStore3DESCipherSpi.ECB { + public NoPadding() { + super(KeymasterDefs.KM_PAD_NONE); + } + } + + public static class PKCS7Padding extends + AndroidKeyStore3DESCipherSpi.ECB { + public PKCS7Padding() { + super(KeymasterDefs.KM_PAD_PKCS7); + } + } + } + + abstract static class CBC extends AndroidKeyStore3DESCipherSpi { + protected CBC(int keymasterPadding) { + super(KeymasterDefs.KM_MODE_CBC, keymasterPadding, true); + } + + public static class NoPadding extends + AndroidKeyStore3DESCipherSpi.CBC { + public NoPadding() { + super(KeymasterDefs.KM_PAD_NONE); + } + } + + public static class PKCS7Padding extends + AndroidKeyStore3DESCipherSpi.CBC { + public PKCS7Padding() { + super(KeymasterDefs.KM_PAD_PKCS7); + } + } + } + + @Override + protected void initKey(int i, Key key) throws InvalidKeyException { + if (!(key instanceof AndroidKeyStoreSecretKey)) { + throw new InvalidKeyException( + "Unsupported key: " + ((key != null) ? key.getClass().getName() : "null")); + } + if (!KeyProperties.KEY_ALGORITHM_3DES.equalsIgnoreCase(key.getAlgorithm())) { + throw new InvalidKeyException( + "Unsupported key algorithm: " + key.getAlgorithm() + ". Only " + + KeyProperties.KEY_ALGORITHM_3DES + " supported"); + } + setKey((AndroidKeyStoreSecretKey) key); + } + + @Override + protected int engineGetBlockSize() { + return BLOCK_SIZE_BYTES; + } + + @Override + protected int engineGetOutputSize(int inputLen) { + return inputLen + 3 * BLOCK_SIZE_BYTES; + } + + @Override + protected final byte[] engineGetIV() { + return ArrayUtils.cloneIfNotEmpty(mIv); + } + + @Override + protected AlgorithmParameters engineGetParameters() { + if (!mIvRequired) { + return null; + } + if ((mIv != null) && (mIv.length > 0)) { + try { + AlgorithmParameters params = AlgorithmParameters.getInstance("DESede"); + params.init(new IvParameterSpec(mIv)); + return params; + } catch (NoSuchAlgorithmException e) { + throw new ProviderException( + "Failed to obtain 3DES AlgorithmParameters", e); + } catch (InvalidParameterSpecException e) { + throw new ProviderException( + "Failed to initialize 3DES AlgorithmParameters with an IV", + e); + } + } + return null; + } + + @Override + protected void initAlgorithmSpecificParameters() throws InvalidKeyException { + if (!mIvRequired) { + return; + } + + // IV is used + if (!isEncrypting()) { + throw new InvalidKeyException("IV required when decrypting" + + ". Use IvParameterSpec or AlgorithmParameters to provide it."); + } + } + + @Override + protected void initAlgorithmSpecificParameters(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + if (!mIvRequired) { + if (params != null) { + throw new InvalidAlgorithmParameterException("Unsupported parameters: " + params); + } + return; + } + + // IV is used + if (params == null) { + if (!isEncrypting()) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException( + "IvParameterSpec must be provided when decrypting"); + } + return; + } + if (!(params instanceof IvParameterSpec)) { + throw new InvalidAlgorithmParameterException("Only IvParameterSpec supported"); + } + mIv = ((IvParameterSpec) params).getIV(); + if (mIv == null) { + throw new InvalidAlgorithmParameterException("Null IV in IvParameterSpec"); + } + } + + @Override + protected void initAlgorithmSpecificParameters(AlgorithmParameters params) + throws InvalidAlgorithmParameterException { + if (!mIvRequired) { + if (params != null) { + throw new InvalidAlgorithmParameterException("Unsupported parameters: " + params); + } + return; + } + + // IV is used + if (params == null) { + if (!isEncrypting()) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException("IV required when decrypting" + + ". Use IvParameterSpec or AlgorithmParameters to provide it."); + } + return; + } + + if (!"DESede".equalsIgnoreCase(params.getAlgorithm())) { + throw new InvalidAlgorithmParameterException( + "Unsupported AlgorithmParameters algorithm: " + params.getAlgorithm() + + ". Supported: DESede"); + } + + IvParameterSpec ivSpec; + try { + ivSpec = params.getParameterSpec(IvParameterSpec.class); + } catch (InvalidParameterSpecException e) { + if (!isEncrypting()) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException("IV required when decrypting" + + ", but not found in parameters: " + params, e); + } + mIv = null; + return; + } + mIv = ivSpec.getIV(); + if (mIv == null) { + throw new InvalidAlgorithmParameterException("Null IV in AlgorithmParameters"); + } + } + + @Override + protected final int getAdditionalEntropyAmountForBegin() { + if ((mIvRequired) && (mIv == null) && (isEncrypting())) { + // IV will need to be generated + return BLOCK_SIZE_BYTES; + } + + return 0; + } + + @Override + protected int getAdditionalEntropyAmountForFinish() { + return 0; + } + + @Override + protected void addAlgorithmSpecificParametersToBegin(KeymasterArguments keymasterArgs) { + if ((isEncrypting()) && (mIvRequired) && (mIvHasBeenUsed)) { + // IV is being reused for encryption: this violates security best practices. + throw new IllegalStateException( + "IV has already been used. Reusing IV in encryption mode violates security best" + + " practices."); + } + + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_3DES); + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_BLOCK_MODE, mKeymasterBlockMode); + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_PADDING, mKeymasterPadding); + if ((mIvRequired) && (mIv != null)) { + keymasterArgs.addBytes(KeymasterDefs.KM_TAG_NONCE, mIv); + } + } + + @Override + protected void loadAlgorithmSpecificParametersFromBeginResult( + KeymasterArguments keymasterArgs) { + mIvHasBeenUsed = true; + + // NOTE: Keymaster doesn't always return an IV, even if it's used. + byte[] returnedIv = keymasterArgs.getBytes(KeymasterDefs.KM_TAG_NONCE, null); + if ((returnedIv != null) && (returnedIv.length == 0)) { + returnedIv = null; + } + + if (mIvRequired) { + if (mIv == null) { + mIv = returnedIv; + } else if ((returnedIv != null) && (!Arrays.equals(returnedIv, mIv))) { + throw new ProviderException("IV in use differs from provided IV"); + } + } else { + if (returnedIv != null) { + throw new ProviderException( + "IV in use despite IV not being used by this transformation"); + } + } + } + + @Override + protected final void resetAll() { + mIv = null; + mIvHasBeenUsed = false; + super.resetAll(); + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreAuthenticatedAESCipherSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreAuthenticatedAESCipherSpi.java new file mode 100644 index 000000000000..43381c0078d9 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreAuthenticatedAESCipherSpi.java @@ -0,0 +1,454 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.IBinder; +import android.security.KeyStore; +import android.security.KeyStoreException; +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterDefs; +import android.security.keymaster.OperationResult; +import android.security.keystore.ArrayUtils; +import android.security.keystore.KeyProperties; +import android.security.keystore2.KeyStoreCryptoOperationChunkedStreamer.Stream; + +import libcore.util.EmptyArray; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.ProviderException; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidParameterSpecException; +import java.util.Arrays; + +import javax.crypto.CipherSpi; +import javax.crypto.spec.GCMParameterSpec; + +/** + * Base class for Android Keystore authenticated AES {@link CipherSpi} implementations. + * + * @hide + */ +abstract class AndroidKeyStoreAuthenticatedAESCipherSpi extends AndroidKeyStoreCipherSpiBase { + + abstract static class GCM extends AndroidKeyStoreAuthenticatedAESCipherSpi { + static final int MIN_SUPPORTED_TAG_LENGTH_BITS = 96; + private static final int MAX_SUPPORTED_TAG_LENGTH_BITS = 128; + private static final int DEFAULT_TAG_LENGTH_BITS = 128; + private static final int IV_LENGTH_BYTES = 12; + + private int mTagLengthBits = DEFAULT_TAG_LENGTH_BITS; + + GCM(int keymasterPadding) { + super(KeymasterDefs.KM_MODE_GCM, keymasterPadding); + } + + @Override + protected final void resetAll() { + mTagLengthBits = DEFAULT_TAG_LENGTH_BITS; + super.resetAll(); + } + + @Override + protected final void resetWhilePreservingInitState() { + super.resetWhilePreservingInitState(); + } + + @Override + protected final void initAlgorithmSpecificParameters() throws InvalidKeyException { + if (!isEncrypting()) { + throw new InvalidKeyException("IV required when decrypting" + + ". Use IvParameterSpec or AlgorithmParameters to provide it."); + } + } + + @Override + protected final void initAlgorithmSpecificParameters(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + // IV is used + if (params == null) { + if (!isEncrypting()) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException( + "GCMParameterSpec must be provided when decrypting"); + } + return; + } + if (!(params instanceof GCMParameterSpec)) { + throw new InvalidAlgorithmParameterException("Only GCMParameterSpec supported"); + } + GCMParameterSpec spec = (GCMParameterSpec) params; + byte[] iv = spec.getIV(); + if (iv == null) { + throw new InvalidAlgorithmParameterException("Null IV in GCMParameterSpec"); + } else if (iv.length != IV_LENGTH_BYTES) { + throw new InvalidAlgorithmParameterException("Unsupported IV length: " + + iv.length + " bytes. Only " + IV_LENGTH_BYTES + + " bytes long IV supported"); + } + int tagLengthBits = spec.getTLen(); + if ((tagLengthBits < MIN_SUPPORTED_TAG_LENGTH_BITS) + || (tagLengthBits > MAX_SUPPORTED_TAG_LENGTH_BITS) + || ((tagLengthBits % 8) != 0)) { + throw new InvalidAlgorithmParameterException( + "Unsupported tag length: " + tagLengthBits + " bits" + + ". Supported lengths: 96, 104, 112, 120, 128"); + } + setIv(iv); + mTagLengthBits = tagLengthBits; + } + + @Override + protected final void initAlgorithmSpecificParameters(AlgorithmParameters params) + throws InvalidAlgorithmParameterException { + if (params == null) { + if (!isEncrypting()) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException("IV required when decrypting" + + ". Use GCMParameterSpec or GCM AlgorithmParameters to provide it."); + } + return; + } + + if (!"GCM".equalsIgnoreCase(params.getAlgorithm())) { + throw new InvalidAlgorithmParameterException( + "Unsupported AlgorithmParameters algorithm: " + params.getAlgorithm() + + ". Supported: GCM"); + } + + GCMParameterSpec spec; + try { + spec = params.getParameterSpec(GCMParameterSpec.class); + } catch (InvalidParameterSpecException e) { + if (!isEncrypting()) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException("IV and tag length required when" + + " decrypting, but not found in parameters: " + params, e); + } + setIv(null); + return; + } + initAlgorithmSpecificParameters(spec); + } + + @Nullable + @Override + protected final AlgorithmParameters engineGetParameters() { + byte[] iv = getIv(); + if ((iv != null) && (iv.length > 0)) { + try { + AlgorithmParameters params = AlgorithmParameters.getInstance("GCM"); + params.init(new GCMParameterSpec(mTagLengthBits, iv)); + return params; + } catch (NoSuchAlgorithmException e) { + throw new ProviderException( + "Failed to obtain GCM AlgorithmParameters", e); + } catch (InvalidParameterSpecException e) { + throw new ProviderException( + "Failed to initialize GCM AlgorithmParameters", e); + } + } + return null; + } + + @NonNull + @Override + protected KeyStoreCryptoOperationStreamer createMainDataStreamer( + KeyStore keyStore, IBinder operationToken) { + KeyStoreCryptoOperationStreamer + streamer = new KeyStoreCryptoOperationChunkedStreamer( + new KeyStoreCryptoOperationChunkedStreamer.MainDataStream( + keyStore, operationToken), 0); + if (isEncrypting()) { + return streamer; + } else { + // When decrypting, to avoid leaking unauthenticated plaintext, do not return any + // plaintext before ciphertext is authenticated by KeyStore.finish. + return new AndroidKeyStoreAuthenticatedAESCipherSpi.BufferAllOutputUntilDoFinalStreamer(streamer); + } + } + + @NonNull + @Override + protected final KeyStoreCryptoOperationStreamer createAdditionalAuthenticationDataStreamer( + KeyStore keyStore, IBinder operationToken) { + return new KeyStoreCryptoOperationChunkedStreamer( + new AndroidKeyStoreAuthenticatedAESCipherSpi.AdditionalAuthenticationDataStream(keyStore, operationToken), 0); + } + + @Override + protected final int getAdditionalEntropyAmountForBegin() { + if ((getIv() == null) && (isEncrypting())) { + // IV will need to be generated + return IV_LENGTH_BYTES; + } + + return 0; + } + + @Override + protected final int getAdditionalEntropyAmountForFinish() { + return 0; + } + + @Override + protected final void addAlgorithmSpecificParametersToBegin( + @NonNull KeymasterArguments keymasterArgs) { + super.addAlgorithmSpecificParametersToBegin(keymasterArgs); + keymasterArgs.addUnsignedInt(KeymasterDefs.KM_TAG_MAC_LENGTH, mTagLengthBits); + } + + protected final int getTagLengthBits() { + return mTagLengthBits; + } + + public static final class NoPadding extends + AndroidKeyStoreAuthenticatedAESCipherSpi.GCM { + public NoPadding() { + super(KeymasterDefs.KM_PAD_NONE); + } + + @Override + protected final int engineGetOutputSize(int inputLen) { + int tagLengthBytes = (getTagLengthBits() + 7) / 8; + long result; + if (isEncrypting()) { + result = getConsumedInputSizeBytes() - getProducedOutputSizeBytes() + inputLen + + tagLengthBytes; + } else { + result = getConsumedInputSizeBytes() - getProducedOutputSizeBytes() + inputLen + - tagLengthBytes; + } + if (result < 0) { + return 0; + } else if (result > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + return (int) result; + } + } + } + + private static final int BLOCK_SIZE_BYTES = 16; + + private final int mKeymasterBlockMode; + private final int mKeymasterPadding; + + private byte[] mIv; + + /** Whether the current {@code #mIv} has been used by the underlying crypto operation. */ + private boolean mIvHasBeenUsed; + + AndroidKeyStoreAuthenticatedAESCipherSpi( + int keymasterBlockMode, + int keymasterPadding) { + mKeymasterBlockMode = keymasterBlockMode; + mKeymasterPadding = keymasterPadding; + } + + @Override + protected void resetAll() { + mIv = null; + mIvHasBeenUsed = false; + super.resetAll(); + } + + @Override + protected final void initKey(int opmode, Key key) throws InvalidKeyException { + if (!(key instanceof AndroidKeyStoreSecretKey)) { + throw new InvalidKeyException( + "Unsupported key: " + ((key != null) ? key.getClass().getName() : "null")); + } + if (!KeyProperties.KEY_ALGORITHM_AES.equalsIgnoreCase(key.getAlgorithm())) { + throw new InvalidKeyException( + "Unsupported key algorithm: " + key.getAlgorithm() + ". Only " + + KeyProperties.KEY_ALGORITHM_AES + " supported"); + } + setKey((AndroidKeyStoreSecretKey) key); + } + + @Override + protected void addAlgorithmSpecificParametersToBegin( + @NonNull KeymasterArguments keymasterArgs) { + if ((isEncrypting()) && (mIvHasBeenUsed)) { + // IV is being reused for encryption: this violates security best practices. + throw new IllegalStateException( + "IV has already been used. Reusing IV in encryption mode violates security best" + + " practices."); + } + + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_AES); + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_BLOCK_MODE, mKeymasterBlockMode); + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_PADDING, mKeymasterPadding); + if (mIv != null) { + keymasterArgs.addBytes(KeymasterDefs.KM_TAG_NONCE, mIv); + } + } + + @Override + protected final void loadAlgorithmSpecificParametersFromBeginResult( + @NonNull KeymasterArguments keymasterArgs) { + mIvHasBeenUsed = true; + + // NOTE: Keymaster doesn't always return an IV, even if it's used. + byte[] returnedIv = keymasterArgs.getBytes(KeymasterDefs.KM_TAG_NONCE, null); + if ((returnedIv != null) && (returnedIv.length == 0)) { + returnedIv = null; + } + + if (mIv == null) { + mIv = returnedIv; + } else if ((returnedIv != null) && (!Arrays.equals(returnedIv, mIv))) { + throw new ProviderException("IV in use differs from provided IV"); + } + } + + @Override + protected final int engineGetBlockSize() { + return BLOCK_SIZE_BYTES; + } + + @Override + protected final byte[] engineGetIV() { + return ArrayUtils.cloneIfNotEmpty(mIv); + } + + protected void setIv(byte[] iv) { + mIv = iv; + } + + protected byte[] getIv() { + return mIv; + } + + /** + * {@link KeyStoreCryptoOperationStreamer} which buffers all output until {@code doFinal} from + * which it returns all output in one go, provided {@code doFinal} succeeds. + */ + private static class BufferAllOutputUntilDoFinalStreamer + implements KeyStoreCryptoOperationStreamer { + + private final KeyStoreCryptoOperationStreamer mDelegate; + private ByteArrayOutputStream mBufferedOutput = new ByteArrayOutputStream(); + private long mProducedOutputSizeBytes; + + private BufferAllOutputUntilDoFinalStreamer( + KeyStoreCryptoOperationStreamer delegate) { + mDelegate = delegate; + } + + @Override + public byte[] update(byte[] input, int inputOffset, int inputLength) + throws KeyStoreException { + byte[] output = mDelegate.update(input, inputOffset, inputLength); + if (output != null) { + try { + mBufferedOutput.write(output); + } catch (IOException e) { + throw new ProviderException("Failed to buffer output", e); + } + } + return EmptyArray.BYTE; + } + + @Override + public byte[] doFinal(byte[] input, int inputOffset, int inputLength, + byte[] signature, byte[] additionalEntropy) throws KeyStoreException { + byte[] output = mDelegate.doFinal(input, inputOffset, inputLength, signature, + additionalEntropy); + if (output != null) { + try { + mBufferedOutput.write(output); + } catch (IOException e) { + throw new ProviderException("Failed to buffer output", e); + } + } + byte[] result = mBufferedOutput.toByteArray(); + mBufferedOutput.reset(); + mProducedOutputSizeBytes += result.length; + return result; + } + + @Override + public long getConsumedInputSizeBytes() { + return mDelegate.getConsumedInputSizeBytes(); + } + + @Override + public long getProducedOutputSizeBytes() { + return mProducedOutputSizeBytes; + } + } + + /** + * Additional Authentication Data (AAD) stream via a KeyStore streaming operation. This stream + * sends AAD into the KeyStore. + */ + private static class AdditionalAuthenticationDataStream implements Stream { + + private final KeyStore mKeyStore; + private final IBinder mOperationToken; + + private AdditionalAuthenticationDataStream(KeyStore keyStore, IBinder operationToken) { + mKeyStore = keyStore; + mOperationToken = operationToken; + } + + @Override + public OperationResult update(byte[] input) { + KeymasterArguments keymasterArgs = new KeymasterArguments(); + keymasterArgs.addBytes(KeymasterDefs.KM_TAG_ASSOCIATED_DATA, input); + + // KeyStore does not reflect AAD in inputConsumed, but users of Stream rely on this + // field. We fix this discrepancy here. KeyStore.update contract is that all of AAD + // has been consumed if the method succeeds. + OperationResult result = mKeyStore.update(mOperationToken, keymasterArgs, null); + if (result.resultCode == KeyStore.NO_ERROR) { + result = new OperationResult( + result.resultCode, + result.token, + result.operationHandle, + input.length, // inputConsumed + result.output, + result.outParams); + } + return result; + } + + @Override + public OperationResult finish(byte[] input, byte[] signature, byte[] additionalEntropy) { + if ((additionalEntropy != null) && (additionalEntropy.length > 0)) { + throw new ProviderException("AAD stream does not support additional entropy"); + } + return new OperationResult( + KeyStore.NO_ERROR, + mOperationToken, + 0, // operation handle -- nobody cares about this being returned from finish + 0, // inputConsumed + EmptyArray.BYTE, // output + new KeymasterArguments() // additional params returned by finish + ); + } + } +}
\ No newline at end of file diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreBCWorkaroundProvider.java b/keystore/java/android/security/keystore2/AndroidKeyStoreBCWorkaroundProvider.java new file mode 100644 index 000000000000..5b6971007077 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreBCWorkaroundProvider.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import java.security.Provider; + +/** + * {@link Provider} of JCA crypto operations operating on Android KeyStore keys. + * + * <p>This provider was separated out of {@link AndroidKeyStoreProvider} to work around the issue + * that Bouncy Castle provider incorrectly declares that it accepts arbitrary keys (incl. Android + * KeyStore ones). This causes JCA to select the Bouncy Castle's implementation of JCA crypto + * operations for Android KeyStore keys unless Android KeyStore's own implementations are installed + * as higher-priority than Bouncy Castle ones. The purpose of this provider is to do just that: to + * offer crypto operations operating on Android KeyStore keys and to be installed at higher priority + * than the Bouncy Castle provider. + * + * <p>Once Bouncy Castle provider is fixed, this provider can be merged into the + * {@code AndroidKeyStoreProvider}. + * + * @hide + */ +class AndroidKeyStoreBCWorkaroundProvider extends Provider { + + // IMPLEMENTATION NOTE: Class names are hard-coded in this provider to avoid loading these + // classes when this provider is instantiated and installed early on during each app's + // initialization process. + + private static final String PACKAGE_NAME = "android.security.keystore"; + private static final String KEYSTORE_SECRET_KEY_CLASS_NAME = + PACKAGE_NAME + ".AndroidKeyStoreSecretKey"; + private static final String KEYSTORE_PRIVATE_KEY_CLASS_NAME = + PACKAGE_NAME + ".AndroidKeyStorePrivateKey"; + private static final String KEYSTORE_PUBLIC_KEY_CLASS_NAME = + PACKAGE_NAME + ".AndroidKeyStorePublicKey"; + + private static final String DESEDE_SYSTEM_PROPERTY = "ro.hardware.keystore_desede"; + + AndroidKeyStoreBCWorkaroundProvider() { + super("AndroidKeyStoreBCWorkaround", + 1.0, + "Android KeyStore security provider to work around Bouncy Castle"); + + // --------------------- javax.crypto.Mac + putMacImpl("HmacSHA1", PACKAGE_NAME + ".AndroidKeyStoreHmacSpi$HmacSHA1"); + put("Alg.Alias.Mac.1.2.840.113549.2.7", "HmacSHA1"); + put("Alg.Alias.Mac.HMAC-SHA1", "HmacSHA1"); + put("Alg.Alias.Mac.HMAC/SHA1", "HmacSHA1"); + + putMacImpl("HmacSHA224", PACKAGE_NAME + ".AndroidKeyStoreHmacSpi$HmacSHA224"); + put("Alg.Alias.Mac.1.2.840.113549.2.9", "HmacSHA224"); + put("Alg.Alias.Mac.HMAC-SHA224", "HmacSHA224"); + put("Alg.Alias.Mac.HMAC/SHA224", "HmacSHA224"); + + putMacImpl("HmacSHA256", PACKAGE_NAME + ".AndroidKeyStoreHmacSpi$HmacSHA256"); + put("Alg.Alias.Mac.1.2.840.113549.2.9", "HmacSHA256"); + put("Alg.Alias.Mac.HMAC-SHA256", "HmacSHA256"); + put("Alg.Alias.Mac.HMAC/SHA256", "HmacSHA256"); + + putMacImpl("HmacSHA384", PACKAGE_NAME + ".AndroidKeyStoreHmacSpi$HmacSHA384"); + put("Alg.Alias.Mac.1.2.840.113549.2.10", "HmacSHA384"); + put("Alg.Alias.Mac.HMAC-SHA384", "HmacSHA384"); + put("Alg.Alias.Mac.HMAC/SHA384", "HmacSHA384"); + + putMacImpl("HmacSHA512", PACKAGE_NAME + ".AndroidKeyStoreHmacSpi$HmacSHA512"); + put("Alg.Alias.Mac.1.2.840.113549.2.11", "HmacSHA512"); + put("Alg.Alias.Mac.HMAC-SHA512", "HmacSHA512"); + put("Alg.Alias.Mac.HMAC/SHA512", "HmacSHA512"); + + // --------------------- javax.crypto.Cipher + putSymmetricCipherImpl("AES/ECB/NoPadding", + PACKAGE_NAME + ".AndroidKeyStoreUnauthenticatedAESCipherSpi$ECB$NoPadding"); + putSymmetricCipherImpl("AES/ECB/PKCS7Padding", + PACKAGE_NAME + ".AndroidKeyStoreUnauthenticatedAESCipherSpi$ECB$PKCS7Padding"); + + putSymmetricCipherImpl("AES/CBC/NoPadding", + PACKAGE_NAME + ".AndroidKeyStoreUnauthenticatedAESCipherSpi$CBC$NoPadding"); + putSymmetricCipherImpl("AES/CBC/PKCS7Padding", + PACKAGE_NAME + ".AndroidKeyStoreUnauthenticatedAESCipherSpi$CBC$PKCS7Padding"); + + putSymmetricCipherImpl("AES/CTR/NoPadding", + PACKAGE_NAME + ".AndroidKeyStoreUnauthenticatedAESCipherSpi$CTR$NoPadding"); + + if ("true".equals(android.os.SystemProperties.get(DESEDE_SYSTEM_PROPERTY))) { + putSymmetricCipherImpl("DESede/CBC/NoPadding", + PACKAGE_NAME + ".AndroidKeyStore3DESCipherSpi$CBC$NoPadding"); + putSymmetricCipherImpl("DESede/CBC/PKCS7Padding", + PACKAGE_NAME + ".AndroidKeyStore3DESCipherSpi$CBC$PKCS7Padding"); + + putSymmetricCipherImpl("DESede/ECB/NoPadding", + PACKAGE_NAME + ".AndroidKeyStore3DESCipherSpi$ECB$NoPadding"); + putSymmetricCipherImpl("DESede/ECB/PKCS7Padding", + PACKAGE_NAME + ".AndroidKeyStore3DESCipherSpi$ECB$PKCS7Padding"); + } + + putSymmetricCipherImpl("AES/GCM/NoPadding", + PACKAGE_NAME + ".AndroidKeyStoreAuthenticatedAESCipherSpi$GCM$NoPadding"); + + putAsymmetricCipherImpl("RSA/ECB/NoPadding", + PACKAGE_NAME + ".AndroidKeyStoreRSACipherSpi$NoPadding"); + put("Alg.Alias.Cipher.RSA/None/NoPadding", "RSA/ECB/NoPadding"); + putAsymmetricCipherImpl("RSA/ECB/PKCS1Padding", + PACKAGE_NAME + ".AndroidKeyStoreRSACipherSpi$PKCS1Padding"); + put("Alg.Alias.Cipher.RSA/None/PKCS1Padding", "RSA/ECB/PKCS1Padding"); + putAsymmetricCipherImpl("RSA/ECB/OAEPPadding", + PACKAGE_NAME + ".AndroidKeyStoreRSACipherSpi$OAEPWithSHA1AndMGF1Padding"); + put("Alg.Alias.Cipher.RSA/None/OAEPPadding", "RSA/ECB/OAEPPadding"); + putAsymmetricCipherImpl("RSA/ECB/OAEPWithSHA-1AndMGF1Padding", + PACKAGE_NAME + ".AndroidKeyStoreRSACipherSpi$OAEPWithSHA1AndMGF1Padding"); + put("Alg.Alias.Cipher.RSA/None/OAEPWithSHA-1AndMGF1Padding", + "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); + putAsymmetricCipherImpl("RSA/ECB/OAEPWithSHA-224AndMGF1Padding", + PACKAGE_NAME + ".AndroidKeyStoreRSACipherSpi$OAEPWithSHA224AndMGF1Padding"); + put("Alg.Alias.Cipher.RSA/None/OAEPWithSHA-224AndMGF1Padding", + "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); + putAsymmetricCipherImpl("RSA/ECB/OAEPWithSHA-256AndMGF1Padding", + PACKAGE_NAME + ".AndroidKeyStoreRSACipherSpi$OAEPWithSHA256AndMGF1Padding"); + put("Alg.Alias.Cipher.RSA/None/OAEPWithSHA-256AndMGF1Padding", + "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); + putAsymmetricCipherImpl("RSA/ECB/OAEPWithSHA-384AndMGF1Padding", + PACKAGE_NAME + ".AndroidKeyStoreRSACipherSpi$OAEPWithSHA384AndMGF1Padding"); + put("Alg.Alias.Cipher.RSA/None/OAEPWithSHA-384AndMGF1Padding", + "RSA/ECB/OAEPWithSHA-384AndMGF1Padding"); + putAsymmetricCipherImpl("RSA/ECB/OAEPWithSHA-512AndMGF1Padding", + PACKAGE_NAME + ".AndroidKeyStoreRSACipherSpi$OAEPWithSHA512AndMGF1Padding"); + put("Alg.Alias.Cipher.RSA/None/OAEPWithSHA-512AndMGF1Padding", + "RSA/ECB/OAEPWithSHA-512AndMGF1Padding"); + + // --------------------- java.security.Signature + putSignatureImpl("NONEwithRSA", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$NONEWithPKCS1Padding"); + + putSignatureImpl("MD5withRSA", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$MD5WithPKCS1Padding"); + put("Alg.Alias.Signature.MD5WithRSAEncryption", "MD5withRSA"); + put("Alg.Alias.Signature.MD5/RSA", "MD5withRSA"); + put("Alg.Alias.Signature.1.2.840.113549.1.1.4", "MD5withRSA"); + put("Alg.Alias.Signature.1.2.840.113549.2.5with1.2.840.113549.1.1.1", "MD5withRSA"); + + putSignatureImpl("SHA1withRSA", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA1WithPKCS1Padding"); + put("Alg.Alias.Signature.SHA1WithRSAEncryption", "SHA1withRSA"); + put("Alg.Alias.Signature.SHA1/RSA", "SHA1withRSA"); + put("Alg.Alias.Signature.SHA-1/RSA", "SHA1withRSA"); + put("Alg.Alias.Signature.1.2.840.113549.1.1.5", "SHA1withRSA"); + put("Alg.Alias.Signature.1.3.14.3.2.26with1.2.840.113549.1.1.1", "SHA1withRSA"); + put("Alg.Alias.Signature.1.3.14.3.2.26with1.2.840.113549.1.1.5", "SHA1withRSA"); + put("Alg.Alias.Signature.1.3.14.3.2.29", "SHA1withRSA"); + + putSignatureImpl("SHA224withRSA", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA224WithPKCS1Padding"); + put("Alg.Alias.Signature.SHA224WithRSAEncryption", "SHA224withRSA"); + put("Alg.Alias.Signature.1.2.840.113549.1.1.11", "SHA224withRSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.4with1.2.840.113549.1.1.1", + "SHA224withRSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.4with1.2.840.113549.1.1.11", + "SHA224withRSA"); + + putSignatureImpl("SHA256withRSA", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA256WithPKCS1Padding"); + put("Alg.Alias.Signature.SHA256WithRSAEncryption", "SHA256withRSA"); + put("Alg.Alias.Signature.1.2.840.113549.1.1.11", "SHA256withRSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.1with1.2.840.113549.1.1.1", + "SHA256withRSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.1with1.2.840.113549.1.1.11", + "SHA256withRSA"); + + putSignatureImpl("SHA384withRSA", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA384WithPKCS1Padding"); + put("Alg.Alias.Signature.SHA384WithRSAEncryption", "SHA384withRSA"); + put("Alg.Alias.Signature.1.2.840.113549.1.1.12", "SHA384withRSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.2with1.2.840.113549.1.1.1", + "SHA384withRSA"); + + putSignatureImpl("SHA512withRSA", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA512WithPKCS1Padding"); + put("Alg.Alias.Signature.SHA512WithRSAEncryption", "SHA512withRSA"); + put("Alg.Alias.Signature.1.2.840.113549.1.1.13", "SHA512withRSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.3with1.2.840.113549.1.1.1", + "SHA512withRSA"); + + putSignatureImpl("SHA1withRSA/PSS", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA1WithPSSPadding"); + putSignatureImpl("SHA224withRSA/PSS", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA224WithPSSPadding"); + putSignatureImpl("SHA256withRSA/PSS", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA256WithPSSPadding"); + putSignatureImpl("SHA384withRSA/PSS", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA384WithPSSPadding"); + putSignatureImpl("SHA512withRSA/PSS", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA512WithPSSPadding"); + + putSignatureImpl("NONEwithECDSA", + PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$NONE"); + + putSignatureImpl("SHA1withECDSA", PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$SHA1"); + put("Alg.Alias.Signature.ECDSA", "SHA1withECDSA"); + put("Alg.Alias.Signature.ECDSAwithSHA1", "SHA1withECDSA"); + // iso(1) member-body(2) us(840) ansi-x962(10045) signatures(4) ecdsa-with-SHA1(1) + put("Alg.Alias.Signature.1.2.840.10045.4.1", "SHA1withECDSA"); + put("Alg.Alias.Signature.1.3.14.3.2.26with1.2.840.10045.2.1", "SHA1withECDSA"); + + // iso(1) member-body(2) us(840) ansi-x962(10045) signatures(4) ecdsa-with-SHA2(3) + putSignatureImpl("SHA224withECDSA", + PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$SHA224"); + // ecdsa-with-SHA224(1) + put("Alg.Alias.Signature.1.2.840.10045.4.3.1", "SHA224withECDSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.4with1.2.840.10045.2.1", "SHA224withECDSA"); + + // iso(1) member-body(2) us(840) ansi-x962(10045) signatures(4) ecdsa-with-SHA2(3) + putSignatureImpl("SHA256withECDSA", + PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$SHA256"); + // ecdsa-with-SHA256(2) + put("Alg.Alias.Signature.1.2.840.10045.4.3.2", "SHA256withECDSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.1with1.2.840.10045.2.1", "SHA256withECDSA"); + + putSignatureImpl("SHA384withECDSA", + PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$SHA384"); + // ecdsa-with-SHA384(3) + put("Alg.Alias.Signature.1.2.840.10045.4.3.3", "SHA384withECDSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.2with1.2.840.10045.2.1", "SHA384withECDSA"); + + putSignatureImpl("SHA512withECDSA", + PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$SHA512"); + // ecdsa-with-SHA512(4) + put("Alg.Alias.Signature.1.2.840.10045.4.3.4", "SHA512withECDSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.3with1.2.840.10045.2.1", "SHA512withECDSA"); + } + + private void putMacImpl(String algorithm, String implClass) { + put("Mac." + algorithm, implClass); + put("Mac." + algorithm + " SupportedKeyClasses", KEYSTORE_SECRET_KEY_CLASS_NAME); + } + + private void putSymmetricCipherImpl(String transformation, String implClass) { + put("Cipher." + transformation, implClass); + put("Cipher." + transformation + " SupportedKeyClasses", KEYSTORE_SECRET_KEY_CLASS_NAME); + } + + private void putAsymmetricCipherImpl(String transformation, String implClass) { + put("Cipher." + transformation, implClass); + put("Cipher." + transformation + " SupportedKeyClasses", + KEYSTORE_PRIVATE_KEY_CLASS_NAME + "|" + KEYSTORE_PUBLIC_KEY_CLASS_NAME); + } + + private void putSignatureImpl(String algorithm, String implClass) { + put("Signature." + algorithm, implClass); + put("Signature." + algorithm + " SupportedKeyClasses", + KEYSTORE_PRIVATE_KEY_CLASS_NAME + "|" + KEYSTORE_PUBLIC_KEY_CLASS_NAME); + } + + public static String[] getSupportedEcdsaSignatureDigests() { + return new String[] {"NONE", "SHA-1", "SHA-224", "SHA-256", "SHA-384", "SHA-512"}; + } + + public static String[] getSupportedRsaSignatureWithPkcs1PaddingDigests() { + return new String[] {"NONE", "MD5", "SHA-1", "SHA-224", "SHA-256", "SHA-384", "SHA-512"}; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java b/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java new file mode 100644 index 000000000000..94b5c4d23afe --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java @@ -0,0 +1,923 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.annotation.CallSuper; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.IBinder; +import android.security.KeyStore; +import android.security.KeyStoreException; +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterDefs; +import android.security.keymaster.OperationResult; +import android.security.keystore.KeyStoreConnectException; +import android.security.keystore.KeyStoreCryptoOperation; + +import libcore.util.EmptyArray; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.security.AlgorithmParameters; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.InvalidParameterException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.ProviderException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import javax.crypto.AEADBadTagException; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.CipherSpi; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.SecretKeySpec; + +/** + * Base class for {@link CipherSpi} implementations of Android KeyStore backed ciphers. + * + * @hide + */ +abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStoreCryptoOperation { + private final KeyStore mKeyStore; + + // Fields below are populated by Cipher.init and KeyStore.begin and should be preserved after + // doFinal finishes. + private boolean mEncrypting; + private int mKeymasterPurposeOverride = -1; + private AndroidKeyStoreKey mKey; + private SecureRandom mRng; + + /** + * Token referencing this operation inside keystore service. It is initialized by + * {@code engineInit} and is invalidated when {@code engineDoFinal} succeeds and on some error + * conditions in between. + */ + private IBinder mOperationToken; + private long mOperationHandle; + private KeyStoreCryptoOperationStreamer mMainDataStreamer; + private KeyStoreCryptoOperationStreamer + mAdditionalAuthenticationDataStreamer; + private boolean mAdditionalAuthenticationDataStreamerClosed; + + /** + * Encountered exception which could not be immediately thrown because it was encountered inside + * a method that does not throw checked exception. This exception will be thrown from + * {@code engineDoFinal}. Once such an exception is encountered, {@code engineUpdate} and + * {@code engineDoFinal} start ignoring input data. + */ + private Exception mCachedException; + + AndroidKeyStoreCipherSpiBase() { + mKeyStore = KeyStore.getInstance(); + } + + @Override + protected final void engineInit(int opmode, Key key, SecureRandom random) + throws InvalidKeyException { + resetAll(); + + boolean success = false; + try { + init(opmode, key, random); + initAlgorithmSpecificParameters(); + try { + ensureKeystoreOperationInitialized(); + } catch (InvalidAlgorithmParameterException e) { + throw new InvalidKeyException(e); + } + success = true; + } finally { + if (!success) { + resetAll(); + } + } + } + + @Override + protected final void engineInit(int opmode, Key key, AlgorithmParameters params, + SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { + resetAll(); + + boolean success = false; + try { + init(opmode, key, random); + initAlgorithmSpecificParameters(params); + ensureKeystoreOperationInitialized(); + success = true; + } finally { + if (!success) { + resetAll(); + } + } + } + + @Override + protected final void engineInit(int opmode, Key key, AlgorithmParameterSpec params, + SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { + resetAll(); + + boolean success = false; + try { + init(opmode, key, random); + initAlgorithmSpecificParameters(params); + ensureKeystoreOperationInitialized(); + success = true; + } finally { + if (!success) { + resetAll(); + } + } + } + + private void init(int opmode, Key key, SecureRandom random) throws InvalidKeyException { + switch (opmode) { + case Cipher.ENCRYPT_MODE: + case Cipher.WRAP_MODE: + mEncrypting = true; + break; + case Cipher.DECRYPT_MODE: + case Cipher.UNWRAP_MODE: + mEncrypting = false; + break; + default: + throw new InvalidParameterException("Unsupported opmode: " + opmode); + } + initKey(opmode, key); + if (mKey == null) { + throw new ProviderException("initKey did not initialize the key"); + } + mRng = random; + } + + /** + * Resets this cipher to its pristine pre-init state. This must be equivalent to obtaining a new + * cipher instance. + * + * <p>Subclasses storing additional state should override this method, reset the additional + * state, and then chain to superclass. + */ + @CallSuper + protected void resetAll() { + IBinder operationToken = mOperationToken; + if (operationToken != null) { + mKeyStore.abort(operationToken); + } + mEncrypting = false; + mKeymasterPurposeOverride = -1; + mKey = null; + mRng = null; + mOperationToken = null; + mOperationHandle = 0; + mMainDataStreamer = null; + mAdditionalAuthenticationDataStreamer = null; + mAdditionalAuthenticationDataStreamerClosed = false; + mCachedException = null; + } + + /** + * Resets this cipher while preserving the initialized state. This must be equivalent to + * rolling back the cipher's state to just after the most recent {@code engineInit} completed + * successfully. + * + * <p>Subclasses storing additional post-init state should override this method, reset the + * additional state, and then chain to superclass. + */ + @CallSuper + protected void resetWhilePreservingInitState() { + IBinder operationToken = mOperationToken; + if (operationToken != null) { + mKeyStore.abort(operationToken); + } + mOperationToken = null; + mOperationHandle = 0; + mMainDataStreamer = null; + mAdditionalAuthenticationDataStreamer = null; + mAdditionalAuthenticationDataStreamerClosed = false; + mCachedException = null; + } + + private void ensureKeystoreOperationInitialized() throws InvalidKeyException, + InvalidAlgorithmParameterException { + if (mMainDataStreamer != null) { + return; + } + if (mCachedException != null) { + return; + } + if (mKey == null) { + throw new IllegalStateException("Not initialized"); + } + + KeymasterArguments keymasterInputArgs = new KeymasterArguments(); + addAlgorithmSpecificParametersToBegin(keymasterInputArgs); + byte[] additionalEntropy = KeyStoreCryptoOperationUtils.getRandomBytesToMixIntoKeystoreRng( + mRng, getAdditionalEntropyAmountForBegin()); + + int purpose; + if (mKeymasterPurposeOverride != -1) { + purpose = mKeymasterPurposeOverride; + } else { + purpose = mEncrypting + ? KeymasterDefs.KM_PURPOSE_ENCRYPT : KeymasterDefs.KM_PURPOSE_DECRYPT; + } + OperationResult opResult = mKeyStore.begin( + mKey.getAlias(), + purpose, + true, // permit aborting this operation if keystore runs out of resources + keymasterInputArgs, + additionalEntropy, + mKey.getUid()); + if (opResult == null) { + throw new KeyStoreConnectException(); + } + + // Store operation token and handle regardless of the error code returned by KeyStore to + // ensure that the operation gets aborted immediately if the code below throws an exception. + mOperationToken = opResult.token; + mOperationHandle = opResult.operationHandle; + + // If necessary, throw an exception due to KeyStore operation having failed. + GeneralSecurityException e = KeyStoreCryptoOperationUtils.getExceptionForCipherInit( + mKeyStore, mKey, opResult.resultCode); + if (e != null) { + if (e instanceof InvalidKeyException) { + throw (InvalidKeyException) e; + } else if (e instanceof InvalidAlgorithmParameterException) { + throw (InvalidAlgorithmParameterException) e; + } else { + throw new ProviderException("Unexpected exception type", e); + } + } + + if (mOperationToken == null) { + throw new ProviderException("Keystore returned null operation token"); + } + if (mOperationHandle == 0) { + throw new ProviderException("Keystore returned invalid operation handle"); + } + + loadAlgorithmSpecificParametersFromBeginResult(opResult.outParams); + mMainDataStreamer = createMainDataStreamer(mKeyStore, opResult.token); + mAdditionalAuthenticationDataStreamer = + createAdditionalAuthenticationDataStreamer(mKeyStore, opResult.token); + mAdditionalAuthenticationDataStreamerClosed = false; + } + + /** + * Creates a streamer which sends plaintext/ciphertext into the provided KeyStore and receives + * the corresponding ciphertext/plaintext from the KeyStore. + * + * <p>This implementation returns a working streamer. + */ + @NonNull + protected KeyStoreCryptoOperationStreamer createMainDataStreamer( + KeyStore keyStore, IBinder operationToken) { + return new KeyStoreCryptoOperationChunkedStreamer( + new KeyStoreCryptoOperationChunkedStreamer.MainDataStream( + keyStore, operationToken), 0); + } + + /** + * Creates a streamer which sends Additional Authentication Data (AAD) into the KeyStore. + * + * <p>This implementation returns {@code null}. + * + * @return stream or {@code null} if AAD is not supported by this cipher. + */ + @Nullable + protected KeyStoreCryptoOperationStreamer createAdditionalAuthenticationDataStreamer( + @SuppressWarnings("unused") KeyStore keyStore, + @SuppressWarnings("unused") IBinder operationToken) { + return null; + } + + @Override + protected final byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) { + if (mCachedException != null) { + return null; + } + try { + ensureKeystoreOperationInitialized(); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + mCachedException = e; + return null; + } + + if (inputLen == 0) { + return null; + } + + byte[] output; + try { + flushAAD(); + output = mMainDataStreamer.update(input, inputOffset, inputLen); + } catch (KeyStoreException e) { + mCachedException = e; + return null; + } + + if (output.length == 0) { + return null; + } + + return output; + } + + private void flushAAD() throws KeyStoreException { + if ((mAdditionalAuthenticationDataStreamer != null) + && (!mAdditionalAuthenticationDataStreamerClosed)) { + byte[] output; + try { + output = mAdditionalAuthenticationDataStreamer.doFinal( + EmptyArray.BYTE, 0, 0, + null, // no signature + null // no additional entropy needed flushing AAD + ); + } finally { + mAdditionalAuthenticationDataStreamerClosed = true; + } + if ((output != null) && (output.length > 0)) { + throw new ProviderException( + "AAD update unexpectedly returned data: " + output.length + " bytes"); + } + } + } + + @Override + protected final int engineUpdate(byte[] input, int inputOffset, int inputLen, byte[] output, + int outputOffset) throws ShortBufferException { + byte[] outputCopy = engineUpdate(input, inputOffset, inputLen); + if (outputCopy == null) { + return 0; + } + int outputAvailable = output.length - outputOffset; + if (outputCopy.length > outputAvailable) { + throw new ShortBufferException("Output buffer too short. Produced: " + + outputCopy.length + ", available: " + outputAvailable); + } + System.arraycopy(outputCopy, 0, output, outputOffset, outputCopy.length); + return outputCopy.length; + } + + @Override + protected final int engineUpdate(ByteBuffer input, ByteBuffer output) + throws ShortBufferException { + if (input == null) { + throw new NullPointerException("input == null"); + } + if (output == null) { + throw new NullPointerException("output == null"); + } + + int inputSize = input.remaining(); + byte[] outputArray; + if (input.hasArray()) { + outputArray = + engineUpdate( + input.array(), input.arrayOffset() + input.position(), inputSize); + input.position(input.position() + inputSize); + } else { + byte[] inputArray = new byte[inputSize]; + input.get(inputArray); + outputArray = engineUpdate(inputArray, 0, inputSize); + } + + int outputSize = (outputArray != null) ? outputArray.length : 0; + if (outputSize > 0) { + int outputBufferAvailable = output.remaining(); + try { + output.put(outputArray); + } catch (BufferOverflowException e) { + throw new ShortBufferException( + "Output buffer too small. Produced: " + outputSize + ", available: " + + outputBufferAvailable); + } + } + return outputSize; + } + + @Override + protected final void engineUpdateAAD(byte[] input, int inputOffset, int inputLen) { + if (mCachedException != null) { + return; + } + + try { + ensureKeystoreOperationInitialized(); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + mCachedException = e; + return; + } + + if (mAdditionalAuthenticationDataStreamerClosed) { + throw new IllegalStateException( + "AAD can only be provided before Cipher.update is invoked"); + } + + if (mAdditionalAuthenticationDataStreamer == null) { + throw new IllegalStateException("This cipher does not support AAD"); + } + + byte[] output; + try { + output = mAdditionalAuthenticationDataStreamer.update(input, inputOffset, inputLen); + } catch (KeyStoreException e) { + mCachedException = e; + return; + } + + if ((output != null) && (output.length > 0)) { + throw new ProviderException("AAD update unexpectedly produced output: " + + output.length + " bytes"); + } + } + + @Override + protected final void engineUpdateAAD(ByteBuffer src) { + if (src == null) { + throw new IllegalArgumentException("src == null"); + } + if (!src.hasRemaining()) { + return; + } + + byte[] input; + int inputOffset; + int inputLen; + if (src.hasArray()) { + input = src.array(); + inputOffset = src.arrayOffset() + src.position(); + inputLen = src.remaining(); + src.position(src.limit()); + } else { + input = new byte[src.remaining()]; + inputOffset = 0; + inputLen = input.length; + src.get(input); + } + engineUpdateAAD(input, inputOffset, inputLen); + } + + @Override + protected final byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) + throws IllegalBlockSizeException, BadPaddingException { + if (mCachedException != null) { + throw (IllegalBlockSizeException) + new IllegalBlockSizeException().initCause(mCachedException); + } + + try { + ensureKeystoreOperationInitialized(); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw (IllegalBlockSizeException) new IllegalBlockSizeException().initCause(e); + } + + byte[] output; + try { + flushAAD(); + byte[] additionalEntropy = + KeyStoreCryptoOperationUtils.getRandomBytesToMixIntoKeystoreRng( + mRng, getAdditionalEntropyAmountForFinish()); + output = mMainDataStreamer.doFinal( + input, inputOffset, inputLen, + null, // no signature involved + additionalEntropy); + } catch (KeyStoreException e) { + switch (e.getErrorCode()) { + case KeymasterDefs.KM_ERROR_INVALID_INPUT_LENGTH: + throw (IllegalBlockSizeException) new IllegalBlockSizeException().initCause(e); + case KeymasterDefs.KM_ERROR_INVALID_ARGUMENT: + throw (BadPaddingException) new BadPaddingException().initCause(e); + case KeymasterDefs.KM_ERROR_VERIFICATION_FAILED: + throw (AEADBadTagException) new AEADBadTagException().initCause(e); + default: + throw (IllegalBlockSizeException) new IllegalBlockSizeException().initCause(e); + } + } + + resetWhilePreservingInitState(); + return output; + } + + @Override + protected final int engineDoFinal(byte[] input, int inputOffset, int inputLen, byte[] output, + int outputOffset) throws ShortBufferException, IllegalBlockSizeException, + BadPaddingException { + byte[] outputCopy = engineDoFinal(input, inputOffset, inputLen); + if (outputCopy == null) { + return 0; + } + int outputAvailable = output.length - outputOffset; + if (outputCopy.length > outputAvailable) { + throw new ShortBufferException("Output buffer too short. Produced: " + + outputCopy.length + ", available: " + outputAvailable); + } + System.arraycopy(outputCopy, 0, output, outputOffset, outputCopy.length); + return outputCopy.length; + } + + @Override + protected final int engineDoFinal(ByteBuffer input, ByteBuffer output) + throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { + if (input == null) { + throw new NullPointerException("input == null"); + } + if (output == null) { + throw new NullPointerException("output == null"); + } + + int inputSize = input.remaining(); + byte[] outputArray; + if (input.hasArray()) { + outputArray = + engineDoFinal( + input.array(), input.arrayOffset() + input.position(), inputSize); + input.position(input.position() + inputSize); + } else { + byte[] inputArray = new byte[inputSize]; + input.get(inputArray); + outputArray = engineDoFinal(inputArray, 0, inputSize); + } + + int outputSize = (outputArray != null) ? outputArray.length : 0; + if (outputSize > 0) { + int outputBufferAvailable = output.remaining(); + try { + output.put(outputArray); + } catch (BufferOverflowException e) { + throw new ShortBufferException( + "Output buffer too small. Produced: " + outputSize + ", available: " + + outputBufferAvailable); + } + } + return outputSize; + } + + @Override + protected final byte[] engineWrap(Key key) + throws IllegalBlockSizeException, InvalidKeyException { + if (mKey == null) { + throw new IllegalStateException("Not initilized"); + } + + if (!isEncrypting()) { + throw new IllegalStateException( + "Cipher must be initialized in Cipher.WRAP_MODE to wrap keys"); + } + + if (key == null) { + throw new NullPointerException("key == null"); + } + byte[] encoded = null; + if (key instanceof SecretKey) { + if ("RAW".equalsIgnoreCase(key.getFormat())) { + encoded = key.getEncoded(); + } + if (encoded == null) { + try { + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(key.getAlgorithm()); + SecretKeySpec spec = + (SecretKeySpec) keyFactory.getKeySpec( + (SecretKey) key, SecretKeySpec.class); + encoded = spec.getEncoded(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new InvalidKeyException( + "Failed to wrap key because it does not export its key material", + e); + } + } + } else if (key instanceof PrivateKey) { + if ("PKCS8".equalsIgnoreCase(key.getFormat())) { + encoded = key.getEncoded(); + } + if (encoded == null) { + try { + KeyFactory keyFactory = KeyFactory.getInstance(key.getAlgorithm()); + PKCS8EncodedKeySpec spec = + keyFactory.getKeySpec(key, PKCS8EncodedKeySpec.class); + encoded = spec.getEncoded(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new InvalidKeyException( + "Failed to wrap key because it does not export its key material", + e); + } + } + } else if (key instanceof PublicKey) { + if ("X.509".equalsIgnoreCase(key.getFormat())) { + encoded = key.getEncoded(); + } + if (encoded == null) { + try { + KeyFactory keyFactory = KeyFactory.getInstance(key.getAlgorithm()); + X509EncodedKeySpec spec = + keyFactory.getKeySpec(key, X509EncodedKeySpec.class); + encoded = spec.getEncoded(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new InvalidKeyException( + "Failed to wrap key because it does not export its key material", + e); + } + } + } else { + throw new InvalidKeyException("Unsupported key type: " + key.getClass().getName()); + } + + if (encoded == null) { + throw new InvalidKeyException( + "Failed to wrap key because it does not export its key material"); + } + + try { + return engineDoFinal(encoded, 0, encoded.length); + } catch (BadPaddingException e) { + throw (IllegalBlockSizeException) new IllegalBlockSizeException().initCause(e); + } + } + + @Override + protected final Key engineUnwrap(byte[] wrappedKey, String wrappedKeyAlgorithm, + int wrappedKeyType) throws InvalidKeyException, NoSuchAlgorithmException { + if (mKey == null) { + throw new IllegalStateException("Not initilized"); + } + + if (isEncrypting()) { + throw new IllegalStateException( + "Cipher must be initialized in Cipher.WRAP_MODE to wrap keys"); + } + + if (wrappedKey == null) { + throw new NullPointerException("wrappedKey == null"); + } + + byte[] encoded; + try { + encoded = engineDoFinal(wrappedKey, 0, wrappedKey.length); + } catch (IllegalBlockSizeException | BadPaddingException e) { + throw new InvalidKeyException("Failed to unwrap key", e); + } + + switch (wrappedKeyType) { + case Cipher.SECRET_KEY: + { + return new SecretKeySpec(encoded, wrappedKeyAlgorithm); + // break; + } + case Cipher.PRIVATE_KEY: + { + KeyFactory keyFactory = KeyFactory.getInstance(wrappedKeyAlgorithm); + try { + return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } catch (InvalidKeySpecException e) { + throw new InvalidKeyException( + "Failed to create private key from its PKCS#8 encoded form", e); + } + // break; + } + case Cipher.PUBLIC_KEY: + { + KeyFactory keyFactory = KeyFactory.getInstance(wrappedKeyAlgorithm); + try { + return keyFactory.generatePublic(new X509EncodedKeySpec(encoded)); + } catch (InvalidKeySpecException e) { + throw new InvalidKeyException( + "Failed to create public key from its X.509 encoded form", e); + } + // break; + } + default: + throw new InvalidParameterException( + "Unsupported wrappedKeyType: " + wrappedKeyType); + } + } + + @Override + protected final void engineSetMode(String mode) throws NoSuchAlgorithmException { + // This should never be invoked because all algorithms registered with the AndroidKeyStore + // provide explicitly specify block mode. + throw new UnsupportedOperationException(); + } + + @Override + protected final void engineSetPadding(String arg0) throws NoSuchPaddingException { + // This should never be invoked because all algorithms registered with the AndroidKeyStore + // provide explicitly specify padding mode. + throw new UnsupportedOperationException(); + } + + @Override + protected final int engineGetKeySize(Key key) throws InvalidKeyException { + throw new UnsupportedOperationException(); + } + + @CallSuper + @Override + public void finalize() throws Throwable { + try { + IBinder operationToken = mOperationToken; + if (operationToken != null) { + mKeyStore.abort(operationToken); + } + } finally { + super.finalize(); + } + } + + @Override + public final long getOperationHandle() { + return mOperationHandle; + } + + protected final void setKey(@NonNull AndroidKeyStoreKey key) { + mKey = key; + } + + /** + * Overrides the default purpose/type of the crypto operation. + */ + protected final void setKeymasterPurposeOverride(int keymasterPurpose) { + mKeymasterPurposeOverride = keymasterPurpose; + } + + protected final int getKeymasterPurposeOverride() { + return mKeymasterPurposeOverride; + } + + /** + * Returns {@code true} if this cipher is initialized for encryption, {@code false} if this + * cipher is initialized for decryption. + */ + protected final boolean isEncrypting() { + return mEncrypting; + } + + @NonNull + protected final KeyStore getKeyStore() { + return mKeyStore; + } + + protected final long getConsumedInputSizeBytes() { + if (mMainDataStreamer == null) { + throw new IllegalStateException("Not initialized"); + } + return mMainDataStreamer.getConsumedInputSizeBytes(); + } + + protected final long getProducedOutputSizeBytes() { + if (mMainDataStreamer == null) { + throw new IllegalStateException("Not initialized"); + } + return mMainDataStreamer.getProducedOutputSizeBytes(); + } + + static String opmodeToString(int opmode) { + switch (opmode) { + case Cipher.ENCRYPT_MODE: + return "ENCRYPT_MODE"; + case Cipher.DECRYPT_MODE: + return "DECRYPT_MODE"; + case Cipher.WRAP_MODE: + return "WRAP_MODE"; + case Cipher.UNWRAP_MODE: + return "UNWRAP_MODE"; + default: + return String.valueOf(opmode); + } + } + + // The methods below need to be implemented by subclasses. + + /** + * Initializes this cipher with the provided key. + * + * @throws InvalidKeyException if the {@code key} is not suitable for this cipher in the + * specified {@code opmode}. + * + * @see #setKey(AndroidKeyStoreKey) + */ + protected abstract void initKey(int opmode, @Nullable Key key) throws InvalidKeyException; + + /** + * Returns algorithm-specific parameters used by this cipher or {@code null} if no + * algorithm-specific parameters are used. + */ + @Nullable + @Override + protected abstract AlgorithmParameters engineGetParameters(); + + /** + * Invoked by {@code engineInit} to initialize algorithm-specific parameters when no additional + * initialization parameters were provided. + * + * @throws InvalidKeyException if this cipher cannot be configured based purely on the provided + * key and needs additional parameters to be provided to {@code Cipher.init}. + */ + protected abstract void initAlgorithmSpecificParameters() throws InvalidKeyException; + + /** + * Invoked by {@code engineInit} to initialize algorithm-specific parameters when additional + * parameters were provided. + * + * @param params additional algorithm parameters or {@code null} if not specified. + * + * @throws InvalidAlgorithmParameterException if there is insufficient information to configure + * this cipher or if the provided parameters are not suitable for this cipher. + */ + protected abstract void initAlgorithmSpecificParameters( + @Nullable AlgorithmParameterSpec params) throws InvalidAlgorithmParameterException; + + /** + * Invoked by {@code engineInit} to initialize algorithm-specific parameters when additional + * parameters were provided. + * + * @param params additional algorithm parameters or {@code null} if not specified. + * + * @throws InvalidAlgorithmParameterException if there is insufficient information to configure + * this cipher or if the provided parameters are not suitable for this cipher. + */ + protected abstract void initAlgorithmSpecificParameters(@Nullable AlgorithmParameters params) + throws InvalidAlgorithmParameterException; + + /** + * Returns the amount of additional entropy (in bytes) to be provided to the KeyStore's + * {@code begin} operation. This amount of entropy is typically what's consumed to generate + * random parameters, such as IV. + * + * <p>For decryption, the return value should be {@code 0} because decryption should not be + * consuming any entropy. For encryption, the value combined with + * {@link #getAdditionalEntropyAmountForFinish()} should match (or exceed) the amount of Shannon + * entropy of the ciphertext produced by this cipher assuming the key, the plaintext, and all + * explicitly provided parameters to {@code Cipher.init} are known. For example, for AES CBC + * encryption with an explicitly provided IV the return value should be {@code 0}, whereas for + * the case where IV is generated by the KeyStore's {@code begin} operation it should be + * {@code 16}. + */ + protected abstract int getAdditionalEntropyAmountForBegin(); + + /** + * Returns the amount of additional entropy (in bytes) to be provided to the KeyStore's + * {@code finish} operation. This amount of entropy is typically what's consumed by encryption + * padding scheme. + * + * <p>For decryption, the return value should be {@code 0} because decryption should not be + * consuming any entropy. For encryption, the value combined with + * {@link #getAdditionalEntropyAmountForBegin()} should match (or exceed) the amount of Shannon + * entropy of the ciphertext produced by this cipher assuming the key, the plaintext, and all + * explicitly provided parameters to {@code Cipher.init} are known. For example, for RSA with + * OAEP the return value should be the size of the OAEP hash output. For RSA with PKCS#1 padding + * the return value should be the size of the padding string or could be raised (for simplicity) + * to the size of the modulus. + */ + protected abstract int getAdditionalEntropyAmountForFinish(); + + /** + * Invoked to add algorithm-specific parameters for the KeyStore's {@code begin} operation. + * + * @param keymasterArgs keystore/keymaster arguments to be populated with algorithm-specific + * parameters. + */ + protected abstract void addAlgorithmSpecificParametersToBegin( + @NonNull KeymasterArguments keymasterArgs); + + /** + * Invoked to obtain algorithm-specific parameters from the result of the KeyStore's + * {@code begin} operation. + * + * <p>Some parameters, such as IV, are not required to be provided to {@code Cipher.init}. Such + * parameters, if not provided, must be generated by KeyStore and returned to the user of + * {@code Cipher} and potentially reused after {@code doFinal}. + * + * @param keymasterArgs keystore/keymaster arguments returned by KeyStore {@code begin} + * operation. + */ + protected abstract void loadAlgorithmSpecificParametersFromBeginResult( + @NonNull KeymasterArguments keymasterArgs); +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java new file mode 100644 index 000000000000..95e4c52591e0 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.annotation.NonNull; +import android.os.IBinder; +import android.security.KeyStore; +import android.security.KeyStoreException; +import android.security.keymaster.KeyCharacteristics; +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyProperties; + +import libcore.util.EmptyArray; + +import java.io.ByteArrayOutputStream; +import java.security.InvalidKeyException; +import java.security.SignatureSpi; + +/** + * Base class for {@link SignatureSpi} providing Android KeyStore backed ECDSA signatures. + * + * @hide + */ +abstract class AndroidKeyStoreECDSASignatureSpi extends AndroidKeyStoreSignatureSpiBase { + + public final static class NONE extends AndroidKeyStoreECDSASignatureSpi { + public NONE() { + super(KeymasterDefs.KM_DIGEST_NONE); + } + + @Override + protected KeyStoreCryptoOperationStreamer createMainDataStreamer(KeyStore keyStore, + IBinder operationToken) { + return new TruncateToFieldSizeMessageStreamer( + super.createMainDataStreamer(keyStore, operationToken), + getGroupSizeBits()); + } + + /** + * Streamer which buffers all input, then truncates it to field size, and then sends it into + * KeyStore via the provided delegate streamer. + */ + private static class TruncateToFieldSizeMessageStreamer + implements KeyStoreCryptoOperationStreamer { + + private final KeyStoreCryptoOperationStreamer mDelegate; + private final int mGroupSizeBits; + private final ByteArrayOutputStream mInputBuffer = new ByteArrayOutputStream(); + private long mConsumedInputSizeBytes; + + private TruncateToFieldSizeMessageStreamer( + KeyStoreCryptoOperationStreamer delegate, + int groupSizeBits) { + mDelegate = delegate; + mGroupSizeBits = groupSizeBits; + } + + @Override + public byte[] update(byte[] input, int inputOffset, int inputLength) + throws KeyStoreException { + if (inputLength > 0) { + mInputBuffer.write(input, inputOffset, inputLength); + mConsumedInputSizeBytes += inputLength; + } + return EmptyArray.BYTE; + } + + @Override + public byte[] doFinal(byte[] input, int inputOffset, int inputLength, byte[] signature, + byte[] additionalEntropy) throws KeyStoreException { + if (inputLength > 0) { + mConsumedInputSizeBytes += inputLength; + mInputBuffer.write(input, inputOffset, inputLength); + } + + byte[] bufferedInput = mInputBuffer.toByteArray(); + mInputBuffer.reset(); + // Truncate input at field size (bytes) + return mDelegate.doFinal(bufferedInput, + 0, + Math.min(bufferedInput.length, ((mGroupSizeBits + 7) / 8)), + signature, additionalEntropy); + } + + @Override + public long getConsumedInputSizeBytes() { + return mConsumedInputSizeBytes; + } + + @Override + public long getProducedOutputSizeBytes() { + return mDelegate.getProducedOutputSizeBytes(); + } + } + } + + public final static class SHA1 extends AndroidKeyStoreECDSASignatureSpi { + public SHA1() { + super(KeymasterDefs.KM_DIGEST_SHA1); + } + } + + public final static class SHA224 extends AndroidKeyStoreECDSASignatureSpi { + public SHA224() { + super(KeymasterDefs.KM_DIGEST_SHA_2_224); + } + } + + public final static class SHA256 extends AndroidKeyStoreECDSASignatureSpi { + public SHA256() { + super(KeymasterDefs.KM_DIGEST_SHA_2_256); + } + } + + public final static class SHA384 extends AndroidKeyStoreECDSASignatureSpi { + public SHA384() { + super(KeymasterDefs.KM_DIGEST_SHA_2_384); + } + } + + public final static class SHA512 extends AndroidKeyStoreECDSASignatureSpi { + public SHA512() { + super(KeymasterDefs.KM_DIGEST_SHA_2_512); + } + } + + private final int mKeymasterDigest; + + private int mGroupSizeBits = -1; + + AndroidKeyStoreECDSASignatureSpi(int keymasterDigest) { + mKeymasterDigest = keymasterDigest; + } + + @Override + protected final void initKey(AndroidKeyStoreKey key) throws InvalidKeyException { + if (!KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(key.getAlgorithm())) { + throw new InvalidKeyException("Unsupported key algorithm: " + key.getAlgorithm() + + ". Only" + KeyProperties.KEY_ALGORITHM_EC + " supported"); + } + + KeyCharacteristics keyCharacteristics = new KeyCharacteristics(); + int errorCode = getKeyStore().getKeyCharacteristics( + key.getAlias(), null, null, key.getUid(), keyCharacteristics); + if (errorCode != KeyStore.NO_ERROR) { + throw getKeyStore().getInvalidKeyException(key.getAlias(), key.getUid(), errorCode); + } + long keySizeBits = keyCharacteristics.getUnsignedInt(KeymasterDefs.KM_TAG_KEY_SIZE, -1); + if (keySizeBits == -1) { + throw new InvalidKeyException("Size of key not known"); + } else if (keySizeBits > Integer.MAX_VALUE) { + throw new InvalidKeyException("Key too large: " + keySizeBits + " bits"); + } + mGroupSizeBits = (int) keySizeBits; + + super.initKey(key); + } + + @Override + protected final void resetAll() { + mGroupSizeBits = -1; + super.resetAll(); + } + + @Override + protected final void resetWhilePreservingInitState() { + super.resetWhilePreservingInitState(); + } + + @Override + protected final void addAlgorithmSpecificParametersToBegin( + @NonNull KeymasterArguments keymasterArgs) { + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_EC); + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_DIGEST, mKeymasterDigest); + } + + @Override + protected final int getAdditionalEntropyAmountForSign() { + return (mGroupSizeBits + 7) / 8; + } + + protected final int getGroupSizeBits() { + if (mGroupSizeBits == -1) { + throw new IllegalStateException("Not initialized"); + } + return mGroupSizeBits; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreECPrivateKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreECPrivateKey.java new file mode 100644 index 000000000000..0ef75cd30c37 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreECPrivateKey.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.security.keystore.KeyProperties; + +import java.security.PrivateKey; +import java.security.interfaces.ECKey; +import java.security.spec.ECParameterSpec; + +/** + * EC private key (instance of {@link PrivateKey} and {@link ECKey}) backed by keystore. + * + * @hide + */ +public class AndroidKeyStoreECPrivateKey extends AndroidKeyStorePrivateKey implements ECKey { + private final ECParameterSpec mParams; + + public AndroidKeyStoreECPrivateKey(String alias, int uid, ECParameterSpec params) { + super(alias, uid, KeyProperties.KEY_ALGORITHM_EC); + mParams = params; + } + + @Override + public ECParameterSpec getParams() { + return mParams; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java new file mode 100644 index 000000000000..bc1b0eeb9b19 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.security.keystore.KeyProperties; + +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; + +/** + * {@link ECPublicKey} backed by keystore. + * + * @hide + */ +public class AndroidKeyStoreECPublicKey extends AndroidKeyStorePublicKey implements ECPublicKey { + + private final ECParameterSpec mParams; + private final ECPoint mW; + + public AndroidKeyStoreECPublicKey(String alias, int uid, byte[] x509EncodedForm, ECParameterSpec params, + ECPoint w) { + super(alias, uid, KeyProperties.KEY_ALGORITHM_EC, x509EncodedForm); + mParams = params; + mW = w; + } + + public AndroidKeyStoreECPublicKey(String alias, int uid, ECPublicKey info) { + this(alias, uid, info.getEncoded(), info.getParams(), info.getW()); + if (!"X.509".equalsIgnoreCase(info.getFormat())) { + throw new IllegalArgumentException( + "Unsupported key export format: " + info.getFormat()); + } + } + + @Override + public ECParameterSpec getParams() { + return mParams; + } + + @Override + public ECPoint getW() { + return mW; + } +}
\ No newline at end of file diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreHmacSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreHmacSpi.java new file mode 100644 index 000000000000..f7155b09750c --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreHmacSpi.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.os.IBinder; +import android.security.KeyStore; +import android.security.KeyStoreException; +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterDefs; +import android.security.keymaster.OperationResult; +import android.security.keystore.KeyStoreConnectException; +import android.security.keystore.KeyStoreCryptoOperation; +import android.security.keystore.KeymasterUtils; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.ProviderException; +import java.security.spec.AlgorithmParameterSpec; + +import javax.crypto.MacSpi; + +/** + * {@link MacSpi} which provides HMAC implementations backed by Android KeyStore. + * + * @hide + */ +public abstract class AndroidKeyStoreHmacSpi extends MacSpi implements KeyStoreCryptoOperation { + + public static class HmacSHA1 extends AndroidKeyStoreHmacSpi { + public HmacSHA1() { + super(KeymasterDefs.KM_DIGEST_SHA1); + } + } + + public static class HmacSHA224 extends AndroidKeyStoreHmacSpi { + public HmacSHA224() { + super(KeymasterDefs.KM_DIGEST_SHA_2_224); + } + } + + public static class HmacSHA256 extends AndroidKeyStoreHmacSpi { + public HmacSHA256() { + super(KeymasterDefs.KM_DIGEST_SHA_2_256); + } + } + + public static class HmacSHA384 extends AndroidKeyStoreHmacSpi { + public HmacSHA384() { + super(KeymasterDefs.KM_DIGEST_SHA_2_384); + } + } + + public static class HmacSHA512 extends AndroidKeyStoreHmacSpi { + public HmacSHA512() { + super(KeymasterDefs.KM_DIGEST_SHA_2_512); + } + } + + private final KeyStore mKeyStore = KeyStore.getInstance(); + private final int mKeymasterDigest; + private final int mMacSizeBits; + + // Fields below are populated by engineInit and should be preserved after engineDoFinal. + private AndroidKeyStoreSecretKey mKey; + + // Fields below are reset when engineDoFinal succeeds. + private KeyStoreCryptoOperationChunkedStreamer mChunkedStreamer; + private IBinder mOperationToken; + private long mOperationHandle; + + protected AndroidKeyStoreHmacSpi(int keymasterDigest) { + mKeymasterDigest = keymasterDigest; + mMacSizeBits = KeymasterUtils.getDigestOutputSizeBits(keymasterDigest); + } + + @Override + protected int engineGetMacLength() { + return (mMacSizeBits + 7) / 8; + } + + @Override + protected void engineInit(Key key, AlgorithmParameterSpec params) throws InvalidKeyException, + InvalidAlgorithmParameterException { + resetAll(); + + boolean success = false; + try { + init(key, params); + ensureKeystoreOperationInitialized(); + success = true; + } finally { + if (!success) { + resetAll(); + } + } + } + + private void init(Key key, AlgorithmParameterSpec params) throws InvalidKeyException, + InvalidAlgorithmParameterException { + if (key == null) { + throw new InvalidKeyException("key == null"); + } else if (!(key instanceof AndroidKeyStoreSecretKey)) { + throw new InvalidKeyException( + "Only Android KeyStore secret keys supported. Key: " + key); + } + mKey = (AndroidKeyStoreSecretKey) key; + + if (params != null) { + throw new InvalidAlgorithmParameterException( + "Unsupported algorithm parameters: " + params); + } + + } + + private void resetAll() { + mKey = null; + IBinder operationToken = mOperationToken; + if (operationToken != null) { + mKeyStore.abort(operationToken); + } + mOperationToken = null; + mOperationHandle = 0; + mChunkedStreamer = null; + } + + private void resetWhilePreservingInitState() { + IBinder operationToken = mOperationToken; + if (operationToken != null) { + mKeyStore.abort(operationToken); + } + mOperationToken = null; + mOperationHandle = 0; + mChunkedStreamer = null; + } + + @Override + protected void engineReset() { + resetWhilePreservingInitState(); + } + + private void ensureKeystoreOperationInitialized() throws InvalidKeyException { + if (mChunkedStreamer != null) { + return; + } + if (mKey == null) { + throw new IllegalStateException("Not initialized"); + } + + KeymasterArguments keymasterArgs = new KeymasterArguments(); + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_HMAC); + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_DIGEST, mKeymasterDigest); + keymasterArgs.addUnsignedInt(KeymasterDefs.KM_TAG_MAC_LENGTH, mMacSizeBits); + + OperationResult opResult = mKeyStore.begin( + mKey.getAlias(), + KeymasterDefs.KM_PURPOSE_SIGN, + true, + keymasterArgs, + null, // no additional entropy needed for HMAC because it's deterministic + mKey.getUid()); + + if (opResult == null) { + throw new KeyStoreConnectException(); + } + + // Store operation token and handle regardless of the error code returned by KeyStore to + // ensure that the operation gets aborted immediately if the code below throws an exception. + mOperationToken = opResult.token; + mOperationHandle = opResult.operationHandle; + + // If necessary, throw an exception due to KeyStore operation having failed. + InvalidKeyException e = KeyStoreCryptoOperationUtils.getInvalidKeyExceptionForInit( + mKeyStore, mKey, opResult.resultCode); + if (e != null) { + throw e; + } + + if (mOperationToken == null) { + throw new ProviderException("Keystore returned null operation token"); + } + if (mOperationHandle == 0) { + throw new ProviderException("Keystore returned invalid operation handle"); + } + + mChunkedStreamer = new KeyStoreCryptoOperationChunkedStreamer( + new KeyStoreCryptoOperationChunkedStreamer.MainDataStream( + mKeyStore, mOperationToken)); + } + + @Override + protected void engineUpdate(byte input) { + engineUpdate(new byte[] {input}, 0, 1); + } + + @Override + protected void engineUpdate(byte[] input, int offset, int len) { + try { + ensureKeystoreOperationInitialized(); + } catch (InvalidKeyException e) { + throw new ProviderException("Failed to reinitialize MAC", e); + } + + byte[] output; + try { + output = mChunkedStreamer.update(input, offset, len); + } catch (KeyStoreException e) { + throw new ProviderException("Keystore operation failed", e); + } + if ((output != null) && (output.length != 0)) { + throw new ProviderException("Update operation unexpectedly produced output"); + } + } + + @Override + protected byte[] engineDoFinal() { + try { + ensureKeystoreOperationInitialized(); + } catch (InvalidKeyException e) { + throw new ProviderException("Failed to reinitialize MAC", e); + } + + byte[] result; + try { + result = mChunkedStreamer.doFinal( + null, 0, 0, + null, // no signature provided -- this invocation will generate one + null // no additional entropy needed -- HMAC is deterministic + ); + } catch (KeyStoreException e) { + throw new ProviderException("Keystore operation failed", e); + } + + resetWhilePreservingInitState(); + return result; + } + + @Override + public void finalize() throws Throwable { + try { + IBinder operationToken = mOperationToken; + if (operationToken != null) { + mKeyStore.abort(operationToken); + } + } finally { + super.finalize(); + } + } + + @Override + public long getOperationHandle() { + return mOperationHandle; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKey.java new file mode 100644 index 000000000000..e2a3d61d4cfc --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKey.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import java.security.Key; + +/** + * {@link Key} backed by Android Keystore. + * + * @hide + */ +public class AndroidKeyStoreKey implements Key { + private final String mAlias; + private final int mUid; + private final String mAlgorithm; + + public AndroidKeyStoreKey(String alias, int uid, String algorithm) { + mAlias = alias; + mUid = uid; + mAlgorithm = algorithm; + } + + String getAlias() { + return mAlias; + } + + int getUid() { + return mUid; + } + + @Override + public String getAlgorithm() { + return mAlgorithm; + } + + @Override + public String getFormat() { + // This key does not export its key material + return null; + } + + @Override + public byte[] getEncoded() { + // This key does not export its key material + return null; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mAlgorithm == null) ? 0 : mAlgorithm.hashCode()); + result = prime * result + ((mAlias == null) ? 0 : mAlias.hashCode()); + result = prime * result + mUid; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AndroidKeyStoreKey other = (AndroidKeyStoreKey) obj; + if (mAlgorithm == null) { + if (other.mAlgorithm != null) { + return false; + } + } else if (!mAlgorithm.equals(other.mAlgorithm)) { + return false; + } + if (mAlias == null) { + if (other.mAlias != null) { + return false; + } + } else if (!mAlias.equals(other.mAlias)) { + return false; + } + if (mUid != other.mUid) { + return false; + } + return true; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyFactorySpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyFactorySpi.java new file mode 100644 index 000000000000..607bcae6570d --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyFactorySpi.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.security.Credentials; +import android.security.KeyStore; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyInfo; + +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyFactorySpi; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.security.spec.X509EncodedKeySpec; + +/** + * {@link KeyFactorySpi} backed by Android KeyStore. + * + * @hide + */ +public class AndroidKeyStoreKeyFactorySpi extends KeyFactorySpi { + + private final KeyStore mKeyStore = KeyStore.getInstance(); + + @Override + protected <T extends KeySpec> T engineGetKeySpec(Key key, Class<T> keySpecClass) + throws InvalidKeySpecException { + if (key == null) { + throw new InvalidKeySpecException("key == null"); + } else if ((!(key instanceof AndroidKeyStorePrivateKey)) + && (!(key instanceof AndroidKeyStorePublicKey))) { + throw new InvalidKeySpecException( + "Unsupported key type: " + key.getClass().getName() + + ". This KeyFactory supports only Android Keystore asymmetric keys"); + } + + // key is an Android Keystore private or public key + + if (keySpecClass == null) { + throw new InvalidKeySpecException("keySpecClass == null"); + } else if (KeyInfo.class.equals(keySpecClass)) { + if (!(key instanceof AndroidKeyStorePrivateKey)) { + throw new InvalidKeySpecException( + "Unsupported key type: " + key.getClass().getName() + + ". KeyInfo can be obtained only for Android Keystore private keys"); + } + AndroidKeyStorePrivateKey + keystorePrivateKey = (AndroidKeyStorePrivateKey) key; + String keyAliasInKeystore = keystorePrivateKey.getAlias(); + String entryAlias; + if (keyAliasInKeystore.startsWith(Credentials.USER_PRIVATE_KEY)) { + entryAlias = keyAliasInKeystore.substring(Credentials.USER_PRIVATE_KEY.length()); + } else { + throw new InvalidKeySpecException("Invalid key alias: " + keyAliasInKeystore); + } + @SuppressWarnings("unchecked") + T result = (T) AndroidKeyStoreSecretKeyFactorySpi.getKeyInfo( + mKeyStore, entryAlias, keyAliasInKeystore, keystorePrivateKey.getUid()); + return result; + } else if (X509EncodedKeySpec.class.equals(keySpecClass)) { + if (!(key instanceof AndroidKeyStorePublicKey)) { + throw new InvalidKeySpecException( + "Unsupported key type: " + key.getClass().getName() + + ". X509EncodedKeySpec can be obtained only for Android Keystore public" + + " keys"); + } + @SuppressWarnings("unchecked") + T result = (T) new X509EncodedKeySpec(((AndroidKeyStorePublicKey) key).getEncoded()); + return result; + } else if (PKCS8EncodedKeySpec.class.equals(keySpecClass)) { + if (key instanceof AndroidKeyStorePrivateKey) { + throw new InvalidKeySpecException( + "Key material export of Android Keystore private keys is not supported"); + } else { + throw new InvalidKeySpecException( + "Cannot export key material of public key in PKCS#8 format." + + " Only X.509 format (X509EncodedKeySpec) supported for public keys."); + } + } else if (RSAPublicKeySpec.class.equals(keySpecClass)) { + if (key instanceof AndroidKeyStoreRSAPublicKey) { + AndroidKeyStoreRSAPublicKey + rsaKey = (AndroidKeyStoreRSAPublicKey) key; + @SuppressWarnings("unchecked") + T result = + (T) new RSAPublicKeySpec(rsaKey.getModulus(), rsaKey.getPublicExponent()); + return result; + } else { + throw new InvalidKeySpecException( + "Obtaining RSAPublicKeySpec not supported for " + key.getAlgorithm() + " " + + ((key instanceof AndroidKeyStorePrivateKey) ? "private" : "public") + + " key"); + } + } else if (ECPublicKeySpec.class.equals(keySpecClass)) { + if (key instanceof AndroidKeyStoreECPublicKey) { + AndroidKeyStoreECPublicKey + ecKey = (AndroidKeyStoreECPublicKey) key; + @SuppressWarnings("unchecked") + T result = (T) new ECPublicKeySpec(ecKey.getW(), ecKey.getParams()); + return result; + } else { + throw new InvalidKeySpecException( + "Obtaining ECPublicKeySpec not supported for " + key.getAlgorithm() + " " + + ((key instanceof AndroidKeyStorePrivateKey) ? "private" : "public") + + " key"); + } + } else { + throw new InvalidKeySpecException("Unsupported key spec: " + keySpecClass.getName()); + } + } + + @Override + protected PrivateKey engineGeneratePrivate(KeySpec spec) throws InvalidKeySpecException { + throw new InvalidKeySpecException( + "To generate a key pair in Android Keystore, use KeyPairGenerator initialized with" + + " " + KeyGenParameterSpec.class.getName()); + } + + @Override + protected PublicKey engineGeneratePublic(KeySpec spec) throws InvalidKeySpecException { + throw new InvalidKeySpecException( + "To generate a key pair in Android Keystore, use KeyPairGenerator initialized with" + + " " + KeyGenParameterSpec.class.getName()); + } + + @Override + protected Key engineTranslateKey(Key key) throws InvalidKeyException { + if (key == null) { + throw new InvalidKeyException("key == null"); + } else if ((!(key instanceof AndroidKeyStorePrivateKey)) + && (!(key instanceof AndroidKeyStorePublicKey))) { + throw new InvalidKeyException( + "To import a key into Android Keystore, use KeyStore.setEntry"); + } + return key; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyGeneratorSpi.java new file mode 100644 index 000000000000..7d18be553122 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyGeneratorSpi.java @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.security.Credentials; +import android.security.KeyStore; +import android.security.keymaster.KeyCharacteristics; +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.security.keystore.KeymasterUtils; +import android.security.keystore.StrongBoxUnavailableException; + +import libcore.util.EmptyArray; + +import java.security.InvalidAlgorithmParameterException; +import java.security.ProviderException; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; + +import javax.crypto.KeyGeneratorSpi; +import javax.crypto.SecretKey; + +/** + * {@link KeyGeneratorSpi} backed by Android KeyStore. + * + * @hide + */ +public abstract class AndroidKeyStoreKeyGeneratorSpi extends KeyGeneratorSpi { + + public static class AES extends AndroidKeyStoreKeyGeneratorSpi { + public AES() { + super(KeymasterDefs.KM_ALGORITHM_AES, 128); + } + + @Override + protected void engineInit(AlgorithmParameterSpec params, SecureRandom random) + throws InvalidAlgorithmParameterException { + super.engineInit(params, random); + if ((mKeySizeBits != 128) && (mKeySizeBits != 192) && (mKeySizeBits != 256)) { + throw new InvalidAlgorithmParameterException( + "Unsupported key size: " + mKeySizeBits + + ". Supported: 128, 192, 256."); + } + } + } + + public static class DESede extends AndroidKeyStoreKeyGeneratorSpi { + public DESede() { + super(KeymasterDefs.KM_ALGORITHM_3DES, 168); + } + } + + protected static abstract class HmacBase extends AndroidKeyStoreKeyGeneratorSpi { + protected HmacBase(int keymasterDigest) { + super(KeymasterDefs.KM_ALGORITHM_HMAC, + keymasterDigest, + KeymasterUtils.getDigestOutputSizeBits(keymasterDigest)); + } + } + + public static class HmacSHA1 extends HmacBase { + public HmacSHA1() { + super(KeymasterDefs.KM_DIGEST_SHA1); + } + } + + public static class HmacSHA224 extends HmacBase { + public HmacSHA224() { + super(KeymasterDefs.KM_DIGEST_SHA_2_224); + } + } + + public static class HmacSHA256 extends HmacBase { + public HmacSHA256() { + super(KeymasterDefs.KM_DIGEST_SHA_2_256); + } + } + + public static class HmacSHA384 extends HmacBase { + public HmacSHA384() { + super(KeymasterDefs.KM_DIGEST_SHA_2_384); + } + } + + public static class HmacSHA512 extends HmacBase { + public HmacSHA512() { + super(KeymasterDefs.KM_DIGEST_SHA_2_512); + } + } + + private final KeyStore mKeyStore = KeyStore.getInstance(); + private final int mKeymasterAlgorithm; + private final int mKeymasterDigest; + private final int mDefaultKeySizeBits; + + private KeyGenParameterSpec mSpec; + private SecureRandom mRng; + + protected int mKeySizeBits; + private int[] mKeymasterPurposes; + private int[] mKeymasterBlockModes; + private int[] mKeymasterPaddings; + private int[] mKeymasterDigests; + + protected AndroidKeyStoreKeyGeneratorSpi( + int keymasterAlgorithm, + int defaultKeySizeBits) { + this(keymasterAlgorithm, -1, defaultKeySizeBits); + } + + protected AndroidKeyStoreKeyGeneratorSpi( + int keymasterAlgorithm, + int keymasterDigest, + int defaultKeySizeBits) { + mKeymasterAlgorithm = keymasterAlgorithm; + mKeymasterDigest = keymasterDigest; + mDefaultKeySizeBits = defaultKeySizeBits; + if (mDefaultKeySizeBits <= 0) { + throw new IllegalArgumentException("Default key size must be positive"); + } + + if ((mKeymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC) && (mKeymasterDigest == -1)) { + throw new IllegalArgumentException( + "Digest algorithm must be specified for HMAC key"); + } + } + + @Override + protected void engineInit(SecureRandom random) { + throw new UnsupportedOperationException("Cannot initialize without a " + + KeyGenParameterSpec.class.getName() + " parameter"); + } + + @Override + protected void engineInit(int keySize, SecureRandom random) { + throw new UnsupportedOperationException("Cannot initialize without a " + + KeyGenParameterSpec.class.getName() + " parameter"); + } + + @Override + protected void engineInit(AlgorithmParameterSpec params, SecureRandom random) + throws InvalidAlgorithmParameterException { + resetAll(); + + boolean success = false; + try { + if ((params == null) || (!(params instanceof KeyGenParameterSpec))) { + throw new InvalidAlgorithmParameterException("Cannot initialize without a " + + KeyGenParameterSpec.class.getName() + " parameter"); + } + KeyGenParameterSpec spec = (KeyGenParameterSpec) params; + if (spec.getKeystoreAlias() == null) { + throw new InvalidAlgorithmParameterException("KeyStore entry alias not provided"); + } + + mRng = random; + mSpec = spec; + + mKeySizeBits = (spec.getKeySize() != -1) ? spec.getKeySize() : mDefaultKeySizeBits; + if (mKeySizeBits <= 0) { + throw new InvalidAlgorithmParameterException( + "Key size must be positive: " + mKeySizeBits); + } else if ((mKeySizeBits % 8) != 0) { + throw new InvalidAlgorithmParameterException( + "Key size must be a multiple of 8: " + mKeySizeBits); + } + + try { + mKeymasterPurposes = KeyProperties.Purpose.allToKeymaster(spec.getPurposes()); + mKeymasterPaddings = KeyProperties.EncryptionPadding.allToKeymaster( + spec.getEncryptionPaddings()); + if (spec.getSignaturePaddings().length > 0) { + throw new InvalidAlgorithmParameterException( + "Signature paddings not supported for symmetric key algorithms"); + } + mKeymasterBlockModes = KeyProperties.BlockMode.allToKeymaster(spec.getBlockModes()); + if (((spec.getPurposes() & KeyProperties.PURPOSE_ENCRYPT) != 0) + && (spec.isRandomizedEncryptionRequired())) { + for (int keymasterBlockMode : mKeymasterBlockModes) { + if (!KeymasterUtils.isKeymasterBlockModeIndCpaCompatibleWithSymmetricCrypto( + keymasterBlockMode)) { + throw new InvalidAlgorithmParameterException( + "Randomized encryption (IND-CPA) required but may be violated" + + " by block mode: " + + KeyProperties.BlockMode.fromKeymaster(keymasterBlockMode) + + ". See " + KeyGenParameterSpec.class.getName() + + " documentation."); + } + } + } + if (mKeymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_3DES) { + if (mKeySizeBits != 168) { + throw new InvalidAlgorithmParameterException( + "3DES key size must be 168 bits."); + } + } + if (mKeymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC) { + if (mKeySizeBits < 64 || mKeySizeBits > 512) { + throw new InvalidAlgorithmParameterException( + "HMAC key sizes must be within 64-512 bits, inclusive."); + } + + // JCA HMAC key algorithm implies a digest (e.g., HmacSHA256 key algorithm + // implies SHA-256 digest). Because keymaster HMAC key is authorized only for + // one digest, we don't let algorithm parameter spec override the digest implied + // by the key. If the spec specifies digests at all, it must specify only one + // digest, the only implied by key algorithm. + mKeymasterDigests = new int[] {mKeymasterDigest}; + if (spec.isDigestsSpecified()) { + // Digest(s) explicitly specified in the spec. Check that the list + // consists of exactly one digest, the one implied by key algorithm. + int[] keymasterDigestsFromSpec = + KeyProperties.Digest.allToKeymaster(spec.getDigests()); + if ((keymasterDigestsFromSpec.length != 1) + || (keymasterDigestsFromSpec[0] != mKeymasterDigest)) { + throw new InvalidAlgorithmParameterException( + "Unsupported digests specification: " + + Arrays.asList(spec.getDigests()) + ". Only " + + KeyProperties.Digest.fromKeymaster(mKeymasterDigest) + + " supported for this HMAC key algorithm"); + } + } + } else { + // Key algorithm does not imply a digest. + if (spec.isDigestsSpecified()) { + mKeymasterDigests = KeyProperties.Digest.allToKeymaster(spec.getDigests()); + } else { + mKeymasterDigests = EmptyArray.INT; + } + } + + // Check that user authentication related parameters are acceptable. This method + // will throw an IllegalStateException if there are issues (e.g., secure lock screen + // not set up). + KeymasterUtils.addUserAuthArgs(new KeymasterArguments(), spec); + } catch (IllegalStateException | IllegalArgumentException e) { + throw new InvalidAlgorithmParameterException(e); + } + + success = true; + } finally { + if (!success) { + resetAll(); + } + } + } + + private void resetAll() { + mSpec = null; + mRng = null; + mKeySizeBits = -1; + mKeymasterPurposes = null; + mKeymasterPaddings = null; + mKeymasterBlockModes = null; + } + + @Override + protected SecretKey engineGenerateKey() { + KeyGenParameterSpec spec = mSpec; + if (spec == null) { + throw new IllegalStateException("Not initialized"); + } + + KeymasterArguments args = new KeymasterArguments(); + args.addUnsignedInt(KeymasterDefs.KM_TAG_KEY_SIZE, mKeySizeBits); + args.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, mKeymasterAlgorithm); + args.addEnums(KeymasterDefs.KM_TAG_PURPOSE, mKeymasterPurposes); + args.addEnums(KeymasterDefs.KM_TAG_BLOCK_MODE, mKeymasterBlockModes); + args.addEnums(KeymasterDefs.KM_TAG_PADDING, mKeymasterPaddings); + args.addEnums(KeymasterDefs.KM_TAG_DIGEST, mKeymasterDigests); + KeymasterUtils.addUserAuthArgs(args, spec); + KeymasterUtils.addMinMacLengthAuthorizationIfNecessary( + args, + mKeymasterAlgorithm, + mKeymasterBlockModes, + mKeymasterDigests); + args.addDateIfNotNull(KeymasterDefs.KM_TAG_ACTIVE_DATETIME, spec.getKeyValidityStart()); + args.addDateIfNotNull(KeymasterDefs.KM_TAG_ORIGINATION_EXPIRE_DATETIME, + spec.getKeyValidityForOriginationEnd()); + args.addDateIfNotNull(KeymasterDefs.KM_TAG_USAGE_EXPIRE_DATETIME, + spec.getKeyValidityForConsumptionEnd()); + + if (((spec.getPurposes() & KeyProperties.PURPOSE_ENCRYPT) != 0) + && (!spec.isRandomizedEncryptionRequired())) { + // Permit caller-provided IV when encrypting with this key + args.addBoolean(KeymasterDefs.KM_TAG_CALLER_NONCE); + } + + byte[] additionalEntropy = + KeyStoreCryptoOperationUtils.getRandomBytesToMixIntoKeystoreRng( + mRng, (mKeySizeBits + 7) / 8); + int flags = 0; + if (spec.isStrongBoxBacked()) { + flags |= KeyStore.FLAG_STRONGBOX; + } + if (spec.isCriticalToDeviceEncryption()) { + flags |= KeyStore.FLAG_CRITICAL_TO_DEVICE_ENCRYPTION; + } + String keyAliasInKeystore = Credentials.USER_PRIVATE_KEY + spec.getKeystoreAlias(); + KeyCharacteristics resultingKeyCharacteristics = new KeyCharacteristics(); + boolean success = false; + try { + Credentials.deleteAllTypesForAlias(mKeyStore, spec.getKeystoreAlias(), spec.getUid()); + int errorCode = mKeyStore.generateKey( + keyAliasInKeystore, + args, + additionalEntropy, + spec.getUid(), + flags, + resultingKeyCharacteristics); + if (errorCode != KeyStore.NO_ERROR) { + if (errorCode == KeyStore.HARDWARE_TYPE_UNAVAILABLE) { + throw new StrongBoxUnavailableException("Failed to generate key"); + } else { + throw new ProviderException( + "Keystore operation failed", KeyStore.getKeyStoreException(errorCode)); + } + } + @KeyProperties.KeyAlgorithmEnum String keyAlgorithmJCA; + try { + keyAlgorithmJCA = KeyProperties.KeyAlgorithm.fromKeymasterSecretKeyAlgorithm( + mKeymasterAlgorithm, mKeymasterDigest); + } catch (IllegalArgumentException e) { + throw new ProviderException("Failed to obtain JCA secret key algorithm name", e); + } + SecretKey result = new AndroidKeyStoreSecretKey( + keyAliasInKeystore, spec.getUid(), keyAlgorithmJCA); + success = true; + return result; + } finally { + if (!success) { + Credentials.deleteAllTypesForAlias( + mKeyStore, spec.getKeystoreAlias(), spec.getUid()); + } + } + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java new file mode 100644 index 000000000000..ff40c1586212 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java @@ -0,0 +1,933 @@ +/* + * Copyright (C) 2012 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 android.security.keystore2; + +import android.annotation.Nullable; +import android.os.Build; +import android.security.Credentials; +import android.security.KeyPairGeneratorSpec; +import android.security.KeyStore; +import android.security.keymaster.KeyCharacteristics; +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterCertificateChain; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyPermanentlyInvalidatedException; +import android.security.keystore.KeymasterUtils; +import android.security.keystore.SecureKeyImportUnavailableException; +import android.security.keystore.StrongBoxUnavailableException; + +import com.android.org.bouncycastle.asn1.ASN1EncodableVector; +import com.android.org.bouncycastle.asn1.ASN1InputStream; +import com.android.org.bouncycastle.asn1.ASN1Integer; +import com.android.org.bouncycastle.asn1.ASN1ObjectIdentifier; +import com.android.org.bouncycastle.asn1.DERBitString; +import com.android.org.bouncycastle.asn1.DERNull; +import com.android.org.bouncycastle.asn1.DERSequence; +import com.android.org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import com.android.org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import com.android.org.bouncycastle.asn1.x509.Certificate; +import com.android.org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import com.android.org.bouncycastle.asn1.x509.TBSCertificate; +import com.android.org.bouncycastle.asn1.x509.Time; +import com.android.org.bouncycastle.asn1.x509.V3TBSCertificateGenerator; +import com.android.org.bouncycastle.asn1.x9.X9ObjectIdentifiers; +import com.android.org.bouncycastle.jce.X509Principal; +import com.android.org.bouncycastle.jce.provider.X509CertificateObject; +import com.android.org.bouncycastle.x509.X509V3CertificateGenerator; + +import libcore.util.EmptyArray; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyPairGeneratorSpi; +import java.security.PrivateKey; +import java.security.ProviderException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.RSAKeyGenParameterSpec; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * Provides a way to create instances of a KeyPair which will be placed in the + * Android keystore service usable only by the application that called it. This + * can be used in conjunction with + * {@link java.security.KeyStore#getInstance(String)} using the + * {@code "AndroidKeyStore"} type. + * <p> + * This class can not be directly instantiated and must instead be used via the + * {@link KeyPairGenerator#getInstance(String) + * KeyPairGenerator.getInstance("AndroidKeyStore")} API. + * + * @hide + */ +public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGeneratorSpi { + + public static class RSA extends AndroidKeyStoreKeyPairGeneratorSpi { + public RSA() { + super(KeymasterDefs.KM_ALGORITHM_RSA); + } + } + + public static class EC extends AndroidKeyStoreKeyPairGeneratorSpi { + public EC() { + super(KeymasterDefs.KM_ALGORITHM_EC); + } + } + + /* + * These must be kept in sync with system/security/keystore/defaults.h + */ + + /* EC */ + private static final int EC_DEFAULT_KEY_SIZE = 256; + + /* RSA */ + private static final int RSA_DEFAULT_KEY_SIZE = 2048; + private static final int RSA_MIN_KEY_SIZE = 512; + private static final int RSA_MAX_KEY_SIZE = 8192; + + private static final Map<String, Integer> SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE = + new HashMap<String, Integer>(); + private static final List<String> SUPPORTED_EC_NIST_CURVE_NAMES = new ArrayList<String>(); + private static final List<Integer> SUPPORTED_EC_NIST_CURVE_SIZES = new ArrayList<Integer>(); + static { + // Aliases for NIST P-224 + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("p-224", 224); + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("secp224r1", 224); + + + // Aliases for NIST P-256 + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("p-256", 256); + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("secp256r1", 256); + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("prime256v1", 256); + + // Aliases for NIST P-384 + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("p-384", 384); + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("secp384r1", 384); + + // Aliases for NIST P-521 + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("p-521", 521); + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("secp521r1", 521); + + SUPPORTED_EC_NIST_CURVE_NAMES.addAll(SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.keySet()); + Collections.sort(SUPPORTED_EC_NIST_CURVE_NAMES); + + SUPPORTED_EC_NIST_CURVE_SIZES.addAll( + new HashSet<Integer>(SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.values())); + Collections.sort(SUPPORTED_EC_NIST_CURVE_SIZES); + } + + private final int mOriginalKeymasterAlgorithm; + + private KeyStore mKeyStore; + + private KeyGenParameterSpec mSpec; + + private String mEntryAlias; + private int mEntryUid; + private boolean mEncryptionAtRestRequired; + private @KeyProperties.KeyAlgorithmEnum String mJcaKeyAlgorithm; + private int mKeymasterAlgorithm = -1; + private int mKeySizeBits; + private SecureRandom mRng; + + private int[] mKeymasterPurposes; + private int[] mKeymasterBlockModes; + private int[] mKeymasterEncryptionPaddings; + private int[] mKeymasterSignaturePaddings; + private int[] mKeymasterDigests; + + private BigInteger mRSAPublicExponent; + + protected AndroidKeyStoreKeyPairGeneratorSpi(int keymasterAlgorithm) { + mOriginalKeymasterAlgorithm = keymasterAlgorithm; + } + + @SuppressWarnings("deprecation") + @Override + public void initialize(int keysize, SecureRandom random) { + throw new IllegalArgumentException( + KeyGenParameterSpec.class.getName() + " or " + KeyPairGeneratorSpec.class.getName() + + " required to initialize this KeyPairGenerator"); + } + + @SuppressWarnings("deprecation") + @Override + public void initialize(AlgorithmParameterSpec params, SecureRandom random) + throws InvalidAlgorithmParameterException { + resetAll(); + + boolean success = false; + try { + if (params == null) { + throw new InvalidAlgorithmParameterException( + "Must supply params of type " + KeyGenParameterSpec.class.getName() + + " or " + KeyPairGeneratorSpec.class.getName()); + } + + KeyGenParameterSpec spec; + boolean encryptionAtRestRequired = false; + int keymasterAlgorithm = mOriginalKeymasterAlgorithm; + if (params instanceof KeyGenParameterSpec) { + spec = (KeyGenParameterSpec) params; + } else if (params instanceof KeyPairGeneratorSpec) { + // Legacy/deprecated spec + KeyPairGeneratorSpec legacySpec = (KeyPairGeneratorSpec) params; + try { + KeyGenParameterSpec.Builder specBuilder; + String specKeyAlgorithm = legacySpec.getKeyType(); + if (specKeyAlgorithm != null) { + // Spec overrides the generator's default key algorithm + try { + keymasterAlgorithm = + KeyProperties.KeyAlgorithm.toKeymasterAsymmetricKeyAlgorithm( + specKeyAlgorithm); + } catch (IllegalArgumentException e) { + throw new InvalidAlgorithmParameterException( + "Invalid key type in parameters", e); + } + } + switch (keymasterAlgorithm) { + case KeymasterDefs.KM_ALGORITHM_EC: + specBuilder = new KeyGenParameterSpec.Builder( + legacySpec.getKeystoreAlias(), + KeyProperties.PURPOSE_SIGN + | KeyProperties.PURPOSE_VERIFY); + // Authorized to be used with any digest (including no digest). + // MD5 was never offered for Android Keystore for ECDSA. + specBuilder.setDigests( + KeyProperties.DIGEST_NONE, + KeyProperties.DIGEST_SHA1, + KeyProperties.DIGEST_SHA224, + KeyProperties.DIGEST_SHA256, + KeyProperties.DIGEST_SHA384, + KeyProperties.DIGEST_SHA512); + break; + case KeymasterDefs.KM_ALGORITHM_RSA: + specBuilder = new KeyGenParameterSpec.Builder( + legacySpec.getKeystoreAlias(), + KeyProperties.PURPOSE_ENCRYPT + | KeyProperties.PURPOSE_DECRYPT + | KeyProperties.PURPOSE_SIGN + | KeyProperties.PURPOSE_VERIFY); + // Authorized to be used with any digest (including no digest). + specBuilder.setDigests( + KeyProperties.DIGEST_NONE, + KeyProperties.DIGEST_MD5, + KeyProperties.DIGEST_SHA1, + KeyProperties.DIGEST_SHA224, + KeyProperties.DIGEST_SHA256, + KeyProperties.DIGEST_SHA384, + KeyProperties.DIGEST_SHA512); + // Authorized to be used with any encryption and signature padding + // schemes (including no padding). + specBuilder.setEncryptionPaddings( + KeyProperties.ENCRYPTION_PADDING_NONE, + KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1, + KeyProperties.ENCRYPTION_PADDING_RSA_OAEP); + specBuilder.setSignaturePaddings( + KeyProperties.SIGNATURE_PADDING_RSA_PKCS1, + KeyProperties.SIGNATURE_PADDING_RSA_PSS); + // Disable randomized encryption requirement to support encryption + // padding NONE above. + specBuilder.setRandomizedEncryptionRequired(false); + break; + default: + throw new ProviderException( + "Unsupported algorithm: " + mKeymasterAlgorithm); + } + + if (legacySpec.getKeySize() != -1) { + specBuilder.setKeySize(legacySpec.getKeySize()); + } + if (legacySpec.getAlgorithmParameterSpec() != null) { + specBuilder.setAlgorithmParameterSpec( + legacySpec.getAlgorithmParameterSpec()); + } + specBuilder.setCertificateSubject(legacySpec.getSubjectDN()); + specBuilder.setCertificateSerialNumber(legacySpec.getSerialNumber()); + specBuilder.setCertificateNotBefore(legacySpec.getStartDate()); + specBuilder.setCertificateNotAfter(legacySpec.getEndDate()); + encryptionAtRestRequired = legacySpec.isEncryptionRequired(); + specBuilder.setUserAuthenticationRequired(false); + + spec = specBuilder.build(); + } catch (NullPointerException | IllegalArgumentException e) { + throw new InvalidAlgorithmParameterException(e); + } + } else { + throw new InvalidAlgorithmParameterException( + "Unsupported params class: " + params.getClass().getName() + + ". Supported: " + KeyGenParameterSpec.class.getName() + + ", " + KeyPairGeneratorSpec.class.getName()); + } + + mEntryAlias = spec.getKeystoreAlias(); + mEntryUid = spec.getUid(); + mSpec = spec; + mKeymasterAlgorithm = keymasterAlgorithm; + mEncryptionAtRestRequired = encryptionAtRestRequired; + mKeySizeBits = spec.getKeySize(); + initAlgorithmSpecificParameters(); + if (mKeySizeBits == -1) { + mKeySizeBits = getDefaultKeySize(keymasterAlgorithm); + } + checkValidKeySize(keymasterAlgorithm, mKeySizeBits, mSpec.isStrongBoxBacked()); + + if (spec.getKeystoreAlias() == null) { + throw new InvalidAlgorithmParameterException("KeyStore entry alias not provided"); + } + + String jcaKeyAlgorithm; + try { + jcaKeyAlgorithm = KeyProperties.KeyAlgorithm.fromKeymasterAsymmetricKeyAlgorithm( + keymasterAlgorithm); + mKeymasterPurposes = KeyProperties.Purpose.allToKeymaster(spec.getPurposes()); + mKeymasterBlockModes = KeyProperties.BlockMode.allToKeymaster(spec.getBlockModes()); + mKeymasterEncryptionPaddings = KeyProperties.EncryptionPadding.allToKeymaster( + spec.getEncryptionPaddings()); + if (((spec.getPurposes() & KeyProperties.PURPOSE_ENCRYPT) != 0) + && (spec.isRandomizedEncryptionRequired())) { + for (int keymasterPadding : mKeymasterEncryptionPaddings) { + if (!KeymasterUtils + .isKeymasterPaddingSchemeIndCpaCompatibleWithAsymmetricCrypto( + keymasterPadding)) { + throw new InvalidAlgorithmParameterException( + "Randomized encryption (IND-CPA) required but may be violated" + + " by padding scheme: " + + KeyProperties.EncryptionPadding.fromKeymaster( + keymasterPadding) + + ". See " + KeyGenParameterSpec.class.getName() + + " documentation."); + } + } + } + mKeymasterSignaturePaddings = KeyProperties.SignaturePadding.allToKeymaster( + spec.getSignaturePaddings()); + if (spec.isDigestsSpecified()) { + mKeymasterDigests = KeyProperties.Digest.allToKeymaster(spec.getDigests()); + } else { + mKeymasterDigests = EmptyArray.INT; + } + + // Check that user authentication related parameters are acceptable. This method + // will throw an IllegalStateException if there are issues (e.g., secure lock screen + // not set up). + KeymasterUtils.addUserAuthArgs(new KeymasterArguments(), mSpec); + } catch (IllegalArgumentException | IllegalStateException e) { + throw new InvalidAlgorithmParameterException(e); + } + + mJcaKeyAlgorithm = jcaKeyAlgorithm; + mRng = random; + mKeyStore = KeyStore.getInstance(); + success = true; + } finally { + if (!success) { + resetAll(); + } + } + } + + private void resetAll() { + mEntryAlias = null; + mEntryUid = KeyStore.UID_SELF; + mJcaKeyAlgorithm = null; + mKeymasterAlgorithm = -1; + mKeymasterPurposes = null; + mKeymasterBlockModes = null; + mKeymasterEncryptionPaddings = null; + mKeymasterSignaturePaddings = null; + mKeymasterDigests = null; + mKeySizeBits = 0; + mSpec = null; + mRSAPublicExponent = null; + mEncryptionAtRestRequired = false; + mRng = null; + mKeyStore = null; + } + + private void initAlgorithmSpecificParameters() throws InvalidAlgorithmParameterException { + AlgorithmParameterSpec algSpecificSpec = mSpec.getAlgorithmParameterSpec(); + switch (mKeymasterAlgorithm) { + case KeymasterDefs.KM_ALGORITHM_RSA: + { + BigInteger publicExponent = null; + if (algSpecificSpec instanceof RSAKeyGenParameterSpec) { + RSAKeyGenParameterSpec rsaSpec = (RSAKeyGenParameterSpec) algSpecificSpec; + if (mKeySizeBits == -1) { + mKeySizeBits = rsaSpec.getKeysize(); + } else if (mKeySizeBits != rsaSpec.getKeysize()) { + throw new InvalidAlgorithmParameterException("RSA key size must match " + + " between " + mSpec + " and " + algSpecificSpec + + ": " + mKeySizeBits + " vs " + rsaSpec.getKeysize()); + } + publicExponent = rsaSpec.getPublicExponent(); + } else if (algSpecificSpec != null) { + throw new InvalidAlgorithmParameterException( + "RSA may only use RSAKeyGenParameterSpec"); + } + if (publicExponent == null) { + publicExponent = RSAKeyGenParameterSpec.F4; + } + if (publicExponent.compareTo(BigInteger.ZERO) < 1) { + throw new InvalidAlgorithmParameterException( + "RSA public exponent must be positive: " + publicExponent); + } + if (publicExponent.compareTo(KeymasterArguments.UINT64_MAX_VALUE) > 0) { + throw new InvalidAlgorithmParameterException( + "Unsupported RSA public exponent: " + publicExponent + + ". Maximum supported value: " + KeymasterArguments.UINT64_MAX_VALUE); + } + mRSAPublicExponent = publicExponent; + break; + } + case KeymasterDefs.KM_ALGORITHM_EC: + if (algSpecificSpec instanceof ECGenParameterSpec) { + ECGenParameterSpec ecSpec = (ECGenParameterSpec) algSpecificSpec; + String curveName = ecSpec.getName(); + Integer ecSpecKeySizeBits = SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.get( + curveName.toLowerCase(Locale.US)); + if (ecSpecKeySizeBits == null) { + throw new InvalidAlgorithmParameterException( + "Unsupported EC curve name: " + curveName + + ". Supported: " + SUPPORTED_EC_NIST_CURVE_NAMES); + } + if (mKeySizeBits == -1) { + mKeySizeBits = ecSpecKeySizeBits; + } else if (mKeySizeBits != ecSpecKeySizeBits) { + throw new InvalidAlgorithmParameterException("EC key size must match " + + " between " + mSpec + " and " + algSpecificSpec + + ": " + mKeySizeBits + " vs " + ecSpecKeySizeBits); + } + } else if (algSpecificSpec != null) { + throw new InvalidAlgorithmParameterException( + "EC may only use ECGenParameterSpec"); + } + break; + default: + throw new ProviderException("Unsupported algorithm: " + mKeymasterAlgorithm); + } + } + + @Override + public KeyPair generateKeyPair() { + if (mKeyStore == null || mSpec == null) { + throw new IllegalStateException("Not initialized"); + } + + int flags = (mEncryptionAtRestRequired) ? KeyStore.FLAG_ENCRYPTED : 0; + if (((flags & KeyStore.FLAG_ENCRYPTED) != 0) + && (mKeyStore.state() != KeyStore.State.UNLOCKED)) { + throw new IllegalStateException( + "Encryption at rest using secure lock screen credential requested for key pair" + + ", but the user has not yet entered the credential"); + } + + if (mSpec.isStrongBoxBacked()) { + flags |= KeyStore.FLAG_STRONGBOX; + } + if (mSpec.isCriticalToDeviceEncryption()) { + flags |= KeyStore.FLAG_CRITICAL_TO_DEVICE_ENCRYPTION; + } + + byte[] additionalEntropy = + KeyStoreCryptoOperationUtils.getRandomBytesToMixIntoKeystoreRng( + mRng, (mKeySizeBits + 7) / 8); + + Credentials.deleteAllTypesForAlias(mKeyStore, mEntryAlias, mEntryUid); + final String privateKeyAlias = Credentials.USER_PRIVATE_KEY + mEntryAlias; + boolean success = false; + try { + generateKeystoreKeyPair( + privateKeyAlias, constructKeyGenerationArguments(), additionalEntropy, flags); + KeyPair keyPair = loadKeystoreKeyPair(privateKeyAlias); + + storeCertificateChain(flags, createCertificateChain(privateKeyAlias, keyPair)); + + success = true; + return keyPair; + } catch (ProviderException e) { + if ((mSpec.getPurposes() & KeyProperties.PURPOSE_WRAP_KEY) != 0) { + throw new SecureKeyImportUnavailableException(e); + } else { + throw e; + } + } finally { + if (!success) { + Credentials.deleteAllTypesForAlias(mKeyStore, mEntryAlias, mEntryUid); + } + } + } + + private Iterable<byte[]> createCertificateChain(final String privateKeyAlias, KeyPair keyPair) + throws ProviderException { + byte[] challenge = mSpec.getAttestationChallenge(); + if (challenge != null) { + KeymasterArguments args = new KeymasterArguments(); + args.addBytes(KeymasterDefs.KM_TAG_ATTESTATION_CHALLENGE, challenge); + + if (mSpec.isDevicePropertiesAttestationIncluded()) { + args.addBytes(KeymasterDefs.KM_TAG_ATTESTATION_ID_BRAND, + Build.BRAND.getBytes(StandardCharsets.UTF_8)); + args.addBytes(KeymasterDefs.KM_TAG_ATTESTATION_ID_DEVICE, + Build.DEVICE.getBytes(StandardCharsets.UTF_8)); + args.addBytes(KeymasterDefs.KM_TAG_ATTESTATION_ID_PRODUCT, + Build.PRODUCT.getBytes(StandardCharsets.UTF_8)); + args.addBytes(KeymasterDefs.KM_TAG_ATTESTATION_ID_MANUFACTURER, + Build.MANUFACTURER.getBytes(StandardCharsets.UTF_8)); + args.addBytes(KeymasterDefs.KM_TAG_ATTESTATION_ID_MODEL, + Build.MODEL.getBytes(StandardCharsets.UTF_8)); + } + + return getAttestationChain(privateKeyAlias, keyPair, args); + } + + // Very short certificate chain in the non-attestation case. + return Collections.singleton(generateSelfSignedCertificateBytes(keyPair)); + } + + private void generateKeystoreKeyPair(final String privateKeyAlias, KeymasterArguments args, + byte[] additionalEntropy, final int flags) throws ProviderException { + KeyCharacteristics resultingKeyCharacteristics = new KeyCharacteristics(); + int errorCode = mKeyStore.generateKey(privateKeyAlias, args, additionalEntropy, + mEntryUid, flags, resultingKeyCharacteristics); + if (errorCode != KeyStore.NO_ERROR) { + if (errorCode == KeyStore.HARDWARE_TYPE_UNAVAILABLE) { + throw new StrongBoxUnavailableException("Failed to generate key pair"); + } else { + throw new ProviderException( + "Failed to generate key pair", KeyStore.getKeyStoreException(errorCode)); + } + } + } + + private KeyPair loadKeystoreKeyPair(final String privateKeyAlias) throws ProviderException { + try { + KeyPair result = AndroidKeyStoreProvider.loadAndroidKeyStoreKeyPairFromKeystore( + mKeyStore, privateKeyAlias, mEntryUid); + if (!mJcaKeyAlgorithm.equalsIgnoreCase(result.getPrivate().getAlgorithm())) { + throw new ProviderException( + "Generated key pair algorithm does not match requested algorithm: " + + result.getPrivate().getAlgorithm() + " vs " + mJcaKeyAlgorithm); + } + return result; + } catch (UnrecoverableKeyException | KeyPermanentlyInvalidatedException e) { + throw new ProviderException("Failed to load generated key pair from keystore", e); + } + } + + private KeymasterArguments constructKeyGenerationArguments() { + KeymasterArguments args = new KeymasterArguments(); + args.addUnsignedInt(KeymasterDefs.KM_TAG_KEY_SIZE, mKeySizeBits); + args.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, mKeymasterAlgorithm); + args.addEnums(KeymasterDefs.KM_TAG_PURPOSE, mKeymasterPurposes); + args.addEnums(KeymasterDefs.KM_TAG_BLOCK_MODE, mKeymasterBlockModes); + args.addEnums(KeymasterDefs.KM_TAG_PADDING, mKeymasterEncryptionPaddings); + args.addEnums(KeymasterDefs.KM_TAG_PADDING, mKeymasterSignaturePaddings); + args.addEnums(KeymasterDefs.KM_TAG_DIGEST, mKeymasterDigests); + + KeymasterUtils.addUserAuthArgs(args, mSpec); + args.addDateIfNotNull(KeymasterDefs.KM_TAG_ACTIVE_DATETIME, mSpec.getKeyValidityStart()); + args.addDateIfNotNull(KeymasterDefs.KM_TAG_ORIGINATION_EXPIRE_DATETIME, + mSpec.getKeyValidityForOriginationEnd()); + args.addDateIfNotNull(KeymasterDefs.KM_TAG_USAGE_EXPIRE_DATETIME, + mSpec.getKeyValidityForConsumptionEnd()); + addAlgorithmSpecificParameters(args); + + if (mSpec.isUniqueIdIncluded()) + args.addBoolean(KeymasterDefs.KM_TAG_INCLUDE_UNIQUE_ID); + + return args; + } + + private void storeCertificateChain(final int flags, Iterable<byte[]> iterable) + throws ProviderException { + Iterator<byte[]> iter = iterable.iterator(); + storeCertificate( + Credentials.USER_CERTIFICATE, iter.next(), flags, "Failed to store certificate"); + + if (!iter.hasNext()) { + return; + } + + ByteArrayOutputStream certificateConcatenationStream = new ByteArrayOutputStream(); + while (iter.hasNext()) { + byte[] data = iter.next(); + certificateConcatenationStream.write(data, 0, data.length); + } + + storeCertificate(Credentials.CA_CERTIFICATE, certificateConcatenationStream.toByteArray(), + flags, "Failed to store attestation CA certificate"); + } + + private void storeCertificate(String prefix, byte[] certificateBytes, final int flags, + String failureMessage) throws ProviderException { + int insertErrorCode = mKeyStore.insert( + prefix + mEntryAlias, + certificateBytes, + mEntryUid, + flags); + if (insertErrorCode != KeyStore.NO_ERROR) { + throw new ProviderException(failureMessage, + KeyStore.getKeyStoreException(insertErrorCode)); + } + } + + private byte[] generateSelfSignedCertificateBytes(KeyPair keyPair) throws ProviderException { + try { + return generateSelfSignedCertificate(keyPair.getPrivate(), keyPair.getPublic()) + .getEncoded(); + } catch (IOException | CertificateParsingException e) { + throw new ProviderException("Failed to generate self-signed certificate", e); + } catch (CertificateEncodingException e) { + throw new ProviderException( + "Failed to obtain encoded form of self-signed certificate", e); + } + } + + private Iterable<byte[]> getAttestationChain(String privateKeyAlias, + KeyPair keyPair, KeymasterArguments args) + throws ProviderException { + final KeymasterCertificateChain outChain = new KeymasterCertificateChain(); + final int errorCode; + if (mSpec.isDevicePropertiesAttestationIncluded() + && mSpec.getAttestationChallenge() == null) { + throw new ProviderException("An attestation challenge must be provided when requesting " + + "device properties attestation."); + } + errorCode = mKeyStore.attestKey(privateKeyAlias, args, outChain); + if (errorCode != KeyStore.NO_ERROR) { + throw new ProviderException("Failed to generate attestation certificate chain", + KeyStore.getKeyStoreException(errorCode)); + } + Collection<byte[]> chain = outChain.getCertificates(); + if (chain.size() < 2) { + throw new ProviderException("Attestation certificate chain contained " + + chain.size() + " entries. At least two are required."); + } + return chain; + } + + private void addAlgorithmSpecificParameters(KeymasterArguments keymasterArgs) { + switch (mKeymasterAlgorithm) { + case KeymasterDefs.KM_ALGORITHM_RSA: + keymasterArgs.addUnsignedLong( + KeymasterDefs.KM_TAG_RSA_PUBLIC_EXPONENT, mRSAPublicExponent); + break; + case KeymasterDefs.KM_ALGORITHM_EC: + break; + default: + throw new ProviderException("Unsupported algorithm: " + mKeymasterAlgorithm); + } + } + + private X509Certificate generateSelfSignedCertificate(PrivateKey privateKey, + PublicKey publicKey) throws CertificateParsingException, IOException { + String signatureAlgorithm = + getCertificateSignatureAlgorithm(mKeymasterAlgorithm, mKeySizeBits, mSpec); + if (signatureAlgorithm == null) { + // Key cannot be used to sign a certificate + return generateSelfSignedCertificateWithFakeSignature(publicKey); + } else { + // Key can be used to sign a certificate + try { + return generateSelfSignedCertificateWithValidSignature( + privateKey, publicKey, signatureAlgorithm); + } catch (Exception e) { + // Failed to generate the self-signed certificate with valid signature. Fall back + // to generating a self-signed certificate with a fake signature. This is done for + // all exception types because we prefer key pair generation to succeed and end up + // producing a self-signed certificate with an invalid signature to key pair + // generation failing. + return generateSelfSignedCertificateWithFakeSignature(publicKey); + } + } + } + + @SuppressWarnings("deprecation") + private X509Certificate generateSelfSignedCertificateWithValidSignature( + PrivateKey privateKey, PublicKey publicKey, String signatureAlgorithm) throws Exception { + final X509V3CertificateGenerator certGen = new X509V3CertificateGenerator(); + certGen.setPublicKey(publicKey); + certGen.setSerialNumber(mSpec.getCertificateSerialNumber()); + certGen.setSubjectDN(mSpec.getCertificateSubject()); + certGen.setIssuerDN(mSpec.getCertificateSubject()); + certGen.setNotBefore(mSpec.getCertificateNotBefore()); + certGen.setNotAfter(mSpec.getCertificateNotAfter()); + certGen.setSignatureAlgorithm(signatureAlgorithm); + return certGen.generate(privateKey); + } + + @SuppressWarnings("deprecation") + private X509Certificate generateSelfSignedCertificateWithFakeSignature( + PublicKey publicKey) throws IOException, CertificateParsingException { + V3TBSCertificateGenerator tbsGenerator = new V3TBSCertificateGenerator(); + ASN1ObjectIdentifier sigAlgOid; + AlgorithmIdentifier sigAlgId; + byte[] signature; + switch (mKeymasterAlgorithm) { + case KeymasterDefs.KM_ALGORITHM_EC: + sigAlgOid = X9ObjectIdentifiers.ecdsa_with_SHA256; + sigAlgId = new AlgorithmIdentifier(sigAlgOid); + ASN1EncodableVector v = new ASN1EncodableVector(); + v.add(new ASN1Integer(BigInteger.valueOf(0))); + v.add(new ASN1Integer(BigInteger.valueOf(0))); + signature = new DERSequence().getEncoded(); + break; + case KeymasterDefs.KM_ALGORITHM_RSA: + sigAlgOid = PKCSObjectIdentifiers.sha256WithRSAEncryption; + sigAlgId = new AlgorithmIdentifier(sigAlgOid, DERNull.INSTANCE); + signature = new byte[1]; + break; + default: + throw new ProviderException("Unsupported key algorithm: " + mKeymasterAlgorithm); + } + + try (ASN1InputStream publicKeyInfoIn = new ASN1InputStream(publicKey.getEncoded())) { + tbsGenerator.setSubjectPublicKeyInfo( + SubjectPublicKeyInfo.getInstance(publicKeyInfoIn.readObject())); + } + tbsGenerator.setSerialNumber(new ASN1Integer(mSpec.getCertificateSerialNumber())); + X509Principal subject = + new X509Principal(mSpec.getCertificateSubject().getEncoded()); + tbsGenerator.setSubject(subject); + tbsGenerator.setIssuer(subject); + tbsGenerator.setStartDate(new Time(mSpec.getCertificateNotBefore())); + tbsGenerator.setEndDate(new Time(mSpec.getCertificateNotAfter())); + tbsGenerator.setSignature(sigAlgId); + TBSCertificate tbsCertificate = tbsGenerator.generateTBSCertificate(); + + ASN1EncodableVector result = new ASN1EncodableVector(); + result.add(tbsCertificate); + result.add(sigAlgId); + result.add(new DERBitString(signature)); + return new X509CertificateObject(Certificate.getInstance(new DERSequence(result))); + } + + private static int getDefaultKeySize(int keymasterAlgorithm) { + switch (keymasterAlgorithm) { + case KeymasterDefs.KM_ALGORITHM_EC: + return EC_DEFAULT_KEY_SIZE; + case KeymasterDefs.KM_ALGORITHM_RSA: + return RSA_DEFAULT_KEY_SIZE; + default: + throw new ProviderException("Unsupported algorithm: " + keymasterAlgorithm); + } + } + + private static void checkValidKeySize( + int keymasterAlgorithm, + int keySize, + boolean isStrongBoxBacked) + throws InvalidAlgorithmParameterException { + switch (keymasterAlgorithm) { + case KeymasterDefs.KM_ALGORITHM_EC: + if (isStrongBoxBacked && keySize != 256) { + throw new InvalidAlgorithmParameterException( + "Unsupported StrongBox EC key size: " + + keySize + " bits. Supported: 256"); + } + if (!SUPPORTED_EC_NIST_CURVE_SIZES.contains(keySize)) { + throw new InvalidAlgorithmParameterException("Unsupported EC key size: " + + keySize + " bits. Supported: " + SUPPORTED_EC_NIST_CURVE_SIZES); + } + break; + case KeymasterDefs.KM_ALGORITHM_RSA: + if (keySize < RSA_MIN_KEY_SIZE || keySize > RSA_MAX_KEY_SIZE) { + throw new InvalidAlgorithmParameterException("RSA key size must be >= " + + RSA_MIN_KEY_SIZE + " and <= " + RSA_MAX_KEY_SIZE); + } + break; + default: + throw new ProviderException("Unsupported algorithm: " + keymasterAlgorithm); + } + } + + /** + * Returns the {@code Signature} algorithm to be used for signing a certificate using the + * specified key or {@code null} if the key cannot be used for signing a certificate. + */ + @Nullable + private static String getCertificateSignatureAlgorithm( + int keymasterAlgorithm, + int keySizeBits, + KeyGenParameterSpec spec) { + // Constraints: + // 1. Key must be authorized for signing without user authentication. + // 2. Signature digest must be one of key's authorized digests. + // 3. For RSA keys, the digest output size must not exceed modulus size minus space overhead + // of RSA PKCS#1 signature padding scheme (about 30 bytes). + // 4. For EC keys, the there is no point in using a digest whose output size is longer than + // key/field size because the digest will be truncated to that size. + + if ((spec.getPurposes() & KeyProperties.PURPOSE_SIGN) == 0) { + // Key not authorized for signing + return null; + } + if (spec.isUserAuthenticationRequired()) { + // Key not authorized for use without user authentication + return null; + } + if (!spec.isDigestsSpecified()) { + // Key not authorized for any digests -- can't sign + return null; + } + switch (keymasterAlgorithm) { + case KeymasterDefs.KM_ALGORITHM_EC: + { + Set<Integer> availableKeymasterDigests = getAvailableKeymasterSignatureDigests( + spec.getDigests(), + AndroidKeyStoreBCWorkaroundProvider.getSupportedEcdsaSignatureDigests()); + + int bestKeymasterDigest = -1; + int bestDigestOutputSizeBits = -1; + for (int keymasterDigest : availableKeymasterDigests) { + int outputSizeBits = KeymasterUtils.getDigestOutputSizeBits(keymasterDigest); + if (outputSizeBits == keySizeBits) { + // Perfect match -- use this digest + bestKeymasterDigest = keymasterDigest; + bestDigestOutputSizeBits = outputSizeBits; + break; + } + // Not a perfect match -- check against the best digest so far + if (bestKeymasterDigest == -1) { + // First digest tested -- definitely the best so far + bestKeymasterDigest = keymasterDigest; + bestDigestOutputSizeBits = outputSizeBits; + } else { + // Prefer output size to be as close to key size as possible, with output + // sizes larger than key size preferred to those smaller than key size. + if (bestDigestOutputSizeBits < keySizeBits) { + // Output size of the best digest so far is smaller than key size. + // Anything larger is a win. + if (outputSizeBits > bestDigestOutputSizeBits) { + bestKeymasterDigest = keymasterDigest; + bestDigestOutputSizeBits = outputSizeBits; + } + } else { + // Output size of the best digest so far is larger than key size. + // Anything smaller is a win, as long as it's not smaller than key size. + if ((outputSizeBits < bestDigestOutputSizeBits) + && (outputSizeBits >= keySizeBits)) { + bestKeymasterDigest = keymasterDigest; + bestDigestOutputSizeBits = outputSizeBits; + } + } + } + } + if (bestKeymasterDigest == -1) { + return null; + } + return KeyProperties.Digest.fromKeymasterToSignatureAlgorithmDigest( + bestKeymasterDigest) + "WithECDSA"; + } + case KeymasterDefs.KM_ALGORITHM_RSA: + { + // Check whether this key is authorized for PKCS#1 signature padding. + // We use Bouncy Castle to generate self-signed RSA certificates. Bouncy Castle + // only supports RSA certificates signed using PKCS#1 padding scheme. The key needs + // to be authorized for PKCS#1 padding or padding NONE which means any padding. + boolean pkcs1SignaturePaddingSupported = + com.android.internal.util.ArrayUtils.contains( + KeyProperties.SignaturePadding.allToKeymaster( + spec.getSignaturePaddings()), + KeymasterDefs.KM_PAD_RSA_PKCS1_1_5_SIGN); + if (!pkcs1SignaturePaddingSupported) { + // Key not authorized for PKCS#1 signature padding -- can't sign + return null; + } + + Set<Integer> availableKeymasterDigests = getAvailableKeymasterSignatureDigests( + spec.getDigests(), + AndroidKeyStoreBCWorkaroundProvider.getSupportedEcdsaSignatureDigests()); + + // The amount of space available for the digest is less than modulus size by about + // 30 bytes because padding must be at least 11 bytes long (00 || 01 || PS || 00, + // where PS must be at least 8 bytes long), and then there's also the 15--19 bytes + // overhead (depending the on chosen digest) for encoding digest OID and digest + // value in DER. + int maxDigestOutputSizeBits = keySizeBits - 30 * 8; + int bestKeymasterDigest = -1; + int bestDigestOutputSizeBits = -1; + for (int keymasterDigest : availableKeymasterDigests) { + int outputSizeBits = KeymasterUtils.getDigestOutputSizeBits(keymasterDigest); + if (outputSizeBits > maxDigestOutputSizeBits) { + // Digest too long (signature generation will fail) -- skip + continue; + } + if (bestKeymasterDigest == -1) { + // First digest tested -- definitely the best so far + bestKeymasterDigest = keymasterDigest; + bestDigestOutputSizeBits = outputSizeBits; + } else { + // The longer the better + if (outputSizeBits > bestDigestOutputSizeBits) { + bestKeymasterDigest = keymasterDigest; + bestDigestOutputSizeBits = outputSizeBits; + } + } + } + if (bestKeymasterDigest == -1) { + return null; + } + return KeyProperties.Digest.fromKeymasterToSignatureAlgorithmDigest( + bestKeymasterDigest) + "WithRSA"; + } + default: + throw new ProviderException("Unsupported algorithm: " + keymasterAlgorithm); + } + } + + private static Set<Integer> getAvailableKeymasterSignatureDigests( + @KeyProperties.DigestEnum String[] authorizedKeyDigests, + @KeyProperties.DigestEnum String[] supportedSignatureDigests) { + Set<Integer> authorizedKeymasterKeyDigests = new HashSet<Integer>(); + for (int keymasterDigest : KeyProperties.Digest.allToKeymaster(authorizedKeyDigests)) { + authorizedKeymasterKeyDigests.add(keymasterDigest); + } + Set<Integer> supportedKeymasterSignatureDigests = new HashSet<Integer>(); + for (int keymasterDigest + : KeyProperties.Digest.allToKeymaster(supportedSignatureDigests)) { + supportedKeymasterSignatureDigests.add(keymasterDigest); + } + Set<Integer> result = new HashSet<Integer>(supportedKeymasterSignatureDigests); + result.retainAll(authorizedKeymasterKeyDigests); + return result; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreLoadStoreParameter.java b/keystore/java/android/security/keystore2/AndroidKeyStoreLoadStoreParameter.java new file mode 100644 index 000000000000..38db36020fb7 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreLoadStoreParameter.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import java.security.KeyStore; +import java.security.KeyStore.ProtectionParameter; + +class AndroidKeyStoreLoadStoreParameter implements KeyStore.LoadStoreParameter { + + private final int mUid; + + AndroidKeyStoreLoadStoreParameter(int uid) { + mUid = uid; + } + + @Override + public ProtectionParameter getProtectionParameter() { + return null; + } + + int getUid() { + return mUid; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStorePrivateKey.java b/keystore/java/android/security/keystore2/AndroidKeyStorePrivateKey.java new file mode 100644 index 000000000000..f071fe89fe37 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStorePrivateKey.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import java.security.PrivateKey; + +/** + * {@link PrivateKey} backed by Android Keystore. + * + * @hide + */ +public class AndroidKeyStorePrivateKey extends AndroidKeyStoreKey implements PrivateKey { + + public AndroidKeyStorePrivateKey(String alias, int uid, String algorithm) { + super(alias, uid, algorithm); + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java new file mode 100644 index 000000000000..b759733a7573 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2012 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 android.security.keystore2; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.compat.annotation.UnsupportedAppUsage; +import android.security.KeyStore; +import android.security.keymaster.ExportResult; +import android.security.keymaster.KeyCharacteristics; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyPermanentlyInvalidatedException; +import android.security.keystore.KeyProperties; +import android.security.keystore.KeyStoreCryptoOperation; + +import java.io.IOException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.ProviderException; +import java.security.PublicKey; +import java.security.Security; +import java.security.Signature; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.interfaces.ECKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.List; + +import javax.crypto.Cipher; +import javax.crypto.Mac; + +/** + * A provider focused on providing JCA interfaces for the Android KeyStore. + * + * @hide + */ +@SystemApi +public class AndroidKeyStoreProvider extends Provider { + private static final String PROVIDER_NAME = "AndroidKeyStore"; + + // IMPLEMENTATION NOTE: Class names are hard-coded in this provider to avoid loading these + // classes when this provider is instantiated and installed early on during each app's + // initialization process. + // + // Crypto operations operating on the AndroidKeyStore keys must not be offered by this provider. + // Instead, they need to be offered by AndroidKeyStoreBCWorkaroundProvider. See its Javadoc + // for details. + + private static final String PACKAGE_NAME = "android.security.keystore"; + + private static final String DESEDE_SYSTEM_PROPERTY = + "ro.hardware.keystore_desede"; + + /** @hide **/ + public AndroidKeyStoreProvider() { + super(PROVIDER_NAME, 1.0, "Android KeyStore security provider"); + + boolean supports3DES = "true".equals(android.os.SystemProperties.get(DESEDE_SYSTEM_PROPERTY)); + + // java.security.KeyStore + put("KeyStore.AndroidKeyStore", PACKAGE_NAME + ".AndroidKeyStoreSpi"); + + // java.security.KeyPairGenerator + put("KeyPairGenerator.EC", PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$EC"); + put("KeyPairGenerator.RSA", PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$RSA"); + + // java.security.KeyFactory + putKeyFactoryImpl("EC"); + putKeyFactoryImpl("RSA"); + + // javax.crypto.KeyGenerator + put("KeyGenerator.AES", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$AES"); + put("KeyGenerator.HmacSHA1", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA1"); + put("KeyGenerator.HmacSHA224", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA224"); + put("KeyGenerator.HmacSHA256", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA256"); + put("KeyGenerator.HmacSHA384", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA384"); + put("KeyGenerator.HmacSHA512", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA512"); + + if (supports3DES) { + put("KeyGenerator.DESede", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$DESede"); + } + + // java.security.SecretKeyFactory + putSecretKeyFactoryImpl("AES"); + if (supports3DES) { + putSecretKeyFactoryImpl("DESede"); + } + putSecretKeyFactoryImpl("HmacSHA1"); + putSecretKeyFactoryImpl("HmacSHA224"); + putSecretKeyFactoryImpl("HmacSHA256"); + putSecretKeyFactoryImpl("HmacSHA384"); + putSecretKeyFactoryImpl("HmacSHA512"); + } + + /** + * Installs a new instance of this provider (and the + * {@link AndroidKeyStoreBCWorkaroundProvider}). + * @hide + */ + public static void install() { + Provider[] providers = Security.getProviders(); + int bcProviderIndex = -1; + for (int i = 0; i < providers.length; i++) { + Provider provider = providers[i]; + if ("BC".equals(provider.getName())) { + bcProviderIndex = i; + break; + } + } + + Security.addProvider(new AndroidKeyStoreProvider()); + Provider workaroundProvider = new AndroidKeyStoreBCWorkaroundProvider(); + if (bcProviderIndex != -1) { + // Bouncy Castle provider found -- install the workaround provider above it. + // insertProviderAt uses 1-based positions. + Security.insertProviderAt(workaroundProvider, bcProviderIndex + 1); + } else { + // Bouncy Castle provider not found -- install the workaround provider at lowest + // priority. + Security.addProvider(workaroundProvider); + } + } + + private void putSecretKeyFactoryImpl(String algorithm) { + put("SecretKeyFactory." + algorithm, PACKAGE_NAME + ".AndroidKeyStoreSecretKeyFactorySpi"); + } + + private void putKeyFactoryImpl(String algorithm) { + put("KeyFactory." + algorithm, PACKAGE_NAME + ".AndroidKeyStoreKeyFactorySpi"); + } + + /** + * Gets the {@link KeyStore} operation handle corresponding to the provided JCA crypto + * primitive. + * + * <p>The following primitives are supported: {@link Cipher} and {@link Mac}. + * + * @return KeyStore operation handle or {@code 0} if the provided primitive's KeyStore operation + * is not in progress. + * + * @throws IllegalArgumentException if the provided primitive is not supported or is not backed + * by AndroidKeyStore provider. + * @throws IllegalStateException if the provided primitive is not initialized. + * @hide + */ + @UnsupportedAppUsage + public static long getKeyStoreOperationHandle(Object cryptoPrimitive) { + if (cryptoPrimitive == null) { + throw new NullPointerException(); + } + Object spi; + if (cryptoPrimitive instanceof Signature) { + spi = ((Signature) cryptoPrimitive).getCurrentSpi(); + } else if (cryptoPrimitive instanceof Mac) { + spi = ((Mac) cryptoPrimitive).getCurrentSpi(); + } else if (cryptoPrimitive instanceof Cipher) { + spi = ((Cipher) cryptoPrimitive).getCurrentSpi(); + } else { + throw new IllegalArgumentException("Unsupported crypto primitive: " + cryptoPrimitive + + ". Supported: Signature, Mac, Cipher"); + } + if (spi == null) { + throw new IllegalStateException("Crypto primitive not initialized"); + } else if (!(spi instanceof KeyStoreCryptoOperation)) { + throw new IllegalArgumentException( + "Crypto primitive not backed by AndroidKeyStore provider: " + cryptoPrimitive + + ", spi: " + spi); + } + return ((KeyStoreCryptoOperation) spi).getOperationHandle(); + } + + /** @hide **/ + @NonNull + public static AndroidKeyStorePublicKey getAndroidKeyStorePublicKey( + @NonNull String alias, + int uid, + @NonNull @KeyProperties.KeyAlgorithmEnum String keyAlgorithm, + @NonNull byte[] x509EncodedForm) { + PublicKey publicKey; + try { + KeyFactory keyFactory = KeyFactory.getInstance(keyAlgorithm); + publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(x509EncodedForm)); + } catch (NoSuchAlgorithmException e) { + throw new ProviderException( + "Failed to obtain " + keyAlgorithm + " KeyFactory", e); + } catch (InvalidKeySpecException e) { + throw new ProviderException("Invalid X.509 encoding of public key", e); + } + if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(keyAlgorithm)) { + return new AndroidKeyStoreECPublicKey(alias, uid, (ECPublicKey) publicKey); + } else if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(keyAlgorithm)) { + return new AndroidKeyStoreRSAPublicKey(alias, uid, (RSAPublicKey) publicKey); + } else { + throw new ProviderException("Unsupported Android Keystore public key algorithm: " + + keyAlgorithm); + } + } + + @NonNull + private static AndroidKeyStorePrivateKey getAndroidKeyStorePrivateKey( + @NonNull AndroidKeyStorePublicKey publicKey) { + String keyAlgorithm = publicKey.getAlgorithm(); + if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(keyAlgorithm)) { + return new AndroidKeyStoreECPrivateKey( + publicKey.getAlias(), publicKey.getUid(), ((ECKey) publicKey).getParams()); + } else if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(keyAlgorithm)) { + return new AndroidKeyStoreRSAPrivateKey( + publicKey.getAlias(), publicKey.getUid(), ((RSAKey) publicKey).getModulus()); + } else { + throw new ProviderException("Unsupported Android Keystore public key algorithm: " + + keyAlgorithm); + } + } + + @NonNull + private static KeyCharacteristics getKeyCharacteristics(@NonNull KeyStore keyStore, + @NonNull String alias, int uid) + throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException { + KeyCharacteristics keyCharacteristics = new KeyCharacteristics(); + int errorCode = keyStore.getKeyCharacteristics( + alias, null, null, uid, keyCharacteristics); + if (errorCode == KeyStore.KEY_PERMANENTLY_INVALIDATED) { + throw (KeyPermanentlyInvalidatedException) + new KeyPermanentlyInvalidatedException( + "User changed or deleted their auth credentials", + KeyStore.getKeyStoreException(errorCode)); + } + if (errorCode != KeyStore.NO_ERROR) { + throw (UnrecoverableKeyException) + new UnrecoverableKeyException("Failed to obtain information about key") + .initCause(KeyStore.getKeyStoreException(errorCode)); + } + return keyCharacteristics; + } + + @NonNull + private static AndroidKeyStorePublicKey loadAndroidKeyStorePublicKeyFromKeystore( + @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid, + KeyCharacteristics keyCharacteristics) + throws UnrecoverableKeyException { + ExportResult exportResult = keyStore.exportKey( + privateKeyAlias, KeymasterDefs.KM_KEY_FORMAT_X509, null, null, uid); + if (exportResult.resultCode != KeyStore.NO_ERROR) { + throw (UnrecoverableKeyException) + new UnrecoverableKeyException("Failed to obtain X.509 form of public key") + .initCause(KeyStore.getKeyStoreException(exportResult.resultCode)); + } + final byte[] x509EncodedPublicKey = exportResult.exportData; + + Integer keymasterAlgorithm = keyCharacteristics.getEnum(KeymasterDefs.KM_TAG_ALGORITHM); + if (keymasterAlgorithm == null) { + throw new UnrecoverableKeyException("Key algorithm unknown"); + } + + String jcaKeyAlgorithm; + try { + jcaKeyAlgorithm = KeyProperties.KeyAlgorithm.fromKeymasterAsymmetricKeyAlgorithm( + keymasterAlgorithm); + } catch (IllegalArgumentException e) { + throw (UnrecoverableKeyException) + new UnrecoverableKeyException("Failed to load private key") + .initCause(e); + } + + return AndroidKeyStoreProvider.getAndroidKeyStorePublicKey( + privateKeyAlias, uid, jcaKeyAlgorithm, x509EncodedPublicKey); + } + + /** @hide **/ + @NonNull + public static AndroidKeyStorePublicKey loadAndroidKeyStorePublicKeyFromKeystore( + @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid) + throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException { + return loadAndroidKeyStorePublicKeyFromKeystore(keyStore, privateKeyAlias, uid, + getKeyCharacteristics(keyStore, privateKeyAlias, uid)); + } + + @NonNull + private static KeyPair loadAndroidKeyStoreKeyPairFromKeystore( + @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid, + @NonNull KeyCharacteristics keyCharacteristics) + throws UnrecoverableKeyException { + AndroidKeyStorePublicKey publicKey = + loadAndroidKeyStorePublicKeyFromKeystore(keyStore, privateKeyAlias, uid, + keyCharacteristics); + AndroidKeyStorePrivateKey privateKey = + AndroidKeyStoreProvider.getAndroidKeyStorePrivateKey(publicKey); + return new KeyPair(publicKey, privateKey); + } + + /** @hide **/ + @NonNull + public static KeyPair loadAndroidKeyStoreKeyPairFromKeystore( + @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid) + throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException { + return loadAndroidKeyStoreKeyPairFromKeystore(keyStore, privateKeyAlias, uid, + getKeyCharacteristics(keyStore, privateKeyAlias, uid)); + } + + @NonNull + private static AndroidKeyStorePrivateKey loadAndroidKeyStorePrivateKeyFromKeystore( + @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid, + @NonNull KeyCharacteristics keyCharacteristics) + throws UnrecoverableKeyException { + KeyPair keyPair = loadAndroidKeyStoreKeyPairFromKeystore(keyStore, privateKeyAlias, uid, + keyCharacteristics); + return (AndroidKeyStorePrivateKey) keyPair.getPrivate(); + } + + /** @hide **/ + @NonNull + public static AndroidKeyStorePrivateKey loadAndroidKeyStorePrivateKeyFromKeystore( + @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid) + throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException { + return loadAndroidKeyStorePrivateKeyFromKeystore(keyStore, privateKeyAlias, uid, + getKeyCharacteristics(keyStore, privateKeyAlias, uid)); + } + + @NonNull + private static AndroidKeyStoreSecretKey loadAndroidKeyStoreSecretKeyFromKeystore( + @NonNull String secretKeyAlias, int uid, @NonNull KeyCharacteristics keyCharacteristics) + throws UnrecoverableKeyException { + Integer keymasterAlgorithm = keyCharacteristics.getEnum(KeymasterDefs.KM_TAG_ALGORITHM); + if (keymasterAlgorithm == null) { + throw new UnrecoverableKeyException("Key algorithm unknown"); + } + + List<Integer> keymasterDigests = keyCharacteristics.getEnums(KeymasterDefs.KM_TAG_DIGEST); + int keymasterDigest; + if (keymasterDigests.isEmpty()) { + keymasterDigest = -1; + } else { + // More than one digest can be permitted for this key. Use the first one to form the + // JCA key algorithm name. + keymasterDigest = keymasterDigests.get(0); + } + + @KeyProperties.KeyAlgorithmEnum String keyAlgorithmString; + try { + keyAlgorithmString = KeyProperties.KeyAlgorithm.fromKeymasterSecretKeyAlgorithm( + keymasterAlgorithm, keymasterDigest); + } catch (IllegalArgumentException e) { + throw (UnrecoverableKeyException) + new UnrecoverableKeyException("Unsupported secret key type").initCause(e); + } + + return new AndroidKeyStoreSecretKey(secretKeyAlias, uid, keyAlgorithmString); + } + + /** @hide **/ + @NonNull + public static AndroidKeyStoreKey loadAndroidKeyStoreKeyFromKeystore( + @NonNull KeyStore keyStore, @NonNull String userKeyAlias, int uid) + throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException { + KeyCharacteristics keyCharacteristics = getKeyCharacteristics(keyStore, userKeyAlias, uid); + + Integer keymasterAlgorithm = keyCharacteristics.getEnum(KeymasterDefs.KM_TAG_ALGORITHM); + if (keymasterAlgorithm == null) { + throw new UnrecoverableKeyException("Key algorithm unknown"); + } + + if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC || + keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_AES || + keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_3DES) { + return loadAndroidKeyStoreSecretKeyFromKeystore(userKeyAlias, uid, + keyCharacteristics); + } else if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_RSA || + keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_EC) { + return loadAndroidKeyStorePrivateKeyFromKeystore(keyStore, userKeyAlias, uid, + keyCharacteristics); + } else { + throw new UnrecoverableKeyException("Key algorithm unknown"); + } + } + + /** + * Returns an {@code AndroidKeyStore} {@link java.security.KeyStore}} of the specified UID. + * The {@code KeyStore} contains keys and certificates owned by that UID. Such cross-UID + * access is permitted to a few system UIDs and only to a few other UIDs (e.g., Wi-Fi, VPN) + * all of which are system. + * + * <p>Note: the returned {@code KeyStore} is already initialized/loaded. Thus, there is + * no need to invoke {@code load} on it. + * + * @param uid Uid for which the keystore provider is requested. + * @throws KeyStoreException if a KeyStoreSpi implementation for the specified type is not + * available from the specified provider. + * @throws NoSuchProviderException If the specified provider is not registered in the security + * provider list. + * @hide + */ + @SystemApi + @NonNull + public static java.security.KeyStore getKeyStoreForUid(int uid) + throws KeyStoreException, NoSuchProviderException { + java.security.KeyStore result = + java.security.KeyStore.getInstance("AndroidKeyStore", PROVIDER_NAME); + try { + result.load(new AndroidKeyStoreLoadStoreParameter(uid)); + } catch (NoSuchAlgorithmException | CertificateException | IOException e) { + throw new KeyStoreException( + "Failed to load AndroidKeyStore KeyStore for UID " + uid, e); + } + return result; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java b/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java new file mode 100644 index 000000000000..a030efb64ff6 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.security.keystore.ArrayUtils; + +import java.security.PublicKey; +import java.util.Arrays; + +/** + * {@link PublicKey} backed by Android Keystore. + * + * @hide + */ +public class AndroidKeyStorePublicKey extends AndroidKeyStoreKey implements PublicKey { + + private final byte[] mEncoded; + + public AndroidKeyStorePublicKey(String alias, int uid, String algorithm, byte[] x509EncodedForm) { + super(alias, uid, algorithm); + mEncoded = ArrayUtils.cloneIfNotEmpty(x509EncodedForm); + } + + @Override + public String getFormat() { + return "X.509"; + } + + @Override + public byte[] getEncoded() { + return ArrayUtils.cloneIfNotEmpty(mEncoded); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Arrays.hashCode(mEncoded); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AndroidKeyStorePublicKey other = (AndroidKeyStorePublicKey) obj; + if (!Arrays.equals(mEncoded, other.mEncoded)) { + return false; + } + return true; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreRSACipherSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreRSACipherSpi.java new file mode 100644 index 000000000000..c9c0b0de3463 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreRSACipherSpi.java @@ -0,0 +1,517 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.security.KeyStore; +import android.security.keymaster.KeyCharacteristics; +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyProperties; +import android.security.keystore.KeymasterUtils; + +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.ProviderException; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidParameterSpecException; +import java.security.spec.MGF1ParameterSpec; + +import javax.crypto.Cipher; +import javax.crypto.CipherSpi; +import javax.crypto.spec.OAEPParameterSpec; +import javax.crypto.spec.PSource; + +/** + * Base class for {@link CipherSpi} providing Android KeyStore backed RSA encryption/decryption. + * + * @hide + */ +abstract class AndroidKeyStoreRSACipherSpi extends AndroidKeyStoreCipherSpiBase { + + /** + * Raw RSA cipher without any padding. + */ + public static final class NoPadding extends AndroidKeyStoreRSACipherSpi { + public NoPadding() { + super(KeymasterDefs.KM_PAD_NONE); + } + + @Override + protected boolean adjustConfigForEncryptingWithPrivateKey() { + // RSA encryption with no padding using private key is a way to implement raw RSA + // signatures which JCA does not expose via Signature. We thus have to support this. + setKeymasterPurposeOverride(KeymasterDefs.KM_PURPOSE_SIGN); + return true; + } + + @Override + protected void initAlgorithmSpecificParameters() throws InvalidKeyException {} + + @Override + protected void initAlgorithmSpecificParameters(@Nullable AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + if (params != null) { + throw new InvalidAlgorithmParameterException( + "Unexpected parameters: " + params + ". No parameters supported"); + } + } + + @Override + protected void initAlgorithmSpecificParameters(@Nullable AlgorithmParameters params) + throws InvalidAlgorithmParameterException { + + if (params != null) { + throw new InvalidAlgorithmParameterException( + "Unexpected parameters: " + params + ". No parameters supported"); + } + } + + @Override + protected AlgorithmParameters engineGetParameters() { + return null; + } + + @Override + protected final int getAdditionalEntropyAmountForBegin() { + return 0; + } + + @Override + protected final int getAdditionalEntropyAmountForFinish() { + return 0; + } + } + + /** + * RSA cipher with PKCS#1 v1.5 encryption padding. + */ + public static final class PKCS1Padding extends AndroidKeyStoreRSACipherSpi { + public PKCS1Padding() { + super(KeymasterDefs.KM_PAD_RSA_PKCS1_1_5_ENCRYPT); + } + + @Override + protected boolean adjustConfigForEncryptingWithPrivateKey() { + // RSA encryption with PCKS#1 padding using private key is a way to implement RSA + // signatures with PKCS#1 padding. We have to support this for legacy reasons. + setKeymasterPurposeOverride(KeymasterDefs.KM_PURPOSE_SIGN); + setKeymasterPaddingOverride(KeymasterDefs.KM_PAD_RSA_PKCS1_1_5_SIGN); + return true; + } + + @Override + protected void initAlgorithmSpecificParameters() throws InvalidKeyException {} + + @Override + protected void initAlgorithmSpecificParameters(@Nullable AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + if (params != null) { + throw new InvalidAlgorithmParameterException( + "Unexpected parameters: " + params + ". No parameters supported"); + } + } + + @Override + protected void initAlgorithmSpecificParameters(@Nullable AlgorithmParameters params) + throws InvalidAlgorithmParameterException { + + if (params != null) { + throw new InvalidAlgorithmParameterException( + "Unexpected parameters: " + params + ". No parameters supported"); + } + } + + @Override + protected AlgorithmParameters engineGetParameters() { + return null; + } + + @Override + protected final int getAdditionalEntropyAmountForBegin() { + return 0; + } + + @Override + protected final int getAdditionalEntropyAmountForFinish() { + return (isEncrypting()) ? getModulusSizeBytes() : 0; + } + } + + /** + * RSA cipher with OAEP encryption padding. Only SHA-1 based MGF1 is supported as MGF. + */ + abstract static class OAEPWithMGF1Padding extends AndroidKeyStoreRSACipherSpi { + + private static final String MGF_ALGORITGM_MGF1 = "MGF1"; + + private int mKeymasterDigest = -1; + private int mDigestOutputSizeBytes; + + OAEPWithMGF1Padding(int keymasterDigest) { + super(KeymasterDefs.KM_PAD_RSA_OAEP); + mKeymasterDigest = keymasterDigest; + mDigestOutputSizeBytes = + (KeymasterUtils.getDigestOutputSizeBits(keymasterDigest) + 7) / 8; + } + + @Override + protected final void initAlgorithmSpecificParameters() throws InvalidKeyException {} + + @Override + protected final void initAlgorithmSpecificParameters( + @Nullable AlgorithmParameterSpec params) throws InvalidAlgorithmParameterException { + if (params == null) { + return; + } + + if (!(params instanceof OAEPParameterSpec)) { + throw new InvalidAlgorithmParameterException( + "Unsupported parameter spec: " + params + + ". Only OAEPParameterSpec supported"); + } + OAEPParameterSpec spec = (OAEPParameterSpec) params; + if (!MGF_ALGORITGM_MGF1.equalsIgnoreCase(spec.getMGFAlgorithm())) { + throw new InvalidAlgorithmParameterException( + "Unsupported MGF: " + spec.getMGFAlgorithm() + + ". Only " + MGF_ALGORITGM_MGF1 + " supported"); + } + String jcaDigest = spec.getDigestAlgorithm(); + int keymasterDigest; + try { + keymasterDigest = KeyProperties.Digest.toKeymaster(jcaDigest); + } catch (IllegalArgumentException e) { + throw new InvalidAlgorithmParameterException( + "Unsupported digest: " + jcaDigest, e); + } + switch (keymasterDigest) { + case KeymasterDefs.KM_DIGEST_SHA1: + case KeymasterDefs.KM_DIGEST_SHA_2_224: + case KeymasterDefs.KM_DIGEST_SHA_2_256: + case KeymasterDefs.KM_DIGEST_SHA_2_384: + case KeymasterDefs.KM_DIGEST_SHA_2_512: + // Permitted. + break; + default: + throw new InvalidAlgorithmParameterException( + "Unsupported digest: " + jcaDigest); + } + AlgorithmParameterSpec mgfParams = spec.getMGFParameters(); + if (mgfParams == null) { + throw new InvalidAlgorithmParameterException("MGF parameters must be provided"); + } + // Check whether MGF parameters match the OAEPParameterSpec + if (!(mgfParams instanceof MGF1ParameterSpec)) { + throw new InvalidAlgorithmParameterException("Unsupported MGF parameters" + + ": " + mgfParams + ". Only MGF1ParameterSpec supported"); + } + MGF1ParameterSpec mgfSpec = (MGF1ParameterSpec) mgfParams; + String mgf1JcaDigest = mgfSpec.getDigestAlgorithm(); + if (!KeyProperties.DIGEST_SHA1.equalsIgnoreCase(mgf1JcaDigest)) { + throw new InvalidAlgorithmParameterException( + "Unsupported MGF1 digest: " + mgf1JcaDigest + + ". Only " + KeyProperties.DIGEST_SHA1 + " supported"); + } + PSource pSource = spec.getPSource(); + if (!(pSource instanceof PSource.PSpecified)) { + throw new InvalidAlgorithmParameterException( + "Unsupported source of encoding input P: " + pSource + + ". Only pSpecifiedEmpty (PSource.PSpecified.DEFAULT) supported"); + } + PSource.PSpecified pSourceSpecified = (PSource.PSpecified) pSource; + byte[] pSourceValue = pSourceSpecified.getValue(); + if ((pSourceValue != null) && (pSourceValue.length > 0)) { + throw new InvalidAlgorithmParameterException( + "Unsupported source of encoding input P: " + pSource + + ". Only pSpecifiedEmpty (PSource.PSpecified.DEFAULT) supported"); + } + mKeymasterDigest = keymasterDigest; + mDigestOutputSizeBytes = + (KeymasterUtils.getDigestOutputSizeBits(keymasterDigest) + 7) / 8; + } + + @Override + protected final void initAlgorithmSpecificParameters(@Nullable AlgorithmParameters params) + throws InvalidAlgorithmParameterException { + if (params == null) { + return; + } + + OAEPParameterSpec spec; + try { + spec = params.getParameterSpec(OAEPParameterSpec.class); + } catch (InvalidParameterSpecException e) { + throw new InvalidAlgorithmParameterException("OAEP parameters required" + + ", but not found in parameters: " + params, e); + } + if (spec == null) { + throw new InvalidAlgorithmParameterException("OAEP parameters required" + + ", but not provided in parameters: " + params); + } + initAlgorithmSpecificParameters(spec); + } + + @Override + protected final AlgorithmParameters engineGetParameters() { + OAEPParameterSpec spec = + new OAEPParameterSpec( + KeyProperties.Digest.fromKeymaster(mKeymasterDigest), + MGF_ALGORITGM_MGF1, + MGF1ParameterSpec.SHA1, + PSource.PSpecified.DEFAULT); + try { + AlgorithmParameters params = AlgorithmParameters.getInstance("OAEP"); + params.init(spec); + return params; + } catch (NoSuchAlgorithmException e) { + throw new ProviderException( + "Failed to obtain OAEP AlgorithmParameters", e); + } catch (InvalidParameterSpecException e) { + throw new ProviderException( + "Failed to initialize OAEP AlgorithmParameters with an IV", + e); + } + } + + @Override + protected final void addAlgorithmSpecificParametersToBegin( + KeymasterArguments keymasterArgs) { + super.addAlgorithmSpecificParametersToBegin(keymasterArgs); + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_DIGEST, mKeymasterDigest); + } + + @Override + protected final void loadAlgorithmSpecificParametersFromBeginResult( + @NonNull KeymasterArguments keymasterArgs) { + super.loadAlgorithmSpecificParametersFromBeginResult(keymasterArgs); + } + + @Override + protected final int getAdditionalEntropyAmountForBegin() { + return 0; + } + + @Override + protected final int getAdditionalEntropyAmountForFinish() { + return (isEncrypting()) ? mDigestOutputSizeBytes : 0; + } + } + + public static class OAEPWithSHA1AndMGF1Padding extends OAEPWithMGF1Padding { + public OAEPWithSHA1AndMGF1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA1); + } + } + + public static class OAEPWithSHA224AndMGF1Padding extends OAEPWithMGF1Padding { + public OAEPWithSHA224AndMGF1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_224); + } + } + + public static class OAEPWithSHA256AndMGF1Padding extends OAEPWithMGF1Padding { + public OAEPWithSHA256AndMGF1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_256); + } + } + + public static class OAEPWithSHA384AndMGF1Padding extends OAEPWithMGF1Padding { + public OAEPWithSHA384AndMGF1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_384); + } + } + + public static class OAEPWithSHA512AndMGF1Padding extends OAEPWithMGF1Padding { + public OAEPWithSHA512AndMGF1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_512); + } + } + + private final int mKeymasterPadding; + private int mKeymasterPaddingOverride; + + private int mModulusSizeBytes = -1; + + AndroidKeyStoreRSACipherSpi(int keymasterPadding) { + mKeymasterPadding = keymasterPadding; + } + + @Override + protected final void initKey(int opmode, Key key) throws InvalidKeyException { + if (key == null) { + throw new InvalidKeyException("Unsupported key: null"); + } + if (!KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(key.getAlgorithm())) { + throw new InvalidKeyException("Unsupported key algorithm: " + key.getAlgorithm() + + ". Only " + KeyProperties.KEY_ALGORITHM_RSA + " supported"); + } + AndroidKeyStoreKey keystoreKey; + if (key instanceof AndroidKeyStorePrivateKey) { + keystoreKey = (AndroidKeyStoreKey) key; + } else if (key instanceof AndroidKeyStorePublicKey) { + keystoreKey = (AndroidKeyStoreKey) key; + } else { + throw new InvalidKeyException("Unsupported key type: " + key); + } + + if (keystoreKey instanceof PrivateKey) { + // Private key + switch (opmode) { + case Cipher.DECRYPT_MODE: + case Cipher.UNWRAP_MODE: + // Permitted + break; + case Cipher.ENCRYPT_MODE: + case Cipher.WRAP_MODE: + if (!adjustConfigForEncryptingWithPrivateKey()) { + throw new InvalidKeyException( + "RSA private keys cannot be used with " + opmodeToString(opmode) + + " and padding " + + KeyProperties.EncryptionPadding.fromKeymaster(mKeymasterPadding) + + ". Only RSA public keys supported for this mode"); + } + break; + default: + throw new InvalidKeyException( + "RSA private keys cannot be used with opmode: " + opmode); + } + } else { + // Public key + switch (opmode) { + case Cipher.ENCRYPT_MODE: + case Cipher.WRAP_MODE: + // Permitted + break; + case Cipher.DECRYPT_MODE: + case Cipher.UNWRAP_MODE: + throw new InvalidKeyException( + "RSA public keys cannot be used with " + opmodeToString(opmode) + + " and padding " + + KeyProperties.EncryptionPadding.fromKeymaster(mKeymasterPadding) + + ". Only RSA private keys supported for this opmode."); + // break; + default: + throw new InvalidKeyException( + "RSA public keys cannot be used with " + opmodeToString(opmode)); + } + } + + KeyCharacteristics keyCharacteristics = new KeyCharacteristics(); + int errorCode = getKeyStore().getKeyCharacteristics( + keystoreKey.getAlias(), null, null, keystoreKey.getUid(), keyCharacteristics); + if (errorCode != KeyStore.NO_ERROR) { + throw getKeyStore().getInvalidKeyException( + keystoreKey.getAlias(), keystoreKey.getUid(), errorCode); + } + long keySizeBits = keyCharacteristics.getUnsignedInt(KeymasterDefs.KM_TAG_KEY_SIZE, -1); + if (keySizeBits == -1) { + throw new InvalidKeyException("Size of key not known"); + } else if (keySizeBits > Integer.MAX_VALUE) { + throw new InvalidKeyException("Key too large: " + keySizeBits + " bits"); + } + mModulusSizeBytes = (int) ((keySizeBits + 7) / 8); + + setKey(keystoreKey); + } + + /** + * Adjusts the configuration of this cipher for encrypting using the private key. + * + * <p>The default implementation does nothing and refuses to adjust the configuration. + * + * @return {@code true} if the configuration has been adjusted, {@code false} if encrypting + * using private key is not permitted for this cipher. + */ + protected boolean adjustConfigForEncryptingWithPrivateKey() { + return false; + } + + @Override + protected final void resetAll() { + mModulusSizeBytes = -1; + mKeymasterPaddingOverride = -1; + super.resetAll(); + } + + @Override + protected final void resetWhilePreservingInitState() { + super.resetWhilePreservingInitState(); + } + + @Override + protected void addAlgorithmSpecificParametersToBegin( + @NonNull KeymasterArguments keymasterArgs) { + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_RSA); + int keymasterPadding = getKeymasterPaddingOverride(); + if (keymasterPadding == -1) { + keymasterPadding = mKeymasterPadding; + } + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_PADDING, keymasterPadding); + int purposeOverride = getKeymasterPurposeOverride(); + if ((purposeOverride != -1) + && ((purposeOverride == KeymasterDefs.KM_PURPOSE_SIGN) + || (purposeOverride == KeymasterDefs.KM_PURPOSE_VERIFY))) { + // Keymaster sign/verify requires digest to be specified. For raw sign/verify it's NONE. + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_DIGEST, KeymasterDefs.KM_DIGEST_NONE); + } + } + + @Override + protected void loadAlgorithmSpecificParametersFromBeginResult( + @NonNull KeymasterArguments keymasterArgs) { + } + + @Override + protected final int engineGetBlockSize() { + // Not a block cipher, according to the RI + return 0; + } + + @Override + protected final byte[] engineGetIV() { + // IV never used + return null; + } + + @Override + protected final int engineGetOutputSize(int inputLen) { + return getModulusSizeBytes(); + } + + protected final int getModulusSizeBytes() { + if (mModulusSizeBytes == -1) { + throw new IllegalStateException("Not initialized"); + } + return mModulusSizeBytes; + } + + /** + * Overrides the default padding of the crypto operation. + */ + protected final void setKeymasterPaddingOverride(int keymasterPadding) { + mKeymasterPaddingOverride = keymasterPadding; + } + + protected final int getKeymasterPaddingOverride() { + return mKeymasterPaddingOverride; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreRSAPrivateKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreRSAPrivateKey.java new file mode 100644 index 000000000000..4c1231b674c0 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreRSAPrivateKey.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.security.keystore.KeyProperties; + +import java.math.BigInteger; +import java.security.PrivateKey; +import java.security.interfaces.RSAKey; + +/** + * RSA private key (instance of {@link PrivateKey} and {@link RSAKey}) backed by keystore. + * + * @hide + */ +public class AndroidKeyStoreRSAPrivateKey extends AndroidKeyStorePrivateKey implements RSAKey { + + private final BigInteger mModulus; + + public AndroidKeyStoreRSAPrivateKey(String alias, int uid, BigInteger modulus) { + super(alias, uid, KeyProperties.KEY_ALGORITHM_RSA); + mModulus = modulus; + } + + @Override + public BigInteger getModulus() { + return mModulus; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreRSAPublicKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreRSAPublicKey.java new file mode 100644 index 000000000000..7a59abb38719 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreRSAPublicKey.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.security.keystore.KeyProperties; + +import java.math.BigInteger; +import java.security.interfaces.RSAPublicKey; + +/** + * {@link RSAPublicKey} backed by Android Keystore. + * + * @hide + */ +public class AndroidKeyStoreRSAPublicKey extends AndroidKeyStorePublicKey implements RSAPublicKey { + private final BigInteger mModulus; + private final BigInteger mPublicExponent; + + public AndroidKeyStoreRSAPublicKey(String alias, int uid, byte[] x509EncodedForm, BigInteger modulus, + BigInteger publicExponent) { + super(alias, uid, KeyProperties.KEY_ALGORITHM_RSA, x509EncodedForm); + mModulus = modulus; + mPublicExponent = publicExponent; + } + + public AndroidKeyStoreRSAPublicKey(String alias, int uid, RSAPublicKey info) { + this(alias, uid, info.getEncoded(), info.getModulus(), info.getPublicExponent()); + if (!"X.509".equalsIgnoreCase(info.getFormat())) { + throw new IllegalArgumentException( + "Unsupported key export format: " + info.getFormat()); + } + } + + @Override + public BigInteger getModulus() { + return mModulus; + } + + @Override + public BigInteger getPublicExponent() { + return mPublicExponent; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreRSASignatureSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreRSASignatureSpi.java new file mode 100644 index 000000000000..6b2c098810e4 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreRSASignatureSpi.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.annotation.NonNull; +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyProperties; + +import java.security.InvalidKeyException; +import java.security.SignatureSpi; + +/** + * Base class for {@link SignatureSpi} providing Android KeyStore backed RSA signatures. + * + * @hide + */ +abstract class AndroidKeyStoreRSASignatureSpi extends + AndroidKeyStoreSignatureSpiBase { + + abstract static class PKCS1Padding extends AndroidKeyStoreRSASignatureSpi { + PKCS1Padding(int keymasterDigest) { + super(keymasterDigest, KeymasterDefs.KM_PAD_RSA_PKCS1_1_5_SIGN); + } + + @Override + protected final int getAdditionalEntropyAmountForSign() { + // No entropy required for this deterministic signature scheme. + return 0; + } + } + + public static final class NONEWithPKCS1Padding extends PKCS1Padding { + public NONEWithPKCS1Padding() { + super(KeymasterDefs.KM_DIGEST_NONE); + } + } + + public static final class MD5WithPKCS1Padding extends PKCS1Padding { + public MD5WithPKCS1Padding() { + super(KeymasterDefs.KM_DIGEST_MD5); + } + } + + public static final class SHA1WithPKCS1Padding extends PKCS1Padding { + public SHA1WithPKCS1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA1); + } + } + + public static final class SHA224WithPKCS1Padding extends PKCS1Padding { + public SHA224WithPKCS1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_224); + } + } + + public static final class SHA256WithPKCS1Padding extends PKCS1Padding { + public SHA256WithPKCS1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_256); + } + } + + public static final class SHA384WithPKCS1Padding extends PKCS1Padding { + public SHA384WithPKCS1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_384); + } + } + + public static final class SHA512WithPKCS1Padding extends PKCS1Padding { + public SHA512WithPKCS1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_512); + } + } + + abstract static class PSSPadding extends AndroidKeyStoreRSASignatureSpi { + private static final int SALT_LENGTH_BYTES = 20; + + PSSPadding(int keymasterDigest) { + super(keymasterDigest, KeymasterDefs.KM_PAD_RSA_PSS); + } + + @Override + protected final int getAdditionalEntropyAmountForSign() { + return SALT_LENGTH_BYTES; + } + } + + public static final class SHA1WithPSSPadding extends PSSPadding { + public SHA1WithPSSPadding() { + super(KeymasterDefs.KM_DIGEST_SHA1); + } + } + + public static final class SHA224WithPSSPadding extends PSSPadding { + public SHA224WithPSSPadding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_224); + } + } + + public static final class SHA256WithPSSPadding extends PSSPadding { + public SHA256WithPSSPadding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_256); + } + } + + public static final class SHA384WithPSSPadding extends PSSPadding { + public SHA384WithPSSPadding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_384); + } + } + + public static final class SHA512WithPSSPadding extends PSSPadding { + public SHA512WithPSSPadding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_512); + } + } + + private final int mKeymasterDigest; + private final int mKeymasterPadding; + + AndroidKeyStoreRSASignatureSpi(int keymasterDigest, int keymasterPadding) { + mKeymasterDigest = keymasterDigest; + mKeymasterPadding = keymasterPadding; + } + + @Override + protected final void initKey(AndroidKeyStoreKey key) throws InvalidKeyException { + if (!KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(key.getAlgorithm())) { + throw new InvalidKeyException("Unsupported key algorithm: " + key.getAlgorithm() + + ". Only" + KeyProperties.KEY_ALGORITHM_RSA + " supported"); + } + super.initKey(key); + } + + @Override + protected final void resetAll() { + super.resetAll(); + } + + @Override + protected final void resetWhilePreservingInitState() { + super.resetWhilePreservingInitState(); + } + + @Override + protected final void addAlgorithmSpecificParametersToBegin( + @NonNull KeymasterArguments keymasterArgs) { + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_RSA); + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_DIGEST, mKeymasterDigest); + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_PADDING, mKeymasterPadding); + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKey.java new file mode 100644 index 000000000000..8adf27a6189a --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKey.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import javax.crypto.SecretKey; + +/** + * {@link SecretKey} backed by Android Keystore. + * + * @hide + */ +public class AndroidKeyStoreSecretKey extends AndroidKeyStoreKey implements SecretKey { + + public AndroidKeyStoreSecretKey(String alias, int uid, String algorithm) { + super(alias, uid, algorithm); + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java new file mode 100644 index 000000000000..c2a8e10c1e51 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.security.Credentials; +import android.security.GateKeeper; +import android.security.KeyStore; +import android.security.keymaster.KeyCharacteristics; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyInfo; + +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.ProviderException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactorySpi; +import javax.crypto.spec.SecretKeySpec; + +/** + * {@link SecretKeyFactorySpi} backed by Android Keystore. + * + * @hide + */ +public class AndroidKeyStoreSecretKeyFactorySpi extends SecretKeyFactorySpi { + + private final KeyStore mKeyStore = KeyStore.getInstance(); + + @Override + protected KeySpec engineGetKeySpec(SecretKey key, + @SuppressWarnings("rawtypes") Class keySpecClass) throws InvalidKeySpecException { + if (keySpecClass == null) { + throw new InvalidKeySpecException("keySpecClass == null"); + } + if (!(key instanceof AndroidKeyStoreSecretKey)) { + throw new InvalidKeySpecException("Only Android KeyStore secret keys supported: " + + ((key != null) ? key.getClass().getName() : "null")); + } + if (SecretKeySpec.class.isAssignableFrom(keySpecClass)) { + throw new InvalidKeySpecException( + "Key material export of Android KeyStore keys is not supported"); + } + if (!KeyInfo.class.equals(keySpecClass)) { + throw new InvalidKeySpecException("Unsupported key spec: " + keySpecClass.getName()); + } + AndroidKeyStoreKey keystoreKey = (AndroidKeyStoreKey) key; + String keyAliasInKeystore = keystoreKey.getAlias(); + String entryAlias; + if (keyAliasInKeystore.startsWith(Credentials.USER_PRIVATE_KEY)) { + entryAlias = keyAliasInKeystore.substring(Credentials.USER_PRIVATE_KEY.length()); + } else if (keyAliasInKeystore.startsWith(Credentials.USER_SECRET_KEY)){ + // key has legacy prefix + entryAlias = keyAliasInKeystore.substring(Credentials.USER_SECRET_KEY.length()); + } else { + throw new InvalidKeySpecException("Invalid key alias: " + keyAliasInKeystore); + } + + return getKeyInfo(mKeyStore, entryAlias, keyAliasInKeystore, keystoreKey.getUid()); + } + + static KeyInfo getKeyInfo(KeyStore keyStore, String entryAlias, String keyAliasInKeystore, + int keyUid) { + KeyCharacteristics keyCharacteristics = new KeyCharacteristics(); + int errorCode = keyStore.getKeyCharacteristics( + keyAliasInKeystore, null, null, keyUid, keyCharacteristics); + if (errorCode != KeyStore.NO_ERROR) { + throw new ProviderException("Failed to obtain information about key." + + " Keystore error: " + errorCode); + } + + boolean insideSecureHardware; + @KeyProperties.OriginEnum int origin; + int keySize; + @KeyProperties.PurposeEnum int purposes; + String[] encryptionPaddings; + String[] signaturePaddings; + @KeyProperties.DigestEnum String[] digests; + @KeyProperties.BlockModeEnum String[] blockModes; + int keymasterSwEnforcedUserAuthenticators; + int keymasterHwEnforcedUserAuthenticators; + List<BigInteger> keymasterSecureUserIds; + try { + if (keyCharacteristics.hwEnforced.containsTag(KeymasterDefs.KM_TAG_ORIGIN)) { + insideSecureHardware = true; + origin = KeyProperties.Origin.fromKeymaster( + keyCharacteristics.hwEnforced.getEnum(KeymasterDefs.KM_TAG_ORIGIN, -1)); + } else if (keyCharacteristics.swEnforced.containsTag(KeymasterDefs.KM_TAG_ORIGIN)) { + insideSecureHardware = false; + origin = KeyProperties.Origin.fromKeymaster( + keyCharacteristics.swEnforced.getEnum(KeymasterDefs.KM_TAG_ORIGIN, -1)); + } else { + throw new ProviderException("Key origin not available"); + } + long keySizeUnsigned = + keyCharacteristics.getUnsignedInt(KeymasterDefs.KM_TAG_KEY_SIZE, -1); + if (keySizeUnsigned == -1) { + throw new ProviderException("Key size not available"); + } else if (keySizeUnsigned > Integer.MAX_VALUE) { + throw new ProviderException("Key too large: " + keySizeUnsigned + " bits"); + } + keySize = (int) keySizeUnsigned; + purposes = KeyProperties.Purpose.allFromKeymaster( + keyCharacteristics.getEnums(KeymasterDefs.KM_TAG_PURPOSE)); + + List<String> encryptionPaddingsList = new ArrayList<String>(); + List<String> signaturePaddingsList = new ArrayList<String>(); + // Keymaster stores both types of paddings in the same array -- we split it into two. + for (int keymasterPadding : keyCharacteristics.getEnums(KeymasterDefs.KM_TAG_PADDING)) { + try { + @KeyProperties.EncryptionPaddingEnum String jcaPadding = + KeyProperties.EncryptionPadding.fromKeymaster(keymasterPadding); + encryptionPaddingsList.add(jcaPadding); + } catch (IllegalArgumentException e) { + try { + @KeyProperties.SignaturePaddingEnum String padding = + KeyProperties.SignaturePadding.fromKeymaster(keymasterPadding); + signaturePaddingsList.add(padding); + } catch (IllegalArgumentException e2) { + throw new ProviderException( + "Unsupported encryption padding: " + keymasterPadding); + } + } + + } + encryptionPaddings = + encryptionPaddingsList.toArray(new String[encryptionPaddingsList.size()]); + signaturePaddings = + signaturePaddingsList.toArray(new String[signaturePaddingsList.size()]); + + digests = KeyProperties.Digest.allFromKeymaster( + keyCharacteristics.getEnums(KeymasterDefs.KM_TAG_DIGEST)); + blockModes = KeyProperties.BlockMode.allFromKeymaster( + keyCharacteristics.getEnums(KeymasterDefs.KM_TAG_BLOCK_MODE)); + keymasterSwEnforcedUserAuthenticators = + keyCharacteristics.swEnforced.getEnum(KeymasterDefs.KM_TAG_USER_AUTH_TYPE, 0); + keymasterHwEnforcedUserAuthenticators = + keyCharacteristics.hwEnforced.getEnum(KeymasterDefs.KM_TAG_USER_AUTH_TYPE, 0); + keymasterSecureUserIds = + keyCharacteristics.getUnsignedLongs(KeymasterDefs.KM_TAG_USER_SECURE_ID); + } catch (IllegalArgumentException e) { + throw new ProviderException("Unsupported key characteristic", e); + } + + Date keyValidityStart = keyCharacteristics.getDate(KeymasterDefs.KM_TAG_ACTIVE_DATETIME); + Date keyValidityForOriginationEnd = + keyCharacteristics.getDate(KeymasterDefs.KM_TAG_ORIGINATION_EXPIRE_DATETIME); + Date keyValidityForConsumptionEnd = + keyCharacteristics.getDate(KeymasterDefs.KM_TAG_USAGE_EXPIRE_DATETIME); + boolean userAuthenticationRequired = + !keyCharacteristics.getBoolean(KeymasterDefs.KM_TAG_NO_AUTH_REQUIRED); + long userAuthenticationValidityDurationSeconds = + keyCharacteristics.getUnsignedInt(KeymasterDefs.KM_TAG_AUTH_TIMEOUT, 0); + if (userAuthenticationValidityDurationSeconds > Integer.MAX_VALUE) { + throw new ProviderException("User authentication timeout validity too long: " + + userAuthenticationValidityDurationSeconds + " seconds"); + } + boolean userAuthenticationRequirementEnforcedBySecureHardware = (userAuthenticationRequired) + && (keymasterHwEnforcedUserAuthenticators != 0) + && (keymasterSwEnforcedUserAuthenticators == 0); + boolean userAuthenticationValidWhileOnBody = + keyCharacteristics.hwEnforced.getBoolean(KeymasterDefs.KM_TAG_ALLOW_WHILE_ON_BODY); + boolean trustedUserPresenceRequired = + keyCharacteristics.hwEnforced.getBoolean( + KeymasterDefs.KM_TAG_TRUSTED_USER_PRESENCE_REQUIRED); + + boolean invalidatedByBiometricEnrollment = false; + if (keymasterSwEnforcedUserAuthenticators == KeymasterDefs.HW_AUTH_BIOMETRIC + || keymasterHwEnforcedUserAuthenticators == KeymasterDefs.HW_AUTH_BIOMETRIC) { + // Fingerprint-only key; will be invalidated if the root SID isn't in the SID list. + invalidatedByBiometricEnrollment = keymasterSecureUserIds != null + && !keymasterSecureUserIds.isEmpty() + && !keymasterSecureUserIds.contains(getGateKeeperSecureUserId()); + } + + boolean userConfirmationRequired = keyCharacteristics.hwEnforced.getBoolean(KeymasterDefs.KM_TAG_TRUSTED_CONFIRMATION_REQUIRED); + + return new KeyInfo(entryAlias, + insideSecureHardware, + origin, + keySize, + keyValidityStart, + keyValidityForOriginationEnd, + keyValidityForConsumptionEnd, + purposes, + encryptionPaddings, + signaturePaddings, + digests, + blockModes, + userAuthenticationRequired, + (int) userAuthenticationValidityDurationSeconds, + keymasterHwEnforcedUserAuthenticators, + userAuthenticationRequirementEnforcedBySecureHardware, + userAuthenticationValidWhileOnBody, + trustedUserPresenceRequired, + invalidatedByBiometricEnrollment, + userConfirmationRequired, + // Keystore 1.0 does not tell us the exact security level of the key + // so we assume TEE if the key is in secure hardware. + insideSecureHardware ? KeyProperties.SecurityLevelEnum.TRUSTED_ENVIRONMENT + : KeyProperties.SecurityLevelEnum.SOFTWARE); + } + + private static BigInteger getGateKeeperSecureUserId() throws ProviderException { + try { + return BigInteger.valueOf(GateKeeper.getSecureUserId()); + } catch (IllegalStateException e) { + throw new ProviderException("Failed to get GateKeeper secure user ID", e); + } + } + + @Override + protected SecretKey engineGenerateSecret(KeySpec keySpec) throws InvalidKeySpecException { + throw new InvalidKeySpecException( + "To generate secret key in Android Keystore, use KeyGenerator initialized with " + + KeyGenParameterSpec.class.getName()); + } + + @Override + protected SecretKey engineTranslateKey(SecretKey key) throws InvalidKeyException { + if (key == null) { + throw new InvalidKeyException("key == null"); + } else if (!(key instanceof AndroidKeyStoreSecretKey)) { + throw new InvalidKeyException( + "To import a secret key into Android Keystore, use KeyStore.setEntry"); + } + + return key; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSignatureSpiBase.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSignatureSpiBase.java new file mode 100644 index 000000000000..23818a784f89 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSignatureSpiBase.java @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.annotation.CallSuper; +import android.annotation.NonNull; +import android.os.IBinder; +import android.security.KeyStore; +import android.security.KeyStoreException; +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterDefs; +import android.security.keymaster.OperationResult; +import android.security.keystore.ArrayUtils; +import android.security.keystore.KeyStoreConnectException; +import android.security.keystore.KeyStoreCryptoOperation; + +import libcore.util.EmptyArray; + +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.InvalidParameterException; +import java.security.PrivateKey; +import java.security.ProviderException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.SignatureException; +import java.security.SignatureSpi; + +/** + * Base class for {@link SignatureSpi} implementations of Android KeyStore backed ciphers. + * + * @hide + */ +abstract class AndroidKeyStoreSignatureSpiBase extends SignatureSpi + implements KeyStoreCryptoOperation { + private final KeyStore mKeyStore; + + // Fields below are populated by SignatureSpi.engineInitSign/engineInitVerify and KeyStore.begin + // and should be preserved after SignatureSpi.engineSign/engineVerify finishes. + private boolean mSigning; + private AndroidKeyStoreKey mKey; + + /** + * Token referencing this operation inside keystore service. It is initialized by + * {@code engineInitSign}/{@code engineInitVerify} and is invalidated when + * {@code engineSign}/{@code engineVerify} succeeds and on some error conditions in between. + */ + private IBinder mOperationToken; + private long mOperationHandle; + private KeyStoreCryptoOperationStreamer mMessageStreamer; + + /** + * Encountered exception which could not be immediately thrown because it was encountered inside + * a method that does not throw checked exception. This exception will be thrown from + * {@code engineSign} or {@code engineVerify}. Once such an exception is encountered, + * {@code engineUpdate} starts ignoring input data. + */ + private Exception mCachedException; + + AndroidKeyStoreSignatureSpiBase() { + mKeyStore = KeyStore.getInstance(); + } + + @Override + protected final void engineInitSign(PrivateKey key) throws InvalidKeyException { + engineInitSign(key, null); + } + + @Override + protected final void engineInitSign(PrivateKey privateKey, SecureRandom random) + throws InvalidKeyException { + resetAll(); + + boolean success = false; + try { + if (privateKey == null) { + throw new InvalidKeyException("Unsupported key: null"); + } + AndroidKeyStoreKey keystoreKey; + if (privateKey instanceof AndroidKeyStorePrivateKey) { + keystoreKey = (AndroidKeyStoreKey) privateKey; + } else { + throw new InvalidKeyException("Unsupported private key type: " + privateKey); + } + mSigning = true; + initKey(keystoreKey); + appRandom = random; + ensureKeystoreOperationInitialized(); + success = true; + } finally { + if (!success) { + resetAll(); + } + } + } + + @Override + protected final void engineInitVerify(PublicKey publicKey) throws InvalidKeyException { + resetAll(); + + boolean success = false; + try { + if (publicKey == null) { + throw new InvalidKeyException("Unsupported key: null"); + } + AndroidKeyStoreKey keystoreKey; + if (publicKey instanceof AndroidKeyStorePublicKey) { + keystoreKey = (AndroidKeyStorePublicKey) publicKey; + } else { + throw new InvalidKeyException("Unsupported public key type: " + publicKey); + } + mSigning = false; + initKey(keystoreKey); + appRandom = null; + ensureKeystoreOperationInitialized(); + success = true; + } finally { + if (!success) { + resetAll(); + } + } + } + + /** + * Configures this signature instance to use the provided key. + * + * @throws InvalidKeyException if the {@code key} is not suitable. + */ + @CallSuper + protected void initKey(AndroidKeyStoreKey key) throws InvalidKeyException { + mKey = key; + } + + /** + * Resets this cipher to its pristine pre-init state. This must be equivalent to obtaining a new + * cipher instance. + * + * <p>Subclasses storing additional state should override this method, reset the additional + * state, and then chain to superclass. + */ + @CallSuper + protected void resetAll() { + IBinder operationToken = mOperationToken; + if (operationToken != null) { + mOperationToken = null; + mKeyStore.abort(operationToken); + } + mSigning = false; + mKey = null; + appRandom = null; + mOperationToken = null; + mOperationHandle = 0; + mMessageStreamer = null; + mCachedException = null; + } + + /** + * Resets this cipher while preserving the initialized state. This must be equivalent to + * rolling back the cipher's state to just after the most recent {@code engineInit} completed + * successfully. + * + * <p>Subclasses storing additional post-init state should override this method, reset the + * additional state, and then chain to superclass. + */ + @CallSuper + protected void resetWhilePreservingInitState() { + IBinder operationToken = mOperationToken; + if (operationToken != null) { + mOperationToken = null; + mKeyStore.abort(operationToken); + } + mOperationHandle = 0; + mMessageStreamer = null; + mCachedException = null; + } + + private void ensureKeystoreOperationInitialized() throws InvalidKeyException { + if (mMessageStreamer != null) { + return; + } + if (mCachedException != null) { + return; + } + if (mKey == null) { + throw new IllegalStateException("Not initialized"); + } + + KeymasterArguments keymasterInputArgs = new KeymasterArguments(); + addAlgorithmSpecificParametersToBegin(keymasterInputArgs); + + OperationResult opResult = mKeyStore.begin( + mKey.getAlias(), + mSigning ? KeymasterDefs.KM_PURPOSE_SIGN : KeymasterDefs.KM_PURPOSE_VERIFY, + true, // permit aborting this operation if keystore runs out of resources + keymasterInputArgs, + null, // no additional entropy for begin -- only finish might need some + mKey.getUid()); + if (opResult == null) { + throw new KeyStoreConnectException(); + } + + // Store operation token and handle regardless of the error code returned by KeyStore to + // ensure that the operation gets aborted immediately if the code below throws an exception. + mOperationToken = opResult.token; + mOperationHandle = opResult.operationHandle; + + // If necessary, throw an exception due to KeyStore operation having failed. + InvalidKeyException e = KeyStoreCryptoOperationUtils.getInvalidKeyExceptionForInit( + mKeyStore, mKey, opResult.resultCode); + if (e != null) { + throw e; + } + + if (mOperationToken == null) { + throw new ProviderException("Keystore returned null operation token"); + } + if (mOperationHandle == 0) { + throw new ProviderException("Keystore returned invalid operation handle"); + } + + mMessageStreamer = createMainDataStreamer(mKeyStore, opResult.token); + } + + /** + * Creates a streamer which sends the message to be signed/verified into the provided KeyStore + * + * <p>This implementation returns a working streamer. + */ + @NonNull + protected KeyStoreCryptoOperationStreamer createMainDataStreamer( + KeyStore keyStore, IBinder operationToken) { + return new KeyStoreCryptoOperationChunkedStreamer( + new KeyStoreCryptoOperationChunkedStreamer.MainDataStream( + keyStore, operationToken)); + } + + @Override + public final long getOperationHandle() { + return mOperationHandle; + } + + @Override + protected final void engineUpdate(byte[] b, int off, int len) throws SignatureException { + if (mCachedException != null) { + throw new SignatureException(mCachedException); + } + + try { + ensureKeystoreOperationInitialized(); + } catch (InvalidKeyException e) { + throw new SignatureException(e); + } + + if (len == 0) { + return; + } + + byte[] output; + try { + output = mMessageStreamer.update(b, off, len); + } catch (KeyStoreException e) { + throw new SignatureException(e); + } + + if (output.length != 0) { + throw new ProviderException( + "Update operation unexpectedly produced output: " + output.length + " bytes"); + } + } + + @Override + protected final void engineUpdate(byte b) throws SignatureException { + engineUpdate(new byte[] {b}, 0, 1); + } + + @Override + protected final void engineUpdate(ByteBuffer input) { + byte[] b; + int off; + int len = input.remaining(); + if (input.hasArray()) { + b = input.array(); + off = input.arrayOffset() + input.position(); + input.position(input.limit()); + } else { + b = new byte[len]; + off = 0; + input.get(b); + } + + try { + engineUpdate(b, off, len); + } catch (SignatureException e) { + mCachedException = e; + } + } + + @Override + protected final int engineSign(byte[] out, int outOffset, int outLen) + throws SignatureException { + return super.engineSign(out, outOffset, outLen); + } + + @Override + protected final byte[] engineSign() throws SignatureException { + if (mCachedException != null) { + throw new SignatureException(mCachedException); + } + + byte[] signature; + try { + ensureKeystoreOperationInitialized(); + + byte[] additionalEntropy = + KeyStoreCryptoOperationUtils.getRandomBytesToMixIntoKeystoreRng( + appRandom, getAdditionalEntropyAmountForSign()); + signature = mMessageStreamer.doFinal( + EmptyArray.BYTE, 0, 0, + null, // no signature provided -- it'll be generated by this invocation + additionalEntropy); + } catch (InvalidKeyException | KeyStoreException e) { + throw new SignatureException(e); + } + + resetWhilePreservingInitState(); + return signature; + } + + @Override + protected final boolean engineVerify(byte[] signature) throws SignatureException { + if (mCachedException != null) { + throw new SignatureException(mCachedException); + } + + try { + ensureKeystoreOperationInitialized(); + } catch (InvalidKeyException e) { + throw new SignatureException(e); + } + + boolean verified; + try { + byte[] output = mMessageStreamer.doFinal( + EmptyArray.BYTE, 0, 0, + signature, + null // no additional entropy needed -- verification is deterministic + ); + if (output.length != 0) { + throw new ProviderException( + "Signature verification unexpected produced output: " + output.length + + " bytes"); + } + verified = true; + } catch (KeyStoreException e) { + switch (e.getErrorCode()) { + case KeymasterDefs.KM_ERROR_VERIFICATION_FAILED: + verified = false; + break; + default: + throw new SignatureException(e); + } + } + + resetWhilePreservingInitState(); + return verified; + } + + @Override + protected final boolean engineVerify(byte[] sigBytes, int offset, int len) + throws SignatureException { + return engineVerify(ArrayUtils.subarray(sigBytes, offset, len)); + } + + @Deprecated + @Override + protected final Object engineGetParameter(String param) throws InvalidParameterException { + throw new InvalidParameterException(); + } + + @Deprecated + @Override + protected final void engineSetParameter(String param, Object value) + throws InvalidParameterException { + throw new InvalidParameterException(); + } + + protected final KeyStore getKeyStore() { + return mKeyStore; + } + + /** + * Returns {@code true} if this signature is initialized for signing, {@code false} if this + * signature is initialized for verification. + */ + protected final boolean isSigning() { + return mSigning; + } + + // The methods below need to be implemented by subclasses. + + /** + * Returns the amount of additional entropy (in bytes) to be provided to the KeyStore's + * {@code finish} operation when generating a signature. + * + * <p>This value should match (or exceed) the amount of Shannon entropy of the produced + * signature assuming the key and the message are known. For example, for ECDSA signature this + * should be the size of {@code R}, whereas for the RSA signature with PKCS#1 padding this + * should be {@code 0}. + */ + protected abstract int getAdditionalEntropyAmountForSign(); + + /** + * Invoked to add algorithm-specific parameters for the KeyStore's {@code begin} operation. + * + * @param keymasterArgs keystore/keymaster arguments to be populated with algorithm-specific + * parameters. + */ + protected abstract void addAlgorithmSpecificParametersToBegin( + @NonNull KeymasterArguments keymasterArgs); +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java new file mode 100644 index 000000000000..96cfe411e73a --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java @@ -0,0 +1,1113 @@ +/* + * Copyright (C) 2012 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 android.security.keystore2; + +import android.security.Credentials; +import android.security.GateKeeper; +import android.security.KeyStore; +import android.security.KeyStoreParameter; +import android.security.keymaster.KeyCharacteristics; +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyPermanentlyInvalidatedException; +import android.security.keystore.KeyProtection; +import android.security.keystore.KeymasterUtils; +import android.security.keystore.SecureKeyImportUnavailableException; +import android.security.keystore.WrappedKeyEntry; +import android.util.Log; + +import libcore.util.EmptyArray; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.Key; +import java.security.KeyStore.Entry; +import java.security.KeyStore.LoadStoreParameter; +import java.security.KeyStore.PrivateKeyEntry; +import java.security.KeyStore.ProtectionParameter; +import java.security.KeyStore.SecretKeyEntry; +import java.security.KeyStoreException; +import java.security.KeyStoreSpi; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.ProviderException; +import java.security.PublicKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import javax.crypto.SecretKey; + +/** + * A java.security.KeyStore interface for the Android KeyStore. An instance of + * it can be created via the {@link java.security.KeyStore#getInstance(String) + * KeyStore.getInstance("AndroidKeyStore")} interface. This returns a + * java.security.KeyStore backed by this "AndroidKeyStore" implementation. + * <p> + * This is built on top of Android's keystore daemon. The convention of alias + * use is: + * <p> + * PrivateKeyEntry will have a Credentials.USER_PRIVATE_KEY as the private key, + * Credentials.USER_CERTIFICATE as the first certificate in the chain (the one + * that corresponds to the private key), and then a Credentials.CA_CERTIFICATE + * entry which will have the rest of the chain concatenated in BER format. + * <p> + * TrustedCertificateEntry will just have a Credentials.CA_CERTIFICATE entry + * with a single certificate. + * + * @hide + */ +public class AndroidKeyStoreSpi extends KeyStoreSpi { + public static final String NAME = "AndroidKeyStore"; + + private KeyStore mKeyStore; + private int mUid = KeyStore.UID_SELF; + + @Override + public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, + UnrecoverableKeyException { + String userKeyAlias = Credentials.USER_PRIVATE_KEY + alias; + AndroidKeyStoreKey key; + if (!mKeyStore.contains(userKeyAlias, mUid)) { + // try legacy prefix for backward compatibility + userKeyAlias = Credentials.USER_SECRET_KEY + alias; + if (!mKeyStore.contains(userKeyAlias, mUid)) return null; + } + try { + key = AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(mKeyStore, + userKeyAlias, + mUid); + } catch (KeyPermanentlyInvalidatedException e) { + throw new UnrecoverableKeyException(e.getMessage()); + } + return key; + } + + @Override + public Certificate[] engineGetCertificateChain(String alias) { + if (alias == null) { + throw new NullPointerException("alias == null"); + } + + final X509Certificate leaf = (X509Certificate) engineGetCertificate(alias); + if (leaf == null) { + return null; + } + + final Certificate[] caList; + + // Suppress the key not found warning for this call. It seems that this error is exclusively + // being thrown when there is a self signed certificate chain, so when the keystore service + // attempts to query for the CA details, it obviously fails to find them and returns a + // key not found exception. This is WAI, and throwing a stack trace here can be very + // misleading since the trace is not clear. + final byte[] caBytes = mKeyStore.get(Credentials.CA_CERTIFICATE + alias, + mUid, + true /* suppressKeyNotFoundWarning */); + if (caBytes != null) { + final Collection<X509Certificate> caChain = toCertificates(caBytes); + + caList = new Certificate[caChain.size() + 1]; + + final Iterator<X509Certificate> it = caChain.iterator(); + int i = 1; + while (it.hasNext()) { + caList[i++] = it.next(); + } + } else { + caList = new Certificate[1]; + } + + caList[0] = leaf; + + return caList; + } + + @Override + public Certificate engineGetCertificate(String alias) { + if (alias == null) { + throw new NullPointerException("alias == null"); + } + + byte[] encodedCert = mKeyStore.get(Credentials.USER_CERTIFICATE + alias, mUid); + if (encodedCert != null) { + return getCertificateForPrivateKeyEntry(alias, encodedCert); + } + + encodedCert = mKeyStore.get(Credentials.CA_CERTIFICATE + alias, mUid); + if (encodedCert != null) { + return getCertificateForTrustedCertificateEntry(encodedCert); + } + + // This entry/alias does not contain a certificate. + return null; + } + + private Certificate getCertificateForTrustedCertificateEntry(byte[] encodedCert) { + // For this certificate there shouldn't be a private key in this KeyStore entry. Thus, + // there's no need to wrap this certificate as opposed to the certificate associated with + // a private key entry. + return toCertificate(encodedCert); + } + + private Certificate getCertificateForPrivateKeyEntry(String alias, byte[] encodedCert) { + // All crypto algorithms offered by Android Keystore for its private keys must also + // be offered for the corresponding public keys stored in the Android Keystore. The + // complication is that the underlying keystore service operates only on full key pairs, + // rather than just public keys or private keys. As a result, Android Keystore-backed + // crypto can only be offered for public keys for which keystore contains the + // corresponding private key. This is not the case for certificate-only entries (e.g., + // trusted certificates). + // + // getCertificate().getPublicKey() is the only way to obtain the public key + // corresponding to the private key stored in the KeyStore. Thus, we need to make sure + // that the returned public key points to the underlying key pair / private key + // when available. + + X509Certificate cert = toCertificate(encodedCert); + if (cert == null) { + // Failed to parse the certificate. + return null; + } + + String privateKeyAlias = Credentials.USER_PRIVATE_KEY + alias; + if (mKeyStore.contains(privateKeyAlias, mUid)) { + // As expected, keystore contains the private key corresponding to this public key. Wrap + // the certificate so that its getPublicKey method returns an Android Keystore + // PublicKey. This key will delegate crypto operations involving this public key to + // Android Keystore when higher-priority providers do not offer these crypto + // operations for this key. + return wrapIntoKeyStoreCertificate(privateKeyAlias, mUid, cert); + } else { + // This KeyStore entry/alias is supposed to contain the private key corresponding to + // the public key in this certificate, but it does not for some reason. It's probably a + // bug. Let other providers handle crypto operations involving the public key returned + // by this certificate's getPublicKey. + return cert; + } + } + + /** + * Wraps the provided cerificate into {@link KeyStoreX509Certificate} so that the public key + * returned by the certificate contains information about the alias of the private key in + * keystore. This is needed so that Android Keystore crypto operations using public keys can + * find out which key alias to use. These operations cannot work without an alias. + */ + private static KeyStoreX509Certificate wrapIntoKeyStoreCertificate( + String privateKeyAlias, int uid, X509Certificate certificate) { + return (certificate != null) + ? new KeyStoreX509Certificate(privateKeyAlias, uid, certificate) : null; + } + + private static X509Certificate toCertificate(byte[] bytes) { + try { + final CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(bytes)); + } catch (CertificateException e) { + Log.w(NAME, "Couldn't parse certificate in keystore", e); + return null; + } + } + + @SuppressWarnings("unchecked") + private static Collection<X509Certificate> toCertificates(byte[] bytes) { + try { + final CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return (Collection<X509Certificate>) certFactory.generateCertificates( + new ByteArrayInputStream(bytes)); + } catch (CertificateException e) { + Log.w(NAME, "Couldn't parse certificates in keystore", e); + return new ArrayList<X509Certificate>(); + } + } + + private Date getModificationDate(String alias) { + final long epochMillis = mKeyStore.getmtime(alias, mUid); + if (epochMillis == -1L) { + return null; + } + + return new Date(epochMillis); + } + + @Override + public Date engineGetCreationDate(String alias) { + if (alias == null) { + throw new NullPointerException("alias == null"); + } + + Date d = getModificationDate(Credentials.USER_PRIVATE_KEY + alias); + if (d != null) { + return d; + } + + d = getModificationDate(Credentials.USER_SECRET_KEY + alias); + if (d != null) { + return d; + } + + d = getModificationDate(Credentials.USER_CERTIFICATE + alias); + if (d != null) { + return d; + } + + return getModificationDate(Credentials.CA_CERTIFICATE + alias); + } + + @Override + public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) + throws KeyStoreException { + if ((password != null) && (password.length > 0)) { + throw new KeyStoreException("entries cannot be protected with passwords"); + } + + if (key instanceof PrivateKey) { + setPrivateKeyEntry(alias, (PrivateKey) key, chain, null); + } else if (key instanceof SecretKey) { + setSecretKeyEntry(alias, (SecretKey) key, null); + } else { + throw new KeyStoreException("Only PrivateKey and SecretKey are supported"); + } + } + + private static KeyProtection getLegacyKeyProtectionParameter(PrivateKey key) + throws KeyStoreException { + String keyAlgorithm = key.getAlgorithm(); + KeyProtection.Builder specBuilder; + if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(keyAlgorithm)) { + specBuilder = + new KeyProtection.Builder( + KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY); + // Authorized to be used with any digest (including no digest). + // MD5 was never offered for Android Keystore for ECDSA. + specBuilder.setDigests( + KeyProperties.DIGEST_NONE, + KeyProperties.DIGEST_SHA1, + KeyProperties.DIGEST_SHA224, + KeyProperties.DIGEST_SHA256, + KeyProperties.DIGEST_SHA384, + KeyProperties.DIGEST_SHA512); + } else if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(keyAlgorithm)) { + specBuilder = + new KeyProtection.Builder( + KeyProperties.PURPOSE_ENCRYPT + | KeyProperties.PURPOSE_DECRYPT + | KeyProperties.PURPOSE_SIGN + | KeyProperties.PURPOSE_VERIFY); + // Authorized to be used with any digest (including no digest). + specBuilder.setDigests( + KeyProperties.DIGEST_NONE, + KeyProperties.DIGEST_MD5, + KeyProperties.DIGEST_SHA1, + KeyProperties.DIGEST_SHA224, + KeyProperties.DIGEST_SHA256, + KeyProperties.DIGEST_SHA384, + KeyProperties.DIGEST_SHA512); + // Authorized to be used with any encryption and signature padding + // schemes (including no padding). + specBuilder.setEncryptionPaddings( + KeyProperties.ENCRYPTION_PADDING_NONE, + KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1, + KeyProperties.ENCRYPTION_PADDING_RSA_OAEP); + specBuilder.setSignaturePaddings( + KeyProperties.SIGNATURE_PADDING_RSA_PKCS1, + KeyProperties.SIGNATURE_PADDING_RSA_PSS); + // Disable randomized encryption requirement to support encryption + // padding NONE above. + specBuilder.setRandomizedEncryptionRequired(false); + } else { + throw new KeyStoreException("Unsupported key algorithm: " + keyAlgorithm); + } + specBuilder.setUserAuthenticationRequired(false); + + return specBuilder.build(); + } + + private void setPrivateKeyEntry(String alias, PrivateKey key, Certificate[] chain, + ProtectionParameter param) throws KeyStoreException { + int flags = 0; + KeyProtection spec; + if (param == null) { + spec = getLegacyKeyProtectionParameter(key); + } else if (param instanceof KeyStoreParameter) { + spec = getLegacyKeyProtectionParameter(key); + KeyStoreParameter legacySpec = (KeyStoreParameter) param; + if (legacySpec.isEncryptionRequired()) { + flags = KeyStore.FLAG_ENCRYPTED; + } + } else if (param instanceof KeyProtection) { + spec = (KeyProtection) param; + if (spec.isCriticalToDeviceEncryption()) { + flags |= KeyStore.FLAG_CRITICAL_TO_DEVICE_ENCRYPTION; + } + + if (spec.isStrongBoxBacked()) { + flags |= KeyStore.FLAG_STRONGBOX; + } + } else { + throw new KeyStoreException( + "Unsupported protection parameter class:" + param.getClass().getName() + + ". Supported: " + KeyProtection.class.getName() + ", " + + KeyStoreParameter.class.getName()); + } + + // Make sure the chain exists since this is a PrivateKey + if ((chain == null) || (chain.length == 0)) { + throw new KeyStoreException("Must supply at least one Certificate with PrivateKey"); + } + + // Do chain type checking. + X509Certificate[] x509chain = new X509Certificate[chain.length]; + for (int i = 0; i < chain.length; i++) { + if (!"X.509".equals(chain[i].getType())) { + throw new KeyStoreException("Certificates must be in X.509 format: invalid cert #" + + i); + } + + if (!(chain[i] instanceof X509Certificate)) { + throw new KeyStoreException("Certificates must be in X.509 format: invalid cert #" + + i); + } + + x509chain[i] = (X509Certificate) chain[i]; + } + + final byte[] userCertBytes; + try { + userCertBytes = x509chain[0].getEncoded(); + } catch (CertificateEncodingException e) { + throw new KeyStoreException("Failed to encode certificate #0", e); + } + + /* + * If we have a chain, store it in the CA certificate slot for this + * alias as concatenated DER-encoded certificates. These can be + * deserialized by {@link CertificateFactory#generateCertificates}. + */ + final byte[] chainBytes; + if (chain.length > 1) { + /* + * The chain is passed in as {user_cert, ca_cert_1, ca_cert_2, ...} + * so we only need the certificates starting at index 1. + */ + final byte[][] certsBytes = new byte[x509chain.length - 1][]; + int totalCertLength = 0; + for (int i = 0; i < certsBytes.length; i++) { + try { + certsBytes[i] = x509chain[i + 1].getEncoded(); + totalCertLength += certsBytes[i].length; + } catch (CertificateEncodingException e) { + throw new KeyStoreException("Failed to encode certificate #" + i, e); + } + } + + /* + * Serialize this into one byte array so we can later call + * CertificateFactory#generateCertificates to recover them. + */ + chainBytes = new byte[totalCertLength]; + int outputOffset = 0; + for (int i = 0; i < certsBytes.length; i++) { + final int certLength = certsBytes[i].length; + System.arraycopy(certsBytes[i], 0, chainBytes, outputOffset, certLength); + outputOffset += certLength; + certsBytes[i] = null; + } + } else { + chainBytes = null; + } + + final String pkeyAlias; + if (key instanceof AndroidKeyStorePrivateKey) { + pkeyAlias = ((AndroidKeyStoreKey) key).getAlias(); + } else { + pkeyAlias = null; + } + + byte[] pkcs8EncodedPrivateKeyBytes; + KeymasterArguments importArgs; + final boolean shouldReplacePrivateKey; + if (pkeyAlias != null && pkeyAlias.startsWith(Credentials.USER_PRIVATE_KEY)) { + final String keySubalias = pkeyAlias.substring(Credentials.USER_PRIVATE_KEY.length()); + if (!alias.equals(keySubalias)) { + throw new KeyStoreException("Can only replace keys with same alias: " + alias + + " != " + keySubalias); + } + shouldReplacePrivateKey = false; + importArgs = null; + pkcs8EncodedPrivateKeyBytes = null; + } else { + shouldReplacePrivateKey = true; + // Make sure the PrivateKey format is the one we support. + final String keyFormat = key.getFormat(); + if ((keyFormat == null) || (!"PKCS#8".equals(keyFormat))) { + throw new KeyStoreException( + "Unsupported private key export format: " + keyFormat + + ". Only private keys which export their key material in PKCS#8 format are" + + " supported."); + } + + // Make sure we can actually encode the key. + pkcs8EncodedPrivateKeyBytes = key.getEncoded(); + if (pkcs8EncodedPrivateKeyBytes == null) { + throw new KeyStoreException("Private key did not export any key material"); + } + + importArgs = new KeymasterArguments(); + try { + importArgs.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, + KeyProperties.KeyAlgorithm.toKeymasterAsymmetricKeyAlgorithm( + key.getAlgorithm())); + @KeyProperties.PurposeEnum int purposes = spec.getPurposes(); + importArgs.addEnums(KeymasterDefs.KM_TAG_PURPOSE, + KeyProperties.Purpose.allToKeymaster(purposes)); + if (spec.isDigestsSpecified()) { + importArgs.addEnums(KeymasterDefs.KM_TAG_DIGEST, + KeyProperties.Digest.allToKeymaster(spec.getDigests())); + } + + importArgs.addEnums(KeymasterDefs.KM_TAG_BLOCK_MODE, + KeyProperties.BlockMode.allToKeymaster(spec.getBlockModes())); + int[] keymasterEncryptionPaddings = + KeyProperties.EncryptionPadding.allToKeymaster( + spec.getEncryptionPaddings()); + if (((purposes & KeyProperties.PURPOSE_ENCRYPT) != 0) + && (spec.isRandomizedEncryptionRequired())) { + for (int keymasterPadding : keymasterEncryptionPaddings) { + if (!KeymasterUtils + .isKeymasterPaddingSchemeIndCpaCompatibleWithAsymmetricCrypto( + keymasterPadding)) { + throw new KeyStoreException( + "Randomized encryption (IND-CPA) required but is violated by" + + " encryption padding mode: " + + KeyProperties.EncryptionPadding.fromKeymaster( + keymasterPadding) + + ". See KeyProtection documentation."); + } + } + } + importArgs.addEnums(KeymasterDefs.KM_TAG_PADDING, keymasterEncryptionPaddings); + importArgs.addEnums(KeymasterDefs.KM_TAG_PADDING, + KeyProperties.SignaturePadding.allToKeymaster(spec.getSignaturePaddings())); + KeymasterUtils.addUserAuthArgs(importArgs, spec); + importArgs.addDateIfNotNull(KeymasterDefs.KM_TAG_ACTIVE_DATETIME, + spec.getKeyValidityStart()); + importArgs.addDateIfNotNull(KeymasterDefs.KM_TAG_ORIGINATION_EXPIRE_DATETIME, + spec.getKeyValidityForOriginationEnd()); + importArgs.addDateIfNotNull(KeymasterDefs.KM_TAG_USAGE_EXPIRE_DATETIME, + spec.getKeyValidityForConsumptionEnd()); + } catch (IllegalArgumentException | IllegalStateException e) { + throw new KeyStoreException(e); + } + } + + + boolean success = false; + try { + // Store the private key, if necessary + if (shouldReplacePrivateKey) { + // Delete the stored private key and any related entries before importing the + // provided key + Credentials.deleteAllTypesForAlias(mKeyStore, alias, mUid); + KeyCharacteristics resultingKeyCharacteristics = new KeyCharacteristics(); + int errorCode = mKeyStore.importKey( + Credentials.USER_PRIVATE_KEY + alias, + importArgs, + KeymasterDefs.KM_KEY_FORMAT_PKCS8, + pkcs8EncodedPrivateKeyBytes, + mUid, + flags, + resultingKeyCharacteristics); + if (errorCode != KeyStore.NO_ERROR) { + throw new KeyStoreException("Failed to store private key", + KeyStore.getKeyStoreException(errorCode)); + } + } else { + // Keep the stored private key around -- delete all other entry types + Credentials.deleteCertificateTypesForAlias(mKeyStore, alias, mUid); + Credentials.deleteLegacyKeyForAlias(mKeyStore, alias, mUid); + } + + // Store the leaf certificate + int errorCode = mKeyStore.insert(Credentials.USER_CERTIFICATE + alias, userCertBytes, + mUid, flags); + if (errorCode != KeyStore.NO_ERROR) { + throw new KeyStoreException("Failed to store certificate #0", + KeyStore.getKeyStoreException(errorCode)); + } + + // Store the certificate chain + errorCode = mKeyStore.insert(Credentials.CA_CERTIFICATE + alias, chainBytes, + mUid, flags); + if (errorCode != KeyStore.NO_ERROR) { + throw new KeyStoreException("Failed to store certificate chain", + KeyStore.getKeyStoreException(errorCode)); + } + success = true; + } finally { + if (!success) { + if (shouldReplacePrivateKey) { + Credentials.deleteAllTypesForAlias(mKeyStore, alias, mUid); + } else { + Credentials.deleteCertificateTypesForAlias(mKeyStore, alias, mUid); + Credentials.deleteLegacyKeyForAlias(mKeyStore, alias, mUid); + } + } + } + } + + private void setSecretKeyEntry(String entryAlias, SecretKey key, + ProtectionParameter param) + throws KeyStoreException { + if ((param != null) && (!(param instanceof KeyProtection))) { + throw new KeyStoreException( + "Unsupported protection parameter class: " + param.getClass().getName() + + ". Supported: " + KeyProtection.class.getName()); + } + KeyProtection params = (KeyProtection) param; + + if (key instanceof AndroidKeyStoreSecretKey) { + // KeyStore-backed secret key. It cannot be duplicated into another entry and cannot + // overwrite its own entry. + String keyAliasInKeystore = ((AndroidKeyStoreSecretKey) key).getAlias(); + if (keyAliasInKeystore == null) { + throw new KeyStoreException("KeyStore-backed secret key does not have an alias"); + } + String keyAliasPrefix = Credentials.USER_PRIVATE_KEY; + if (!keyAliasInKeystore.startsWith(keyAliasPrefix)) { + // try legacy prefix + keyAliasPrefix = Credentials.USER_SECRET_KEY; + if (!keyAliasInKeystore.startsWith(keyAliasPrefix)) { + throw new KeyStoreException("KeyStore-backed secret key has invalid alias: " + + keyAliasInKeystore); + } + } + String keyEntryAlias = + keyAliasInKeystore.substring(keyAliasPrefix.length()); + if (!entryAlias.equals(keyEntryAlias)) { + throw new KeyStoreException("Can only replace KeyStore-backed keys with same" + + " alias: " + entryAlias + " != " + keyEntryAlias); + } + // This is the entry where this key is already stored. No need to do anything. + if (params != null) { + throw new KeyStoreException("Modifying KeyStore-backed key using protection" + + " parameters not supported"); + } + return; + } + + if (params == null) { + throw new KeyStoreException( + "Protection parameters must be specified when importing a symmetric key"); + } + + // Not a KeyStore-backed secret key -- import its key material into keystore. + String keyExportFormat = key.getFormat(); + if (keyExportFormat == null) { + throw new KeyStoreException( + "Only secret keys that export their key material are supported"); + } else if (!"RAW".equals(keyExportFormat)) { + throw new KeyStoreException( + "Unsupported secret key material export format: " + keyExportFormat); + } + byte[] keyMaterial = key.getEncoded(); + if (keyMaterial == null) { + throw new KeyStoreException("Key did not export its key material despite supporting" + + " RAW format export"); + } + + KeymasterArguments args = new KeymasterArguments(); + try { + int keymasterAlgorithm = + KeyProperties.KeyAlgorithm.toKeymasterSecretKeyAlgorithm(key.getAlgorithm()); + args.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, keymasterAlgorithm); + + int[] keymasterDigests; + if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC) { + // JCA HMAC key algorithm implies a digest (e.g., HmacSHA256 key algorithm + // implies SHA-256 digest). Because keymaster HMAC key is authorized only for one + // digest, we don't let import parameters override the digest implied by the key. + // If the parameters specify digests at all, they must specify only one digest, the + // only implied by key algorithm. + int keymasterImpliedDigest = + KeyProperties.KeyAlgorithm.toKeymasterDigest(key.getAlgorithm()); + if (keymasterImpliedDigest == -1) { + throw new ProviderException( + "HMAC key algorithm digest unknown for key algorithm " + + key.getAlgorithm()); + } + keymasterDigests = new int[] {keymasterImpliedDigest}; + if (params.isDigestsSpecified()) { + // Digest(s) explicitly specified in params -- check that the list consists of + // exactly one digest, the one implied by key algorithm. + int[] keymasterDigestsFromParams = + KeyProperties.Digest.allToKeymaster(params.getDigests()); + if ((keymasterDigestsFromParams.length != 1) + || (keymasterDigestsFromParams[0] != keymasterImpliedDigest)) { + throw new KeyStoreException( + "Unsupported digests specification: " + + Arrays.asList(params.getDigests()) + ". Only " + + KeyProperties.Digest.fromKeymaster(keymasterImpliedDigest) + + " supported for HMAC key algorithm " + key.getAlgorithm()); + } + } + } else { + // Key algorithm does not imply a digest. + if (params.isDigestsSpecified()) { + keymasterDigests = KeyProperties.Digest.allToKeymaster(params.getDigests()); + } else { + keymasterDigests = EmptyArray.INT; + } + } + args.addEnums(KeymasterDefs.KM_TAG_DIGEST, keymasterDigests); + + @KeyProperties.PurposeEnum int purposes = params.getPurposes(); + int[] keymasterBlockModes = + KeyProperties.BlockMode.allToKeymaster(params.getBlockModes()); + if (((purposes & KeyProperties.PURPOSE_ENCRYPT) != 0) + && (params.isRandomizedEncryptionRequired())) { + for (int keymasterBlockMode : keymasterBlockModes) { + if (!KeymasterUtils.isKeymasterBlockModeIndCpaCompatibleWithSymmetricCrypto( + keymasterBlockMode)) { + throw new KeyStoreException( + "Randomized encryption (IND-CPA) required but may be violated by" + + " block mode: " + + KeyProperties.BlockMode.fromKeymaster(keymasterBlockMode) + + ". See KeyProtection documentation."); + } + } + } + args.addEnums(KeymasterDefs.KM_TAG_PURPOSE, + KeyProperties.Purpose.allToKeymaster(purposes)); + args.addEnums(KeymasterDefs.KM_TAG_BLOCK_MODE, keymasterBlockModes); + if (params.getSignaturePaddings().length > 0) { + throw new KeyStoreException("Signature paddings not supported for symmetric keys"); + } + int[] keymasterPaddings = KeyProperties.EncryptionPadding.allToKeymaster( + params.getEncryptionPaddings()); + args.addEnums(KeymasterDefs.KM_TAG_PADDING, keymasterPaddings); + KeymasterUtils.addUserAuthArgs(args, params); + KeymasterUtils.addMinMacLengthAuthorizationIfNecessary( + args, + keymasterAlgorithm, + keymasterBlockModes, + keymasterDigests); + args.addDateIfNotNull(KeymasterDefs.KM_TAG_ACTIVE_DATETIME, + params.getKeyValidityStart()); + args.addDateIfNotNull(KeymasterDefs.KM_TAG_ORIGINATION_EXPIRE_DATETIME, + params.getKeyValidityForOriginationEnd()); + args.addDateIfNotNull(KeymasterDefs.KM_TAG_USAGE_EXPIRE_DATETIME, + params.getKeyValidityForConsumptionEnd()); + + if (((purposes & KeyProperties.PURPOSE_ENCRYPT) != 0) + && (!params.isRandomizedEncryptionRequired())) { + // Permit caller-provided IV when encrypting with this key + args.addBoolean(KeymasterDefs.KM_TAG_CALLER_NONCE); + } + } catch (IllegalArgumentException | IllegalStateException e) { + throw new KeyStoreException(e); + } + int flags = 0; + if (params.isCriticalToDeviceEncryption()) { + flags |= KeyStore.FLAG_CRITICAL_TO_DEVICE_ENCRYPTION; + } + if (params.isStrongBoxBacked()) { + flags |= KeyStore.FLAG_STRONGBOX; + } + + Credentials.deleteAllTypesForAlias(mKeyStore, entryAlias, mUid); + String keyAliasInKeystore = Credentials.USER_PRIVATE_KEY + entryAlias; + int errorCode = mKeyStore.importKey( + keyAliasInKeystore, + args, + KeymasterDefs.KM_KEY_FORMAT_RAW, + keyMaterial, + mUid, + flags, + new KeyCharacteristics()); + if (errorCode != KeyStore.NO_ERROR) { + throw new KeyStoreException("Failed to import secret key. Keystore error code: " + + errorCode); + } + } + + private void setWrappedKeyEntry(String alias, WrappedKeyEntry entry, + ProtectionParameter param) throws KeyStoreException { + if (param != null) { + throw new KeyStoreException("Protection parameters are specified inside wrapped keys"); + } + + byte[] maskingKey = new byte[32]; + + + KeymasterArguments args = new KeymasterArguments(); + String[] parts = entry.getTransformation().split("/"); + + String algorithm = parts[0]; + if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(algorithm)) { + args.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_RSA); + } else if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(algorithm)) { + args.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_RSA); + } + + if (parts.length > 1) { + String mode = parts[1]; + if (KeyProperties.BLOCK_MODE_ECB.equalsIgnoreCase(mode)) { + args.addEnums(KeymasterDefs.KM_TAG_BLOCK_MODE, KeymasterDefs.KM_MODE_ECB); + } else if (KeyProperties.BLOCK_MODE_CBC.equalsIgnoreCase(mode)) { + args.addEnums(KeymasterDefs.KM_TAG_BLOCK_MODE, KeymasterDefs.KM_MODE_CBC); + } else if (KeyProperties.BLOCK_MODE_CTR.equalsIgnoreCase(mode)) { + args.addEnums(KeymasterDefs.KM_TAG_BLOCK_MODE, KeymasterDefs.KM_MODE_CTR); + } else if (KeyProperties.BLOCK_MODE_GCM.equalsIgnoreCase(mode)) { + args.addEnums(KeymasterDefs.KM_TAG_BLOCK_MODE, KeymasterDefs.KM_MODE_GCM); + } + } + + if (parts.length > 2) { + String padding = parts[2]; + if (KeyProperties.ENCRYPTION_PADDING_NONE.equalsIgnoreCase(padding)) { + // Noop + } else if (KeyProperties.ENCRYPTION_PADDING_PKCS7.equalsIgnoreCase(padding)) { + args.addEnums(KeymasterDefs.KM_TAG_PADDING, KeymasterDefs.KM_PAD_PKCS7); + } else if (KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1.equalsIgnoreCase(padding)) { + args.addEnums(KeymasterDefs.KM_TAG_PADDING, + KeymasterDefs.KM_PAD_RSA_PKCS1_1_5_ENCRYPT); + } else if (KeyProperties.ENCRYPTION_PADDING_RSA_OAEP.equalsIgnoreCase(padding)) { + args.addEnums(KeymasterDefs.KM_TAG_PADDING, KeymasterDefs.KM_PAD_RSA_OAEP); + } + } + + KeyGenParameterSpec spec = (KeyGenParameterSpec) entry.getAlgorithmParameterSpec(); + if (spec.isDigestsSpecified()) { + String digest = spec.getDigests()[0]; + if (KeyProperties.DIGEST_NONE.equalsIgnoreCase(digest)) { + // Noop + } else if (KeyProperties.DIGEST_MD5.equalsIgnoreCase(digest)) { + args.addEnums(KeymasterDefs.KM_TAG_DIGEST, KeymasterDefs.KM_DIGEST_MD5); + } else if (KeyProperties.DIGEST_SHA1.equalsIgnoreCase(digest)) { + args.addEnums(KeymasterDefs.KM_TAG_DIGEST, KeymasterDefs.KM_DIGEST_SHA1); + } else if (KeyProperties.DIGEST_SHA224.equalsIgnoreCase(digest)) { + args.addEnums(KeymasterDefs.KM_TAG_DIGEST, KeymasterDefs.KM_DIGEST_SHA_2_224); + } else if (KeyProperties.DIGEST_SHA256.equalsIgnoreCase(digest)) { + args.addEnums(KeymasterDefs.KM_TAG_DIGEST, KeymasterDefs.KM_DIGEST_SHA_2_256); + } else if (KeyProperties.DIGEST_SHA384.equalsIgnoreCase(digest)) { + args.addEnums(KeymasterDefs.KM_TAG_DIGEST, KeymasterDefs.KM_DIGEST_SHA_2_384); + } else if (KeyProperties.DIGEST_SHA512.equalsIgnoreCase(digest)) { + args.addEnums(KeymasterDefs.KM_TAG_DIGEST, KeymasterDefs.KM_DIGEST_SHA_2_512); + } + } + + int errorCode = mKeyStore.importWrappedKey( + Credentials.USER_PRIVATE_KEY + alias, + entry.getWrappedKeyBytes(), + Credentials.USER_PRIVATE_KEY + entry.getWrappingKeyAlias(), + maskingKey, + args, + GateKeeper.getSecureUserId(), + 0, // FIXME fingerprint id? + mUid, + new KeyCharacteristics()); + if (errorCode == KeymasterDefs.KM_ERROR_UNIMPLEMENTED) { + throw new SecureKeyImportUnavailableException("Could not import wrapped key"); + } else if (errorCode != KeyStore.NO_ERROR) { + throw new KeyStoreException("Failed to import wrapped key. Keystore error code: " + + errorCode); + } + } + + @Override + public void engineSetKeyEntry(String alias, byte[] userKey, Certificate[] chain) + throws KeyStoreException { + throw new KeyStoreException("Operation not supported because key encoding is unknown"); + } + + @Override + public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException { + if (isKeyEntry(alias)) { + throw new KeyStoreException("Entry exists and is not a trusted certificate"); + } + + // We can't set something to null. + if (cert == null) { + throw new NullPointerException("cert == null"); + } + + final byte[] encoded; + try { + encoded = cert.getEncoded(); + } catch (CertificateEncodingException e) { + throw new KeyStoreException(e); + } + + if (!mKeyStore.put(Credentials.CA_CERTIFICATE + alias, encoded, mUid, KeyStore.FLAG_NONE)) { + throw new KeyStoreException("Couldn't insert certificate; is KeyStore initialized?"); + } + } + + @Override + public void engineDeleteEntry(String alias) throws KeyStoreException { + if (!Credentials.deleteAllTypesForAlias(mKeyStore, alias, mUid)) { + throw new KeyStoreException("Failed to delete entry: " + alias); + } + } + + private Set<String> getUniqueAliases() { + final String[] rawAliases = mKeyStore.list("", mUid); + if (rawAliases == null) { + return new HashSet<String>(); + } + + final Set<String> aliases = new HashSet<String>(rawAliases.length); + for (String alias : rawAliases) { + final int idx = alias.indexOf('_'); + if ((idx == -1) || (alias.length() <= idx)) { + Log.e(NAME, "invalid alias: " + alias); + continue; + } + + aliases.add(new String(alias.substring(idx + 1))); + } + + return aliases; + } + + @Override + public Enumeration<String> engineAliases() { + return Collections.enumeration(getUniqueAliases()); + } + + @Override + public boolean engineContainsAlias(String alias) { + if (alias == null) { + throw new NullPointerException("alias == null"); + } + + return mKeyStore.contains(Credentials.USER_PRIVATE_KEY + alias, mUid) + || mKeyStore.contains(Credentials.USER_SECRET_KEY + alias, mUid) + || mKeyStore.contains(Credentials.USER_CERTIFICATE + alias, mUid) + || mKeyStore.contains(Credentials.CA_CERTIFICATE + alias, mUid); + } + + @Override + public int engineSize() { + return getUniqueAliases().size(); + } + + @Override + public boolean engineIsKeyEntry(String alias) { + return isKeyEntry(alias); + } + + private boolean isKeyEntry(String alias) { + return mKeyStore.contains(Credentials.USER_PRIVATE_KEY + alias, mUid) || + mKeyStore.contains(Credentials.USER_SECRET_KEY + alias, mUid); + } + + + private boolean isCertificateEntry(String alias) { + if (alias == null) { + throw new NullPointerException("alias == null"); + } + + return mKeyStore.contains(Credentials.CA_CERTIFICATE + alias, mUid); + } + + @Override + public boolean engineIsCertificateEntry(String alias) { + return !isKeyEntry(alias) && isCertificateEntry(alias); + } + + @Override + public String engineGetCertificateAlias(Certificate cert) { + if (cert == null) { + return null; + } + if (!"X.509".equalsIgnoreCase(cert.getType())) { + // Only X.509 certificates supported + return null; + } + byte[] targetCertBytes; + try { + targetCertBytes = cert.getEncoded(); + } catch (CertificateEncodingException e) { + return null; + } + if (targetCertBytes == null) { + return null; + } + + final Set<String> nonCaEntries = new HashSet<String>(); + + /* + * First scan the PrivateKeyEntry types. The KeyStoreSpi documentation + * says to only compare the first certificate in the chain which is + * equivalent to the USER_CERTIFICATE prefix for the Android keystore + * convention. + */ + final String[] certAliases = mKeyStore.list(Credentials.USER_CERTIFICATE, mUid); + if (certAliases != null) { + for (String alias : certAliases) { + final byte[] certBytes = mKeyStore.get(Credentials.USER_CERTIFICATE + alias, mUid); + if (certBytes == null) { + continue; + } + + nonCaEntries.add(alias); + + if (Arrays.equals(certBytes, targetCertBytes)) { + return alias; + } + } + } + + /* + * Look at all the TrustedCertificateEntry types. Skip all the + * PrivateKeyEntry we looked at above. + */ + final String[] caAliases = mKeyStore.list(Credentials.CA_CERTIFICATE, mUid); + if (certAliases != null) { + for (String alias : caAliases) { + if (nonCaEntries.contains(alias)) { + continue; + } + + final byte[] certBytes = mKeyStore.get(Credentials.CA_CERTIFICATE + alias, mUid); + if (certBytes == null) { + continue; + } + + if (Arrays.equals(certBytes, targetCertBytes)) { + return alias; + } + } + } + + return null; + } + + @Override + public void engineStore(OutputStream stream, char[] password) throws IOException, + NoSuchAlgorithmException, CertificateException { + throw new UnsupportedOperationException("Can not serialize AndroidKeyStore to OutputStream"); + } + + @Override + public void engineLoad(InputStream stream, char[] password) throws IOException, + NoSuchAlgorithmException, CertificateException { + if (stream != null) { + throw new IllegalArgumentException("InputStream not supported"); + } + + if (password != null) { + throw new IllegalArgumentException("password not supported"); + } + + // Unfortunate name collision. + mKeyStore = KeyStore.getInstance(); + mUid = KeyStore.UID_SELF; + } + + @Override + public void engineLoad(LoadStoreParameter param) throws IOException, + NoSuchAlgorithmException, CertificateException { + int uid = KeyStore.UID_SELF; + if (param != null) { + if (param instanceof AndroidKeyStoreLoadStoreParameter) { + uid = ((AndroidKeyStoreLoadStoreParameter) param).getUid(); + } else { + throw new IllegalArgumentException( + "Unsupported param type: " + param.getClass()); + } + } + mKeyStore = KeyStore.getInstance(); + mUid = uid; + } + + @Override + public void engineSetEntry(String alias, Entry entry, ProtectionParameter param) + throws KeyStoreException { + if (entry == null) { + throw new KeyStoreException("entry == null"); + } + + Credentials.deleteAllTypesForAlias(mKeyStore, alias, mUid); + + if (entry instanceof java.security.KeyStore.TrustedCertificateEntry) { + java.security.KeyStore.TrustedCertificateEntry trE = + (java.security.KeyStore.TrustedCertificateEntry) entry; + engineSetCertificateEntry(alias, trE.getTrustedCertificate()); + return; + } + + if (entry instanceof PrivateKeyEntry) { + PrivateKeyEntry prE = (PrivateKeyEntry) entry; + setPrivateKeyEntry(alias, prE.getPrivateKey(), prE.getCertificateChain(), param); + } else if (entry instanceof SecretKeyEntry) { + SecretKeyEntry secE = (SecretKeyEntry) entry; + setSecretKeyEntry(alias, secE.getSecretKey(), param); + } else if (entry instanceof WrappedKeyEntry) { + WrappedKeyEntry wke = (WrappedKeyEntry) entry; + setWrappedKeyEntry(alias, wke, param); + } else { + throw new KeyStoreException( + "Entry must be a PrivateKeyEntry, SecretKeyEntry or TrustedCertificateEntry" + + "; was " + entry); + } + } + + /** + * {@link X509Certificate} which returns {@link AndroidKeyStorePublicKey} from + * {@link #getPublicKey()}. This is so that crypto operations on these public keys contain + * can find out which keystore private key entry to use. This is needed so that Android Keystore + * crypto operations using public keys can find out which key alias to use. These operations + * require an alias. + */ + static class KeyStoreX509Certificate extends DelegatingX509Certificate { + private final String mPrivateKeyAlias; + private final int mPrivateKeyUid; + KeyStoreX509Certificate(String privateKeyAlias, int privateKeyUid, + X509Certificate delegate) { + super(delegate); + mPrivateKeyAlias = privateKeyAlias; + mPrivateKeyUid = privateKeyUid; + } + + @Override + public PublicKey getPublicKey() { + PublicKey original = super.getPublicKey(); + return AndroidKeyStoreProvider.getAndroidKeyStorePublicKey( + mPrivateKeyAlias, mPrivateKeyUid, + original.getAlgorithm(), original.getEncoded()); + } + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreUnauthenticatedAESCipherSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreUnauthenticatedAESCipherSpi.java new file mode 100644 index 000000000000..65678a37b938 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreUnauthenticatedAESCipherSpi.java @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.ArrayUtils; +import android.security.keystore.KeyProperties; + +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.ProviderException; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidParameterSpecException; +import java.util.Arrays; + +import javax.crypto.CipherSpi; +import javax.crypto.spec.IvParameterSpec; + +/** + * Base class for Android Keystore unauthenticated AES {@link CipherSpi} implementations. + * + * @hide + */ +class AndroidKeyStoreUnauthenticatedAESCipherSpi extends AndroidKeyStoreCipherSpiBase { + + abstract static class ECB extends AndroidKeyStoreUnauthenticatedAESCipherSpi { + protected ECB(int keymasterPadding) { + super(KeymasterDefs.KM_MODE_ECB, keymasterPadding, false); + } + + public static class NoPadding extends + AndroidKeyStoreUnauthenticatedAESCipherSpi.ECB { + public NoPadding() { + super(KeymasterDefs.KM_PAD_NONE); + } + } + + public static class PKCS7Padding extends + AndroidKeyStoreUnauthenticatedAESCipherSpi.ECB { + public PKCS7Padding() { + super(KeymasterDefs.KM_PAD_PKCS7); + } + } + } + + abstract static class CBC extends AndroidKeyStoreUnauthenticatedAESCipherSpi { + protected CBC(int keymasterPadding) { + super(KeymasterDefs.KM_MODE_CBC, keymasterPadding, true); + } + + public static class NoPadding extends + AndroidKeyStoreUnauthenticatedAESCipherSpi.CBC { + public NoPadding() { + super(KeymasterDefs.KM_PAD_NONE); + } + } + + public static class PKCS7Padding extends + AndroidKeyStoreUnauthenticatedAESCipherSpi.CBC { + public PKCS7Padding() { + super(KeymasterDefs.KM_PAD_PKCS7); + } + } + } + + abstract static class CTR extends AndroidKeyStoreUnauthenticatedAESCipherSpi { + protected CTR(int keymasterPadding) { + super(KeymasterDefs.KM_MODE_CTR, keymasterPadding, true); + } + + public static class NoPadding extends + AndroidKeyStoreUnauthenticatedAESCipherSpi.CTR { + public NoPadding() { + super(KeymasterDefs.KM_PAD_NONE); + } + } + } + + private static final int BLOCK_SIZE_BYTES = 16; + + private final int mKeymasterBlockMode; + private final int mKeymasterPadding; + /** Whether this transformation requires an IV. */ + private final boolean mIvRequired; + + private byte[] mIv; + + /** Whether the current {@code #mIv} has been used by the underlying crypto operation. */ + private boolean mIvHasBeenUsed; + + AndroidKeyStoreUnauthenticatedAESCipherSpi( + int keymasterBlockMode, + int keymasterPadding, + boolean ivRequired) { + mKeymasterBlockMode = keymasterBlockMode; + mKeymasterPadding = keymasterPadding; + mIvRequired = ivRequired; + } + + @Override + protected final void resetAll() { + mIv = null; + mIvHasBeenUsed = false; + super.resetAll(); + } + + @Override + protected final void resetWhilePreservingInitState() { + super.resetWhilePreservingInitState(); + } + + @Override + protected final void initKey(int opmode, Key key) throws InvalidKeyException { + if (!(key instanceof AndroidKeyStoreSecretKey)) { + throw new InvalidKeyException( + "Unsupported key: " + ((key != null) ? key.getClass().getName() : "null")); + } + if (!KeyProperties.KEY_ALGORITHM_AES.equalsIgnoreCase(key.getAlgorithm())) { + throw new InvalidKeyException( + "Unsupported key algorithm: " + key.getAlgorithm() + ". Only " + + KeyProperties.KEY_ALGORITHM_AES + " supported"); + } + setKey((AndroidKeyStoreSecretKey) key); + } + + @Override + protected final void initAlgorithmSpecificParameters() throws InvalidKeyException { + if (!mIvRequired) { + return; + } + + // IV is used + if (!isEncrypting()) { + throw new InvalidKeyException("IV required when decrypting" + + ". Use IvParameterSpec or AlgorithmParameters to provide it."); + } + } + + @Override + protected final void initAlgorithmSpecificParameters(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + if (!mIvRequired) { + if (params != null) { + throw new InvalidAlgorithmParameterException("Unsupported parameters: " + params); + } + return; + } + + // IV is used + if (params == null) { + if (!isEncrypting()) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException( + "IvParameterSpec must be provided when decrypting"); + } + return; + } + if (!(params instanceof IvParameterSpec)) { + throw new InvalidAlgorithmParameterException("Only IvParameterSpec supported"); + } + mIv = ((IvParameterSpec) params).getIV(); + if (mIv == null) { + throw new InvalidAlgorithmParameterException("Null IV in IvParameterSpec"); + } + } + + @Override + protected final void initAlgorithmSpecificParameters(AlgorithmParameters params) + throws InvalidAlgorithmParameterException { + if (!mIvRequired) { + if (params != null) { + throw new InvalidAlgorithmParameterException("Unsupported parameters: " + params); + } + return; + } + + // IV is used + if (params == null) { + if (!isEncrypting()) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException("IV required when decrypting" + + ". Use IvParameterSpec or AlgorithmParameters to provide it."); + } + return; + } + + if (!"AES".equalsIgnoreCase(params.getAlgorithm())) { + throw new InvalidAlgorithmParameterException( + "Unsupported AlgorithmParameters algorithm: " + params.getAlgorithm() + + ". Supported: AES"); + } + + IvParameterSpec ivSpec; + try { + ivSpec = params.getParameterSpec(IvParameterSpec.class); + } catch (InvalidParameterSpecException e) { + if (!isEncrypting()) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException("IV required when decrypting" + + ", but not found in parameters: " + params, e); + } + mIv = null; + return; + } + mIv = ivSpec.getIV(); + if (mIv == null) { + throw new InvalidAlgorithmParameterException("Null IV in AlgorithmParameters"); + } + } + + @Override + protected final int getAdditionalEntropyAmountForBegin() { + if ((mIvRequired) && (mIv == null) && (isEncrypting())) { + // IV will need to be generated + return BLOCK_SIZE_BYTES; + } + + return 0; + } + + @Override + protected final int getAdditionalEntropyAmountForFinish() { + return 0; + } + + @Override + protected final void addAlgorithmSpecificParametersToBegin( + @NonNull KeymasterArguments keymasterArgs) { + if ((isEncrypting()) && (mIvRequired) && (mIvHasBeenUsed)) { + // IV is being reused for encryption: this violates security best practices. + throw new IllegalStateException( + "IV has already been used. Reusing IV in encryption mode violates security best" + + " practices."); + } + + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_AES); + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_BLOCK_MODE, mKeymasterBlockMode); + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_PADDING, mKeymasterPadding); + if ((mIvRequired) && (mIv != null)) { + keymasterArgs.addBytes(KeymasterDefs.KM_TAG_NONCE, mIv); + } + } + + @Override + protected final void loadAlgorithmSpecificParametersFromBeginResult( + @NonNull KeymasterArguments keymasterArgs) { + mIvHasBeenUsed = true; + + // NOTE: Keymaster doesn't always return an IV, even if it's used. + byte[] returnedIv = keymasterArgs.getBytes(KeymasterDefs.KM_TAG_NONCE, null); + if ((returnedIv != null) && (returnedIv.length == 0)) { + returnedIv = null; + } + + if (mIvRequired) { + if (mIv == null) { + mIv = returnedIv; + } else if ((returnedIv != null) && (!Arrays.equals(returnedIv, mIv))) { + throw new ProviderException("IV in use differs from provided IV"); + } + } else { + if (returnedIv != null) { + throw new ProviderException( + "IV in use despite IV not being used by this transformation"); + } + } + } + + @Override + protected final int engineGetBlockSize() { + return BLOCK_SIZE_BYTES; + } + + @Override + protected final int engineGetOutputSize(int inputLen) { + return inputLen + 3 * BLOCK_SIZE_BYTES; + } + + @Override + protected final byte[] engineGetIV() { + return ArrayUtils.cloneIfNotEmpty(mIv); + } + + @Nullable + @Override + protected final AlgorithmParameters engineGetParameters() { + if (!mIvRequired) { + return null; + } + if ((mIv != null) && (mIv.length > 0)) { + try { + AlgorithmParameters params = AlgorithmParameters.getInstance("AES"); + params.init(new IvParameterSpec(mIv)); + return params; + } catch (NoSuchAlgorithmException e) { + throw new ProviderException( + "Failed to obtain AES AlgorithmParameters", e); + } catch (InvalidParameterSpecException e) { + throw new ProviderException( + "Failed to initialize AES AlgorithmParameters with an IV", + e); + } + } + return null; + } +} diff --git a/keystore/java/android/security/keystore2/DelegatingX509Certificate.java b/keystore/java/android/security/keystore2/DelegatingX509Certificate.java new file mode 100644 index 000000000000..9dfee8ce098c --- /dev/null +++ b/keystore/java/android/security/keystore2/DelegatingX509Certificate.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Principal; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Set; + +import javax.security.auth.x500.X500Principal; + +class DelegatingX509Certificate extends X509Certificate { + private final X509Certificate mDelegate; + + DelegatingX509Certificate(X509Certificate delegate) { + mDelegate = delegate; + } + + @Override + public Set<String> getCriticalExtensionOIDs() { + return mDelegate.getCriticalExtensionOIDs(); + } + + @Override + public byte[] getExtensionValue(String oid) { + return mDelegate.getExtensionValue(oid); + } + + @Override + public Set<String> getNonCriticalExtensionOIDs() { + return mDelegate.getNonCriticalExtensionOIDs(); + } + + @Override + public boolean hasUnsupportedCriticalExtension() { + return mDelegate.hasUnsupportedCriticalExtension(); + } + + @Override + public void checkValidity() throws CertificateExpiredException, + CertificateNotYetValidException { + mDelegate.checkValidity(); + } + + @Override + public void checkValidity(Date date) throws CertificateExpiredException, + CertificateNotYetValidException { + mDelegate.checkValidity(date); + } + + @Override + public int getBasicConstraints() { + return mDelegate.getBasicConstraints(); + } + + @Override + public Principal getIssuerDN() { + return mDelegate.getIssuerDN(); + } + + @Override + public boolean[] getIssuerUniqueID() { + return mDelegate.getIssuerUniqueID(); + } + + @Override + public boolean[] getKeyUsage() { + return mDelegate.getKeyUsage(); + } + + @Override + public Date getNotAfter() { + return mDelegate.getNotAfter(); + } + + @Override + public Date getNotBefore() { + return mDelegate.getNotBefore(); + } + + @Override + public BigInteger getSerialNumber() { + return mDelegate.getSerialNumber(); + } + + @Override + public String getSigAlgName() { + return mDelegate.getSigAlgName(); + } + + @Override + public String getSigAlgOID() { + return mDelegate.getSigAlgOID(); + } + + @Override + public byte[] getSigAlgParams() { + return mDelegate.getSigAlgParams(); + } + + @Override + public byte[] getSignature() { + return mDelegate.getSignature(); + } + + @Override + public Principal getSubjectDN() { + return mDelegate.getSubjectDN(); + } + + @Override + public boolean[] getSubjectUniqueID() { + return mDelegate.getSubjectUniqueID(); + } + + @Override + public byte[] getTBSCertificate() throws CertificateEncodingException { + return mDelegate.getTBSCertificate(); + } + + @Override + public int getVersion() { + return mDelegate.getVersion(); + } + + @Override + public byte[] getEncoded() throws CertificateEncodingException { + return mDelegate.getEncoded(); + } + + @Override + public PublicKey getPublicKey() { + return mDelegate.getPublicKey(); + } + + @Override + public String toString() { + return mDelegate.toString(); + } + + @Override + public void verify(PublicKey key) + throws CertificateException, + NoSuchAlgorithmException, + InvalidKeyException, + NoSuchProviderException, + SignatureException { + mDelegate.verify(key); + } + + @Override + public void verify(PublicKey key, String sigProvider) + throws CertificateException, + NoSuchAlgorithmException, + InvalidKeyException, + NoSuchProviderException, + SignatureException { + mDelegate.verify(key, sigProvider); + } + + @Override + public List<String> getExtendedKeyUsage() throws CertificateParsingException { + return mDelegate.getExtendedKeyUsage(); + } + + @Override + public Collection<List<?>> getIssuerAlternativeNames() throws CertificateParsingException { + return mDelegate.getIssuerAlternativeNames(); + } + + @Override + public X500Principal getIssuerX500Principal() { + return mDelegate.getIssuerX500Principal(); + } + + @Override + public Collection<List<?>> getSubjectAlternativeNames() throws CertificateParsingException { + return mDelegate.getSubjectAlternativeNames(); + } + + @Override + public X500Principal getSubjectX500Principal() { + return mDelegate.getSubjectX500Principal(); + } +} diff --git a/keystore/java/android/security/keystore2/KeyStoreCryptoOperationChunkedStreamer.java b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationChunkedStreamer.java new file mode 100644 index 000000000000..3bf9da080f30 --- /dev/null +++ b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationChunkedStreamer.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.os.IBinder; +import android.security.KeyStore; +import android.security.KeyStoreException; +import android.security.keymaster.KeymasterDefs; +import android.security.keymaster.OperationResult; +import android.security.keystore.ArrayUtils; +import android.security.keystore.KeyStoreConnectException; + +import libcore.util.EmptyArray; + +/** + * Helper for streaming a crypto operation's input and output via {@link KeyStore} service's + * {@code update} and {@code finish} operations. + * + * <p>The helper abstracts away issues that need to be solved in most code that uses KeyStore's + * update and finish operations. Firstly, KeyStore's update operation can consume only a limited + * amount of data in one go because the operations are marshalled via Binder. Secondly, the update + * operation may consume less data than provided, in which case the caller has to buffer the + * remainder for next time. Thirdly, when the input is smaller than a threshold, skipping update + * and passing input data directly to final improves performance. This threshold is configurable; + * using a threshold <= 1 causes the helper act eagerly, which may be required for some types of + * operations (e.g. ciphers). + * + * <p>The helper exposes {@link #update(byte[], int, int) update} and + * {@link #doFinal(byte[], int, int, byte[], byte[]) doFinal} operations which can be used to + * conveniently implement various JCA crypto primitives. + * + * <p>Bidirectional chunked streaming of data via a KeyStore crypto operation is abstracted away as + * a {@link Stream} to avoid having this class deal with operation tokens and occasional additional + * parameters to {@code update} and {@code final} operations. + * + * @hide + */ +class KeyStoreCryptoOperationChunkedStreamer implements KeyStoreCryptoOperationStreamer { + + /** + * Bidirectional chunked data stream over a KeyStore crypto operation. + */ + interface Stream { + /** + * Returns the result of the KeyStore {@code update} operation or null if keystore couldn't + * be reached. + */ + OperationResult update(byte[] input); + + /** + * Returns the result of the KeyStore {@code finish} operation or null if keystore couldn't + * be reached. + */ + OperationResult finish(byte[] input, byte[] siganture, byte[] additionalEntropy); + } + + // Binder buffer is about 1MB, but it's shared between all active transactions of the process. + // Thus, it's safer to use a much smaller upper bound. + private static final int DEFAULT_CHUNK_SIZE_MAX = 64 * 1024; + // The chunk buffer will be sent to update until its size under this threshold. + // This threshold should be <= the max input allowed for finish. + // Setting this threshold <= 1 will effectivley disable buffering between updates. + private static final int DEFAULT_CHUNK_SIZE_THRESHOLD = 2 * 1024; + + private final Stream mKeyStoreStream; + private final int mChunkSizeMax; + private final int mChunkSizeThreshold; + private final byte[] mChunk; + private int mChunkLength = 0; + private long mConsumedInputSizeBytes; + private long mProducedOutputSizeBytes; + + KeyStoreCryptoOperationChunkedStreamer(Stream operation) { + this(operation, DEFAULT_CHUNK_SIZE_THRESHOLD, DEFAULT_CHUNK_SIZE_MAX); + } + + KeyStoreCryptoOperationChunkedStreamer(Stream operation, int chunkSizeThreshold) { + this(operation, chunkSizeThreshold, DEFAULT_CHUNK_SIZE_MAX); + } + + KeyStoreCryptoOperationChunkedStreamer(Stream operation, int chunkSizeThreshold, + int chunkSizeMax) { + mKeyStoreStream = operation; + mChunkSizeMax = chunkSizeMax; + if (chunkSizeThreshold <= 0) { + mChunkSizeThreshold = 1; + } else if (chunkSizeThreshold > chunkSizeMax) { + mChunkSizeThreshold = chunkSizeMax; + } else { + mChunkSizeThreshold = chunkSizeThreshold; + } + mChunk = new byte[mChunkSizeMax]; + } + + public byte[] update(byte[] input, int inputOffset, int inputLength) throws KeyStoreException { + if (inputLength == 0 || input == null) { + // No input provided + return EmptyArray.BYTE; + } + if (inputLength < 0 || inputOffset < 0 || (inputOffset + inputLength) > input.length) { + throw new KeyStoreException(KeymasterDefs.KM_ERROR_UNKNOWN_ERROR, + "Input offset and length out of bounds of input array"); + } + + byte[] output = EmptyArray.BYTE; + + while (inputLength > 0 || mChunkLength >= mChunkSizeThreshold) { + int inputConsumed = ArrayUtils.copy(input, inputOffset, mChunk, mChunkLength, + inputLength); + inputLength -= inputConsumed; + inputOffset += inputConsumed; + mChunkLength += inputConsumed; + mConsumedInputSizeBytes += inputConsumed; + + if (mChunkLength > mChunkSizeMax) { + throw new KeyStoreException(KeymasterDefs.KM_ERROR_INVALID_INPUT_LENGTH, + "Chunk size exceeded max chunk size. Max: " + mChunkSizeMax + + " Actual: " + mChunkLength); + } + + if (mChunkLength >= mChunkSizeThreshold) { + OperationResult opResult = mKeyStoreStream.update( + ArrayUtils.subarray(mChunk, 0, mChunkLength)); + + if (opResult == null) { + throw new KeyStoreConnectException(); + } else if (opResult.resultCode != KeyStore.NO_ERROR) { + throw KeyStore.getKeyStoreException(opResult.resultCode); + } + if (opResult.inputConsumed <= 0) { + throw new KeyStoreException(KeymasterDefs.KM_ERROR_INVALID_INPUT_LENGTH, + "Keystore consumed 0 of " + mChunkLength + " bytes provided."); + } else if (opResult.inputConsumed > mChunkLength) { + throw new KeyStoreException(KeymasterDefs.KM_ERROR_UNKNOWN_ERROR, + "Keystore consumed more input than provided. Provided: " + + mChunkLength + ", consumed: " + opResult.inputConsumed); + } + mChunkLength -= opResult.inputConsumed; + + if (mChunkLength > 0) { + // Partialy consumed, shift chunk contents + ArrayUtils.copy(mChunk, opResult.inputConsumed, mChunk, 0, mChunkLength); + } + + if ((opResult.output != null) && (opResult.output.length > 0)) { + // Output was produced + mProducedOutputSizeBytes += opResult.output.length; + output = ArrayUtils.concat(output, opResult.output); + } + } + } + return output; + } + + public byte[] doFinal(byte[] input, int inputOffset, int inputLength, + byte[] signature, byte[] additionalEntropy) throws KeyStoreException { + byte[] output = update(input, inputOffset, inputLength); + byte[] finalChunk = ArrayUtils.subarray(mChunk, 0, mChunkLength); + OperationResult opResult = mKeyStoreStream.finish(finalChunk, signature, additionalEntropy); + + if (opResult == null) { + throw new KeyStoreConnectException(); + } else if (opResult.resultCode != KeyStore.NO_ERROR) { + throw KeyStore.getKeyStoreException(opResult.resultCode); + } + // If no error, assume all input consumed + mConsumedInputSizeBytes += finalChunk.length; + + if ((opResult.output != null) && (opResult.output.length > 0)) { + mProducedOutputSizeBytes += opResult.output.length; + output = ArrayUtils.concat(output, opResult.output); + } + + return output; + } + + @Override + public long getConsumedInputSizeBytes() { + return mConsumedInputSizeBytes; + } + + @Override + public long getProducedOutputSizeBytes() { + return mProducedOutputSizeBytes; + } + + /** + * Main data stream via a KeyStore streaming operation. + * + * <p>For example, for an encryption operation, this is the stream through which plaintext is + * provided and ciphertext is obtained. + */ + public static class MainDataStream implements Stream { + + private final KeyStore mKeyStore; + private final IBinder mOperationToken; + + public MainDataStream(KeyStore keyStore, IBinder operationToken) { + mKeyStore = keyStore; + mOperationToken = operationToken; + } + + @Override + public OperationResult update(byte[] input) { + return mKeyStore.update(mOperationToken, null, input); + } + + @Override + public OperationResult finish(byte[] input, byte[] signature, byte[] additionalEntropy) { + return mKeyStore.finish(mOperationToken, null, input, signature, additionalEntropy); + } + } +} diff --git a/keystore/java/android/security/keystore2/KeyStoreCryptoOperationStreamer.java b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationStreamer.java new file mode 100644 index 000000000000..fec3bbc3460a --- /dev/null +++ b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationStreamer.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.security.KeyStore; +import android.security.KeyStoreException; + +/** + * Helper for streaming a crypto operation's input and output via {@link KeyStore} service's + * {@code update} and {@code finish} operations. + * + * <p>The helper abstracts away to issues that need to be solved in most code that uses KeyStore's + * update and finish operations. Firstly, KeyStore's update operation can consume only a limited + * amount of data in one go because the operations are marshalled via Binder. Secondly, the update + * operation may consume less data than provided, in which case the caller has to buffer the + * remainder for next time. The helper exposes {@link #update(byte[], int, int) update} and + * {@link #doFinal(byte[], int, int, byte[], byte[]) doFinal} operations which can be used to + * conveniently implement various JCA crypto primitives. + * + * @hide + */ +interface KeyStoreCryptoOperationStreamer { + byte[] update(byte[] input, int inputOffset, int inputLength) throws KeyStoreException; + byte[] doFinal(byte[] input, int inputOffset, int inputLength, byte[] signature, + byte[] additionalEntropy) throws KeyStoreException; + long getConsumedInputSizeBytes(); + long getProducedOutputSizeBytes(); +} diff --git a/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java new file mode 100644 index 000000000000..36590efa82b6 --- /dev/null +++ b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2015 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 android.security.keystore2; + +import android.security.KeyStore; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.UserNotAuthenticatedException; + +import libcore.util.EmptyArray; + +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.SecureRandom; + +/** + * Assorted utility methods for implementing crypto operations on top of KeyStore. + * + * @hide + */ +abstract class KeyStoreCryptoOperationUtils { + + private static volatile SecureRandom sRng; + + private KeyStoreCryptoOperationUtils() {} + + /** + * Returns the {@link InvalidKeyException} to be thrown by the {@code init} method of + * the crypto operation in response to {@code KeyStore.begin} operation or {@code null} if + * the {@code init} method should succeed. + */ + static InvalidKeyException getInvalidKeyExceptionForInit( + KeyStore keyStore, AndroidKeyStoreKey key, int beginOpResultCode) { + if (beginOpResultCode == KeyStore.NO_ERROR) { + return null; + } + + // An error occurred. However, some errors should not lead to init throwing an exception. + // See below. + InvalidKeyException e = + keyStore.getInvalidKeyException(key.getAlias(), key.getUid(), beginOpResultCode); + switch (beginOpResultCode) { + case KeyStore.OP_AUTH_NEEDED: + // Operation needs to be authorized by authenticating the user. Don't throw an + // exception is such authentication is possible for this key + // (UserNotAuthenticatedException). An example of when it's not possible is where + // the key is permanently invalidated (KeyPermanentlyInvalidatedException). + if (e instanceof UserNotAuthenticatedException) { + return null; + } + break; + } + return e; + } + + /** + * Returns the exception to be thrown by the {@code Cipher.init} method of the crypto operation + * in response to {@code KeyStore.begin} operation or {@code null} if the {@code init} method + * should succeed. + */ + public static GeneralSecurityException getExceptionForCipherInit( + KeyStore keyStore, AndroidKeyStoreKey key, int beginOpResultCode) { + if (beginOpResultCode == KeyStore.NO_ERROR) { + return null; + } + + // Cipher-specific cases + switch (beginOpResultCode) { + case KeymasterDefs.KM_ERROR_INVALID_NONCE: + return new InvalidAlgorithmParameterException("Invalid IV"); + case KeymasterDefs.KM_ERROR_CALLER_NONCE_PROHIBITED: + return new InvalidAlgorithmParameterException("Caller-provided IV not permitted"); + } + + // General cases + return getInvalidKeyExceptionForInit(keyStore, key, beginOpResultCode); + } + + /** + * Returns the requested number of random bytes to mix into keystore/keymaster RNG. + * + * @param rng RNG from which to obtain the random bytes or {@code null} for the platform-default + * RNG. + */ + static byte[] getRandomBytesToMixIntoKeystoreRng(SecureRandom rng, int sizeBytes) { + if (sizeBytes <= 0) { + return EmptyArray.BYTE; + } + if (rng == null) { + rng = getRng(); + } + byte[] result = new byte[sizeBytes]; + rng.nextBytes(result); + return result; + } + + private static SecureRandom getRng() { + // IMPLEMENTATION NOTE: It's OK to share a SecureRandom instance because SecureRandom is + // required to be thread-safe. + if (sRng == null) { + sRng = new SecureRandom(); + } + return sRng; + } +} |