package co.infinum.goldfinger; import android.content.Context; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.biometric.BiometricManager; import androidx.biometric.BiometricPrompt; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import co.infinum.goldfinger.crypto.CipherCrypter; import co.infinum.goldfinger.crypto.CipherFactory; import co.infinum.goldfinger.crypto.MacCrypter; import co.infinum.goldfinger.crypto.MacFactory; import co.infinum.goldfinger.crypto.SignatureCrypter; import co.infinum.goldfinger.crypto.SignatureFactory; import co.infinum.goldfinger.crypto.impl.AesCipherFactory; import co.infinum.goldfinger.crypto.impl.Base64CipherCrypter; @SuppressWarnings("unused") public interface Goldfinger { /** * @deprecated Use {@link #hasFingerprintHardware(int)} instead. */ @Deprecated boolean hasFingerprintHardware(); /** * Returns true if user has fingerprint hardware, false otherwise. */ boolean hasFingerprintHardware(int authenticators); /** * @deprecated Use {@link #hasEnrolledFingerprint(int)} instead. */ @Deprecated boolean hasEnrolledFingerprint(); /** * Returns true if user has enrolled fingerprint, false otherwise. */ boolean hasEnrolledFingerprint(int authenticators); /** * @deprecated Use {@link #canAuthenticate(int)} instead. */ @Deprecated boolean canAuthenticate(); /** * @see BiometricManager#canAuthenticate(int) */ boolean canAuthenticate(int authenticators); /** * Authenticate user via Fingerprint. *

* Example - Process payment after successful fingerprint authentication. * * @see PromptParams */ void authenticate(@NonNull PromptParams params, @NonNull Callback callback); /** * Authenticate user via Fingerprint. If user is successfully authenticated, * {@link CrypterProxy} implementation is used to automatically encrypt given value. *

* Use it when saving some data that should not be saved as plain text (e.g. password). * To decrypt the value use {@link Goldfinger#decrypt} method. *

* Example - Allow auto-login via Fingerprint. * * @param params parameters used to build {@link BiometricPrompt} instance * @param key unique key identifier, used to store cipher IV internally * @param value String value which will be encrypted if user successfully authenticates * @param callback callback * @see Goldfinger.Callback */ void encrypt( @NonNull PromptParams params, @NonNull String key, @NonNull String value, @NonNull Callback callback ); /** * Authenticate user via Fingerprint. If user is successfully authenticated, * {@link CrypterProxy} implementation is used to automatically decrypt given value. *

* Should be used together with {@link Goldfinger#encrypt} to decrypt saved data. * * @param key unique key identifier, used to load Cipher IV internally * @param value String value which will be decrypted if user successfully authenticates */ void decrypt( @NonNull PromptParams params, @NonNull String key, @NonNull String value, @NonNull Callback callback ); /** * Cancel current active Fingerprint authentication. */ void cancel(); /** * Become Bob the builder. */ @SuppressWarnings("UnusedReturnValue") class Builder { @NonNull private final Context context; @Nullable private CipherFactory cipherFactory; @Nullable private MacFactory macFactory; @Nullable private SignatureFactory signatureFactory; @Nullable private CipherCrypter cipherCrypter; @Nullable private MacCrypter macCrypter; @Nullable private SignatureCrypter signatureCrypter; @Nullable private String key; @Nullable private String value; public Builder(@NonNull Context context) { this.context = context; } @NonNull public Goldfinger build() { ensureParamsValid(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return buildGoldfinger(); } else { return new GoldfingerMock(); } } @NonNull public Builder cipherCrypter(@Nullable CipherCrypter cipherCrypter) { this.cipherCrypter = cipherCrypter; return this; } @NonNull public Builder cipherFactory(@Nullable CipherFactory cipherFactory) { this.cipherFactory = cipherFactory; return this; } @NonNull public Builder logEnabled(boolean logEnabled) { LogUtils.setEnabled(logEnabled); return this; } @NonNull public Builder macCrypter(@Nullable MacCrypter macCrypter) { this.macCrypter = macCrypter; return this; } @NonNull public Builder macFactory(@Nullable MacFactory macFactory) { this.macFactory = macFactory; return this; } @NonNull public Builder signatureCrypter(@Nullable SignatureCrypter signatureCrypter) { this.signatureCrypter = signatureCrypter; return this; } @NonNull public Builder signatureFactory(@Nullable SignatureFactory signatureFactory) { this.signatureFactory = signatureFactory; return this; } @NonNull @RequiresApi(Build.VERSION_CODES.M) private Goldfinger buildGoldfinger() { if (macCrypter == null && signatureCrypter == null && cipherCrypter == null) { this.cipherCrypter = new Base64CipherCrypter(); } if (macFactory == null && signatureFactory == null && cipherFactory == null) { this.cipherFactory = new AesCipherFactory(context); } AsyncCryptoObjectFactory asyncFactory = new AsyncCryptoObjectFactory( new CryptoObjectFactory(cipherFactory, macFactory, signatureFactory) ); CrypterProxy cryptoProxy = new CrypterProxy(cipherCrypter, macCrypter, signatureCrypter); return new GoldfingerImpl(context, asyncFactory, cryptoProxy); } private void ensureParamsValid() { if (macFactory != null && macCrypter == null || macFactory == null && macCrypter != null) { throw new RuntimeException( "To use CryptoObject with MacObject you must provide both MacFactory and " + "MacCrypter implementation. Use Goldfinger.Builder#macFactory(MacFactory) and " + "Goldfinger.Builder#macCrypter(MacCrypter) methods to set values." ); } if (signatureFactory != null && signatureCrypter == null || signatureFactory == null && signatureCrypter != null) { throw new RuntimeException( "To use CryptoObject with SignatureObject you must provide both SignatureFactory and " + "SignatureCrypter implementation. Use Goldfinger.Builder#signatureFactory(SignatureFactory) and " + "Goldfinger.Builder#signatureCrypter(SignatureCrypter) methods to set values." ); } } } @SuppressWarnings("WeakerAccess") class PromptParams { @NonNull private final Object dialogOwner; @Nullable private final String description; @Nullable private final String negativeButtonText; @Nullable private final String subtitle; @Nullable private final String title; private final boolean confirmationRequired; private final boolean deviceCredentialsAllowed; private final int allowedAuthenticators; private PromptParams( @NonNull Object dialogOwner, @Nullable String title, @Nullable String description, @Nullable String negativeButtonText, @Nullable String subtitle, boolean confirmationRequired, boolean deviceCredentialsAllowed, int allowedAuthenticators ) { this.dialogOwner = dialogOwner; this.title = title; this.description = description; this.negativeButtonText = negativeButtonText; this.subtitle = subtitle; this.confirmationRequired = confirmationRequired; this.deviceCredentialsAllowed = deviceCredentialsAllowed; this.allowedAuthenticators = allowedAuthenticators; } @NonNull public Object dialogOwner() { return dialogOwner; } @Nullable public String title() { return title; } @Nullable public String description() { return description; } @Nullable public String negativeButtonText() { return negativeButtonText; } @Nullable public String subtitle() { return subtitle; } public boolean confirmationRequired() { return confirmationRequired; } public boolean deviceCredentialsAllowed() { return deviceCredentialsAllowed; } public int allowedAuthenticators() { return allowedAuthenticators; } /** * Create new {@link BiometricPrompt.PromptInfo} instance. Parameter * validation is done earlier in the code so we can trust the data at * this step. */ @SuppressWarnings("ConstantConditions") @NonNull BiometricPrompt.PromptInfo buildPromptInfo() { BiometricPrompt.PromptInfo.Builder builder = new BiometricPrompt.PromptInfo.Builder() .setTitle(title) .setSubtitle(subtitle) .setDescription(description) .setAllowedAuthenticators(allowedAuthenticators) .setNegativeButtonText(negativeButtonText) .setConfirmationRequired(confirmationRequired); return builder.build(); } public static class Builder { /* Dialog dialogOwner can be either Fragment or FragmentActivity */ @NonNull private Object dialogOwner; @Nullable private String description; @Nullable private String negativeButtonText; @Nullable private String subtitle; @Nullable private String title; private boolean confirmationRequired; private int allowedAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK; public Builder(@NonNull FragmentActivity activity) { this.dialogOwner = activity; } public Builder(@NonNull Fragment fragment) { this.dialogOwner = fragment; } @NonNull public PromptParams build() { boolean deviceCredentialAllowed = (allowedAuthenticators & BiometricManager.Authenticators.DEVICE_CREDENTIAL) != 0; return new PromptParams( dialogOwner, title, description, negativeButtonText, subtitle, confirmationRequired, deviceCredentialAllowed, allowedAuthenticators ); } /** * @see BiometricPrompt.PromptInfo.Builder#setTitle */ @NonNull public Builder title(@NonNull String title) { this.title = title; return this; } /** * @see BiometricPrompt.PromptInfo.Builder#setTitle */ @NonNull public Builder title(@StringRes int resId) { this.title = getString(resId); return this; } /** * @see BiometricPrompt.PromptInfo.Builder#setDescription */ @NonNull public Builder description(@Nullable String description) { this.description = description; return this; } /** * @see BiometricPrompt.PromptInfo.Builder#setDescription */ @NonNull public Builder description(@StringRes int resId) { this.description = getString(resId); return this; } /** * @see BiometricPrompt.PromptInfo.Builder#setNegativeButtonText */ @NonNull public Builder negativeButtonText(@NonNull String negativeButtonText) { this.negativeButtonText = negativeButtonText; return this; } /** * @see BiometricPrompt.PromptInfo.Builder#setNegativeButtonText */ @NonNull public Builder negativeButtonText(@StringRes int resId) { this.negativeButtonText = getString(resId); return this; } /** * @see BiometricPrompt.PromptInfo.Builder#setSubtitle */ @NonNull public Builder subtitle(@Nullable String subtitle) { this.subtitle = subtitle; return this; } /** * @see BiometricPrompt.PromptInfo.Builder#setSubtitle */ @NonNull public Builder subtitle(@StringRes int resId) { this.subtitle = getString(resId); return this; } /** * @see BiometricPrompt.PromptInfo.Builder#setConfirmationRequired */ @NonNull public Builder confirmationRequired(boolean confirmationRequired) { this.confirmationRequired = confirmationRequired; return this; } /** * @see BiometricPrompt.PromptInfo.Builder#setAllowedAuthenticators(int) */ @NonNull public Builder allowedAuthenticators(int allowedAuthenticators) { this.allowedAuthenticators = allowedAuthenticators; return this; } @Nullable private String getString(@StringRes int resId) { if (dialogOwner instanceof Fragment) { return ((Fragment) dialogOwner).getString(resId); } if (dialogOwner instanceof FragmentActivity) { return ((FragmentActivity) dialogOwner).getString(resId); } return null; } } } /** * Result wrapper class containing all the useful information about * fingerprint authentication and value for encryption/decryption operations. */ class Result { /** * @see Goldfinger.Type */ @NonNull private final Goldfinger.Type type; /** * @see Goldfinger.Reason */ @NonNull private final Goldfinger.Reason reason; /** * Authentication value. If standard {@link Goldfinger#authenticate} method is used, * returned value is null. *

* IFF {@link Goldfinger#encrypt} or {@link Goldfinger#decrypt} * is used, the value contains encrypted or decrypted String. *

* In all other cases, the value is null. */ @Nullable private final String value; /** * System message returned by {@link BiometricPrompt.AuthenticationCallback}. * A human-readable error string that can be shown in UI. */ @Nullable private final String message; Result(@NonNull Type type, @NonNull Reason reason) { this(type, reason, null, null); } Result(@NonNull Type type, @NonNull Reason reason, @Nullable String value, @Nullable String message) { this.type = type; this.reason = reason; this.value = value; this.message = message; } @Nullable public String message() { return message; } @NonNull public Reason reason() { return reason; } @NonNull public Type type() { return type; } @Nullable public String value() { return value; } } /** * Callback used to receive Goldfinger results. */ interface Callback { /** * Returns fingerprint result and will be invoked multiple times during * fingerprint authentication as not all fingerprint results complete * the authentication. *

* Result callback invoked for every fingerprint result (success, error or info). * It can be invoked multiple times during single fingerprint authentication. * * @param result contains fingerprint result information * @see Goldfinger.Result */ void onResult(@NonNull Goldfinger.Result result); /** * Critical error happened and fingerprint authentication is stopped. */ void onError(@NonNull Exception e); } /** * Describes in detail why {@link Callback#onResult} is dispatched. */ enum Reason { /** * @see BiometricPrompt#ERROR_HW_UNAVAILABLE */ HARDWARE_UNAVAILABLE, /** * @see BiometricPrompt#ERROR_UNABLE_TO_PROCESS */ UNABLE_TO_PROCESS, /** * @see BiometricPrompt#ERROR_TIMEOUT */ TIMEOUT, /** * @see BiometricPrompt#ERROR_NO_SPACE */ NO_SPACE, /** * @see BiometricPrompt#ERROR_CANCELED */ CANCELED, /** * @see BiometricPrompt#ERROR_LOCKOUT */ LOCKOUT, /** * @see BiometricPrompt#ERROR_VENDOR */ VENDOR, /** * @see BiometricPrompt#ERROR_LOCKOUT_PERMANENT */ LOCKOUT_PERMANENT, /** * @see BiometricPrompt#ERROR_USER_CANCELED */ USER_CANCELED, /** * @see BiometricPrompt#ERROR_NO_BIOMETRICS */ NO_BIOMETRICS, /** * @see BiometricPrompt#ERROR_HW_NOT_PRESENT */ HW_NOT_PRESENT, /** * @see BiometricPrompt#ERROR_NEGATIVE_BUTTON */ NEGATIVE_BUTTON, /** * @see BiometricPrompt#ERROR_NO_DEVICE_CREDENTIAL */ NO_DEVICE_CREDENTIAL, /** * @see BiometricPrompt#ERROR_SECURITY_UPDATE_REQUIRED */ SECURITY_UPDATE_REQUIRED, /** * Dispatched when Fingerprint authentication is initialized correctly and * just before actual authentication is started. Can be used to update UI if necessary. */ AUTHENTICATION_START, /** * @see BiometricPrompt.AuthenticationCallback#onAuthenticationSucceeded */ AUTHENTICATION_SUCCESS, /** * @see BiometricPrompt.AuthenticationCallback#onAuthenticationFailed */ AUTHENTICATION_FAIL, /** * Unknown reason. */ UNKNOWN } /** * Describes the type of the result received in {@link Callback#onResult} */ enum Type { /** * Fingerprint authentication is successfully finished. {@link Goldfinger.Result} * will contain value in case of {@link PromptParams.Builder#decrypt} or * {@link PromptParams.Builder#encrypt} invocation. */ SUCCESS, /** * Fingerprint authentication is still active. {@link Goldfinger.Result} contains * additional information about currently active fingerprint authentication. */ INFO, /** * Fingerprint authentication is unsuccessfully finished. {@link Goldfinger.Result} * contains the reason why the authentication failed. */ ERROR } }