summaryrefslogtreecommitdiffstats
path: root/keystore
diff options
context:
space:
mode:
authorAlex Klyubin <klyubin@google.com>2015-03-30 21:01:39 +0000
committerAndroid Git Automerger <android-git-automerger@android.com>2015-03-30 21:01:39 +0000
commit8a78286915a8f71eb09b5ae29a3bd8fb977180e6 (patch)
tree07dcf960329d6680697a96aa9094ab0174387687 /keystore
parentdc0078b7947e0b035dce6c61108a48a276b11034 (diff)
parent7ca65f09013e807b6df61b2ba3e650a09ceff432 (diff)
downloadframeworks_base-8a78286915a8f71eb09b5ae29a3bd8fb977180e6.zip
frameworks_base-8a78286915a8f71eb09b5ae29a3bd8fb977180e6.tar.gz
frameworks_base-8a78286915a8f71eb09b5ae29a3bd8fb977180e6.tar.bz2
am 7ca65f09: am b000d129: am 6a6f0c7d: Merge "Add HmacSHA256 backed by AndroidKeyStore."
* commit '7ca65f09013e807b6df61b2ba3e650a09ceff432': Add HmacSHA256 backed by AndroidKeyStore.
Diffstat (limited to 'keystore')
-rw-r--r--keystore/java/android/security/AndroidKeyStore.java13
-rw-r--r--keystore/java/android/security/AndroidKeyStoreProvider.java4
-rw-r--r--keystore/java/android/security/KeyStoreConnectException.java12
-rw-r--r--keystore/java/android/security/KeyStoreCryptoOperationChunkedStreamer.java228
-rw-r--r--keystore/java/android/security/KeyStoreHmacSpi.java174
-rw-r--r--keystore/java/android/security/KeyStoreKeyConstraints.java14
-rw-r--r--keystore/java/android/security/KeyStoreKeyGeneratorSpi.java16
-rw-r--r--keystore/java/android/security/KeymasterException.java9
-rw-r--r--keystore/java/android/security/KeymasterUtils.java6
9 files changed, 472 insertions, 4 deletions
diff --git a/keystore/java/android/security/AndroidKeyStore.java b/keystore/java/android/security/AndroidKeyStore.java
index f3eb317..1d16ca1 100644
--- a/keystore/java/android/security/AndroidKeyStore.java
+++ b/keystore/java/android/security/AndroidKeyStore.java
@@ -494,6 +494,19 @@ public class AndroidKeyStore extends KeyStoreSpi {
args.addInt(KeymasterDefs.KM_TAG_DIGEST,
KeyStoreKeyConstraints.Digest.toKeymaster(digest));
}
+ if (keyAlgorithm == KeyStoreKeyConstraints.Algorithm.HMAC) {
+ if (digest == null) {
+ throw new IllegalStateException("Digest algorithm must be specified for key"
+ + " algorithm " + keyAlgorithmString);
+ }
+ Integer digestOutputSizeBytes =
+ KeyStoreKeyConstraints.Digest.getOutputSizeBytes(digest);
+ if (digestOutputSizeBytes != null) {
+ // TODO: Remove MAC length constraint once Keymaster API no longer requires it.
+ // TODO: Switch to bits instead of bytes, once this is fixed in Keymaster
+ args.addInt(KeymasterDefs.KM_TAG_MAC_LENGTH, digestOutputSizeBytes);
+ }
+ }
@KeyStoreKeyConstraints.PurposeEnum int purposes = (params.getPurposes() != null)
? params.getPurposes()
diff --git a/keystore/java/android/security/AndroidKeyStoreProvider.java b/keystore/java/android/security/AndroidKeyStoreProvider.java
index 598bcd8..7313c48 100644
--- a/keystore/java/android/security/AndroidKeyStoreProvider.java
+++ b/keystore/java/android/security/AndroidKeyStoreProvider.java
@@ -39,5 +39,9 @@ public class AndroidKeyStoreProvider extends Provider {
// javax.crypto.KeyGenerator
put("KeyGenerator.AES", KeyStoreKeyGeneratorSpi.AES.class.getName());
put("KeyGenerator.HmacSHA256", KeyStoreKeyGeneratorSpi.HmacSHA256.class.getName());
+
+ // javax.crypto.Mac
+ put("Mac.HmacSHA256", KeyStoreHmacSpi.HmacSHA256.class.getName());
+ put("Mac.HmacSHA256 SupportedKeyClasses", KeyStoreSecretKey.class.getName());
}
}
diff --git a/keystore/java/android/security/KeyStoreConnectException.java b/keystore/java/android/security/KeyStoreConnectException.java
new file mode 100644
index 0000000..4c465a4
--- /dev/null
+++ b/keystore/java/android/security/KeyStoreConnectException.java
@@ -0,0 +1,12 @@
+package android.security;
+
+/**
+ * Indicates a communications error with keystore service.
+ *
+ * @hide
+ */
+public class KeyStoreConnectException extends CryptoOperationException {
+ public KeyStoreConnectException() {
+ super("Failed to communicate with keystore service");
+ }
+}
diff --git a/keystore/java/android/security/KeyStoreCryptoOperationChunkedStreamer.java b/keystore/java/android/security/KeyStoreCryptoOperationChunkedStreamer.java
new file mode 100644
index 0000000..a37ddce
--- /dev/null
+++ b/keystore/java/android/security/KeyStoreCryptoOperationChunkedStreamer.java
@@ -0,0 +1,228 @@
+package android.security;
+
+import android.security.keymaster.OperationResult;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Helper for streaming a crypto operation's input and output via {@link KeyStore} service's
+ * {@code update} and {@code finish} operations.
+ *
+ * <p>The helper abstracts away to issues that need to be solved in most code that uses KeyStore's
+ * update and finish operations. Firstly, KeyStore's update and finish operations can consume only a
+ * limited amount of data in one go because the operations are marshalled via Binder. Secondly, the
+ * update operation may consume less data than provided, in which case the caller has to buffer
+ * the remainder for next time. The helper exposes {@link #update(byte[], int, int) update} and
+ * {@link #doFinal(byte[], int, int) doFinal} operations which can be used to conveniently implement
+ * various JCA crypto primitives.
+ *
+ * <p>KeyStore operation through which data is streamed is abstracted away as
+ * {@link KeyStoreOperation} to avoid having this class deal with operation tokens and occasional
+ * additional parameters to update and final operations.
+ *
+ * @hide
+ */
+public class KeyStoreCryptoOperationChunkedStreamer {
+ public interface KeyStoreOperation {
+ /**
+ * Returns the result of the KeyStore update operation or null if keystore couldn't be
+ * reached.
+ */
+ OperationResult update(byte[] input);
+
+ /**
+ * Returns the result of the KeyStore finish operation or null if keystore couldn't be
+ * reached.
+ */
+ OperationResult finish(byte[] input);
+ }
+
+ // Binder buffer is about 1MB, but it's shared between all active transactions of the process.
+ // Thus, it's safer to use a much smaller upper bound.
+ private static final int DEFAULT_MAX_CHUNK_SIZE = 64 * 1024;
+ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+ private final KeyStoreOperation mKeyStoreOperation;
+ private final int mMaxChunkSize;
+
+ private byte[] mBuffered = EMPTY_BYTE_ARRAY;
+ private int mBufferedOffset;
+ private int mBufferedLength;
+
+ public KeyStoreCryptoOperationChunkedStreamer(KeyStoreOperation operation) {
+ this(operation, DEFAULT_MAX_CHUNK_SIZE);
+ }
+
+ public KeyStoreCryptoOperationChunkedStreamer(KeyStoreOperation operation, int maxChunkSize) {
+ mKeyStoreOperation = operation;
+ mMaxChunkSize = maxChunkSize;
+ }
+
+ public byte[] update(byte[] input, int inputOffset, int inputLength) throws KeymasterException {
+ if (inputLength == 0) {
+ // No input provided
+ return EMPTY_BYTE_ARRAY;
+ }
+
+ ByteArrayOutputStream bufferedOutput = null;
+
+ while (inputLength > 0) {
+ byte[] chunk;
+ int inputBytesInChunk;
+ if ((mBufferedLength + inputLength) > mMaxChunkSize) {
+ // Too much input for one chunk -- extract one max-sized chunk and feed it into the
+ // update operation.
+ chunk = new byte[mMaxChunkSize];
+ System.arraycopy(mBuffered, mBufferedOffset, chunk, 0, mBufferedLength);
+ inputBytesInChunk = chunk.length - mBufferedLength;
+ System.arraycopy(input, inputOffset, chunk, mBufferedLength, inputBytesInChunk);
+ } else {
+ // All of available input fits into one chunk.
+ if ((mBufferedLength == 0) && (inputOffset == 0)
+ && (inputLength == input.length)) {
+ // Nothing buffered and all of input array needs to be fed into the update
+ // operation.
+ chunk = input;
+ inputBytesInChunk = input.length;
+ } else {
+ // Need to combine buffered data with input data into one array.
+ chunk = new byte[mBufferedLength + inputLength];
+ inputBytesInChunk = inputLength;
+ System.arraycopy(mBuffered, mBufferedOffset, chunk, 0, mBufferedLength);
+ System.arraycopy(input, inputOffset, chunk, mBufferedLength, inputLength);
+ }
+ }
+ // Update input array references to reflect that some of its bytes are now in mBuffered.
+ inputOffset += inputBytesInChunk;
+ inputLength -= inputBytesInChunk;
+
+ OperationResult opResult = mKeyStoreOperation.update(chunk);
+ if (opResult == null) {
+ throw new KeyStoreConnectException();
+ } else if (opResult.resultCode != KeyStore.NO_ERROR) {
+ throw KeymasterUtils.getExceptionForKeymasterError(opResult.resultCode);
+ }
+
+ if (opResult.inputConsumed == chunk.length) {
+ // The whole chunk was consumed
+ mBuffered = EMPTY_BYTE_ARRAY;
+ mBufferedOffset = 0;
+ mBufferedLength = 0;
+ } else if (opResult.inputConsumed == 0) {
+ // Nothing was consumed. More input needed.
+ if (inputLength > 0) {
+ // More input is available, but it wasn't included into the previous chunk
+ // because the chunk reached its maximum permitted size.
+ // Shouldn't have happened.
+ throw new CryptoOperationException("Nothing consumed from max-sized chunk: "
+ + chunk.length + " bytes");
+ }
+ mBuffered = chunk;
+ mBufferedOffset = 0;
+ mBufferedLength = chunk.length;
+ } else if (opResult.inputConsumed < chunk.length) {
+ // The chunk was consumed only partially -- buffer the rest of the chunk
+ mBuffered = chunk;
+ mBufferedOffset = opResult.inputConsumed;
+ mBufferedLength = chunk.length - opResult.inputConsumed;
+ } else {
+ throw new CryptoOperationException("Consumed more than provided: "
+ + opResult.inputConsumed + ", provided: " + chunk.length);
+ }
+
+ if ((opResult.output != null) && (opResult.output.length > 0)) {
+ if (inputLength > 0) {
+ // More output might be produced in this loop -- buffer the current output
+ if (bufferedOutput == null) {
+ bufferedOutput = new ByteArrayOutputStream();
+ try {
+ bufferedOutput.write(opResult.output);
+ } catch (IOException e) {
+ throw new CryptoOperationException("Failed to buffer output", e);
+ }
+ }
+ } else {
+ // No more output will be produced in this loop
+ if (bufferedOutput == null) {
+ // No previously buffered output
+ return opResult.output;
+ } else {
+ // There was some previously buffered output
+ try {
+ bufferedOutput.write(opResult.output);
+ } catch (IOException e) {
+ throw new CryptoOperationException("Failed to buffer output", e);
+ }
+ return bufferedOutput.toByteArray();
+ }
+ }
+ }
+ }
+
+ if (bufferedOutput == null) {
+ // No output produced
+ return EMPTY_BYTE_ARRAY;
+ } else {
+ return bufferedOutput.toByteArray();
+ }
+ }
+
+ public byte[] doFinal(byte[] input, int inputOffset, int inputLength)
+ throws KeymasterException {
+ if (inputLength == 0) {
+ // No input provided -- simplify the rest of the code
+ input = EMPTY_BYTE_ARRAY;
+ inputOffset = 0;
+ }
+
+ byte[] updateOutput = null;
+ if ((mBufferedLength + inputLength) > mMaxChunkSize) {
+ updateOutput = update(input, inputOffset, inputLength);
+ inputOffset += inputLength;
+ inputLength = 0;
+ }
+ // All of available input fits into one chunk.
+
+ byte[] finalChunk;
+ if ((mBufferedLength == 0) && (inputOffset == 0)
+ && (inputLength == input.length)) {
+ // Nothing buffered and all of input array needs to be fed into the finish operation.
+ finalChunk = input;
+ } else {
+ // Need to combine buffered data with input data into one array.
+ finalChunk = new byte[mBufferedLength + inputLength];
+ System.arraycopy(mBuffered, mBufferedOffset, finalChunk, 0, mBufferedLength);
+ System.arraycopy(input, inputOffset, finalChunk, mBufferedLength, inputLength);
+ }
+ mBuffered = EMPTY_BYTE_ARRAY;
+ mBufferedLength = 0;
+ mBufferedOffset = 0;
+
+ OperationResult opResult = mKeyStoreOperation.finish(finalChunk);
+ if (opResult == null) {
+ throw new KeyStoreConnectException();
+ } else if (opResult.resultCode != KeyStore.NO_ERROR) {
+ throw KeymasterUtils.getExceptionForKeymasterError(opResult.resultCode);
+ }
+
+ if (opResult.inputConsumed != finalChunk.length) {
+ throw new CryptoOperationException("Unexpected number of bytes consumed by finish: "
+ + opResult.inputConsumed + " instead of " + finalChunk.length);
+ }
+
+ // Return the concatenation of the output of update and finish.
+ byte[] result;
+ byte[] finishOutput = opResult.output;
+ if ((updateOutput == null) || (updateOutput.length == 0)) {
+ result = finishOutput;
+ } else if ((finishOutput == null) || (finishOutput.length == 0)) {
+ result = updateOutput;
+ } else {
+ result = new byte[updateOutput.length + finishOutput.length];
+ System.arraycopy(updateOutput, 0, result, 0, updateOutput.length);
+ System.arraycopy(finishOutput, 0, result, updateOutput.length, finishOutput.length);
+ }
+ return (result != null) ? result : EMPTY_BYTE_ARRAY;
+ }
+}
diff --git a/keystore/java/android/security/KeyStoreHmacSpi.java b/keystore/java/android/security/KeyStoreHmacSpi.java
new file mode 100644
index 0000000..3080d7b
--- /dev/null
+++ b/keystore/java/android/security/KeyStoreHmacSpi.java
@@ -0,0 +1,174 @@
+package android.security;
+
+import android.os.IBinder;
+import android.security.keymaster.KeymasterArguments;
+import android.security.keymaster.KeymasterDefs;
+import android.security.keymaster.OperationResult;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.spec.AlgorithmParameterSpec;
+
+import javax.crypto.MacSpi;
+
+/**
+ * {@link MacSpi} which provides HMAC implementations backed by Android KeyStore.
+ *
+ * @hide
+ */
+public abstract class KeyStoreHmacSpi extends MacSpi {
+
+ public static class HmacSHA256 extends KeyStoreHmacSpi {
+ public HmacSHA256() {
+ super(KeyStoreKeyConstraints.Digest.SHA256, 256 / 8);
+ }
+ }
+
+ private final KeyStore mKeyStore = KeyStore.getInstance();
+ private final @KeyStoreKeyConstraints.DigestEnum int mDigest;
+ private final int mMacSizeBytes;
+
+ private String mKeyAliasInKeyStore;
+
+ // The fields below are reset by the engineReset operation.
+ private KeyStoreCryptoOperationChunkedStreamer mChunkedStreamer;
+ private IBinder mOperationToken;
+
+ protected KeyStoreHmacSpi(@KeyStoreKeyConstraints.DigestEnum int digest, int macSizeBytes) {
+ mDigest = digest;
+ mMacSizeBytes = macSizeBytes;
+ }
+
+ @Override
+ protected int engineGetMacLength() {
+ return mMacSizeBytes;
+ }
+
+ @Override
+ protected void engineInit(Key key, AlgorithmParameterSpec params) throws InvalidKeyException,
+ InvalidAlgorithmParameterException {
+ if (key == null) {
+ throw new InvalidKeyException("key == null");
+ } else if (!(key instanceof KeyStoreSecretKey)) {
+ throw new InvalidKeyException(
+ "Only Android KeyStore secret keys supported. Key: " + key);
+ }
+
+ if (params != null) {
+ throw new InvalidAlgorithmParameterException(
+ "Unsupported algorithm parameters: " + params);
+ }
+
+ mKeyAliasInKeyStore = ((KeyStoreSecretKey) key).getAlias();
+ engineReset();
+ }
+
+ @Override
+ protected void engineReset() {
+ IBinder operationToken = mOperationToken;
+ if (operationToken != null) {
+ mOperationToken = null;
+ mKeyStore.abort(operationToken);
+ }
+ mChunkedStreamer = null;
+
+ KeymasterArguments keymasterArgs = new KeymasterArguments();
+ keymasterArgs.addInt(KeymasterDefs.KM_TAG_DIGEST, mDigest);
+
+ OperationResult opResult = mKeyStore.begin(mKeyAliasInKeyStore,
+ KeymasterDefs.KM_PURPOSE_SIGN,
+ true,
+ keymasterArgs,
+ null,
+ new KeymasterArguments());
+ if (opResult == null) {
+ throw new KeyStoreConnectException();
+ } else if (opResult.resultCode != KeyStore.NO_ERROR) {
+ throw new CryptoOperationException("Failed to start keystore operation",
+ KeymasterUtils.getExceptionForKeymasterError(opResult.resultCode));
+ }
+ mOperationToken = opResult.token;
+ if (mOperationToken == null) {
+ throw new CryptoOperationException("Keystore returned null operation token");
+ }
+ mChunkedStreamer = new KeyStoreCryptoOperationChunkedStreamer(
+ new KeyStoreStreamingConsumer(mKeyStore, mOperationToken));
+ }
+
+ @Override
+ protected void engineUpdate(byte input) {
+ engineUpdate(new byte[] {input}, 0, 1);
+ }
+
+ @Override
+ protected void engineUpdate(byte[] input, int offset, int len) {
+ if (mChunkedStreamer == null) {
+ throw new IllegalStateException("Not initialized");
+ }
+
+ byte[] output;
+ try {
+ output = mChunkedStreamer.update(input, offset, len);
+ } catch (KeymasterException e) {
+ throw new CryptoOperationException("Keystore operation failed", e);
+ }
+ if ((output != null) && (output.length != 0)) {
+ throw new CryptoOperationException("Update operation unexpectedly produced output");
+ }
+ }
+
+ @Override
+ protected byte[] engineDoFinal() {
+ if (mChunkedStreamer == null) {
+ throw new IllegalStateException("Not initialized");
+ }
+
+ byte[] result;
+ try {
+ result = mChunkedStreamer.doFinal(null, 0, 0);
+ } catch (KeymasterException e) {
+ throw new CryptoOperationException("Keystore operation failed", e);
+ }
+
+ engineReset();
+ return result;
+ }
+
+ @Override
+ public void finalize() throws Throwable {
+ try {
+ IBinder operationToken = mOperationToken;
+ if (operationToken != null) {
+ mOperationToken = null;
+ mKeyStore.abort(operationToken);
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * KeyStore-backed consumer of {@code MacSpi}'s chunked stream.
+ */
+ private static class KeyStoreStreamingConsumer
+ implements KeyStoreCryptoOperationChunkedStreamer.KeyStoreOperation {
+ private final KeyStore mKeyStore;
+ private final IBinder mOperationToken;
+
+ private KeyStoreStreamingConsumer(KeyStore keyStore, IBinder operationToken) {
+ mKeyStore = keyStore;
+ mOperationToken = operationToken;
+ }
+
+ @Override
+ public OperationResult update(byte[] input) {
+ return mKeyStore.update(mOperationToken, null, input);
+ }
+
+ @Override
+ public OperationResult finish(byte[] input) {
+ return mKeyStore.finish(mOperationToken, null, input);
+ }
+ }
+}
diff --git a/keystore/java/android/security/KeyStoreKeyConstraints.java b/keystore/java/android/security/KeyStoreKeyConstraints.java
index 47bb1cc..b5e2436 100644
--- a/keystore/java/android/security/KeyStoreKeyConstraints.java
+++ b/keystore/java/android/security/KeyStoreKeyConstraints.java
@@ -401,6 +401,20 @@ public abstract class KeyStoreKeyConstraints {
throw new IllegalArgumentException("Unknown digest: " + digest);
}
}
+
+ /**
+ * @hide
+ */
+ public static Integer getOutputSizeBytes(@DigestEnum int digest) {
+ switch (digest) {
+ case NONE:
+ return null;
+ case SHA256:
+ return 256 / 8;
+ default:
+ throw new IllegalArgumentException("Unknown digest: " + digest);
+ }
+ }
}
@Retention(RetentionPolicy.SOURCE)
diff --git a/keystore/java/android/security/KeyStoreKeyGeneratorSpi.java b/keystore/java/android/security/KeyStoreKeyGeneratorSpi.java
index 86950dd..f1f9436 100644
--- a/keystore/java/android/security/KeyStoreKeyGeneratorSpi.java
+++ b/keystore/java/android/security/KeyStoreKeyGeneratorSpi.java
@@ -28,7 +28,8 @@ public abstract class KeyStoreKeyGeneratorSpi extends KeyGeneratorSpi {
public HmacSHA256() {
super(KeyStoreKeyConstraints.Algorithm.HMAC,
KeyStoreKeyConstraints.Digest.SHA256,
- 256);
+ KeyStoreKeyConstraints.Digest.getOutputSizeBytes(
+ KeyStoreKeyConstraints.Digest.SHA256) * 8);
}
}
@@ -76,6 +77,19 @@ public abstract class KeyStoreKeyGeneratorSpi extends KeyGeneratorSpi {
args.addInt(KeymasterDefs.KM_TAG_DIGEST,
KeyStoreKeyConstraints.Digest.toKeymaster(mDigest));
}
+ if (mAlgorithm == KeyStoreKeyConstraints.Algorithm.HMAC) {
+ if (mDigest == null) {
+ throw new IllegalStateException("Digest algorithm must be specified for key"
+ + " algorithm " + KeyStoreKeyConstraints.Algorithm.toString(mAlgorithm));
+ }
+ Integer digestOutputSizeBytes =
+ KeyStoreKeyConstraints.Digest.getOutputSizeBytes(mDigest);
+ if (digestOutputSizeBytes != null) {
+ // TODO: Remove MAC length constraint once Keymaster API no longer requires it.
+ // TODO: Switch to bits instead of bytes, once this is fixed in Keymaster
+ args.addInt(KeymasterDefs.KM_TAG_MAC_LENGTH, digestOutputSizeBytes);
+ }
+ }
int keySizeBits = (spec.getKeySize() != null) ? spec.getKeySize() : mDefaultKeySizeBits;
args.addInt(KeymasterDefs.KM_TAG_KEY_SIZE, keySizeBits);
@KeyStoreKeyConstraints.PurposeEnum int purposes = (spec.getPurposes() != null)
diff --git a/keystore/java/android/security/KeymasterException.java b/keystore/java/android/security/KeymasterException.java
index 4ff7115..bc8198f 100644
--- a/keystore/java/android/security/KeymasterException.java
+++ b/keystore/java/android/security/KeymasterException.java
@@ -7,7 +7,14 @@ package android.security;
*/
public class KeymasterException extends Exception {
- public KeymasterException(String message) {
+ private final int mErrorCode;
+
+ public KeymasterException(int errorCode, String message) {
super(message);
+ mErrorCode = errorCode;
+ }
+
+ public int getErrorCode() {
+ return mErrorCode;
}
}
diff --git a/keystore/java/android/security/KeymasterUtils.java b/keystore/java/android/security/KeymasterUtils.java
index e6e88c7..4f17586 100644
--- a/keystore/java/android/security/KeymasterUtils.java
+++ b/keystore/java/android/security/KeymasterUtils.java
@@ -13,9 +13,11 @@ public abstract class KeymasterUtils {
case KeymasterDefs.KM_ERROR_INVALID_AUTHORIZATION_TIMEOUT:
// The name of this parameter significantly differs between Keymaster and framework
// APIs. Use the framework wording to make life easier for developers.
- return new KeymasterException("Invalid user authentication validity duration");
+ return new KeymasterException(keymasterErrorCode,
+ "Invalid user authentication validity duration");
default:
- return new KeymasterException(KeymasterDefs.getErrorMessage(keymasterErrorCode));
+ return new KeymasterException(keymasterErrorCode,
+ KeymasterDefs.getErrorMessage(keymasterErrorCode));
}
}
}