Commit bb46d704 authored by Chok's avatar Chok
Browse files

chok: init commit

parent 3f129598
Pipeline #14 failed with stages
in 0 seconds
#Tue Jul 11 10:54:42 MYT 2023
gradle.version=7.4.2
package de.niklasmerz.cordova.biometric;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class Args {
private static final String TAG = "ARGS";
private JSONArray jsonArray;
private JSONObject argsObject;
Args(JSONArray jsonArray) {
this.jsonArray = jsonArray;
}
public Boolean getBoolean(String name, Boolean defaultValue) {
try {
if (getArgsObject().has(name)){
return getArgsObject().getBoolean(name);
}
} catch (JSONException e) {
Log.e(TAG, "Can't parse '" + name + "'. Default will be used.", e);
}
return defaultValue;
}
public String getString(String name, String defaultValue) {
try {
if (getArgsObject().optString(name) != null
&& !getArgsObject().optString(name).isEmpty()){
return getArgsObject().getString(name);
}
} catch (JSONException e) {
Log.e(TAG, "Can't parse '" + name + "'. Default will be used.", e);
}
return defaultValue;
}
private JSONObject getArgsObject() throws JSONException {
if (this.argsObject != null) {
return this.argsObject;
}
this.argsObject = jsonArray.getJSONObject(0);
return this.argsObject;
}
}
package de.niklasmerz.cordova.biometric;
import android.app.Activity;
import android.app.KeyguardManager;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;
import androidx.biometric.BiometricManager;
import java.util.concurrent.Executor;
import javax.crypto.Cipher;
public class BiometricActivity extends AppCompatActivity {
private static final int REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS = 2;
private PromptInfo mPromptInfo;
private CryptographyManager mCryptographyManager;
private static final String SECRET_KEY = "__aio_secret_key";
private BiometricPrompt mBiometricPrompt;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle(null);
int layout = getResources()
.getIdentifier("biometric_activity", "layout", getPackageName());
setContentView(layout);
if (savedInstanceState != null) {
return;
}
mCryptographyManager = new CryptographyManagerImpl();
mPromptInfo = new PromptInfo.Builder(getIntent().getExtras()).build();
final Handler handler = new Handler(Looper.getMainLooper());
Executor executor = handler::post;
mBiometricPrompt = new BiometricPrompt(this, executor, mAuthenticationCallback);
try {
authenticate();
} catch (CryptoException e) {
finishWithError(e);
} catch (Exception e) {
finishWithError(PluginError.BIOMETRIC_UNKNOWN_ERROR, e.getMessage());
}
}
private void authenticate() throws CryptoException {
switch (mPromptInfo.getType()) {
case JUST_AUTHENTICATE:
justAuthenticate();
return;
case REGISTER_SECRET:
authenticateToEncrypt(mPromptInfo.invalidateOnEnrollment());
return;
case LOAD_SECRET:
authenticateToDecrypt();
return;
}
throw new CryptoException(PluginError.BIOMETRIC_ARGS_PARSING_FAILED);
}
private void authenticateToEncrypt(boolean invalidateOnEnrollment) throws CryptoException {
if (mPromptInfo.getSecret() == null) {
throw new CryptoException(PluginError.BIOMETRIC_ARGS_PARSING_FAILED);
}
Cipher cipher = mCryptographyManager
.getInitializedCipherForEncryption(SECRET_KEY, invalidateOnEnrollment, this);
mBiometricPrompt.authenticate(createPromptInfo(), new BiometricPrompt.CryptoObject(cipher));
}
private void justAuthenticate() {
mBiometricPrompt.authenticate(createPromptInfo());
}
private void authenticateToDecrypt() throws CryptoException {
byte[] initializationVector = EncryptedData.loadInitializationVector(this);
Cipher cipher = mCryptographyManager
.getInitializedCipherForDecryption(SECRET_KEY, initializationVector, this);
mBiometricPrompt.authenticate(createPromptInfo(), new BiometricPrompt.CryptoObject(cipher));
}
private BiometricPrompt.PromptInfo createPromptInfo() {
BiometricPrompt.PromptInfo.Builder promptInfoBuilder = new BiometricPrompt.PromptInfo.Builder()
.setTitle(mPromptInfo.getTitle())
.setSubtitle(mPromptInfo.getSubtitle())
.setConfirmationRequired(true)
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.setDescription(mPromptInfo.getDescription());
if (mPromptInfo.isDeviceCredentialAllowed()
&& mPromptInfo.getType() == BiometricActivityType.JUST_AUTHENTICATE
&& Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { // TODO: remove after fix https://issuetracker.google.com/issues/142740104
promptInfoBuilder.setDeviceCredentialAllowed(true);
} else {
promptInfoBuilder.setNegativeButtonText(mPromptInfo.getCancelButtonTitle());
}
// promptInfoBuilder.setDeviceCredentialAllowed(true);
return promptInfoBuilder.build();
}
private BiometricPrompt.AuthenticationCallback mAuthenticationCallback =
new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
onError(errorCode, errString);
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
try {
finishWithSuccess(result.getCryptoObject());
} catch (CryptoException e) {
finishWithError(e);
}
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
//onError(PluginError.BIOMETRIC_AUTHENTICATION_FAILED.getValue(), PluginError.BIOMETRIC_AUTHENTICATION_FAILED.getMessage());
}
};
// TODO: remove after fix https://issuetracker.google.com/issues/142740104
private void showAuthenticationScreen() {
KeyguardManager keyguardManager = ContextCompat
.getSystemService(this, KeyguardManager.class);
if (keyguardManager == null
|| android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
return;
}
if (keyguardManager.isKeyguardSecure()) {
Intent intent = keyguardManager
.createConfirmDeviceCredentialIntent(mPromptInfo.getTitle(), mPromptInfo.getDescription());
this.startActivityForResult(intent, REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS);
} else {
// Show a message that the user hasn't set up a lock screen.
finishWithError(PluginError.BIOMETRIC_SCREEN_GUARD_UNSECURED);
}
}
// TODO: remove after fix https://issuetracker.google.com/issues/142740104
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS) {
if (resultCode == Activity.RESULT_OK) {
finishWithSuccess();
} else {
finishWithError(PluginError.BIOMETRIC_PIN_OR_PATTERN_DISMISSED);
}
}
}
private void onError(int errorCode, @NonNull CharSequence errString) {
switch (errorCode)
{
case BiometricPrompt.ERROR_USER_CANCELED:
case BiometricPrompt.ERROR_CANCELED:
finishWithError(PluginError.BIOMETRIC_DISMISSED);
return;
case BiometricPrompt.ERROR_NEGATIVE_BUTTON:
// TODO: remove after fix https://issuetracker.google.com/issues/142740104
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P && mPromptInfo.isDeviceCredentialAllowed()) {
showAuthenticationScreen();
return;
}
finishWithError(PluginError.BIOMETRIC_DISMISSED);
break;
case BiometricPrompt.ERROR_LOCKOUT:
finishWithError(PluginError.BIOMETRIC_LOCKED_OUT.getValue(), errString.toString());
break;
case BiometricPrompt.ERROR_LOCKOUT_PERMANENT:
finishWithError(PluginError.BIOMETRIC_LOCKED_OUT_PERMANENT.getValue(), errString.toString());
break;
default:
finishWithError(errorCode, errString.toString());
}
}
private void finishWithSuccess() {
setResult(RESULT_OK);
finish();
}
private void finishWithSuccess(BiometricPrompt.CryptoObject cryptoObject) throws CryptoException {
Intent intent = null;
switch (mPromptInfo.getType()) {
case REGISTER_SECRET:
encrypt(cryptoObject);
break;
case LOAD_SECRET:
intent = getDecryptedIntent(cryptoObject);
break;
}
if (intent == null) {
setResult(RESULT_OK);
} else {
setResult(RESULT_OK, intent);
}
finish();
}
private void encrypt(BiometricPrompt.CryptoObject cryptoObject) throws CryptoException {
String text = mPromptInfo.getSecret();
EncryptedData encryptedData = mCryptographyManager.encryptData(text, cryptoObject.getCipher());
encryptedData.save(this);
}
private Intent getDecryptedIntent(BiometricPrompt.CryptoObject cryptoObject) throws CryptoException {
byte[] ciphertext = EncryptedData.loadCiphertext(this);
String secret = mCryptographyManager.decryptData(ciphertext, cryptoObject.getCipher());
if (secret != null) {
Intent intent = new Intent();
intent.putExtra(PromptInfo.SECRET_EXTRA, secret);
return intent;
}
return null;
}
private void finishWithError(CryptoException e) {
finishWithError(e.getError().getValue(), e.getMessage());
}
private void finishWithError(PluginError error) {
finishWithError(error.getValue(), error.getMessage());
}
private void finishWithError(PluginError error, String message) {
finishWithError(error.getValue(), message);
}
private void finishWithError(int code, String message) {
Intent data = new Intent();
data.putExtra("code", code);
data.putExtra("message", message);
setResult(RESULT_CANCELED, data);
finish();
}
}
package de.niklasmerz.cordova.biometric;
public enum BiometricActivityType {
JUST_AUTHENTICATE(1),
REGISTER_SECRET(2),
LOAD_SECRET(3);
private int value;
BiometricActivityType(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public static BiometricActivityType fromValue(int val) {
for (BiometricActivityType type : values()) {
if (type.getValue() == val) {
return type;
}
}
return null;
}
}
package de.niklasmerz.cordova.biometric;
class CryptoException extends Exception {
private PluginError error;
CryptoException(String message, Exception cause) {
this(PluginError.BIOMETRIC_UNKNOWN_ERROR, message, cause);
}
CryptoException(PluginError error) {
this(error, error.getMessage(), null);
}
CryptoException(PluginError error, Exception cause) {
this(error, error.getMessage(), cause);
}
private CryptoException(PluginError error, String message, Exception cause) {
super(message, cause);
this.error = error;
}
public PluginError getError() {
return error;
}
}
package de.niklasmerz.cordova.biometric;
import android.content.Context;
import javax.crypto.Cipher;
interface CryptographyManager {
/**
* This method first gets or generates an instance of SecretKey and then initializes the Cipher
* with the key. The secret key uses [ENCRYPT_MODE][Cipher.ENCRYPT_MODE] is used.
*/
Cipher getInitializedCipherForEncryption(String keyName, boolean invalidateOnEnrollment, Context context) throws CryptoException;
/**
* This method first gets or generates an instance of SecretKey and then initializes the Cipher
* with the key. The secret key uses [DECRYPT_MODE][Cipher.DECRYPT_MODE] is used.
*/
Cipher getInitializedCipherForDecryption(String keyName, byte[] initializationVector, Context context) throws CryptoException;
/**
* The Cipher created with [getInitializedCipherForEncryption] is used here
*/
EncryptedData encryptData(String plaintext, Cipher cipher) throws CryptoException;
/**
* The Cipher created with [getInitializedCipherForDecryption] is used here
*/
String decryptData(byte[] ciphertext, Cipher cipher) throws CryptoException;
}
package de.niklasmerz.cordova.biometric;
import android.content.Context;
import android.os.Build;
import android.security.KeyPairGeneratorSpec;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import androidx.annotation.RequiresApi;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.util.Calendar;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.security.auth.x500.X500Principal;
class CryptographyManagerImpl implements CryptographyManager {
private static final int KEY_SIZE = 256;
private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
private static final String ENCRYPTION_PADDING = "NoPadding"; // KeyProperties.ENCRYPTION_PADDING_NONE
private static final String ENCRYPTION_ALGORITHM = "AES"; // KeyProperties.KEY_ALGORITHM_AES
private static final String KEY_ALGORITHM_AES = "AES"; // KeyProperties.KEY_ALGORITHM_AES
private static final String ENCRYPTION_BLOCK_MODE = "GCM"; // KeyProperties.BLOCK_MODE_GCM
private Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
String transformation = ENCRYPTION_ALGORITHM + "/" + ENCRYPTION_BLOCK_MODE + "/" + ENCRYPTION_PADDING;
return Cipher.getInstance(transformation);
}
private SecretKey getOrCreateSecretKey(String keyName, boolean invalidateOnEnrollment, Context context) throws CryptoException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return getOrCreateSecretKeyNew(keyName, invalidateOnEnrollment);
} else {
return getOrCreateSecretKeyOld(keyName, context);
}
}
private SecretKey getOrCreateSecretKeyOld(String keyName, Context context) throws CryptoException {
Calendar start = Calendar.getInstance();
Calendar end = Calendar.getInstance();
end.add(Calendar.YEAR, 1);
try {
KeyPairGeneratorSpec keySpec = new KeyPairGeneratorSpec.Builder(context)
.setAlias(keyName)
.setSubject(new X500Principal("CN=FINGERPRINT_AIO ," +
" O=FINGERPRINT_AIO" +
" C=World"))
.setSerialNumber(BigInteger.ONE)
.setStartDate(start.getTime())
.setEndDate(end.getTime())
.build();
KeyGenerator kg = KeyGenerator.getInstance(KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
kg.init(keySpec);
return kg.generateKey();
} catch (Exception e) {
throw new CryptoException(e.getMessage(), e);
}
}
@RequiresApi(api = Build.VERSION_CODES.M)
private SecretKey getOrCreateSecretKeyNew(String keyName, boolean invalidateOnEnrollment) throws CryptoException {
try {
// If Secretkey was previously created for that keyName, then grab and return it.
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null); // Keystore must be loaded before it can be accessed
SecretKey key = (SecretKey) keyStore.getKey(keyName, null);
if (key != null) {
return key;
}
// if you reach here, then a new SecretKey must be generated for that keyName
KeyGenParameterSpec.Builder keyGenParamsBuilder = new KeyGenParameterSpec.Builder(keyName,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(KEY_SIZE)
.setUserAuthenticationRequired(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
keyGenParamsBuilder.setInvalidatedByBiometricEnrollment(invalidateOnEnrollment);
}
KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM_AES,
ANDROID_KEYSTORE);
keyGenerator.init(keyGenParamsBuilder.build());
return keyGenerator.generateKey();
} catch (Exception e) {
throw new CryptoException(e.getMessage(), e);
}
}
@Override
public Cipher getInitializedCipherForEncryption(String keyName, boolean invalidateOnEnrollment, Context context) throws CryptoException {
try {
Cipher cipher = getCipher();
SecretKey secretKey = getOrCreateSecretKey(keyName, invalidateOnEnrollment, context);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher;
} catch (Exception e) {
try {
handleException(e, keyName);
} catch (KeyInvalidatedException kie) {
return getInitializedCipherForEncryption(keyName, invalidateOnEnrollment, context);
}
throw new CryptoException(e.getMessage(), e);
}
}
private void handleException(Exception e, String keyName) throws CryptoException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& e instanceof KeyPermanentlyInvalidatedException) {
removeKey(keyName);
throw new KeyInvalidatedException();
}
}
@Override
public Cipher getInitializedCipherForDecryption(String keyName, byte[] initializationVector, Context context) throws CryptoException {
try {
Cipher cipher = getCipher();
SecretKey secretKey = getOrCreateSecretKey(keyName, true, context);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, initializationVector));
return cipher;
} catch (Exception e) {
handleException(e, keyName);
throw new CryptoException(e.getMessage(), e);
}
}
private void removeKey(String keyName) throws CryptoException {
try {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null); // Keystore must be loaded before it can be accessed
keyStore.deleteEntry(keyName);
} catch (Exception e) {
throw new CryptoException(e.getMessage(), e);
}
}
@Override
public EncryptedData encryptData(String plaintext, Cipher cipher) throws CryptoException {
try {
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
return new EncryptedData(ciphertext, cipher.getIV());
} catch (Exception e) {
throw new CryptoException(e.getMessage(), e);
}
}
@Override
public String decryptData(byte[] ciphertext, Cipher cipher) throws CryptoException {
try {
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new CryptoException(e.getMessage(), e);
}
}
}
package de.niklasmerz.cordova.biometric;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Base64;
class EncryptedData {
private static final String CIPHERTEXT_KEY_NAME = "__biometric-aio-ciphertext";
private static final String IV_KEY_NAME = "__biometric-aio-iv";
private byte[] ciphertext;
private byte[] initializationVector;
EncryptedData(byte[] ciphertext, byte[] initializationVector) {
this.ciphertext = ciphertext;
this.initializationVector = initializationVector;
}
static byte[] loadInitializationVector(Context context) throws CryptoException {
return load(IV_KEY_NAME, context);
}
static byte[] loadCiphertext(Context context) throws CryptoException {
return load(CIPHERTEXT_KEY_NAME, context);
}
void save(Context context) {
save(IV_KEY_NAME, initializationVector, context);
save(CIPHERTEXT_KEY_NAME, ciphertext, context);
}
private void save(String key, byte[] value, Context context) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
preferences.edit()
.putString(key, Base64.encodeToString(value, Base64.DEFAULT))
.apply();
}
private static byte[] load(String key, Context context) throws CryptoException {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
String res = preferences.getString(key, null);
if (res == null) throw new CryptoException(PluginError.BIOMETRIC_NO_SECRET_FOUND);
return Base64.decode(res, Base64.DEFAULT);
}
}
package de.niklasmerz.cordova.biometric;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import androidx.biometric.BiometricManager;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class Fingerprint extends CordovaPlugin {
private static final String TAG = "Fingerprint";
private static final int REQUEST_CODE_BIOMETRIC = 1;
private CallbackContext mCallbackContext = null;
private PromptInfo.Builder mPromptInfoBuilder;
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
super.initialize(cordova, webView);
Log.v(TAG, "Init Fingerprint");
mPromptInfoBuilder = new PromptInfo.Builder(
this.getApplicationLabel(cordova.getActivity())
);
}
public boolean execute(final String action, JSONArray args, CallbackContext callbackContext) {
this.mCallbackContext = callbackContext;
Log.v(TAG, "Fingerprint action: " + action);
if ("authenticate".equals(action)) {
executeAuthenticate(args);
return true;
} else if ("registerBiometricSecret".equals(action)) {
executeRegisterBiometricSecret(args);
return true;
} else if ("loadBiometricSecret".equals(action)) {
executeLoadBiometricSecret(args);
return true;
} else if ("isAvailable".equals(action)) {
executeIsAvailable();
return true;
}
return false;
}
private void executeIsAvailable() {
PluginError error = canAuthenticate();
if (error != null) {
sendError(error);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
sendSuccess("biometric");
} else {
sendSuccess("finger");
}
}
private void executeRegisterBiometricSecret(JSONArray args) {
// should at least contains the secret
if (args == null) {
sendError(PluginError.BIOMETRIC_ARGS_PARSING_FAILED);
return;
}
this.runBiometricActivity(args, BiometricActivityType.REGISTER_SECRET);
}
private void executeLoadBiometricSecret(JSONArray args) {
this.runBiometricActivity(args, BiometricActivityType.LOAD_SECRET);
}
private void executeAuthenticate(JSONArray args) {
this.runBiometricActivity(args, BiometricActivityType.JUST_AUTHENTICATE);
}
private void runBiometricActivity(JSONArray args, BiometricActivityType type) {
PluginError error = canAuthenticate();
if (error != null) {
sendError(error);
return;
}
cordova.getActivity().runOnUiThread(() -> {
mPromptInfoBuilder.parseArgs(args, type);
Intent intent = new Intent(cordova.getActivity().getApplicationContext(), BiometricActivity.class);
intent.putExtras(mPromptInfoBuilder.build().getBundle());
this.cordova.startActivityForResult(this, intent, REQUEST_CODE_BIOMETRIC);
});
PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT);
pluginResult.setKeepCallback(true);
this.mCallbackContext.sendPluginResult(pluginResult);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
if (requestCode != REQUEST_CODE_BIOMETRIC) {
return;
}
if (resultCode != Activity.RESULT_OK) {
sendError(intent);
return;
}
sendSuccess(intent);
}
private void sendSuccess(Intent intent) {
if (intent != null && intent.getExtras() != null) {
sendSuccess(intent.getExtras().getString(PromptInfo.SECRET_EXTRA));
} else {
sendSuccess("biometric_success");
}
}
private void sendError(Intent intent) {
if (intent != null) {
Bundle extras = intent.getExtras();
sendError(extras.getInt("code"), extras.getString("message"));
} else {
sendError(PluginError.BIOMETRIC_DISMISSED);
}
}
private PluginError canAuthenticate() {
int error = BiometricManager.from(cordova.getContext()).canAuthenticate();
switch (error) {
case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE:
case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
return PluginError.BIOMETRIC_HARDWARE_NOT_SUPPORTED;
case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
return PluginError.BIOMETRIC_NOT_ENROLLED;
case BiometricManager.BIOMETRIC_SUCCESS:
default:
return null;
}
}
private void sendError(int code, String message) {
JSONObject resultJson = new JSONObject();
try {
resultJson.put("code", code);
resultJson.put("message", message);
PluginResult result = new PluginResult(PluginResult.Status.ERROR, resultJson);
result.setKeepCallback(true);
cordova.getActivity().runOnUiThread(() ->
Fingerprint.this.mCallbackContext.sendPluginResult(result));
} catch (JSONException e) {
Log.e(TAG, e.getMessage(), e);
}
}
private void sendError(PluginError error) {
sendError(error.getValue(), error.getMessage());
}
private void sendSuccess(String message) {
cordova.getActivity().runOnUiThread(() ->
this.mCallbackContext.success(message));
}
private String getApplicationLabel(Context context) {
try {
PackageManager packageManager = context.getPackageManager();
ApplicationInfo app = packageManager
.getApplicationInfo(context.getPackageName(), 0);
return packageManager.getApplicationLabel(app).toString();
} catch (PackageManager.NameNotFoundException e) {
return null;
}
}
}
package de.niklasmerz.cordova.biometric;
class KeyInvalidatedException extends CryptoException {
KeyInvalidatedException() {
super(PluginError.BIOMETRIC_NO_SECRET_FOUND);
}
}
package de.niklasmerz.cordova.biometric;
public enum PluginError {
BIOMETRIC_UNKNOWN_ERROR(-100),
BIOMETRIC_AUTHENTICATION_FAILED(-102, "Authentication failed"),
BIOMETRIC_HARDWARE_NOT_SUPPORTED(-104),
BIOMETRIC_NOT_ENROLLED(-106),
BIOMETRIC_DISMISSED(-108),
BIOMETRIC_PIN_OR_PATTERN_DISMISSED(-109),
BIOMETRIC_SCREEN_GUARD_UNSECURED(-110,
"Go to 'Settings -> Security -> Screenlock' to set up a lock screen"),
BIOMETRIC_LOCKED_OUT(-111),
BIOMETRIC_LOCKED_OUT_PERMANENT(-112),
BIOMETRIC_NO_SECRET_FOUND(-113),
BIOMETRIC_ARGS_PARSING_FAILED(-115);
private int value;
private String message;
PluginError(int value) {
this.value = value;
this.message = this.name();
}
PluginError(int value, String message) {
this.value = value;
this.message = message;
}
public int getValue() {
return value;
}
public String getMessage() {
return message;
}
}
package de.niklasmerz.cordova.biometric;
import android.os.Bundle;
import org.json.JSONArray;
class PromptInfo {
private static final String DISABLE_BACKUP = "disableBackup";
private static final String TITLE = "title";
private static final String SUBTITLE = "subtitle";
private static final String DESCRIPTION = "description";
private static final String FALLBACK_BUTTON_TITLE = "fallbackButtonTitle";
private static final String CANCEL_BUTTON_TITLE = "cancelButtonTitle";
private static final String CONFIRMATION_REQUIRED = "confirmationRequired";
private static final String INVALIDATE_ON_ENROLLMENT = "invalidateOnEnrollment";
private static final String SECRET = "secret";
private static final String BIOMETRIC_ACTIVITY_TYPE = "biometricActivityType";
static final String SECRET_EXTRA = "secret";
private Bundle bundle = new Bundle();
Bundle getBundle() {
return bundle;
}
String getTitle() {
return bundle.getString(TITLE);
}
String getSubtitle() {
return bundle.getString(SUBTITLE);
}
String getDescription() {
return bundle.getString(DESCRIPTION);
}
boolean isDeviceCredentialAllowed() {
return !bundle.getBoolean(DISABLE_BACKUP);
}
String getFallbackButtonTitle() {
return bundle.getString(FALLBACK_BUTTON_TITLE);
}
String getCancelButtonTitle() {
return bundle.getString(CANCEL_BUTTON_TITLE);
}
boolean getConfirmationRequired() {
return bundle.getBoolean(CONFIRMATION_REQUIRED);
}
String getSecret() {
return bundle.getString(SECRET);
}
boolean invalidateOnEnrollment() {
return bundle.getBoolean(INVALIDATE_ON_ENROLLMENT);
}
BiometricActivityType getType() {
return BiometricActivityType.fromValue(bundle.getInt(BIOMETRIC_ACTIVITY_TYPE));
}
public static final class Builder {
private static final String TAG = "PromptInfo.Builder";
private Bundle bundle;
private boolean disableBackup = false;
private String title;
private String subtitle = null;
private String description = null;
private String fallbackButtonTitle = "Use backup";
private String cancelButtonTitle = "Cancel";
private boolean confirmationRequired = true;
private boolean invalidateOnEnrollment = false;
private String secret = null;
private BiometricActivityType type = null;
Builder(String applicationLabel) {
if (applicationLabel == null) {
title = "Biometric Sign On";
} else {
title = applicationLabel + " Biometric Sign On";
}
}
Builder(Bundle bundle) {
this.bundle = bundle;
}
public PromptInfo build() {
PromptInfo promptInfo = new PromptInfo();
if (this.bundle != null) {
promptInfo.bundle = bundle;
return promptInfo;
}
Bundle bundle = new Bundle();
bundle.putString(SUBTITLE, this.subtitle);
bundle.putString(TITLE, this.title);
bundle.putString(DESCRIPTION, this.description);
bundle.putString(FALLBACK_BUTTON_TITLE, this.fallbackButtonTitle);
bundle.putString(CANCEL_BUTTON_TITLE, this.cancelButtonTitle);
bundle.putString(SECRET, this.secret);
bundle.putBoolean(DISABLE_BACKUP, this.disableBackup);
bundle.putBoolean(CONFIRMATION_REQUIRED, this.confirmationRequired);
bundle.putBoolean(INVALIDATE_ON_ENROLLMENT, this.invalidateOnEnrollment);
bundle.putInt(BIOMETRIC_ACTIVITY_TYPE, this.type.getValue());
promptInfo.bundle = bundle;
return promptInfo;
}
void parseArgs(JSONArray jsonArgs, BiometricActivityType type) {
this.type = type;
Args args = new Args(jsonArgs);
disableBackup = args.getBoolean(DISABLE_BACKUP, disableBackup);
title = args.getString(TITLE, title);
subtitle = args.getString(SUBTITLE, subtitle);
description = args.getString(DESCRIPTION, description);
fallbackButtonTitle = args.getString(FALLBACK_BUTTON_TITLE, "Use Backup");
cancelButtonTitle = args.getString(CANCEL_BUTTON_TITLE, "Cancel");
confirmationRequired = args.getBoolean(CONFIRMATION_REQUIRED, confirmationRequired);
invalidateOnEnrollment = args.getBoolean(INVALIDATE_ON_ENROLLMENT, false);
secret = args.getString(SECRET, null);
}
}
}
dependencies {
implementation "androidx.biometric:biometric:1.1.0"
}
android {
packagingOptions {
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
}
}
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">
</FrameLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 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.
-->
<resources>
<style name="TransparentTheme" parent="Theme.AppCompat">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
</resources>
\ No newline at end of file
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import <Cordova/CDV.h>
\ No newline at end of file
import Foundation
import LocalAuthentication
enum PluginError:Int {
case BIOMETRIC_UNKNOWN_ERROR = -100
case BIOMETRIC_UNAVAILABLE = -101
case BIOMETRIC_AUTHENTICATION_FAILED = -102
case BIOMETRIC_PERMISSION_NOT_GRANTED = -105
case BIOMETRIC_NOT_ENROLLED = -106
case BIOMETRIC_DISMISSED = -108
case BIOMETRIC_SCREEN_GUARD_UNSECURED = -110
case BIOMETRIC_LOCKED_OUT = -111
case BIOMETRIC_SECRET_NOT_FOUND = -113
}
@objc(Fingerprint) class Fingerprint : CDVPlugin {
struct ErrorCodes {
var code: Int
}
@objc(isAvailable:)
func isAvailable(_ command: CDVInvokedUrlCommand){
let authenticationContext = LAContext();
var biometryType = "finger";
var errorResponse: [AnyHashable: Any] = [
"code": 0,
"message": "Not Available"
];
var error:NSError?;
let params = command.argument(at: 0) as? [AnyHashable: Any] ?? [:]
let allowBackup = params["allowBackup"] as? Bool ?? false
let policy:LAPolicy = allowBackup ? .deviceOwnerAuthentication : .deviceOwnerAuthenticationWithBiometrics;
var pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: "Not available");
let available = authenticationContext.canEvaluatePolicy(policy, error: &error);
var results: [String : Any]
if(error != nil){
biometryType = "none";
errorResponse["code"] = error?.code;
errorResponse["message"] = error?.localizedDescription;
}
if (available == true) {
if #available(iOS 11.0, *) {
switch(authenticationContext.biometryType) {
case .none:
biometryType = "none";
case .touchID:
biometryType = "finger";
case .faceID:
biometryType = "face"
@unknown default:
errorResponse["message"] = "Unkown biometry type"
}
}
pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: biometryType);
}else{
var code: Int;
switch(error!._code) {
case Int(kLAErrorBiometryNotAvailable):
code = PluginError.BIOMETRIC_UNAVAILABLE.rawValue;
break;
case Int(kLAErrorBiometryNotEnrolled):
code = PluginError.BIOMETRIC_NOT_ENROLLED.rawValue;
break;
default:
code = PluginError.BIOMETRIC_UNKNOWN_ERROR.rawValue;
break;
}
results = ["code": code, "message": error!.localizedDescription];
pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: results);
}
commandDelegate.send(pluginResult, callbackId:command.callbackId);
}
func justAuthenticate(_ command: CDVInvokedUrlCommand) {
let authenticationContext = LAContext();
let errorResponse: [AnyHashable: Any] = [
"message": "Something went wrong"
];
var pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: errorResponse);
var reason = "Authentication";
var policy:LAPolicy = .deviceOwnerAuthentication;
let data = command.arguments[0] as? [String: Any];
if let disableBackup = data?["disableBackup"] as! Bool? {
if disableBackup {
authenticationContext.localizedFallbackTitle = "";
policy = .deviceOwnerAuthenticationWithBiometrics;
} else {
if let fallbackButtonTitle = data?["fallbackButtonTitle"] as! String? {
authenticationContext.localizedFallbackTitle = fallbackButtonTitle;
}else{
authenticationContext.localizedFallbackTitle = "Use Pin";
}
}
}
// Localized reason
if let description = data?["description"] as! String? {
reason = description;
}
authenticationContext.evaluatePolicy(
policy,
localizedReason: reason,
reply: { [unowned self] (success, error) -> Void in
if( success ) {
pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: "Success");
}else {
if (error != nil) {
var errorCodes = [Int: ErrorCodes]()
var errorResult: [String : Any] = ["code": PluginError.BIOMETRIC_UNKNOWN_ERROR.rawValue, "message": error?.localizedDescription ?? ""];
errorCodes[1] = ErrorCodes(code: PluginError.BIOMETRIC_AUTHENTICATION_FAILED.rawValue)
errorCodes[2] = ErrorCodes(code: PluginError.BIOMETRIC_DISMISSED.rawValue)
errorCodes[5] = ErrorCodes(code: PluginError.BIOMETRIC_SCREEN_GUARD_UNSECURED.rawValue)
errorCodes[6] = ErrorCodes(code: PluginError.BIOMETRIC_UNAVAILABLE.rawValue)
errorCodes[7] = ErrorCodes(code: PluginError.BIOMETRIC_NOT_ENROLLED.rawValue)
errorCodes[8] = ErrorCodes(code: PluginError.BIOMETRIC_LOCKED_OUT.rawValue)
let errorCode = abs(error!._code)
if let e = errorCodes[errorCode] {
errorResult = ["code": e.code, "message": error!.localizedDescription];
}
pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: errorResult);
}
}
self.commandDelegate.send(pluginResult, callbackId:command.callbackId);
}
);
}
func saveSecret(_ secretStr: String, command: CDVInvokedUrlCommand) {
let data = command.arguments[0] as AnyObject?;
var pluginResult: CDVPluginResult
do {
let secret = Secret()
try? secret.delete()
let invalidateOnEnrollment = (data?.object(forKey: "invalidateOnEnrollment") as? Bool) ?? false
try secret.save(secretStr, invalidateOnEnrollment: invalidateOnEnrollment)
pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: "Success");
} catch {
let errorResult = ["code": PluginError.BIOMETRIC_UNKNOWN_ERROR.rawValue, "message": error.localizedDescription] as [String : Any];
pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: errorResult);
}
self.commandDelegate.send(pluginResult, callbackId:command.callbackId)
return
}
func loadSecret(_ command: CDVInvokedUrlCommand) {
let data = command.arguments[0] as AnyObject?;
var prompt = "Authentication"
if let description = data?.object(forKey: "description") as! String? {
prompt = description;
}
var pluginResult: CDVPluginResult
do {
let result = try Secret().load(prompt)
pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: result);
} catch {
var code = PluginError.BIOMETRIC_UNKNOWN_ERROR.rawValue
var message = error.localizedDescription
if let err = error as? KeychainError {
code = err.pluginError.rawValue
message = err.localizedDescription
}
let errorResult = ["code": code, "message": message] as [String : Any]
pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: errorResult);
}
self.commandDelegate.send(pluginResult, callbackId:command.callbackId)
}
@objc(authenticate:)
func authenticate(_ command: CDVInvokedUrlCommand){
justAuthenticate(command)
}
@objc(registerBiometricSecret:)
func registerBiometricSecret(_ command: CDVInvokedUrlCommand){
let data = command.arguments[0] as AnyObject?;
if let secret = data?.object(forKey: "secret") as? String {
self.saveSecret(secret, command: command)
return
}
}
@objc(loadBiometricSecret:)
func loadBiometricSecret(_ command: CDVInvokedUrlCommand){
self.loadSecret(command)
}
override func pluginInitialize() {
super.pluginInitialize()
}
}
/// Keychain errors we might encounter.
struct KeychainError: Error {
var status: OSStatus
var localizedDescription: String {
if #available(iOS 11.3, *) {
if let result = SecCopyErrorMessageString(status, nil) as String? {
return result
}
}
switch status {
case errSecItemNotFound:
return "Secret not found"
case errSecUserCanceled:
return "Biometric dissmissed"
case errSecAuthFailed:
return "Authentication failed"
default:
return "Unknown error \(status)"
}
}
var pluginError: PluginError {
switch status {
case errSecItemNotFound:
return PluginError.BIOMETRIC_SECRET_NOT_FOUND
case errSecUserCanceled:
return PluginError.BIOMETRIC_DISMISSED
case errSecAuthFailed:
return PluginError.BIOMETRIC_AUTHENTICATION_FAILED
default:
return PluginError.BIOMETRIC_UNKNOWN_ERROR
}
}
}
class Secret {
private static let keyName: String = "__aio_key"
private func getBioSecAccessControl(invalidateOnEnrollment: Bool) -> SecAccessControl {
var access: SecAccessControl?
var error: Unmanaged<CFError>?
if #available(iOS 11.3, *) {
access = SecAccessControlCreateWithFlags(nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
invalidateOnEnrollment ? .biometryCurrentSet : .userPresence,
&error)
} else {
access = SecAccessControlCreateWithFlags(nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
invalidateOnEnrollment ? .touchIDCurrentSet : .userPresence,
&error)
}
precondition(access != nil, "SecAccessControlCreateWithFlags failed")
return access!
}
func save(_ secret: String, invalidateOnEnrollment: Bool) throws {
let password = secret.data(using: String.Encoding.utf8)!
// Allow a device unlock in the last 10 seconds to be used to get at keychain items.
// let context = LAContext()
// context.touchIDAuthenticationAllowableReuseDuration = 10
// Build the query for use in the add operation.
let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: Secret.keyName,
kSecAttrAccessControl as String: getBioSecAccessControl(invalidateOnEnrollment: invalidateOnEnrollment),
kSecValueData as String: password]
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError(status: status) }
}
func load(_ prompt: String) throws -> String {
let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: Secret.keyName,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String : kCFBooleanTrue,
kSecAttrAccessControl as String: getBioSecAccessControl(invalidateOnEnrollment: true),
kSecUseOperationPrompt as String: prompt]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess else { throw KeychainError(status: status) }
guard let passwordData = item as? Data,
let password = String(data: passwordData, encoding: String.Encoding.utf8)
// let account = existingItem[kSecAttrAccount as String] as? String
else {
throw KeychainError(status: errSecInternalError)
}
return password
}
func delete() throws {
let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: Secret.keyName]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess else { throw KeychainError(status: status) }
}
}
/*global cordova */
var Fingerprint = function() {
};
// Plugin Errors
Fingerprint.prototype.BIOMETRIC_UNKNOWN_ERROR = -100;
Fingerprint.prototype.BIOMETRIC_UNAVAILABLE = -101;
Fingerprint.prototype.BIOMETRIC_AUTHENTICATION_FAILED = -102;
Fingerprint.prototype.BIOMETRIC_SDK_NOT_SUPPORTED = -103;
Fingerprint.prototype.BIOMETRIC_HARDWARE_NOT_SUPPORTED = -104;
Fingerprint.prototype.BIOMETRIC_PERMISSION_NOT_GRANTED = -105;
Fingerprint.prototype.BIOMETRIC_NOT_ENROLLED = -106;
Fingerprint.prototype.BIOMETRIC_INTERNAL_PLUGIN_ERROR = -107;
Fingerprint.prototype.BIOMETRIC_DISMISSED = -108;
Fingerprint.prototype.BIOMETRIC_PIN_OR_PATTERN_DISMISSED = -109;
Fingerprint.prototype.BIOMETRIC_SCREEN_GUARD_UNSECURED = -110;
Fingerprint.prototype.BIOMETRIC_LOCKED_OUT = -111;
Fingerprint.prototype.BIOMETRIC_LOCKED_OUT_PERMANENT = -112;
Fingerprint.prototype.BIOMETRIC_NO_SECRET_FOUND = -113;
// Biometric types
Fingerprint.prototype.BIOMETRIC_TYPE_FINGERPRINT = "finger";
Fingerprint.prototype.BIOMETRIC_TYPE_FACE = "face";
Fingerprint.prototype.BIOMETRIC_TYPE_COMMON = "biometric";
Fingerprint.prototype.show = function (params, successCallback, errorCallback) {
cordova.exec(
successCallback,
errorCallback,
"Fingerprint",
"authenticate",
[params]
);
};
Fingerprint.prototype.isAvailable = function (successCallback, errorCallback, optionalParams) {
cordova.exec(
successCallback,
errorCallback,
"Fingerprint",
"isAvailable",
[optionalParams]
);
};
Fingerprint.prototype.registerBiometricSecret = function (params, successCallback, errorCallback) {
cordova.exec(
successCallback,
errorCallback,
"Fingerprint",
"registerBiometricSecret",
[params]
);
};
Fingerprint.prototype.loadBiometricSecret = function (params, successCallback, errorCallback) {
cordova.exec(
successCallback,
errorCallback,
"Fingerprint",
"loadBiometricSecret",
[params]
);
};
module.exports = new Fingerprint();
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment