summaryrefslogtreecommitdiffstats
path: root/keystore
diff options
context:
space:
mode:
Diffstat (limited to 'keystore')
-rw-r--r--keystore/java/android/security/keystore/AndroidKeyStoreAuthenticatedAESCipherSpi.java442
-rw-r--r--keystore/java/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java3
-rw-r--r--keystore/java/android/security/keystore/AndroidKeyStoreCipherSpiBase.java116
-rw-r--r--keystore/java/android/security/keystore/AndroidKeyStoreKeyPairGeneratorSpi.java19
-rw-r--r--keystore/java/android/security/keystore/AndroidKeyStoreRSACipherSpi.java13
-rw-r--r--keystore/java/android/security/keystore/AndroidKeyStoreUnauthenticatedAESCipherSpi.java16
-rw-r--r--keystore/java/android/security/keystore/KeyGenParameterSpec.java183
-rw-r--r--keystore/java/android/security/keystore/KeyInfo.java30
-rw-r--r--keystore/java/android/security/keystore/KeyPermanentlyInvalidatedException.java13
-rw-r--r--keystore/java/android/security/keystore/KeyProtection.java116
-rw-r--r--keystore/java/android/security/keystore/KeyStoreCryptoOperationChunkedStreamer.java36
-rw-r--r--keystore/java/android/security/keystore/KeyStoreCryptoOperationStreamer.java2
-rw-r--r--keystore/java/android/security/keystore/Utils.java32
13 files changed, 894 insertions, 127 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..af05578 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;
@@ -598,9 +597,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/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..5775a87 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,16 +112,17 @@ 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 CBC mode with PKCS#7
+ * 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)
- * .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
+ * .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
+ * .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
* .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);
}
/**
@@ -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;
}
@@ -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..91a9a70 100644
--- a/keystore/java/android/security/keystore/KeyInfo.java
+++ b/keystore/java/android/security/keystore/KeyInfo.java
@@ -33,6 +33,8 @@ import javax.crypto.SecretKey;
* is authorized for (e.g., only in {@code CBC} 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);
}
/**
@@ -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..cd46afa 100644
--- a/keystore/java/android/security/keystore/KeyProtection.java
+++ b/keystore/java/android/security/keystore/KeyProtection.java
@@ -19,13 +19,17 @@ 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
@@ -47,6 +51,8 @@ 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
@@ -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);
}
/**
@@ -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;
}
@@ -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;
+ }
+}