diff options
Diffstat (limited to 'keystore')
14 files changed, 917 insertions, 152 deletions
diff --git a/keystore/java/android/security/keystore/AndroidKeyStoreAuthenticatedAESCipherSpi.java b/keystore/java/android/security/keystore/AndroidKeyStoreAuthenticatedAESCipherSpi.java new file mode 100644 index 0000000..f412743 --- /dev/null +++ b/keystore/java/android/security/keystore/AndroidKeyStoreAuthenticatedAESCipherSpi.java @@ -0,0 +1,442 @@ +/* + * 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.keystore; + +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.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 { + private 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; + } + + 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)); + 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 BufferAllOutputUntilDoFinalStreamer(streamer); + } + } + + @NonNull + @Override + protected final KeyStoreCryptoOperationStreamer createAdditionalAuthenticationDataStreamer( + KeyStore keyStore, IBinder operationToken) { + return new KeyStoreCryptoOperationChunkedStreamer( + new AdditionalAuthenticationDataStream(keyStore, operationToken)); + } + + @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.addInt(KeymasterDefs.KM_TAG_MAC_LENGTH, mTagLengthBits); + } + + protected final int getTagLengthBits() { + return mTagLengthBits; + } + + public static final class NoPadding extends 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.addInt(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_AES); + keymasterArgs.addInt(KeymasterDefs.KM_TAG_BLOCK_MODE, mKeymasterBlockMode); + keymasterArgs.addInt(KeymasterDefs.KM_TAG_PADDING, mKeymasterPadding); + if (mIv != null) { + keymasterArgs.addBlob(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.getBlob(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[] additionalEntropy) throws KeyStoreException { + byte[] output = mDelegate.doFinal(input, inputOffset, inputLength, 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.addBlob(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[] 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/keystore/AndroidKeyStoreBCWorkaroundProvider.java b/keystore/java/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java index f37cf07..06b76fa 100644 --- a/keystore/java/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java +++ b/keystore/java/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java @@ -93,6 +93,9 @@ class AndroidKeyStoreBCWorkaroundProvider extends Provider { putSymmetricCipherImpl("AES/CTR/NoPadding", PACKAGE_NAME + ".AndroidKeyStoreUnauthenticatedAESCipherSpi$CTR$NoPadding"); + 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"); diff --git a/keystore/java/android/security/keystore/AndroidKeyStoreCipherSpiBase.java b/keystore/java/android/security/keystore/AndroidKeyStoreCipherSpiBase.java index d2d5850..fc53451 100644 --- a/keystore/java/android/security/keystore/AndroidKeyStoreCipherSpiBase.java +++ b/keystore/java/android/security/keystore/AndroidKeyStoreCipherSpiBase.java @@ -26,6 +26,8 @@ import android.security.keymaster.KeymasterArguments; import android.security.keymaster.KeymasterDefs; import android.security.keymaster.OperationResult; +import libcore.util.EmptyArray; + import java.nio.ByteBuffer; import java.security.AlgorithmParameters; import java.security.GeneralSecurityException; @@ -78,6 +80,8 @@ abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStor 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 @@ -189,6 +193,8 @@ abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStor mOperationToken = null; mOperationHandle = 0; mMainDataStreamer = null; + mAdditionalAuthenticationDataStreamer = null; + mAdditionalAuthenticationDataStreamerClosed = false; mCachedException = null; } @@ -209,6 +215,8 @@ abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStor mOperationToken = null; mOperationHandle = 0; mMainDataStreamer = null; + mAdditionalAuthenticationDataStreamer = null; + mAdditionalAuthenticationDataStreamerClosed = false; mCachedException = null; } @@ -273,6 +281,9 @@ abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStor loadAlgorithmSpecificParametersFromBeginResult(opResult.outParams); mMainDataStreamer = createMainDataStreamer(mKeyStore, opResult.token); + mAdditionalAuthenticationDataStreamer = + createAdditionalAuthenticationDataStreamer(mKeyStore, opResult.token); + mAdditionalAuthenticationDataStreamerClosed = false; } /** @@ -289,6 +300,20 @@ abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStor keyStore, operationToken)); } + /** + * Creates a streamer which sends Additional Authentication Data (AAD) into the KeyStore. + * + * <p>This implementation returns {@code null}. + * + * @returns 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) { @@ -307,6 +332,7 @@ abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStor byte[] output; try { + flushAAD(); output = mMainDataStreamer.update(input, inputOffset, inputLen); } catch (KeyStoreException e) { mCachedException = e; @@ -320,6 +346,25 @@ abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStor return output; } + private void flushAAD() throws KeyStoreException { + if ((mAdditionalAuthenticationDataStreamer != null) + && (!mAdditionalAuthenticationDataStreamerClosed)) { + byte[] output; + try { + output = mAdditionalAuthenticationDataStreamer.doFinal( + EmptyArray.BYTE, 0, 0, + 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 { @@ -344,12 +389,64 @@ abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStor @Override protected final void engineUpdateAAD(byte[] input, int inputOffset, int inputLen) { - super.engineUpdateAAD(input, inputOffset, 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) { - super.engineUpdateAAD(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); + } + super.engineUpdateAAD(input, inputOffset, inputLen); } @Override @@ -368,6 +465,7 @@ abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStor byte[] output; try { + flushAAD(); byte[] additionalEntropy = KeyStoreCryptoOperationUtils.getRandomBytesToMixIntoKeystoreRng( mRng, getAdditionalEntropyAmountForFinish()); @@ -615,6 +713,20 @@ abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStor 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(); + } + // The methods below need to be implemented by subclasses. /** diff --git a/keystore/java/android/security/keystore/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore/AndroidKeyStoreKeyPairGeneratorSpi.java index 2de60fd..2055cdb 100644 --- a/keystore/java/android/security/keystore/AndroidKeyStoreKeyPairGeneratorSpi.java +++ b/keystore/java/android/security/keystore/AndroidKeyStoreKeyPairGeneratorSpi.java @@ -104,8 +104,6 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato /* EC */ private static final int EC_DEFAULT_KEY_SIZE = 256; - private static final int EC_MIN_KEY_SIZE = 192; - private static final int EC_MAX_KEY_SIZE = 521; /* RSA */ private static final int RSA_DEFAULT_KEY_SIZE = 2048; @@ -115,16 +113,13 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato 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-192 - SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("p-192", 192); - SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("secp192r1", 192); - SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("prime192v1", 192); - // 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); @@ -140,6 +135,10 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato 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; @@ -227,9 +226,8 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato | KeyProperties.PURPOSE_VERIFY); // Authorized to be used with any digest (including no digest). specBuilder.setDigests(KeyProperties.DIGEST_NONE); - specBuilder.setSignaturePaddings( - KeyProperties.SIGNATURE_PADDING_RSA_PKCS1); - // Authorized to be used with any padding (including no padding). + // Authorized to be used with any encryption and signature padding + // scheme (including no padding). specBuilder.setEncryptionPaddings( KeyProperties.ENCRYPTION_PADDING_NONE); // Disable randomized encryption requirement to support encryption @@ -598,9 +596,9 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato throws InvalidAlgorithmParameterException { switch (keymasterAlgorithm) { case KeymasterDefs.KM_ALGORITHM_EC: - if (keySize < EC_MIN_KEY_SIZE || keySize > EC_MAX_KEY_SIZE) { - throw new InvalidAlgorithmParameterException("EC key size must be >= " - + EC_MIN_KEY_SIZE + " and <= " + EC_MAX_KEY_SIZE); + 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: diff --git a/keystore/java/android/security/keystore/AndroidKeyStoreRSACipherSpi.java b/keystore/java/android/security/keystore/AndroidKeyStoreRSACipherSpi.java index 6abdf19..38e216d 100644 --- a/keystore/java/android/security/keystore/AndroidKeyStoreRSACipherSpi.java +++ b/keystore/java/android/security/keystore/AndroidKeyStoreRSACipherSpi.java @@ -129,6 +129,7 @@ abstract class AndroidKeyStoreRSACipherSpi extends AndroidKeyStoreCipherSpiBase private final KeyStoreCryptoOperationStreamer mDelegate; private final int mModulusSizeBytes; private final ByteArrayOutputStream mInputBuffer = new ByteArrayOutputStream(); + private long mConsumedInputSizeBytes; private ZeroPaddingEncryptionStreamer( KeyStoreCryptoOperationStreamer delegate, @@ -142,6 +143,7 @@ abstract class AndroidKeyStoreRSACipherSpi extends AndroidKeyStoreCipherSpiBase throws KeyStoreException { if (inputLength > 0) { mInputBuffer.write(input, inputOffset, inputLength); + mConsumedInputSizeBytes += inputLength; } return EmptyArray.BYTE; } @@ -151,6 +153,7 @@ abstract class AndroidKeyStoreRSACipherSpi extends AndroidKeyStoreCipherSpiBase byte[] additionalEntropy) throws KeyStoreException { if (inputLength > 0) { + mConsumedInputSizeBytes += inputLength; mInputBuffer.write(input, inputOffset, inputLength); } byte[] bufferedInput = mInputBuffer.toByteArray(); @@ -173,6 +176,16 @@ abstract class AndroidKeyStoreRSACipherSpi extends AndroidKeyStoreCipherSpiBase } return mDelegate.doFinal(paddedInput, 0, paddedInput.length, additionalEntropy); } + + @Override + public long getConsumedInputSizeBytes() { + return mConsumedInputSizeBytes; + } + + @Override + public long getProducedOutputSizeBytes() { + return mDelegate.getProducedOutputSizeBytes(); + } } } diff --git a/keystore/java/android/security/keystore/AndroidKeyStoreSpi.java b/keystore/java/android/security/keystore/AndroidKeyStoreSpi.java index 3bd9d1d..5fb589e 100644 --- a/keystore/java/android/security/keystore/AndroidKeyStoreSpi.java +++ b/keystore/java/android/security/keystore/AndroidKeyStoreSpi.java @@ -258,9 +258,8 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi { | KeyProperties.PURPOSE_VERIFY); // Authorized to be used with any digest (including no digest). specBuilder.setDigests(KeyProperties.DIGEST_NONE); - specBuilder.setSignaturePaddings( - KeyProperties.SIGNATURE_PADDING_RSA_PKCS1); - // Authorized to be used with any padding (including no padding). + // Authorized to be used with any encryption and signature padding scheme (including no + // padding). specBuilder.setEncryptionPaddings( KeyProperties.ENCRYPTION_PADDING_NONE); // Disable randomized encryption requirement to support encryption padding NONE diff --git a/keystore/java/android/security/keystore/AndroidKeyStoreUnauthenticatedAESCipherSpi.java b/keystore/java/android/security/keystore/AndroidKeyStoreUnauthenticatedAESCipherSpi.java index 76804a9..6c53c6a 100644 --- a/keystore/java/android/security/keystore/AndroidKeyStoreUnauthenticatedAESCipherSpi.java +++ b/keystore/java/android/security/keystore/AndroidKeyStoreUnauthenticatedAESCipherSpi.java @@ -1,3 +1,19 @@ +/* + * 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.keystore; import android.annotation.NonNull; diff --git a/keystore/java/android/security/keystore/KeyGenParameterSpec.java b/keystore/java/android/security/keystore/KeyGenParameterSpec.java index 4c0631f..1732db9 100644 --- a/keystore/java/android/security/keystore/KeyGenParameterSpec.java +++ b/keystore/java/android/security/keystore/KeyGenParameterSpec.java @@ -19,16 +19,20 @@ package android.security.keystore; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.KeyguardManager; +import android.hardware.fingerprint.FingerprintManager; import android.text.TextUtils; import java.math.BigInteger; import java.security.KeyPairGenerator; +import java.security.Signature; import java.security.cert.Certificate; import java.security.spec.AlgorithmParameterSpec; import java.util.Date; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; +import javax.crypto.Mac; import javax.security.auth.x500.X500Principal; /** @@ -62,14 +66,21 @@ import javax.security.auth.x500.X500Principal; * <p>NOTE: If a private key is not authorized to sign the self-signed certificate, then the * certificate will be created with an invalid signature which will not verify. Such a certificate * is still useful because it provides access to the public key. To generate a valid - * signature for the certificate the key needs to be authorized for - * {@link KeyProperties#PURPOSE_SIGN}, a suitable digest or {@link KeyProperties#DIGEST_NONE}, and - * {@link KeyProperties#SIGNATURE_PADDING_RSA_PKCS1} or - * {@link KeyProperties#ENCRYPTION_PADDING_NONE}. + * signature for the certificate the key needs to be authorized for all of the following: + * <ul> + * <li>{@link KeyProperties#PURPOSE_SIGN},</li> + * <li>operation without requiring the user to be authenticated (see + * {@link Builder#setUserAuthenticationRequired(boolean)}),</li> + * <li>suitable digest or {@link KeyProperties#DIGEST_NONE},</li> + * <li>(RSA keys only) padding scheme {@link KeyProperties#SIGNATURE_PADDING_RSA_PKCS1} or + * {@link KeyProperties#ENCRYPTION_PADDING_NONE}.</li> + * </ul> * * <p>NOTE: The key material of the generated symmetric and private keys is not accessible. The key * material of the public keys is accessible. * + * <p>Instances of this class are immutable. + * * <p><h3>Example: Asymmetric key pair</h3> * The following example illustrates how to generate an EC key pair in the Android KeyStore system * under alias {@code key1} authorized to be used only for signing using SHA-256, SHA-384, @@ -79,11 +90,12 @@ import javax.security.auth.x500.X500Principal; * KeyProperties.KEY_ALGORITHM_EC, * "AndroidKeyStore"); * keyPairGenerator.initialize( - * new KeyGenParameterSpec.Builder("key1", + * new KeyGenParameterSpec.Builder( + * "key1", * KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY) - * .setDigests(KeyProperties.DIGEST_SHA256 - * | KeyProperties.DIGEST_SHA384 - * | KeyProperties.DIGEST_SHA512) + * .setDigests(KeyProperties.DIGEST_SHA256, + * KeyProperties.DIGEST_SHA384, + * KeyProperties.DIGEST_SHA512) * // Only permit this key to be used if the user authenticated * // within the last five minutes. * .setUserAuthenticationRequired(true) @@ -100,15 +112,16 @@ import javax.security.auth.x500.X500Principal; * * <p><h3>Example: Symmetric key</h3> * The following example illustrates how to generate an AES key in the Android KeyStore system under - * alias {@code key2} authorized to be used only for encryption/decryption in CTR mode. + * alias {@code key2} authorized to be used only for encryption/decryption in GCM mode with no + * padding. * <pre> {@code * KeyGenerator keyGenerator = KeyGenerator.getInstance( - * KeyProperties.KEY_ALGORITHM_HMAC_SHA256, + * KeyProperties.KEY_ALGORITHM_AES, * "AndroidKeyStore"); * keyGenerator.initialize( * new KeyGenParameterSpec.Builder("key2", * KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) - * .setBlockModes(KeyProperties.BLOCK_MODE_CTR) + * .setBlockModes(KeyProperties.BLOCK_MODE_GCM) * .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) * .build()); * SecretKey key = keyGenerator.generateKey(); @@ -169,10 +182,6 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { int userAuthenticationValidityDurationSeconds) { if (TextUtils.isEmpty(keyStoreAlias)) { throw new IllegalArgumentException("keyStoreAlias must not be empty"); - } else if ((userAuthenticationValidityDurationSeconds < 0) - && (userAuthenticationValidityDurationSeconds != -1)) { - throw new IllegalArgumentException( - "userAuthenticationValidityDurationSeconds must not be negative"); } if (certificateSubject == null) { @@ -197,11 +206,11 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { mSpec = spec; mCertificateSubject = certificateSubject; mCertificateSerialNumber = certificateSerialNumber; - mCertificateNotBefore = certificateNotBefore; - mCertificateNotAfter = certificateNotAfter; - mKeyValidityStart = keyValidityStart; - mKeyValidityForOriginationEnd = keyValidityForOriginationEnd; - mKeyValidityForConsumptionEnd = keyValidityForConsumptionEnd; + mCertificateNotBefore = Utils.cloneIfNotNull(certificateNotBefore); + mCertificateNotAfter = Utils.cloneIfNotNull(certificateNotAfter); + mKeyValidityStart = Utils.cloneIfNotNull(keyValidityStart); + mKeyValidityForOriginationEnd = Utils.cloneIfNotNull(keyValidityForOriginationEnd); + mKeyValidityForConsumptionEnd = Utils.cloneIfNotNull(keyValidityForConsumptionEnd); mPurposes = purposes; mDigests = ArrayUtils.cloneIfNotEmpty(digests); mEncryptionPaddings = @@ -217,6 +226,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { * Returns the alias that will be used in the {@code java.security.KeyStore} * in conjunction with the {@code AndroidKeyStore}. */ + @NonNull public String getKeystoreAlias() { return mKeystoreAlias; } @@ -231,10 +241,10 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { } /** - * Returns the {@link AlgorithmParameterSpec} that will be used for creation - * of the key pair. + * Returns the key algorithm-specific {@link AlgorithmParameterSpec} that will be used for + * creation of the key or {@code null} if algorithm-specific defaults should be used. */ - @NonNull + @Nullable public AlgorithmParameterSpec getAlgorithmParameterSpec() { return mSpec; } @@ -263,7 +273,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { */ @NonNull public Date getCertificateNotBefore() { - return mCertificateNotBefore; + return Utils.cloneIfNotNull(mCertificateNotBefore); } /** @@ -272,7 +282,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { */ @NonNull public Date getCertificateNotAfter() { - return mCertificateNotAfter; + return Utils.cloneIfNotNull(mCertificateNotAfter); } /** @@ -281,7 +291,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { */ @Nullable public Date getKeyValidityStart() { - return mKeyValidityStart; + return Utils.cloneIfNotNull(mKeyValidityStart); } /** @@ -290,7 +300,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { */ @Nullable public Date getKeyValidityForConsumptionEnd() { - return mKeyValidityForConsumptionEnd; + return Utils.cloneIfNotNull(mKeyValidityForConsumptionEnd); } /** @@ -299,7 +309,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { */ @Nullable public Date getKeyValidityForOriginationEnd() { - return mKeyValidityForOriginationEnd; + return Utils.cloneIfNotNull(mKeyValidityForOriginationEnd); } /** @@ -367,7 +377,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { } /** - * Gets the set of block modes (e.g., {@code CBC}, {@code CTR}) with which the key can be used + * Gets the set of block modes (e.g., {@code GCM}, {@code CBC}) with which the key can be used * when encrypting/decrypting. Attempts to use the key with any other block modes will be * rejected. * @@ -392,28 +402,32 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { } /** - * Returns {@code true} if user authentication is required for this key to be used. + * Returns {@code true} if the key is authorized to be used only if the user has been + * authenticated. * - * <p>This restriction applies only to private key operations. Public key operations are not - * restricted. + * <p>This authorization applies only to secret key and private key operations. Public key + * operations are not restricted. * * @see #getUserAuthenticationValidityDurationSeconds() + * @see Builder#setUserAuthenticationRequired(boolean) */ public boolean isUserAuthenticationRequired() { return mUserAuthenticationRequired; } /** - * Gets the duration of time (seconds) for which this key can be used after the user is - * successfully authenticated. This has effect only if user authentication is required. + * Gets the duration of time (seconds) for which this key is authorized to be used after the + * user is successfully authenticated. This has effect only if user authentication is required + * (see {@link #isUserAuthenticationRequired()}). * - * <p>This restriction applies only to private key operations. Public key operations are not - * restricted. + * <p>This authorization applies only to secret key and private key operations. Public key + * operations are not restricted. * * @return duration in seconds or {@code -1} if authentication is required for every use of the * key. * * @see #isUserAuthenticationRequired() + * @see Builder#setUserAuthenticationValidityDurationSeconds(int) */ public int getUserAuthenticationValidityDurationSeconds() { return mUserAuthenticationValidityDurationSeconds; @@ -447,7 +461,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { * Creates a new instance of the {@code Builder}. * * @param keystoreAlias alias of the entry in which the generated key will appear in - * Android KeyStore. + * Android KeyStore. Must not be empty. * @param purposes set of purposes (e.g., encrypt, decrypt, sign) for which the key can be * used. Attempts to use the key for any other purpose will be rejected. * @@ -462,6 +476,8 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { public Builder(@NonNull String keystoreAlias, @KeyProperties.PurposeEnum int purposes) { if (keystoreAlias == null) { throw new NullPointerException("keystoreAlias == null"); + } else if (keystoreAlias.isEmpty()) { + throw new IllegalArgumentException("keystoreAlias must not be empty"); } mKeystoreAlias = keystoreAlias; mPurposes = purposes; @@ -488,7 +504,11 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { /** * Sets the algorithm-specific key generation parameters. For example, for RSA keys this may - * be an instance of {@link java.security.spec.RSAKeyGenParameterSpec}. + * be an instance of {@link java.security.spec.RSAKeyGenParameterSpec} whereas for EC keys + * this may be an instance of {@link java.security.spec.ECGenParameterSpec}. + * + * <p>These key generation parameters must match other explicitly set parameters (if any), + * such as key size. */ public Builder setAlgorithmParameterSpec(@NonNull AlgorithmParameterSpec spec) { if (spec == null) { @@ -537,7 +557,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { if (date == null) { throw new NullPointerException("date == null"); } - mCertificateNotBefore = date; + mCertificateNotBefore = Utils.cloneIfNotNull(date); return this; } @@ -552,7 +572,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { if (date == null) { throw new NullPointerException("date == null"); } - mCertificateNotAfter = date; + mCertificateNotAfter = Utils.cloneIfNotNull(date); return this; } @@ -565,7 +585,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { */ @NonNull public Builder setKeyValidityStart(Date startDate) { - mKeyValidityStart = startDate; + mKeyValidityStart = Utils.cloneIfNotNull(startDate); return this; } @@ -594,7 +614,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { */ @NonNull public Builder setKeyValidityForOriginationEnd(Date endDate) { - mKeyValidityForOriginationEnd = endDate; + mKeyValidityForOriginationEnd = Utils.cloneIfNotNull(endDate); return this; } @@ -608,7 +628,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { */ @NonNull public Builder setKeyValidityForConsumptionEnd(Date endDate) { - mKeyValidityForConsumptionEnd = endDate; + mKeyValidityForConsumptionEnd = Utils.cloneIfNotNull(endDate); return this; } @@ -674,11 +694,11 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { } /** - * Sets the set of block modes (e.g., {@code CBC}, {@code CTR}, {@code ECB}) with which the - * key can be used when encrypting/decrypting. Attempts to use the key with any other block - * modes will be rejected. + * Sets the set of block modes (e.g., {@code GCM}, {@code CBC}) with which the key can be + * used when encrypting/decrypting. Attempts to use the key with any other block modes will + * be rejected. * - * <p>This must be specified for encryption/decryption keys. + * <p>This must be specified for symmetric encryption/decryption keys. * * <p>See {@link KeyProperties}.{@code BLOCK_MODE} constants. */ @@ -704,7 +724,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { * <li>encryption/decryption transformation which do not offer {@code IND-CPA}, such as * {@code ECB} with a symmetric encryption algorithm, or RSA encryption/decryption without * padding, are prohibited;</li> - * <li>in block modes which use an IV, such as {@code CBC}, {@code CTR}, and {@code GCM}, + * <li>in block modes which use an IV, such as {@code GCM}, {@code CBC}, and {@code CTR}, * caller-provided IVs are rejected when encrypting, to ensure that only random IVs are * used.</li> * </ul> @@ -731,22 +751,38 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { } /** - * Sets whether user authentication is required to use this key. + * Sets whether this key is authorized to be used only if the user has been authenticated. * - * <p>By default, the key can be used without user authentication. + * <p>By default, the key is authorized to be used regardless of whether the user has been + * authenticated. * - * <p>When user authentication is required, the user authorizes the use of the key by - * authenticating to this Android device using a subset of their secure lock screen - * credentials. Different authentication methods are used depending on whether the every - * use of the key must be authenticated (as specified by - * {@link #setUserAuthenticationValidityDurationSeconds(int)}). + * <p>When user authentication is required: + * <ul> + * <li>The key can only be generated if secure lock screen is set up (see + * {@link KeyguardManager#isDeviceSecure()}). Additionally, if the key requires that user + * authentication takes place for every use of the key (see + * {@link #setUserAuthenticationValidityDurationSeconds(int)}), at least one fingerprint + * must be enrolled (see {@link FingerprintManager#hasEnrolledFingerprints()}).</li> + * <li>The use of the key must be authorized by the user by authenticating to this Android + * device using a subset of their secure lock screen credentials such as + * password/PIN/pattern or fingerprint. * <a href="{@docRoot}training/articles/keystore.html#UserAuthentication">More * information</a>. + * <li>The key will become <em>irreversibly invalidated</em> once the secure lock screen is + * disabled (reconfigured to None, Swipe or other mode which does not authenticate the user) + * or when the secure lock screen is forcibly reset (e.g., by a Device Administrator). + * Additionally, if the key requires that user authentication takes place for every use of + * the key, it is also irreversibly invalidated once a new fingerprint is enrolled or once\ + * no more fingerprints are enrolled. Attempts to initialize cryptographic operations using + * such keys will throw {@link KeyPermanentlyInvalidatedException}.</li> + * </ul> * - * <p>This restriction applies only to private key operations. Public key operations are not - * restricted. + * <p>This authorization applies only to secret key and private key operations. Public key + * operations are not restricted. * * @see #setUserAuthenticationValidityDurationSeconds(int) + * @see KeyguardManager#isDeviceSecure() + * @see FingerprintManager#hasEnrolledFingerprints() */ @NonNull public Builder setUserAuthenticationRequired(boolean required) { @@ -755,27 +791,52 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { } /** - * Sets the duration of time (seconds) for which this key can be used after the user is - * successfully authenticated. This has effect only if user authentication is required. - * - * <p>By default, the user needs to authenticate for every use of the key. - * - * @param seconds duration in seconds or {@code -1} if the user needs to authenticate for - * every use of the key. + * Sets the duration of time (seconds) for which this key is authorized to be used after the + * user is successfully authenticated. This has effect if the key requires user + * authentication for its use (see {@link #setUserAuthenticationRequired(boolean)}). + * + * <p>By default, if user authentication is required, it must take place for every use of + * the key. + * + * <p>Cryptographic operations involving keys which require user authentication to take + * place for every operation can only use fingerprint authentication. This is achieved by + * initializing a cryptographic operation ({@link Signature}, {@link Cipher}, {@link Mac}) + * with the key, wrapping it into a {@link FingerprintManager.CryptoObject}, invoking + * {@code FingerprintManager.authenticate} with {@code CryptoObject}, and proceeding with + * the cryptographic operation only if the authentication flow succeeds. + * + * <p>Cryptographic operations involving keys which are authorized to be used for a duration + * of time after a successful user authentication event can only use secure lock screen + * authentication. These cryptographic operations will throw + * {@link UserNotAuthenticatedException} during initialization if the user needs to be + * authenticated to proceed. This situation can be resolved by the user unlocking the secure + * lock screen of the Android or by going through the confirm credential flow initiated by + * {@link KeyguardManager#createConfirmDeviceCredentialIntent(CharSequence, CharSequence)}. + * Once resolved, initializing a new cryptographic operation using this key (or any other + * key which is authorized to be used for a fixed duration of time after user + * authentication) should succeed provided the user authentication flow completed + * successfully. + * + * @param seconds duration in seconds or {@code -1} if user authentication must take place + * for every use of the key. * * @see #setUserAuthenticationRequired(boolean) + * @see FingerprintManager + * @see FingerprintManager.CryptoObject + * @see KeyguardManager */ @NonNull public Builder setUserAuthenticationValidityDurationSeconds( @IntRange(from = -1) int seconds) { + if (seconds < -1) { + throw new IllegalArgumentException("seconds must be -1 or larger"); + } mUserAuthenticationValidityDurationSeconds = seconds; return this; } /** * Builds an instance of {@code KeyGenParameterSpec}. - * - * @throws IllegalArgumentException if a required field is missing */ @NonNull public KeyGenParameterSpec build() { diff --git a/keystore/java/android/security/keystore/KeyInfo.java b/keystore/java/android/security/keystore/KeyInfo.java index e4f921e..785ec15 100644 --- a/keystore/java/android/security/keystore/KeyInfo.java +++ b/keystore/java/android/security/keystore/KeyInfo.java @@ -30,9 +30,11 @@ import javax.crypto.SecretKey; * Keystore system</a>. This class describes whether the key material is available in * plaintext outside of secure hardware, whether user authentication is required for using the key * and whether this requirement is enforced by secure hardware, the key's origin, what uses the key - * is authorized for (e.g., only in {@code CBC} mode, or signing only), whether the key should be + * is authorized for (e.g., only in {@code GCM} mode, or signing only), whether the key should be * encrypted at rest, the key's and validity start and end dates. * + * <p>Instances of this class are immutable. + * * <p><h3>Example: Symmetric Key</h3> * The following example illustrates how to obtain a {@code KeyInfo} describing the provided Android * Keystore {@link SecretKey}. @@ -102,9 +104,9 @@ public class KeyInfo implements KeySpec { mInsideSecureHardware = insideSecureHardware; mOrigin = origin; mKeySize = keySize; - mKeyValidityStart = keyValidityStart; - mKeyValidityForOriginationEnd = keyValidityForOriginationEnd; - mKeyValidityForConsumptionEnd = keyValidityForConsumptionEnd; + mKeyValidityStart = Utils.cloneIfNotNull(keyValidityStart); + mKeyValidityForOriginationEnd = Utils.cloneIfNotNull(keyValidityForOriginationEnd); + mKeyValidityForConsumptionEnd = Utils.cloneIfNotNull(keyValidityForConsumptionEnd); mPurposes = purposes; mEncryptionPaddings = ArrayUtils.cloneIfNotEmpty(ArrayUtils.nullToEmpty(encryptionPaddings)); @@ -155,7 +157,7 @@ public class KeyInfo implements KeySpec { */ @Nullable public Date getKeyValidityStart() { - return mKeyValidityStart; + return Utils.cloneIfNotNull(mKeyValidityStart); } /** @@ -165,7 +167,7 @@ public class KeyInfo implements KeySpec { */ @Nullable public Date getKeyValidityForConsumptionEnd() { - return mKeyValidityForConsumptionEnd; + return Utils.cloneIfNotNull(mKeyValidityForConsumptionEnd); } /** @@ -175,7 +177,7 @@ public class KeyInfo implements KeySpec { */ @Nullable public Date getKeyValidityForOriginationEnd() { - return mKeyValidityForOriginationEnd; + return Utils.cloneIfNotNull(mKeyValidityForOriginationEnd); } /** @@ -189,7 +191,7 @@ public class KeyInfo implements KeySpec { } /** - * Gets the set of block modes (e.g., {@code CBC}, {@code CTR}) with which the key can be used + * Gets the set of block modes (e.g., {@code GCM}, {@code CBC}) with which the key can be used * when encrypting/decrypting. Attempts to use the key with any other block modes will be * rejected. * @@ -236,17 +238,27 @@ public class KeyInfo implements KeySpec { } /** - * Returns {@code true} if user authentication is required for this key to be used. + * Returns {@code true} if the key is authorized to be used only if the user has been + * authenticated. + * + * <p>This authorization applies only to secret key and private key operations. Public key + * operations are not restricted. * * @see #getUserAuthenticationValidityDurationSeconds() + * @see KeyGenParameterSpec.Builder#setUserAuthenticationRequired(boolean) + * @see KeyProtection.Builder#setUserAuthenticationRequired(boolean) */ public boolean isUserAuthenticationRequired() { return mUserAuthenticationRequired; } /** - * Gets the duration of time (seconds) for which this key can be used after the user is - * successfully authenticated. This has effect only if user authentication is required. + * Gets the duration of time (seconds) for which this key is authorized to be used after the + * user is successfully authenticated. This has effect only if user authentication is required + * (see {@link #isUserAuthenticationRequired()}). + * + * <p>This authorization applies only to secret key and private key operations. Public key + * operations are not restricted. * * @return duration in seconds or {@code -1} if authentication is required for every use of the * key. diff --git a/keystore/java/android/security/keystore/KeyPermanentlyInvalidatedException.java b/keystore/java/android/security/keystore/KeyPermanentlyInvalidatedException.java index e320c9c..9e82fc0 100644 --- a/keystore/java/android/security/keystore/KeyPermanentlyInvalidatedException.java +++ b/keystore/java/android/security/keystore/KeyPermanentlyInvalidatedException.java @@ -21,12 +21,13 @@ import java.security.InvalidKeyException; /** * Indicates that the key can no longer be used because it has been permanently invalidated. * - * <p>This can currently occur only for keys that require user authentication. Such keys are - * permanently invalidated once the secure lock screen is disabled (i.e., reconfigured to None, - * Swipe or other mode which does not authenticate the user) or when the secure lock screen is - * forcibly reset (e.g., by Device Admin). Additionally, keys configured to require user - * authentication for every use of the key are also permanently invalidated once a new fingerprint - * is enrolled or once no more fingerprints are enrolled. + * <p>This only occurs for keys which are authorized to be used only if the user has been + * authenticated. Such keys are permanently and irreversibly invalidated once the secure lock screen + * is disabled (i.e., reconfigured to None, Swipe or other mode which does not authenticate the + * user) or when the secure lock screen is forcibly reset (e.g., by Device Admin). Additionally, + * keys configured to require user authentication to take place for every of the keys, are also + * permanently invalidated once a new fingerprint is enrolled or once no more fingerprints are + * enrolled. */ public class KeyPermanentlyInvalidatedException extends InvalidKeyException { diff --git a/keystore/java/android/security/keystore/KeyProtection.java b/keystore/java/android/security/keystore/KeyProtection.java index 432fc12..b7a2a0b 100644 --- a/keystore/java/android/security/keystore/KeyProtection.java +++ b/keystore/java/android/security/keystore/KeyProtection.java @@ -19,19 +19,23 @@ package android.security.keystore; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.KeyguardManager; +import android.hardware.fingerprint.FingerprintManager; import java.security.Key; +import java.security.Signature; import java.security.KeyStore.ProtectionParameter; import java.security.cert.Certificate; import java.util.Date; import javax.crypto.Cipher; +import javax.crypto.Mac; /** * Specification of how a key or key pair is secured when imported into the * <a href="{@docRoot}training/articles/keystore.html">Android KeyStore facility</a>. This class * specifies parameters such as whether user authentication is required for using the key, what uses - * the key is authorized for (e.g., only in {@code CTR} mode, or only for signing -- decryption not + * the key is authorized for (e.g., only in {@code GCM} mode, or only for signing -- decryption not * permitted), the key's and validity start and end dates. * * <p>To import a key or key pair into the Android KeyStore, create an instance of this class using @@ -47,10 +51,12 @@ import javax.crypto.Cipher; * * <p>NOTE: The key material of keys stored in the Android KeyStore is not accessible. * + * <p>Instances of this class are immutable. + * * <p><h3>Example: Symmetric Key</h3> * The following example illustrates how to import an AES key into the Android KeyStore under alias - * {@code key1} authorized to be used only for encryption/decryption in CBC mode with PKCS#7 - * padding. The key must export its key material via {@link Key#getEncoded()} in {@code RAW} format. + * {@code key1} authorized to be used only for encryption/decryption in GCM mode with no padding. + * The key must export its key material via {@link Key#getEncoded()} in {@code RAW} format. * <pre> {@code * SecretKey key = ...; // AES key * @@ -60,8 +66,8 @@ import javax.crypto.Cipher; * "key1", * new KeyStore.SecretKeyEntry(key), * new KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) - * .setBlockMode(KeyProperties.BLOCK_MODE_CBC) - * .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) + * .setBlockMode(KeyProperties.BLOCK_MODE_GCM) + * .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) * .build()); * // Key imported, obtain a reference to it. * SecretKey keyStoreKey = (SecretKey) keyStore.getKey("key1", null); @@ -122,15 +128,9 @@ public final class KeyProtection implements ProtectionParameter { boolean randomizedEncryptionRequired, boolean userAuthenticationRequired, int userAuthenticationValidityDurationSeconds) { - if ((userAuthenticationValidityDurationSeconds < 0) - && (userAuthenticationValidityDurationSeconds != -1)) { - throw new IllegalArgumentException( - "userAuthenticationValidityDurationSeconds must not be negative"); - } - - mKeyValidityStart = keyValidityStart; - mKeyValidityForOriginationEnd = keyValidityForOriginationEnd; - mKeyValidityForConsumptionEnd = keyValidityForConsumptionEnd; + mKeyValidityStart = Utils.cloneIfNotNull(keyValidityStart); + mKeyValidityForOriginationEnd = Utils.cloneIfNotNull(keyValidityForOriginationEnd); + mKeyValidityForConsumptionEnd = Utils.cloneIfNotNull(keyValidityForConsumptionEnd); mPurposes = purposes; mEncryptionPaddings = ArrayUtils.cloneIfNotEmpty(ArrayUtils.nullToEmpty(encryptionPaddings)); @@ -150,7 +150,7 @@ public final class KeyProtection implements ProtectionParameter { */ @Nullable public Date getKeyValidityStart() { - return mKeyValidityStart; + return Utils.cloneIfNotNull(mKeyValidityStart); } /** @@ -160,7 +160,7 @@ public final class KeyProtection implements ProtectionParameter { */ @Nullable public Date getKeyValidityForConsumptionEnd() { - return mKeyValidityForConsumptionEnd; + return Utils.cloneIfNotNull(mKeyValidityForConsumptionEnd); } /** @@ -170,7 +170,7 @@ public final class KeyProtection implements ProtectionParameter { */ @Nullable public Date getKeyValidityForOriginationEnd() { - return mKeyValidityForOriginationEnd; + return Utils.cloneIfNotNull(mKeyValidityForOriginationEnd); } /** @@ -236,7 +236,7 @@ public final class KeyProtection implements ProtectionParameter { } /** - * Gets the set of block modes (e.g., {@code CBC}, {@code CTR}) with which the key can be used + * Gets the set of block modes (e.g., {@code GCM}, {@code CBC}) with which the key can be used * when encrypting/decrypting. Attempts to use the key with any other block modes will be * rejected. * @@ -261,22 +261,32 @@ public final class KeyProtection implements ProtectionParameter { } /** - * Returns {@code true} if user authentication is required for this key to be used. + * Returns {@code true} if the key is authorized to be used only if the user has been + * authenticated. + * + * <p>This authorization applies only to secret key and private key operations. Public key + * operations are not restricted. * * @see #getUserAuthenticationValidityDurationSeconds() + * @see Builder#setUserAuthenticationRequired(boolean) */ public boolean isUserAuthenticationRequired() { return mUserAuthenticationRequired; } /** - * Gets the duration of time (seconds) for which this key can be used after the user is - * successfully authenticated. This has effect only if user authentication is required. + * Gets the duration of time (seconds) for which this key is authorized to be used after the + * user is successfully authenticated. This has effect only if user authentication is required + * (see {@link #isUserAuthenticationRequired()}). + * + * <p>This authorization applies only to secret key and private key operations. Public key + * operations are not restricted. * * @return duration in seconds or {@code -1} if authentication is required for every use of the * key. * * @see #isUserAuthenticationRequired() + * @see Builder#setUserAuthenticationValidityDurationSeconds(int) */ public int getUserAuthenticationValidityDurationSeconds() { return mUserAuthenticationValidityDurationSeconds; @@ -320,7 +330,7 @@ public final class KeyProtection implements ProtectionParameter { */ @NonNull public Builder setKeyValidityStart(Date startDate) { - mKeyValidityStart = startDate; + mKeyValidityStart = Utils.cloneIfNotNull(startDate); return this; } @@ -349,7 +359,7 @@ public final class KeyProtection implements ProtectionParameter { */ @NonNull public Builder setKeyValidityForOriginationEnd(Date endDate) { - mKeyValidityForOriginationEnd = endDate; + mKeyValidityForOriginationEnd = Utils.cloneIfNotNull(endDate); return this; } @@ -363,7 +373,7 @@ public final class KeyProtection implements ProtectionParameter { */ @NonNull public Builder setKeyValidityForConsumptionEnd(Date endDate) { - mKeyValidityForConsumptionEnd = endDate; + mKeyValidityForConsumptionEnd = Utils.cloneIfNotNull(endDate); return this; } @@ -428,11 +438,11 @@ public final class KeyProtection implements ProtectionParameter { } /** - * Sets the set of block modes (e.g., {@code CBC}, {@code CTR}, {@code ECB}) with which the - * key can be used when encrypting/decrypting. Attempts to use the key with any other block - * modes will be rejected. + * Sets the set of block modes (e.g., {@code GCM}, {@code CBC}) with which the key can be + * used when encrypting/decrypting. Attempts to use the key with any other block modes will + * be rejected. * - * <p>This must be specified for encryption/decryption keys. + * <p>This must be specified for symmetric encryption/decryption keys. * * <p>See {@link KeyProperties}.{@code BLOCK_MODE} constants. */ @@ -457,8 +467,8 @@ public final class KeyProtection implements ProtectionParameter { * <ul> * <li>transformation which do not offer {@code IND-CPA}, such as symmetric ciphers using * {@code ECB} mode or RSA encryption without padding, are prohibited;</li> - * <li>in transformations which use an IV, such as symmetric ciphers in {@code CBC}, - * {@code CTR}, and {@code GCM} block modes, caller-provided IVs are rejected when + * <li>in transformations which use an IV, such as symmetric ciphers in {@code GCM}, + * {@code CBC}, and {@code CTR} block modes, caller-provided IVs are rejected when * encrypting, to ensure that only random IVs are used.</li> * * <p>Before disabling this requirement, consider the following approaches instead: @@ -483,19 +493,38 @@ public final class KeyProtection implements ProtectionParameter { } /** - * Sets whether user authentication is required to use this key. + * Sets whether this key is authorized to be used only if the user has been authenticated. * - * <p>By default, the key can be used without user authentication. + * <p>By default, the key is authorized to be used regardless of whether the user has been + * authenticated. * - * <p>When user authentication is required, the user authorizes the use of the key by - * authenticating to this Android device using a subset of their secure lock screen - * credentials. Different authentication methods are used depending on whether the every - * use of the key must be authenticated (as specified by - * {@link #setUserAuthenticationValidityDurationSeconds(int)}). + * <p>When user authentication is required: + * <ul> + * <li>The key can only be import if secure lock screen is set up (see + * {@link KeyguardManager#isDeviceSecure()}). Additionally, if the key requires that user + * authentication takes place for every use of the key (see + * {@link #setUserAuthenticationValidityDurationSeconds(int)}), at least one fingerprint + * must be enrolled (see {@link FingerprintManager#hasEnrolledFingerprints()}).</li> + * <li>The use of the key must be authorized by the user by authenticating to this Android + * device using a subset of their secure lock screen credentials such as + * password/PIN/pattern or fingerprint. * <a href="{@docRoot}training/articles/keystore.html#UserAuthentication">More * information</a>. + * <li>The key will become <em>irreversibly invalidated</em> once the secure lock screen is + * disabled (reconfigured to None, Swipe or other mode which does not authenticate the user) + * or when the secure lock screen is forcibly reset (e.g., by a Device Administrator). + * Additionally, if the key requires that user authentication takes place for every use of + * the key, it is also irreversibly invalidated once a new fingerprint is enrolled or once\ + * no more fingerprints are enrolled. Attempts to initialize cryptographic operations using + * such keys will throw {@link KeyPermanentlyInvalidatedException}.</li> + * </ul> + * + * <p>This authorization applies only to secret key and private key operations. Public key + * operations are not restricted. * * @see #setUserAuthenticationValidityDurationSeconds(int) + * @see KeyguardManager#isDeviceSecure() + * @see FingerprintManager#hasEnrolledFingerprints() */ @NonNull public Builder setUserAuthenticationRequired(boolean required) { @@ -504,19 +533,46 @@ public final class KeyProtection implements ProtectionParameter { } /** - * Sets the duration of time (seconds) for which this key can be used after the user is - * successfully authenticated. This has effect only if user authentication is required. + * Sets the duration of time (seconds) for which this key is authorized to be used after the + * user is successfully authenticated. This has effect if the key requires user + * authentication for its use (see {@link #setUserAuthenticationRequired(boolean)}). + * + * <p>By default, if user authentication is required, it must take place for every use of + * the key. + * + * <p>Cryptographic operations involving keys which require user authentication to take + * place for every operation can only use fingerprint authentication. This is achieved by + * initializing a cryptographic operation ({@link Signature}, {@link Cipher}, {@link Mac}) + * with the key, wrapping it into a {@link FingerprintManager.CryptoObject}, invoking + * {@code FingerprintManager.authenticate} with {@code CryptoObject}, and proceeding with + * the cryptographic operation only if the authentication flow succeeds. * - * <p>By default, the user needs to authenticate for every use of the key. + * <p>Cryptographic operations involving keys which are authorized to be used for a duration + * of time after a successful user authentication event can only use secure lock screen + * authentication. These cryptographic operations will throw + * {@link UserNotAuthenticatedException} during initialization if the user needs to be + * authenticated to proceed. This situation can be resolved by the user unlocking the secure + * lock screen of the Android or by going through the confirm credential flow initiated by + * {@link KeyguardManager#createConfirmDeviceCredentialIntent(CharSequence, CharSequence)}. + * Once resolved, initializing a new cryptographic operation using this key (or any other + * key which is authorized to be used for a fixed duration of time after user + * authentication) should succeed provided the user authentication flow completed + * successfully. * - * @param seconds duration in seconds or {@code -1} if the user needs to authenticate for - * every use of the key. + * @param seconds duration in seconds or {@code -1} if user authentication must take place + * for every use of the key. * * @see #setUserAuthenticationRequired(boolean) + * @see FingerprintManager + * @see FingerprintManager.CryptoObject + * @see KeyguardManager */ @NonNull public Builder setUserAuthenticationValidityDurationSeconds( @IntRange(from = -1) int seconds) { + if (seconds < -1) { + throw new IllegalArgumentException("seconds must be -1 or larger"); + } mUserAuthenticationValidityDurationSeconds = seconds; return this; } diff --git a/keystore/java/android/security/keystore/KeyStoreCryptoOperationChunkedStreamer.java b/keystore/java/android/security/keystore/KeyStoreCryptoOperationChunkedStreamer.java index 9957e79..894d52a 100644 --- a/keystore/java/android/security/keystore/KeyStoreCryptoOperationChunkedStreamer.java +++ b/keystore/java/android/security/keystore/KeyStoreCryptoOperationChunkedStreamer.java @@ -73,6 +73,8 @@ class KeyStoreCryptoOperationChunkedStreamer implements KeyStoreCryptoOperationS private byte[] mBuffered = EmptyArray.BYTE; private int mBufferedOffset; private int mBufferedLength; + private long mConsumedInputSizeBytes; + private long mProducedOutputSizeBytes; public KeyStoreCryptoOperationChunkedStreamer(Stream operation) { this(operation, DEFAULT_MAX_CHUNK_SIZE); @@ -119,6 +121,7 @@ class KeyStoreCryptoOperationChunkedStreamer implements KeyStoreCryptoOperationS // Update input array references to reflect that some of its bytes are now in mBuffered. inputOffset += inputBytesInChunk; inputLength -= inputBytesInChunk; + mConsumedInputSizeBytes += inputBytesInChunk; OperationResult opResult = mKeyStoreStream.update(chunk); if (opResult == null) { @@ -167,9 +170,10 @@ class KeyStoreCryptoOperationChunkedStreamer implements KeyStoreCryptoOperationS } } else { // No more output will be produced in this loop + byte[] result; if (bufferedOutput == null) { // No previously buffered output - return opResult.output; + result = opResult.output; } else { // There was some previously buffered output try { @@ -177,18 +181,23 @@ class KeyStoreCryptoOperationChunkedStreamer implements KeyStoreCryptoOperationS } catch (IOException e) { throw new IllegalStateException("Failed to buffer output", e); } - return bufferedOutput.toByteArray(); + result = bufferedOutput.toByteArray(); } + mProducedOutputSizeBytes += result.length; + return result; } } } + byte[] result; if (bufferedOutput == null) { // No output produced - return EmptyArray.BYTE; + result = EmptyArray.BYTE; } else { - return bufferedOutput.toByteArray(); + result = bufferedOutput.toByteArray(); } + mProducedOutputSizeBytes += result.length; + return result; } @Override @@ -210,14 +219,11 @@ class KeyStoreCryptoOperationChunkedStreamer implements KeyStoreCryptoOperationS } else if (opResult.resultCode != KeyStore.NO_ERROR) { throw KeyStore.getKeyStoreException(opResult.resultCode); } + mProducedOutputSizeBytes += opResult.output.length; return ArrayUtils.concat(output, opResult.output); } - /** - * Passes all of buffered input into the the KeyStore operation (via the {@code update} - * operation) and returns output. - */ public byte[] flush() throws KeyStoreException { if (mBufferedLength <= 0) { return EmptyArray.BYTE; @@ -243,7 +249,19 @@ class KeyStoreCryptoOperationChunkedStreamer implements KeyStoreCryptoOperationS + " . Provided: " + chunk.length + ", consumed: " + opResult.inputConsumed); } - return (opResult.output != null) ? opResult.output : EmptyArray.BYTE; + byte[] result = (opResult.output != null) ? opResult.output : EmptyArray.BYTE; + mProducedOutputSizeBytes += result.length; + return result; + } + + @Override + public long getConsumedInputSizeBytes() { + return mConsumedInputSizeBytes; + } + + @Override + public long getProducedOutputSizeBytes() { + return mProducedOutputSizeBytes; } /** diff --git a/keystore/java/android/security/keystore/KeyStoreCryptoOperationStreamer.java b/keystore/java/android/security/keystore/KeyStoreCryptoOperationStreamer.java index 1c6de2d..897bd71 100644 --- a/keystore/java/android/security/keystore/KeyStoreCryptoOperationStreamer.java +++ b/keystore/java/android/security/keystore/KeyStoreCryptoOperationStreamer.java @@ -37,4 +37,6 @@ interface KeyStoreCryptoOperationStreamer { byte[] update(byte[] input, int inputOffset, int inputLength) throws KeyStoreException; byte[] doFinal(byte[] input, int inputOffset, int inputLength, byte[] additionalEntropy) throws KeyStoreException; + long getConsumedInputSizeBytes(); + long getProducedOutputSizeBytes(); } diff --git a/keystore/java/android/security/keystore/Utils.java b/keystore/java/android/security/keystore/Utils.java new file mode 100644 index 0000000..9bec682 --- /dev/null +++ b/keystore/java/android/security/keystore/Utils.java @@ -0,0 +1,32 @@ +/* + * 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.keystore; + +import java.util.Date; + +/** + * Assorted utility methods. + * + * @hide + */ +abstract class Utils { + private Utils() {} + + static Date cloneIfNotNull(Date value) { + return (value != null) ? (Date) value.clone() : null; + } +} |