am 6a6f0c7d: Merge "Add HmacSHA256 backed by AndroidKeyStore."
* commit '6a6f0c7de263743dc22b046dd9d53c694bce3ac5': Add HmacSHA256 backed by AndroidKeyStore.
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
12
keystore/java/android/security/KeyStoreConnectException.java
Normal file
12
keystore/java/android/security/KeyStoreConnectException.java
Normal file
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
174
keystore/java/android/security/KeyStoreHmacSpi.java
Normal file
174
keystore/java/android/security/KeyStoreHmacSpi.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user