diff --git a/build.gradle b/build.gradle index 3b18457..8b3e9eb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.2.10' + ext.kotlin_version = '1.2.21' repositories { jcenter() maven { diff --git a/surelock/build.gradle b/surelock/build.gradle index 5815990..1e541ee 100644 --- a/surelock/build.gradle +++ b/surelock/build.gradle @@ -1,12 +1,12 @@ apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' android { - compileSdkVersion 25 - buildToolsVersion '25.0.1' + compileSdkVersion 27 defaultConfig { minSdkVersion 15 - targetSdkVersion 25 + targetSdkVersion 27 versionCode 1 versionName "1.0" } @@ -18,6 +18,10 @@ android { } dependencies { - compile 'com.android.support:appcompat-v7:25.3.1' + compile 'com.android.support:appcompat-v7:27.0.2' compile 'com.mattprecious.swirl:swirl:1.0.0' + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} +repositories { + mavenCentral() } diff --git a/surelock/src/main/java/com/smashingboxes/surelock/SharedPreferencesStorage.java b/surelock/src/main/java/com/smashingboxes/surelock/SharedPreferencesStorage.java deleted file mode 100644 index 85bfb6a..0000000 --- a/surelock/src/main/java/com/smashingboxes/surelock/SharedPreferencesStorage.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.smashingboxes.surelock; - -import android.content.Context; -import android.content.SharedPreferences; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.util.Base64; - -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Set; - -/** - * Created by Tyler McCraw on 3/5/17. - *
- * Storage mechanism used by Surelock in order to - * persist encrypted objects in SharedPreferences by default - *
- */ - -public class SharedPreferencesStorage implements SurelockStorage { - - private SharedPreferences preferences; - private final Context context; - private final String prefsName; - - /** - * Create a new SurelockStorage which uses SharedPreferences for persistence - * - * @param context context - * @param prefsName Desired preferences file. - */ - public SharedPreferencesStorage(Context context, String prefsName) { - this.context = context; - this.prefsName = prefsName; - } - - private synchronized SharedPreferences getPrefs() { - if (preferences == null) { - preferences = context.getSharedPreferences(prefsName, Context.MODE_PRIVATE); - } - return preferences; - } - - @Override - public void createOrUpdate(String key, @NonNull byte[] objectToStore) { - String encodedString = Base64.encodeToString(objectToStore, Base64.DEFAULT); - getPrefs().edit().putString(key, encodedString).apply(); - } - - @Nullable - @Override - public byte[] get(@NonNull String key) { - String byteString = getPrefs().getString(key, null); - byte[] decodedBytes = null; - if (!TextUtils.isEmpty(byteString)) { - decodedBytes = Base64.decode(byteString, Base64.DEFAULT); - } - return decodedBytes; - } - - @Override - public void remove(String key) { - getPrefs().edit().remove(key).apply(); - } - - @Override - public void clearAll() { - getPrefs().edit().clear().apply(); - } - - @Nullable - @Override - public Set- * Singleton class which manages authentication - * via the FingerprintManager APIs and handles - * encryption & decryption on its own. - * - * Call initialize() before any other functions so - * that Surelock can prepare for fingerprint authentication - * - * Call store() to store credentials on the user's device. - * This will handle encryption and set some things up - * for decryption later on. - * - * Call loginWithFingerprint() once Surelock has stored - * the credentials. Surelock will handle all decryption for you. - * Elementary, my dear Watson! - *
- */ - -@TargetApi(Build.VERSION_CODES.M) -public class Surelock { - - private static final String KEY_INIT_IALIZ_ATION_VEC_TOR = "com.smashingboxes.surelock.KEY_INIT_IALIZ_ATION_VEC_TOR"; - private static final String TAG = Surelock.class.getSimpleName(); - - @Retention(RetentionPolicy.SOURCE) - @IntDef({SYMMETRIC, ASYMMETRIC}) - public @interface EncryptionType {} - public static final int SYMMETRIC = 0; - public static final int ASYMMETRIC = 1; - private int encryptionType = SYMMETRIC; //TODO consider allowing developers to change this if they want - - - private SurelockFingerprintListener listener; - private FingerprintManagerCompat fingerprintManager; - private KeyStore keyStore; - private KeyGenerator keyGenerator; - private KeyPairGenerator keyPairGenerator; - private KeyFactory keyFactory; - - //Set from Builder - private SurelockStorage storage; - private final String keyStoreAlias; - private String surelockFragmentTag; - private SurelockFragment surelockFragment; - private FragmentManager fragmentManager; - private boolean useDefault; - @StyleRes - private int styleId; - - static Surelock initialize(@NonNull Builder builder) { - return new Surelock(builder); - } - - Surelock(Builder builder) { - if (builder.context instanceof SurelockFingerprintListener) { - this.listener = (SurelockFingerprintListener) builder.context; - } else { - throw new RuntimeException(builder.context.toString() - + " must implement FingerprintListener"); - } - - this.storage = builder.storage; - this.keyStoreAlias = builder.keyStoreAlias; - this.surelockFragmentTag = builder.surelockFragmentTag; - this.surelockFragment = builder.surelockFragment; - this.fragmentManager = builder.fragmentManager; - this.useDefault = builder.useDefault; - this.styleId = builder.styleId; - - try { - setUpKeyStoreForEncryption(); - } catch (SurelockException e) { - Log.e(TAG, "Failed to set up KeyStore", e); - } - - fingerprintManager = FingerprintManagerCompat.from(builder.context); - } - - /** - * Check if user's device has fingerprint hardware - * - * @return true if fingerprint hardware is detected - */ - @SuppressWarnings({"MissingPermission"}) - public static boolean hasFingerprintHardware(Context context) { - return FingerprintManagerCompat.from(context).isHardwareDetected(); - } - - /** - * Check if fingerprints have been set up for the user's device - * - * @return true if fingerprints have been enrolled. Otherwise, false. - */ - @SuppressWarnings({"MissingPermission"}) - public static boolean hasUserEnrolledFingerprints(Context context) { - return FingerprintManagerCompat.from(context).hasEnrolledFingerprints(); - } - - /** - * Check if user has set a Screen Lock via PIN, pattern or password for the device - * or a SIM card is currently locked - * - * @return true if user has set one of these screen lock methods or if the SIM card is locked. - */ - public static boolean hasUserEnabledSecureLock(Context context) { - KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class); - return keyguardManager.isKeyguardSecure(); - } - - /** - * Check if user has all of the necessary setup to allow fingerprint authentication - * to be used for this application - * - * @param showMessaging set to true if you want Surelock to handle messaging for you. - * It is recommended to set this to true. - * @return true if user has fingerprint hardware, has enabled secure lock, and has enrolled fingerprints - */ - public static boolean fingerprintAuthIsSetUp(Context context, boolean showMessaging) { - if (!hasFingerprintHardware(context)) { - return false; - } - if (!hasUserEnabledSecureLock(context)) { - if (showMessaging) { - // Show a message telling the user they haven't set up a fingerprint or lock screen. - Toast.makeText(context, context.getString(R.string.error_toast_user_enable_securelock), Toast.LENGTH_LONG).show(); - context.startActivity(new Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS)); - } - return false; - } - if (!hasUserEnrolledFingerprints(context)) { - if (showMessaging) { - // This happens when no fingerprints are registered. - Toast.makeText(context, R.string.error_toast_user_enroll_fingerprints, Toast.LENGTH_LONG).show(); - context.startActivity(new Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS)); - } - return false; - } - return true; - } - - /** - * Encrypt a value and store it at the specified key - * - * @param key pointer in storage to encrypted value - * @param value value to be encrypted and stored - */ - public void store(String key, byte[] value) throws SurelockException { - initKeyStoreKey(); - Cipher cipher; - try { - cipher = initCipher(Cipher.ENCRYPT_MODE); - } catch (InvalidKeyException | UnrecoverableKeyException | KeyStoreException e) { - throw new SurelockException("Failed to init Cipher for encryption", null); - } - try { - final byte[] encryptedValue = cipher.doFinal(value); - storage.createOrUpdate(key, encryptedValue); - } catch (IllegalBlockSizeException | BadPaddingException e) { - Log.e(TAG, "Encryption failed", e); - } - } - - /** - * Enroll a fingerprint, encrypt a value, and store the value at the specified key - * - * @param key he key where encrypted values are stored - * @param valueToEncrypt The value to encrypt and store - * @throws SurelockException - */ - public void enrollFingerprintAndStore(@NonNull String key, @NonNull byte[] valueToEncrypt) throws SurelockException { - initKeyStoreKey(); - Cipher cipher; - try { - try { - cipher = initCipher(Cipher.ENCRYPT_MODE); - } catch (InvalidKeyException | UnrecoverableKeyException | KeyStoreException e) { - throw new SurelockException("Failed to init Cipher for encryption", e); - } - } catch (RuntimeException e) { - listener.onFingerprintError(null); //TODO we need better management of all of these listeners passed everywhere. - return; - } - - if (cipher != null) { - showFingerprintDialog(key, cipher, getSurelockFragment(true), valueToEncrypt); - } else { - throw new SurelockException("Failed to init Cipher for encryption", null); - } - } - - /** - * Log in using fingerprint authentication - * - * @param key The key where encrypted values are stored - * @throws SurelockInvalidKeyException If the cipher could not be initialized - */ - public void loginWithFingerprint(@NonNull String key) throws SurelockInvalidKeyException { - Cipher cipher; - try { - cipher = initCipher(Cipher.DECRYPT_MODE); - } catch (InvalidKeyException | UnrecoverableKeyException | KeyStoreException e) { - // Key may be invalid due to new fingerprint enrollment - // Try taking the user back through a new enrollment - throw new SurelockInvalidKeyException("Failed to init Cipher. Key may be invalidated. Try re-enrolling.", null); - } catch (RuntimeException e) { - listener.onFingerprintError(null); //TODO we need better management of all of these listeners passed everywhere. - return; - } - - if (cipher != null) { - showFingerprintDialog(key, cipher, getSurelockFragment(false), null); - } else { - throw new SurelockInvalidKeyException("Failed to init Cipher. Key may be invalidated. Try re-enrolling.", null); - } - } - - private SurelockFragment getSurelockFragment(boolean isEnrolling) { - if (surelockFragment != null) { - return surelockFragment; - } - if (useDefault) { - return SurelockDefaultDialog.newInstance(isEnrolling ? Cipher.ENCRYPT_MODE : Cipher - .DECRYPT_MODE, styleId); - } else { - return SurelockMaterialDialog.newInstance(isEnrolling ? Cipher.ENCRYPT_MODE : Cipher - .DECRYPT_MODE); - } - } - - private void showFingerprintDialog(String key, @NonNull Cipher cipher, SurelockFragment - surelockFragment, @Nullable byte[] valueToEncrypt) { - surelockFragment.init(fingerprintManager, new FingerprintManagerCompat.CryptoObject(cipher), - key, storage, valueToEncrypt); - surelockFragment.show(fragmentManager, surelockFragmentTag); - } - - /** - * Initialize our KeyStore w/ the default security provider - * Initialize a KeyGenerator using either RSA for asymmetric or AES for symmetric - */ - private void setUpKeyStoreForEncryption() throws SurelockException { - // NOTE: "AndroidKeyStore" is only supported in APIs 18+, - // but since the FingerprintManager APIs support 23+, this doesn't matter. - // https://developer.android.com/reference/java/security/KeyStore.html - final String keyStoreProvider = "AndroidKeyStore"; - try { - keyStore = KeyStore.getInstance(keyStoreProvider); - keyStore.load(null); - } catch (KeyStoreException e) { - throw new SurelockException("Failed to get an instance of KeyStore", e); - } catch (IOException | NoSuchAlgorithmException | CertificateException e) { - throw new SurelockException("Failed to load keystore", e); - } - try { - if (encryptionType == ASYMMETRIC) { - keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, keyStoreProvider); - keyFactory = KeyFactory.getInstance("RSA"); - } else { - keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, keyStoreProvider); - } - } catch (NoSuchAlgorithmException | NoSuchProviderException e) { - throw new SurelockException("Failed to get an instance of KeyGenerator", e); - } - } - - private Cipher getCipherInstance() throws NoSuchAlgorithmException, NoSuchPaddingException { - if (encryptionType == ASYMMETRIC) { - return Cipher.getInstance( - KeyProperties.KEY_ALGORITHM_RSA + "/" - + KeyProperties.BLOCK_MODE_ECB + "/" - + KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1); - } else { - return Cipher.getInstance( - KeyProperties.KEY_ALGORITHM_AES + "/" - + KeyProperties.BLOCK_MODE_CBC + "/" - + KeyProperties.ENCRYPTION_PADDING_PKCS7); - } - } - - /** - * Creates a KeyStore key which can only be used after the user has - * authenticated with their fingerprint. - * - * @param keyName the name of the key to be created - * @param invalidatedByBiometricEnrollment if {@code false} is passed, the created key will not be invalidated - * even if a new fingerprint is enrolled. The default value is {@code true}, - * so passing {@code true} doesn't change the behavior (the key will be - * invalidated if a new fingerprint is enrolled.). - * Note: this parameter is only valid if the app works on Android N developer preview. - */ - private void generateKeyStoreKey(@NonNull String keyName, - boolean invalidatedByBiometricEnrollment) throws SurelockException { - try { - if (encryptionType == ASYMMETRIC) { - keyPairGenerator.initialize( - new KeyGenParameterSpec.Builder(keyStoreAlias, - KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) - .setBlockModes(KeyProperties.BLOCK_MODE_ECB) - .setUserAuthenticationRequired(true) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) - .build()); - - keyPairGenerator.generateKeyPair(); - } else { - // Set the alias of the entry in Android KeyStore where the key will appear - // and the constraints (purposes) in the constructor of the Builder - KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(keyName, - KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) - .setBlockModes(KeyProperties.BLOCK_MODE_CBC) -// .setKeySize(256) //TODO figure out if this is proper key size - // Require the user to authenticate with a fingerprint to authorize every use of the key - .setUserAuthenticationRequired(true) -// .setUserAuthenticationValidityDurationSeconds(AUTHENTICATION_DURATION_SECONDS) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7); - - // This is a workaround to avoid crashes on devices whose API level is < 24 - // because KeyGenParameterSpec.Builder#setInvalidatedByBiometricEnrollment is only visible on API level +24. - // Ideally there should be a compat library for KeyGenParameterSpec.Builder but - // which isn't available yet. -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { -// builder.setInvalidatedByBiometricEnrollment(invalidatedByBiometricEnrollment); -// } - keyGenerator.init(builder.build()); - keyGenerator.generateKey(); - } - } catch (InvalidAlgorithmParameterException | NullPointerException e) { - throw new SurelockException("Failed to generate a key", e); - } - } - - private PublicKey getPublicKey() throws KeyStoreException, InvalidKeySpecException { - PublicKey publicKey = keyStore.getCertificate(keyStoreAlias).getPublicKey(); - KeySpec spec = new X509EncodedKeySpec(publicKey.getEncoded()); - return keyFactory.generatePublic(spec); - } - - private PrivateKey getPrivateKey() throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException { - return (PrivateKey) keyStore.getKey(keyStoreAlias, null); - } - - /** - * Initialize a Key for our KeyStore. - * NOTE: It won't recreate one if a valid key already exists. - */ - private void initKeyStoreKey() { - try { - SecretKey secretKey = (SecretKey) keyStore.getKey(keyStoreAlias, null); - // Check to see if we need to create a new KeyStore key - if (secretKey != null) { - try { - if (encryptionType == ASYMMETRIC) { - getCipherInstance().init(Cipher.DECRYPT_MODE, secretKey); - return; - } else { - byte[] encryptionIv = getEncryptionIv(); - if (encryptionIv != null) { - getCipherInstance().init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(encryptionIv)); - return; - } - } - } catch (KeyPermanentlyInvalidatedException e) { - Log.d(TAG, "Keys were invalidated. Creating new key..."); - } - } - - storage.clearAll(); - - //Create a new key - generateKeyStoreKey(keyStoreAlias, true); - } catch (GeneralSecurityException e) { - throw new SurelockException("Surelock: Failed to prepare KeyStore for encryption", e); - } catch (NoClassDefFoundError e) { - throw new SurelockException("Surelock: API 23 or higher required.", e); - } - } - - /** - * Initialize a Cipher for encryption - * - * @param opmode the operation mode of this cipher (this is one of - * the following: - *ENCRYPT_MODE, DECRYPT_MODE)
- * @return Cipher object to be used for encryption
- */
- private Cipher initCipher(int opmode) throws InvalidKeyException, UnrecoverableKeyException, KeyStoreException {
- Cipher cipher;
- try {
- cipher = getCipherInstance();
- if (encryptionType == ASYMMETRIC) {
- cipher.init(opmode, opmode == Cipher.ENCRYPT_MODE ? getPublicKey() : getPrivateKey());
- } else {
- SecretKey secretKey = (SecretKey) keyStore.getKey(keyStoreAlias, null);
- if (opmode == Cipher.ENCRYPT_MODE) {
- cipher.init(opmode, secretKey);
- setEncryptionIv(cipher.getIV());
- } else {
- cipher.init(opmode, secretKey, new IvParameterSpec(getEncryptionIv()));
- }
- }
- } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeySpecException e) {
- throw new SurelockException("Surelock: Failed to prepare Cipher for encryption", e);
- }
- return cipher;
- }
-
- private void setEncryptionIv(byte[] encryptionIv) {
- storage.createOrUpdate(KEY_INIT_IALIZ_ATION_VEC_TOR, encryptionIv);
- }
-
- /**
- * Get the Initialization Vector to be used for encryption/decryption
- * The IV needs to be persisted if used for encryption, since it will be required for decryption
- * @return initialization vector as byte array
- */
- @Nullable
- private byte[] getEncryptionIv() {
- return storage.get(KEY_INIT_IALIZ_ATION_VEC_TOR);
- }
-
- public static class Builder {
-
- private Context context;
- private FragmentManager fragmentManager;
- private String surelockFragmentTag;
- private SurelockFragment surelockFragment;
- private boolean useDefault;
- @StyleRes
- private int styleId;
- private String keyStoreAlias;
- private SurelockStorage storage;
-
- public Builder(@NonNull Context context) {
- this.context = context;
- }
-
- /**
- * Indicates that fingerprint login should be prompted using the SurelockDefaultDialog
- * class. This is a fullscreen dialog that can be styled to match an app's theme.
- *
- * @param styleId The style resource file to be used for styling the dialog
- * @return This Builder to allow for method chaining
- */
- public Builder withDefaultDialog(@StyleRes int styleId) {
- useDefault = true;
- surelockFragment = null;
- this.styleId = styleId;
- return this;
- }
-
- /**
- * Indicates that fingerprint login should be prompted using the SurelockMaterialDialog.
- * This dialog follows Material Design guidelines.
- *
- * @return This Builder to allow for method chaining
- */
- public Builder withMaterialDialog() {
- useDefault = false;
- surelockFragment = null;
- return this;
- }
-
- /**
- * Indicates that fingerprint login should be prompted using the given dialog.
- *
- * @param surelockFragment The custom dialog to use for fingerprint login
- * @return This Builder to allow for method chaining
- */
- public Builder withCustomDialog(@NonNull SurelockFragment surelockFragment) {
- this.surelockFragment = surelockFragment;
- return this;
- }
-
- /**
- * Indicates the tag to use for the SurelockFragment. This method MUST be called before
- * enrolling and logging in.
- *
- * @param surelockFragmentTag The tag to use
- * @return This Builder to allow for method chaining
- */
- public Builder withSurelockFragmentTag(@NonNull String surelockFragmentTag) {
- this.surelockFragmentTag = surelockFragmentTag;
- return this;
- }
-
- /**
- * Indicates the fragment manager to use to manage the SurelockFragment. This method MUST
- * be called before enrolling and logging in.
- *
- * @param fragmentManager The fragment manager to use
- * @return This Builder to allow for method chaining
- */
- public Builder withFragmentManager(@NonNull FragmentManager fragmentManager) {
- this.fragmentManager = fragmentManager;
- return this;
- }
-
- /**
- * Indicates the alias to use for the keystore when using fingerprint login. This method
- * MUST be called before enrolling and logging in.
- *
- * @param keyStoreAlias The keystore alias to use
- * @return This Builder to allow for method chaining
- */
- public Builder withKeystoreAlias(@NonNull String keyStoreAlias) {
- this.keyStoreAlias = keyStoreAlias;
- return this;
- }
-
- /**
- * Indicates the SurelockStorage instance to use with fingerprint login. This method MUST
- * be called before enrolling and logging in.
- *
- * @param storage The SurelockStorage instance to use
- * @return This Builder to allow for method chaining
- */
- public Builder withSurelockStorage(@NonNull SurelockStorage storage) {
- this.storage = storage;
- return this;
- }
-
- /**
- * Creates the Surelock instance
- */
- public Surelock build() {
- checkFields();
- return Surelock.initialize(this);
- }
-
- private void checkFields() {
- if (TextUtils.isEmpty(keyStoreAlias)) {
- throw new IllegalStateException("The keystore alias cannot be empty.");
- }
- if (storage == null) {
- throw new IllegalStateException("SurelockStorage cannot be null.");
- }
- if (TextUtils.isEmpty(surelockFragmentTag)) {
- throw new IllegalStateException("The dialog fragment tag cannot be empty.");
- }
- if (fragmentManager == null) {
- throw new IllegalStateException("The fragment manager cannot be empty.");
- }
- }
-
- }
-
-}
diff --git a/surelock/src/main/java/com/smashingboxes/surelock/Surelock.kt b/surelock/src/main/java/com/smashingboxes/surelock/Surelock.kt
new file mode 100644
index 0000000..405376b
--- /dev/null
+++ b/surelock/src/main/java/com/smashingboxes/surelock/Surelock.kt
@@ -0,0 +1,646 @@
+package com.smashingboxes.surelock
+
+import android.annotation.TargetApi
+import android.app.FragmentManager
+import android.app.KeyguardManager
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyPermanentlyInvalidatedException
+import android.security.keystore.KeyProperties
+import android.support.annotation.IntDef
+import android.support.annotation.StyleRes
+import android.support.v4.hardware.fingerprint.FingerprintManagerCompat
+import android.util.Log
+import android.widget.Toast
+
+import java.io.IOException
+import java.security.GeneralSecurityException
+import java.security.InvalidAlgorithmParameterException
+import java.security.InvalidKeyException
+import java.security.KeyFactory
+import java.security.KeyPairGenerator
+import java.security.KeyStore
+import java.security.KeyStoreException
+import java.security.NoSuchAlgorithmException
+import java.security.NoSuchProviderException
+import java.security.PrivateKey
+import java.security.PublicKey
+import java.security.UnrecoverableKeyException
+import java.security.cert.CertificateException
+import java.security.spec.InvalidKeySpecException
+import java.security.spec.X509EncodedKeySpec
+
+import javax.crypto.BadPaddingException
+import javax.crypto.Cipher
+import javax.crypto.IllegalBlockSizeException
+import javax.crypto.KeyGenerator
+import javax.crypto.NoSuchPaddingException
+import javax.crypto.SecretKey
+import javax.crypto.spec.IvParameterSpec
+
+/**
+ * Created by Tyler McCraw on 2/17/17.
+ *
+ *
+ * Singleton class which manages authentication
+ * via the FingerprintManager APIs and handles
+ * encryption & decryption on its own.
+ *
+ * Call initialize() before any other functions so
+ * that Surelock can prepare for fingerprint authentication
+ *
+ * Call store() to store credentials on the user's device.
+ * This will handle encryption and set some things up
+ * for decryption later on.
+ *
+ * Call loginWithFingerprint() once Surelock has stored
+ * the credentials. Surelock will handle all decryption for you.
+ * Elementary, my dear Watson!
+ *
+ */
+
+@TargetApi(Build.VERSION_CODES.M)
+class Surelock internal constructor(builder: Builder) {
+ private val encryptionType = SYMMETRIC //TODO consider allowing developers to change this if they want
+
+
+ private var listener: SurelockFingerprintListener? = null
+ private val fingerprintManager: FingerprintManagerCompat
+ private lateinit var keyStore: KeyStore
+ private var keyGenerator: KeyGenerator? = null
+ private var keyPairGenerator: KeyPairGenerator? = null
+ private lateinit var keyFactory: KeyFactory
+
+ //Set from Builder
+ private val storage: SurelockStorage?
+ private val keyStoreAlias: String?
+ private val surelockFragmentTag: String?
+ private val surelockFragment: SurelockFragment?
+ private val fragmentManager: FragmentManager?
+ private val useDefault: Boolean
+ @StyleRes
+ private val styleId: Int
+
+ private val cipherInstance: Cipher
+ @Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class)
+ get() = if (encryptionType == ASYMMETRIC) {
+ Cipher.getInstance(
+ KeyProperties.KEY_ALGORITHM_RSA + "/"
+ + KeyProperties.BLOCK_MODE_ECB + "/"
+ + KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
+ } else {
+ Cipher.getInstance(
+ KeyProperties.KEY_ALGORITHM_AES + "/"
+ + KeyProperties.BLOCK_MODE_CBC + "/"
+ + KeyProperties.ENCRYPTION_PADDING_PKCS7)
+ }
+
+ private val publicKey: PublicKey
+ @Throws(KeyStoreException::class, InvalidKeySpecException::class)
+ get() {
+ val publicKey = keyStore.getCertificate(keyStoreAlias).publicKey
+ val spec = X509EncodedKeySpec(publicKey.encoded)
+ return keyFactory.generatePublic(spec)
+ }
+
+ private val privateKey: PrivateKey
+ @Throws(NoSuchAlgorithmException::class, UnrecoverableKeyException::class,
+ KeyStoreException::class)
+ get() = keyStore.getKey(keyStoreAlias, null) as PrivateKey
+
+ /**
+ * Get the Initialization Vector to be used for encryption/decryption
+ * The IV needs to be persisted if used for encryption, since it will be required for decryption
+ * @return initialization vector as byte array
+ */
+ private var encryptionIv: ByteArray?
+ get() = storage?.get(KEY_INIT_IALIZ_ATION_VEC_TOR)
+ set(encryptionIv) = storage!!.createOrUpdate(KEY_INIT_IALIZ_ATION_VEC_TOR, encryptionIv ?: ByteArray(0))
+
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(SYMMETRIC.toLong(), ASYMMETRIC.toLong())
+ annotation class EncryptionType
+
+ init {
+ if (builder.context is SurelockFingerprintListener) {
+ this.listener = builder.context
+ } else {
+ throw RuntimeException(
+ builder.context.toString() + " must implement FingerprintListener")
+ }
+
+ this.storage = builder.storage
+ this.keyStoreAlias = builder.keyStoreAlias
+ this.surelockFragmentTag = builder.surelockFragmentTag
+ this.surelockFragment = builder.surelockFragment
+ this.fragmentManager = builder.fragmentManager
+ this.useDefault = builder.useDefault
+ this.styleId = builder.styleId
+
+ try {
+ setUpKeyStoreForEncryption()
+ } catch (e: SurelockException) {
+ Log.e(TAG, "Failed to set up KeyStore", e)
+ }
+
+ fingerprintManager = FingerprintManagerCompat.from(builder.context)
+ }
+
+ /**
+ * Encrypt a value and store it at the specified key
+ *
+ * @param key pointer in storage to encrypted value
+ * @param value value to be encrypted and stored
+ */
+ @Throws(SurelockException::class)
+ fun store(key: String, value: ByteArray) {
+ initKeyStoreKey()
+ val cipher: Cipher
+ try {
+ cipher = initCipher(Cipher.ENCRYPT_MODE)
+ } catch (e: InvalidKeyException) {
+ throw SurelockException("Failed to init Cipher for encryption", null)
+ } catch (e: UnrecoverableKeyException) {
+ throw SurelockException("Failed to init Cipher for encryption", null)
+ } catch (e: KeyStoreException) {
+ throw SurelockException("Failed to init Cipher for encryption", null)
+ }
+
+ try {
+ val encryptedValue = cipher.doFinal(value)
+ storage?.createOrUpdate(key, encryptedValue)
+ } catch (e: IllegalBlockSizeException) {
+ Log.e(TAG, "Encryption failed", e)
+ } catch (e: BadPaddingException) {
+ Log.e(TAG, "Encryption failed", e)
+ }
+
+ }
+
+ /**
+ * Enroll a fingerprint, encrypt a value, and store the value at the specified key
+ *
+ * @param key he key where encrypted values are stored
+ * @param valueToEncrypt The value to encrypt and store
+ * @throws SurelockException
+ */
+ @Throws(SurelockException::class)
+ fun enrollFingerprintAndStore(key: String, valueToEncrypt: ByteArray) {
+ initKeyStoreKey()
+ val cipher: Cipher?
+ try {
+ try {
+ cipher = initCipher(Cipher.ENCRYPT_MODE)
+ } catch (e: InvalidKeyException) {
+ throw SurelockException("Failed to init Cipher for encryption", e)
+ } catch (e: UnrecoverableKeyException) {
+ throw SurelockException("Failed to init Cipher for encryption", e)
+ } catch (e: KeyStoreException) {
+ throw SurelockException("Failed to init Cipher for encryption", e)
+ }
+
+ } catch (e: RuntimeException) {
+ listener?.onFingerprintError(
+ null) //TODO we need better management of all of these listeners passed everywhere.
+ return
+ }
+
+ if (cipher != null) {
+ showFingerprintDialog(key, cipher, getSurelockFragment(true), valueToEncrypt)
+ } else {
+ throw SurelockException("Failed to init Cipher for encryption", null)
+ }
+ }
+
+ /**
+ * Log in using fingerprint authentication
+ *
+ * @param key The key where encrypted values are stored
+ * @throws SurelockInvalidKeyException If the cipher could not be initialized
+ */
+ @Throws(SurelockInvalidKeyException::class)
+ fun loginWithFingerprint(key: String) {
+ val cipher: Cipher?
+ try {
+ cipher = initCipher(Cipher.DECRYPT_MODE)
+ } catch (e: InvalidKeyException) {
+ // Key may be invalid due to new fingerprint enrollment
+ // Try taking the user back through a new enrollment
+ throw SurelockInvalidKeyException(
+ "Failed to init Cipher. Key may be invalidated. Try re-enrolling.", null)
+ } catch (e: UnrecoverableKeyException) {
+ throw SurelockInvalidKeyException(
+ "Failed to init Cipher. Key may be invalidated. Try re-enrolling.", null)
+ } catch (e: KeyStoreException) {
+ throw SurelockInvalidKeyException(
+ "Failed to init Cipher. Key may be invalidated. Try re-enrolling.", null)
+ } catch (e: RuntimeException) {
+ listener?.onFingerprintError(
+ null) //TODO we need better management of all of these listeners passed everywhere.
+ return
+ }
+
+ if (cipher != null) {
+ showFingerprintDialog(key, cipher, getSurelockFragment(false), null)
+ } else {
+ throw SurelockInvalidKeyException(
+ "Failed to init Cipher. Key may be invalidated. Try re-enrolling.", null)
+ }
+ }
+
+ private fun getSurelockFragment(isEnrolling: Boolean): SurelockFragment {
+ if (surelockFragment != null) {
+ return surelockFragment
+ }
+ return if (useDefault) {
+ SurelockDefaultDialog.newInstance(if (isEnrolling)
+ Cipher.ENCRYPT_MODE
+ else
+ Cipher.DECRYPT_MODE, styleId)
+ } else {
+ SurelockMaterialDialog.newInstance(if (isEnrolling)
+ Cipher.ENCRYPT_MODE
+ else
+ Cipher.DECRYPT_MODE)
+ }
+ }
+
+ private fun showFingerprintDialog(key: String, cipher: Cipher,
+ surelockFragment: SurelockFragment,
+ valueToEncrypt: ByteArray?) {
+ surelockFragment.init(fingerprintManager, FingerprintManagerCompat.CryptoObject(cipher),
+ key, storage!!, valueToEncrypt)
+ surelockFragment.show(fragmentManager!!, surelockFragmentTag!!)
+ }
+
+ /**
+ * Initialize our KeyStore w/ the default security provider
+ * Initialize a KeyGenerator using either RSA for asymmetric or AES for symmetric
+ */
+ @Throws(SurelockException::class)
+ private fun setUpKeyStoreForEncryption() {
+ // NOTE: "AndroidKeyStore" is only supported in APIs 18+,
+ // but since the FingerprintManager APIs support 23+, this doesn't matter.
+ // https://developer.android.com/reference/java/security/KeyStore.html
+ val keyStoreProvider = "AndroidKeyStore"
+ try {
+ keyStore = KeyStore.getInstance(keyStoreProvider)
+ keyStore?.load(null)
+ } catch (e: KeyStoreException) {
+ throw SurelockException("Failed to get an instance of KeyStore", e)
+ } catch (e: IOException) {
+ throw SurelockException("Failed to load keystore", e)
+ } catch (e: NoSuchAlgorithmException) {
+ throw SurelockException("Failed to load keystore", e)
+ } catch (e: CertificateException) {
+ throw SurelockException("Failed to load keystore", e)
+ }
+
+ try {
+ if (encryptionType == ASYMMETRIC) {
+ keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA,
+ keyStoreProvider)
+ keyFactory = KeyFactory.getInstance("RSA")
+ } else {
+ keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES,
+ keyStoreProvider)
+ }
+ } catch (e: NoSuchAlgorithmException) {
+ throw SurelockException("Failed to get an instance of KeyGenerator", e)
+ } catch (e: NoSuchProviderException) {
+ throw SurelockException("Failed to get an instance of KeyGenerator", e)
+ }
+
+ }
+
+ /**
+ * Creates a KeyStore key which can only be used after the user has
+ * authenticated with their fingerprint.
+ *
+ * @param keyName the name of the key to be created
+ * @param invalidatedByBiometricEnrollment if `false` is passed, the created key will not be invalidated
+ * even if a new fingerprint is enrolled. The default value is `true`,
+ * so passing `true` doesn't change the behavior (the key will be
+ * invalidated if a new fingerprint is enrolled.).
+ * Note: this parameter is only valid if the app works on Android N developer preview.
+ */
+ @Throws(SurelockException::class)
+ private fun generateKeyStoreKey(keyName: String,
+ invalidatedByBiometricEnrollment: Boolean) {
+ try {
+ if (encryptionType == ASYMMETRIC) {
+ keyPairGenerator?.initialize(
+ KeyGenParameterSpec.Builder(keyStoreAlias!!,
+ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_ECB)
+ .setUserAuthenticationRequired(true)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
+ .build())
+
+ keyPairGenerator?.generateKeyPair()
+ } else {
+ // Set the alias of the entry in Android KeyStore where the key will appear
+ // and the constraints (purposes) in the constructor of the Builder
+ val builder = KeyGenParameterSpec.Builder(keyName,
+ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
+ // .setKeySize(256) //TODO figure out if this is proper key size
+ // Require the user to authenticate with a fingerprint to authorize every use of the key
+ .setUserAuthenticationRequired(true)
+ // .setUserAuthenticationValidityDurationSeconds(AUTHENTICATION_DURATION_SECONDS)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
+
+ // This is a workaround to avoid crashes on devices whose API level is < 24
+ // because KeyGenParameterSpec.Builder#setInvalidatedByBiometricEnrollment is only visible on API level +24.
+ // Ideally there should be a compat library for KeyGenParameterSpec.Builder but
+ // which isn't available yet.
+ // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ // builder.setInvalidatedByBiometricEnrollment(invalidatedByBiometricEnrollment);
+ // }
+ keyGenerator?.init(builder.build())
+ keyGenerator?.generateKey()
+ }
+ } catch (e: InvalidAlgorithmParameterException) {
+ throw SurelockException("Failed to generate a key", e)
+ } catch (e: NullPointerException) {
+ throw SurelockException("Failed to generate a key", e)
+ }
+
+ }
+
+ /**
+ * Initialize a Key for our KeyStore.
+ * NOTE: It won't recreate one if a valid key already exists.
+ */
+ private fun initKeyStoreKey() {
+ try {
+ val secretKey = keyStore.getKey(keyStoreAlias, null) as SecretKey?
+ // Check to see if we need to create a new KeyStore key
+ if (secretKey != null) {
+ try {
+ if (encryptionType == ASYMMETRIC) {
+ cipherInstance.init(Cipher.DECRYPT_MODE, secretKey)
+ return
+ } else {
+ val encryptionIv = encryptionIv
+ if (encryptionIv != null) {
+ cipherInstance.init(Cipher.DECRYPT_MODE, secretKey,
+ IvParameterSpec(encryptionIv))
+ return
+ }
+ }
+ } catch (e: KeyPermanentlyInvalidatedException) {
+ Log.d(TAG, "Keys were invalidated. Creating new key...")
+ }
+
+ }
+
+ storage?.clearAll()
+
+ //Create a new key
+ generateKeyStoreKey(keyStoreAlias!!, true)
+ } catch (e: GeneralSecurityException) {
+ throw SurelockException("Surelock: Failed to prepare KeyStore for encryption", e)
+ } catch (e: NoClassDefFoundError) {
+ throw SurelockException("Surelock: API 23 or higher required.", e)
+ }
+
+ }
+
+ /**
+ * Initialize a Cipher for encryption
+ *
+ * @param opmode the operation mode of this cipher (this is one of
+ * the following:
+ * `ENCRYPT_MODE`, `DECRYPT_MODE`)
+ * @return Cipher object to be used for encryption
+ */
+ @Throws(InvalidKeyException::class, UnrecoverableKeyException::class, KeyStoreException::class)
+ private fun initCipher(opmode: Int): Cipher {
+ val cipher: Cipher
+ try {
+ cipher = cipherInstance
+ if (encryptionType == ASYMMETRIC) {
+ cipher.init(opmode, if (opmode == Cipher.ENCRYPT_MODE) publicKey else privateKey)
+ } else {
+ val secretKey = keyStore.getKey(keyStoreAlias, null) as SecretKey?
+ if (opmode == Cipher.ENCRYPT_MODE) {
+ cipher.init(opmode, secretKey)
+ encryptionIv = cipher.iv
+ } else {
+ cipher.init(opmode, secretKey, IvParameterSpec(encryptionIv))
+ }
+ }
+ } catch (e: NoSuchAlgorithmException) {
+ throw SurelockException("Surelock: Failed to prepare Cipher for encryption", e)
+ } catch (e: NoSuchPaddingException) {
+ throw SurelockException("Surelock: Failed to prepare Cipher for encryption", e)
+ } catch (e: InvalidAlgorithmParameterException) {
+ throw SurelockException("Surelock: Failed to prepare Cipher for encryption", e)
+ } catch (e: InvalidKeySpecException) {
+ throw SurelockException("Surelock: Failed to prepare Cipher for encryption", e)
+ }
+
+ return cipher
+ }
+
+ class Builder(val context: Context) {
+ var fragmentManager: FragmentManager? = null
+ var surelockFragmentTag: String? = null
+ var surelockFragment: SurelockFragment? = null
+ var useDefault: Boolean = false
+ @StyleRes
+ var styleId: Int = 0
+ var keyStoreAlias: String? = null
+ var storage: SurelockStorage? = null
+
+ /**
+ * Indicates that fingerprint login should be prompted using the SurelockDefaultDialog
+ * class. This is a fullscreen dialog that can be styled to match an app's theme.
+ *
+ * @param styleId The style resource file to be used for styling the dialog
+ * @return This Builder to allow for method chaining
+ */
+ fun withDefaultDialog(@StyleRes styleId: Int): Builder {
+ useDefault = true
+ surelockFragment = null
+ this.styleId = styleId
+ return this
+ }
+
+ /**
+ * Indicates that fingerprint login should be prompted using the SurelockMaterialDialog.
+ * This dialog follows Material Design guidelines.
+ *
+ * @return This Builder to allow for method chaining
+ */
+ fun withMaterialDialog(): Builder {
+ useDefault = false
+ surelockFragment = null
+ return this
+ }
+
+ /**
+ * Indicates that fingerprint login should be prompted using the given dialog.
+ *
+ * @param surelockFragment The custom dialog to use for fingerprint login
+ * @return This Builder to allow for method chaining
+ */
+ fun withCustomDialog(surelockFragment: SurelockFragment): Builder {
+ this.surelockFragment = surelockFragment
+ return this
+ }
+
+ /**
+ * Indicates the tag to use for the SurelockFragment. This method MUST be called before
+ * enrolling and logging in.
+ *
+ * @param surelockFragmentTag The tag to use
+ * @return This Builder to allow for method chaining
+ */
+ fun withSurelockFragmentTag(surelockFragmentTag: String): Builder {
+ this.surelockFragmentTag = surelockFragmentTag
+ return this
+ }
+
+ /**
+ * Indicates the fragment manager to use to manage the SurelockFragment. This method MUST
+ * be called before enrolling and logging in.
+ *
+ * @param fragmentManager The fragment manager to use
+ * @return This Builder to allow for method chaining
+ */
+ fun withFragmentManager(fragmentManager: FragmentManager): Builder {
+ this.fragmentManager = fragmentManager
+ return this
+ }
+
+ /**
+ * Indicates the alias to use for the keystore when using fingerprint login. This method
+ * MUST be called before enrolling and logging in.
+ *
+ * @param keyStoreAlias The keystore alias to use
+ * @return This Builder to allow for method chaining
+ */
+ fun withKeystoreAlias(keyStoreAlias: String): Builder {
+ this.keyStoreAlias = keyStoreAlias
+ return this
+ }
+
+ /**
+ * Indicates the SurelockStorage instance to use with fingerprint login. This method MUST
+ * be called before enrolling and logging in.
+ *
+ * @param storage The SurelockStorage instance to use
+ * @return This Builder to allow for method chaining
+ */
+ fun withSurelockStorage(storage: SurelockStorage): Builder {
+ this.storage = storage
+ return this
+ }
+
+ /**
+ * Creates the Surelock instance
+ */
+ fun build(): Surelock {
+ checkFields()
+ return Surelock.initialize(this)
+ }
+
+ private fun checkFields() {
+ if (keyStoreAlias.isNullOrEmpty()) {
+ throw IllegalStateException("The keystore alias cannot be empty.")
+ }
+ if (storage == null) {
+ throw IllegalStateException("SurelockStorage cannot be null.")
+ }
+ if (surelockFragmentTag.isNullOrEmpty()) {
+ throw IllegalStateException("The dialog fragment tag cannot be empty.")
+ }
+ if (fragmentManager == null) {
+ throw IllegalStateException("The fragment manager cannot be empty.")
+ }
+ }
+
+ }
+
+ companion object {
+
+ private val KEY_INIT_IALIZ_ATION_VEC_TOR = "com.smashingboxes.surelock.KEY_INIT_IALIZ_ATION_VEC_TOR"
+ private val TAG = Surelock::class.java.simpleName
+ const val SYMMETRIC = 0
+ const val ASYMMETRIC = 1
+
+ internal fun initialize(builder: Builder): Surelock {
+ return Surelock(builder)
+ }
+
+ /**
+ * Check if user's device has fingerprint hardware
+ *
+ * @return true if fingerprint hardware is detected
+ */
+ fun hasFingerprintHardware(context: Context): Boolean {
+ return FingerprintManagerCompat.from(context).isHardwareDetected
+ }
+
+ /**
+ * Check if fingerprints have been set up for the user's device
+ *
+ * @return true if fingerprints have been enrolled. Otherwise, false.
+ */
+ fun hasUserEnrolledFingerprints(context: Context): Boolean {
+ return FingerprintManagerCompat.from(context).hasEnrolledFingerprints()
+ }
+
+ /**
+ * Check if user has set a Screen Lock via PIN, pattern or password for the device
+ * or a SIM card is currently locked
+ *
+ * @return true if user has set one of these screen lock methods or if the SIM card is locked.
+ */
+ fun hasUserEnabledSecureLock(context: Context): Boolean {
+ val keyguardManager = context.getSystemService(KeyguardManager::class.java)
+ return keyguardManager.isKeyguardSecure
+ }
+
+ /**
+ * Check if user has all of the necessary setup to allow fingerprint authentication
+ * to be used for this application
+ *
+ * @param showMessaging set to true if you want Surelock to handle messaging for you.
+ * It is recommended to set this to true.
+ * @return true if user has fingerprint hardware, has enabled secure lock, and has enrolled fingerprints
+ */
+ fun fingerprintAuthIsSetUp(context: Context, showMessaging: Boolean): Boolean {
+ if (!hasFingerprintHardware(context)) {
+ return false
+ }
+ if (!hasUserEnabledSecureLock(context)) {
+ if (showMessaging) {
+ // Show a message telling the user they haven't set up a fingerprint or lock screen.
+ Toast.makeText(context,
+ context.getString(R.string.error_toast_user_enable_securelock),
+ Toast.LENGTH_LONG).show()
+ context.startActivity(
+ Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS))
+ }
+ return false
+ }
+ if (!hasUserEnrolledFingerprints(context)) {
+ if (showMessaging) {
+ // This happens when no fingerprints are registered.
+ Toast.makeText(context, R.string.error_toast_user_enroll_fingerprints,
+ Toast.LENGTH_LONG).show()
+ context.startActivity(
+ Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS))
+ }
+ return false
+ }
+ return true
+ }
+ }
+
+}
diff --git a/surelock/src/main/java/com/smashingboxes/surelock/SurelockDefaultDialog.java b/surelock/src/main/java/com/smashingboxes/surelock/SurelockDefaultDialog.java
deleted file mode 100644
index 749901d..0000000
--- a/surelock/src/main/java/com/smashingboxes/surelock/SurelockDefaultDialog.java
+++ /dev/null
@@ -1,278 +0,0 @@
-package com.smashingboxes.surelock;
-
-import android.app.Dialog;
-import android.app.DialogFragment;
-import android.app.FragmentManager;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.StyleRes;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.hardware.fingerprint.FingerprintManagerCompat;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.Window;
-import android.widget.Button;
-import android.widget.TextView;
-
-import com.mattprecious.swirl.SwirlView;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-
-/**
- * Created by Tyler McCraw on 2/17/17.
- * - * Default login dialog which uses fingerprint APIs to authenticate the user, - * and falls back to password authentication if fingerprint is not available. - *
- */ - -public class SurelockDefaultDialog extends DialogFragment implements SurelockFragment { - - private static final String KEY_CIPHER_OP_MODE = "com.smashingboxes.surelock.SurelockDefaultDialog.KEY_CIPHER_OP_MODE"; - private static final String KEY_STYLE_ID = "com.smashingboxes.surelock.KEY_STYLE_ID"; - - private static final long ERROR_TIMEOUT_MILLIS = 1600; - private static final long SUCCESS_DELAY_MILLIS = 1300; //TODO make these configurable via attrs - - private FingerprintManagerCompat fingerprintManager; - private FingerprintManagerCompat.CryptoObject cryptoObject; - private String keyForDecryption; - private byte[] valueToEncrypt; - private SurelockStorage storage; - private SurelockFingerprintListener listener; - private SurelockFingerprintUiHelper uiHelper; - private int cipherOperationMode; - @StyleRes - private int styleId; - - // TODO clean up and genericize default dialog - add custom attribute set which can be overridden - private SwirlView iconView; - private TextView statusTextView; - - static SurelockDefaultDialog newInstance(int cipherOperationMode, - @StyleRes int styleId) { - Bundle args = new Bundle(); - args.putInt(KEY_CIPHER_OP_MODE, cipherOperationMode); - args.putInt(KEY_STYLE_ID, styleId); - - SurelockDefaultDialog fragment = new SurelockDefaultDialog(); - fragment.setArguments(args); - - return fragment; - } - - @Override - public void init(FingerprintManagerCompat fingerprintManager, - FingerprintManagerCompat.CryptoObject cryptoObject, - @NonNull String key, SurelockStorage storage, byte[] valueToEncrypt) { - this.cryptoObject = cryptoObject; - this.fingerprintManager = fingerprintManager; - this.keyForDecryption = key; //TODO need to be passing these as newInstance params... or figure a better way to do this - this.storage = storage; - this.valueToEncrypt = valueToEncrypt; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - uiHelper = new SurelockFingerprintUiHelper(fingerprintManager, this); - - // Do not create a new Fragment when the Activity is re-created such as orientation changes. - setRetainInstance(true); - - if (savedInstanceState != null) { - cipherOperationMode = savedInstanceState.getInt(KEY_CIPHER_OP_MODE); - styleId = savedInstanceState.getInt(KEY_STYLE_ID); - } else { - cipherOperationMode = getArguments().getInt(KEY_CIPHER_OP_MODE); - styleId = getArguments().getInt(KEY_STYLE_ID); - } - - TypedArray attrs = getActivity().obtainStyledAttributes(styleId, R.styleable - .SurelockDefaultDialog); - int dialogTheme = attrs.getResourceId(R.styleable.SurelockDefaultDialog_sl_dialog_theme, 0); - attrs.recycle(); - - setStyle(DialogFragment.STYLE_NO_TITLE, dialogTheme == 0 ? R.style - .SurelockTheme_NoActionBar : dialogTheme); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle - savedInstanceState) { - View view = inflater.inflate(R.layout.fingerprint_dialog_container, container, false); - TypedArray attrs = getActivity().obtainStyledAttributes(styleId, R.styleable - .SurelockDefaultDialog); - - setUpViews(view, attrs); - - attrs.recycle(); - - return view; - } - - private void setUpViews(View fragmentView, TypedArray attrs) { - iconView = (SwirlView) fragmentView.findViewById(R.id.fingerprint_icon); - statusTextView = (TextView) fragmentView.findViewById(R.id.fingerprint_status); - Button fallbackButton = (Button) fragmentView.findViewById(R.id.fallback_button); - fallbackButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - dismiss(); - } - }); - - String fallbackButtonText = attrs.getString(R.styleable - .SurelockDefaultDialog_sl_fallback_button_text); - int fallbackButtonColor = attrs.getColor(R.styleable - .SurelockDefaultDialog_sl_fallback_button_background, 0); - int fallbackButtonTextColor = attrs.getColor(R.styleable - .SurelockDefaultDialog_sl_fallback_button_text_color, 0); - fallbackButton.setText(fallbackButtonText); - if (fallbackButtonColor != 0) { - fallbackButton.setBackgroundColor(fallbackButtonColor); - } - if (fallbackButtonTextColor != 0) { - fallbackButton.setTextColor(fallbackButtonTextColor); - } - - TextView titleBar = (TextView) fragmentView.findViewById(R.id.sl_title_bar); - String titleBarText = attrs.getString(R.styleable.SurelockDefaultDialog_sl_title_bar_text); - titleBar.setText(titleBarText); - int titleBarColor = attrs.getColor(R.styleable - .SurelockDefaultDialog_sl_title_bar_background, 0); - int titleBarTextColor = attrs.getColor(R.styleable - .SurelockDefaultDialog_sl_title_bar_text_color, 0); - if (titleBarColor != 0) { - titleBar.setBackgroundColor(titleBarColor); - } - if (titleBarTextColor != 0) { - titleBar.setTextColor(titleBarTextColor); - } - - } - - @Override - public Dialog onCreateDialog(final Bundle savedInstanceState) { - final Dialog dialog = super.onCreateDialog(savedInstanceState); - dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); - return dialog; - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - if (context instanceof SurelockFingerprintListener) { - listener = (SurelockFingerprintListener) context; - } else { - throw new RuntimeException(context.toString() - + " must implement SurelockFingerprintListener"); - } - } - - @Override - public void onResume() { - super.onResume(); - uiHelper.startListening(cryptoObject); - iconView.setState(SwirlView.State.ON); - } - - @Override - public void show(FragmentManager manager, String tag) { - if (getDialog() == null || !getDialog().isShowing()) { - super.show(manager, tag); - } - } - - @Override - public void onPause() { - super.onPause(); - uiHelper.stopListening(); - } - - @Override - public void onDetach() { - super.onDetach(); - listener = null; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - outState.putInt(KEY_CIPHER_OP_MODE, cipherOperationMode); - outState.putInt(KEY_STYLE_ID, styleId); - super.onSaveInstanceState(outState); - } - - @Override - public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) { - iconView.postDelayed(new Runnable() { - @Override - public void run() { - //TODO figure out a way to not make user have to run encryption/decryption themselves here - if (Cipher.ENCRYPT_MODE == cipherOperationMode) { - try { - final byte[] encryptedValue = cryptoObject.getCipher().doFinal(valueToEncrypt); - storage.createOrUpdate(keyForDecryption, encryptedValue); - listener.onFingerprintEnrolled(); - } catch (IllegalBlockSizeException | BadPaddingException e) { - listener.onFingerprintError(e.getMessage()); - } - } else if (Cipher.DECRYPT_MODE == cipherOperationMode) { - byte[] encryptedValue = storage.get(keyForDecryption); - byte[] decryptedValue; - try { - decryptedValue = cryptoObject.getCipher().doFinal(encryptedValue); - listener.onFingerprintAuthenticated(decryptedValue); - } catch (BadPaddingException | IllegalBlockSizeException e) { - listener.onFingerprintError(e.getMessage()); - } - } - dismiss(); - } - }, SUCCESS_DELAY_MILLIS); - } - - @Override - public void onAuthenticationError(int errorCode, CharSequence errString) { - showError(errString); - listener.onFingerprintError(errString); - dismiss(); - } - - @Override - public void onAuthenticationHelp(int helpCode, CharSequence helpString) { - showError(helpString); - listener.onFingerprintError(helpString); - } - - @Override - public void onAuthenticationFailed() { - showError(statusTextView.getResources().getString(R.string.fingerprint_not_recognized)); - listener.onFingerprintError(null); - } - - private void showError(CharSequence error) { - iconView.setState(SwirlView.State.ERROR); - statusTextView.setText(error); - statusTextView.setTextColor(ContextCompat.getColor(getActivity(), R.color.error_red)); - statusTextView.removeCallbacks(resetErrorTextRunnable); - statusTextView.postDelayed(resetErrorTextRunnable, ERROR_TIMEOUT_MILLIS); - } - - private Runnable resetErrorTextRunnable = new Runnable() { - @Override - public void run() { - if (isAdded()) { - statusTextView.setTextColor(ContextCompat.getColor(getActivity(), R.color.hint_grey)); - statusTextView.setText(getResources().getString(R.string.fingerprint_hint)); - iconView.setState(SwirlView.State.ON); - } - } - }; -} diff --git a/surelock/src/main/java/com/smashingboxes/surelock/SurelockDefaultDialog.kt b/surelock/src/main/java/com/smashingboxes/surelock/SurelockDefaultDialog.kt new file mode 100644 index 0000000..ed12a28 --- /dev/null +++ b/surelock/src/main/java/com/smashingboxes/surelock/SurelockDefaultDialog.kt @@ -0,0 +1,268 @@ +package com.smashingboxes.surelock + +import android.app.Dialog +import android.app.DialogFragment +import android.app.FragmentManager +import android.content.Context +import android.content.res.TypedArray +import android.os.Bundle +import android.support.annotation.StyleRes +import android.support.v4.content.ContextCompat +import android.support.v4.hardware.fingerprint.FingerprintManagerCompat +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.Window +import android.widget.Button +import android.widget.TextView + +import com.mattprecious.swirl.SwirlView + +import javax.crypto.BadPaddingException +import javax.crypto.Cipher +import javax.crypto.IllegalBlockSizeException + +/** + * Created by Tyler McCraw on 2/17/17. + * + * + * Default login dialog which uses fingerprint APIs to authenticate the user, + * and falls back to password authentication if fingerprint is not available. + * + */ + +class SurelockDefaultDialog : DialogFragment(), SurelockFragment { + + private var fingerprintManager: FingerprintManagerCompat? = null + private var cryptoObject: FingerprintManagerCompat.CryptoObject? = null + private var keyForDecryption: String? = null + private var valueToEncrypt: ByteArray? = null + private var storage: SurelockStorage? = null + private var listener: SurelockFingerprintListener? = null + private var uiHelper: SurelockFingerprintUiHelper? = null + private var cipherOperationMode: Int = 0 + @StyleRes + private var styleId: Int = 0 + + // TODO clean up and genericize default dialog - add custom attribute set which can be overridden + private var iconView: SwirlView? = null + private var statusTextView: TextView? = null + + private val resetErrorTextRunnable = Runnable { + if (isAdded) { + statusTextView?.apply { + setTextColor(ContextCompat.getColor(activity, R.color.hint_grey)) + text = resources.getString(R.string.fingerprint_hint) + } + iconView?.setState(SwirlView.State.ON) + } + } + + override fun init(fingerprintManager: FingerprintManagerCompat, + cryptoObject: FingerprintManagerCompat.CryptoObject, + key: String, storage: SurelockStorage, valueToEncrypt: ByteArray?) { + this.cryptoObject = cryptoObject + this.fingerprintManager = fingerprintManager + this.keyForDecryption = key //TODO need to be passing these as newInstance params... or figure a better way to do this + this.storage = storage + this.valueToEncrypt = valueToEncrypt + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + uiHelper = SurelockFingerprintUiHelper(fingerprintManager!!, this) + + // Do not create a new Fragment when the Activity is re-created such as orientation changes. + retainInstance = true + + if (savedInstanceState != null) { + cipherOperationMode = savedInstanceState.getInt(KEY_CIPHER_OP_MODE) + styleId = savedInstanceState.getInt(KEY_STYLE_ID) + } else { + cipherOperationMode = arguments.getInt(KEY_CIPHER_OP_MODE) + styleId = arguments.getInt(KEY_STYLE_ID) + } + + val attrs = activity.obtainStyledAttributes(styleId, R.styleable + .SurelockDefaultDialog) + val dialogTheme = attrs.getResourceId(R.styleable.SurelockDefaultDialog_sl_dialog_theme, 0) + attrs.recycle() + + setStyle(DialogFragment.STYLE_NO_TITLE, + if (dialogTheme == 0) R.style.SurelockTheme_NoActionBar else dialogTheme) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.fingerprint_dialog_container, container, false) + val attrs = activity.obtainStyledAttributes(styleId, R.styleable.SurelockDefaultDialog) + + setUpViews(view, attrs) + + attrs.recycle() + + return view + } + + private fun setUpViews(fragmentView: View, attrs: TypedArray) { + iconView = fragmentView.findViewById- * Exception for any issues in setting up Surelock - * dependencies for encryption/decryption using FingerprintManager - *
- */ - -public class SurelockException extends RuntimeException { - - public SurelockException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/surelock/src/main/java/com/smashingboxes/surelock/SurelockException.kt b/surelock/src/main/java/com/smashingboxes/surelock/SurelockException.kt new file mode 100644 index 0000000..f4c26ba --- /dev/null +++ b/surelock/src/main/java/com/smashingboxes/surelock/SurelockException.kt @@ -0,0 +1,11 @@ +package com.smashingboxes.surelock + +/** + * Created by Tyler McCraw on 3/5/17. + * + * + * Exception for any issues in setting up Surelock + * dependencies for encryption/decryption using FingerprintManager + * + */ +open class SurelockException(message: String, cause: Throwable?) : RuntimeException(message, cause) diff --git a/surelock/src/main/java/com/smashingboxes/surelock/SurelockFingerprintListener.java b/surelock/src/main/java/com/smashingboxes/surelock/SurelockFingerprintListener.kt similarity index 53% rename from surelock/src/main/java/com/smashingboxes/surelock/SurelockFingerprintListener.java rename to surelock/src/main/java/com/smashingboxes/surelock/SurelockFingerprintListener.kt index f8048f7..30d1862 100644 --- a/surelock/src/main/java/com/smashingboxes/surelock/SurelockFingerprintListener.java +++ b/surelock/src/main/java/com/smashingboxes/surelock/SurelockFingerprintListener.kt @@ -1,32 +1,31 @@ -package com.smashingboxes.surelock; - -import android.support.annotation.Nullable; +package com.smashingboxes.surelock /** * Created by Tyler McCraw on 2/17/17. - *- * Simple interface for handling fingerprint authentication events - *
+ * + * + * Simple interface for handling fingerprint authentication events + * */ -public interface SurelockFingerprintListener { +interface SurelockFingerprintListener { /** * Handle successful fingerprint enrollment event */ - void onFingerprintEnrolled(); + fun onFingerprintEnrolled() /** * Handle successful authentication event * * @param decryptedValue String which represents the decrypted bytes of the store value */ - void onFingerprintAuthenticated(byte[] decryptedValue); + fun onFingerprintAuthenticated(decryptedValue: ByteArray) /** * Handle error occurred during authentication * * @param errorMessage error message (use this for logging) */ - void onFingerprintError(@Nullable CharSequence errorMessage); + fun onFingerprintError(errorMessage: CharSequence?) } \ No newline at end of file diff --git a/surelock/src/main/java/com/smashingboxes/surelock/SurelockFingerprintUiHelper.java b/surelock/src/main/java/com/smashingboxes/surelock/SurelockFingerprintUiHelper.java deleted file mode 100644 index 1b26a46..0000000 --- a/surelock/src/main/java/com/smashingboxes/surelock/SurelockFingerprintUiHelper.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.smashingboxes.surelock; - -import android.support.v4.hardware.fingerprint.FingerprintManagerCompat; -import android.support.v4.os.CancellationSignal; - -/** - * Created by Tyler McCraw on 2/17/17. - *- * Manage fingerprint authentication UI by listening to - * Handles forwarding callbacks from the FingerprintManager - *
- */ - -public class SurelockFingerprintUiHelper extends FingerprintManagerCompat.AuthenticationCallback { - - private final FingerprintManagerCompat fingerprintManager; - private final SurelockFragment callback; - private CancellationSignal cancellationSignal; - private boolean selfCancelled; - - SurelockFingerprintUiHelper(FingerprintManagerCompat fingerprintManager, SurelockFragment callback) { - this.fingerprintManager = fingerprintManager; - this.callback = callback; - } - - public void startListening(FingerprintManagerCompat.CryptoObject cryptoObject) { - cancellationSignal = new CancellationSignal(); - selfCancelled = false; - - //TODO pass in a handler here for background authentication? - //TODO take a look at per-user FingerprintManager.authenticate(..., userId) call - // noinspection ResourceType - fingerprintManager.authenticate(cryptoObject, 0 /* flags */, cancellationSignal, this, null); - } - - public void stopListening() { - if (cancellationSignal != null) { - selfCancelled = true; - cancellationSignal.cancel(); - cancellationSignal = null; - } - } - - @Override - public void onAuthenticationError(int errMsgId, CharSequence errString) { - if (!selfCancelled) { - callback.onAuthenticationError(errMsgId, errString); - } - } - - @Override - public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { - callback.onAuthenticationHelp(helpMsgId, helpString); - } - - @Override - public void onAuthenticationFailed() { - callback.onAuthenticationFailed(); - } - - @Override - public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) { - callback.onAuthenticationSucceeded(result); - } -} \ No newline at end of file diff --git a/surelock/src/main/java/com/smashingboxes/surelock/SurelockFingerprintUiHelper.kt b/surelock/src/main/java/com/smashingboxes/surelock/SurelockFingerprintUiHelper.kt new file mode 100644 index 0000000..4e90a66 --- /dev/null +++ b/surelock/src/main/java/com/smashingboxes/surelock/SurelockFingerprintUiHelper.kt @@ -0,0 +1,56 @@ +package com.smashingboxes.surelock + +import android.support.v4.hardware.fingerprint.FingerprintManagerCompat +import android.support.v4.os.CancellationSignal + +/** + * Created by Tyler McCraw on 2/17/17. + * + * + * Manage fingerprint authentication UI by listening to + * Handles forwarding callbacks from the FingerprintManager + * + */ + +class SurelockFingerprintUiHelper internal constructor( + private val fingerprintManager: FingerprintManagerCompat, + private val callback: SurelockFragment) : FingerprintManagerCompat.AuthenticationCallback() { + private var cancellationSignal: CancellationSignal? = null + private var selfCancelled: Boolean = false + + fun startListening(cryptoObject: FingerprintManagerCompat.CryptoObject) { + cancellationSignal = CancellationSignal() + selfCancelled = false + + //TODO pass in a handler here for background authentication? + //TODO take a look at per-user FingerprintManager.authenticate(..., userId) call + // noinspection ResourceType + fingerprintManager.authenticate(cryptoObject, 0 /* flags */, cancellationSignal, this, null) + } + + fun stopListening() { + cancellationSignal?.let { + selfCancelled = true + it.cancel() + cancellationSignal = null + } + } + + override fun onAuthenticationError(errMsgId: Int, errString: CharSequence?) { + if (!selfCancelled) { + callback.onAuthenticationError(errMsgId, errString) + } + } + + override fun onAuthenticationHelp(helpMsgId: Int, helpString: CharSequence?) { + callback.onAuthenticationHelp(helpMsgId, helpString) + } + + override fun onAuthenticationFailed() { + callback.onAuthenticationFailed() + } + + override fun onAuthenticationSucceeded(result: FingerprintManagerCompat.AuthenticationResult?) { + callback.onAuthenticationSucceeded(result) + } +} \ No newline at end of file diff --git a/surelock/src/main/java/com/smashingboxes/surelock/SurelockFragment.java b/surelock/src/main/java/com/smashingboxes/surelock/SurelockFragment.kt similarity index 65% rename from surelock/src/main/java/com/smashingboxes/surelock/SurelockFragment.java rename to surelock/src/main/java/com/smashingboxes/surelock/SurelockFragment.kt index b7fe6ea..778c419 100644 --- a/surelock/src/main/java/com/smashingboxes/surelock/SurelockFragment.java +++ b/surelock/src/main/java/com/smashingboxes/surelock/SurelockFragment.kt @@ -1,20 +1,19 @@ -package com.smashingboxes.surelock; +package com.smashingboxes.surelock -import android.app.FragmentManager; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.hardware.fingerprint.FingerprintManagerCompat; +import android.app.FragmentManager +import android.support.v4.hardware.fingerprint.FingerprintManagerCompat /** * Created by Tyler McCraw on 2/17/17. - *- * Implement this interface in your custom Fragment or DialogFragment - * to customize your own lock screen and then pass your - * implemented Surelock dialog to {@link Surelock} loginWithFingerprint() - *
+ * + * + * Implement this interface in your custom Fragment or DialogFragment + * to customize your own lock screen and then pass your + * implemented Surelock dialog to [Surelock] loginWithFingerprint() + * */ -public interface SurelockFragment { +interface SurelockFragment { /** * Set up the fragment @@ -26,9 +25,9 @@ public interface SurelockFragment { * @param storage instance of SurelockStorage to be used for decrypting the value at the specified key * @param valueToEncrypt The value to encrypt in storage */ - void init(FingerprintManagerCompat fingerprintManager, FingerprintManagerCompat.CryptoObject - cryptoObject, @NonNull String key, SurelockStorage storage, @Nullable byte[] - valueToEncrypt); + fun init(fingerprintManager: FingerprintManagerCompat, + cryptoObject: FingerprintManagerCompat.CryptoObject, key: String, + storage: SurelockStorage, valueToEncrypt: ByteArray?) /** * Display the fragment @@ -38,7 +37,7 @@ void init(FingerprintManagerCompat fingerprintManager, FingerprintManagerCompat. * @param fragmentManager an instance of FragmentManager * @param fingerprintDialogFragmentTag a tag used for keeping track of the fragment's display state */ - void show(FragmentManager fragmentManager, String fingerprintDialogFragmentTag); + fun show(fragmentManager: FragmentManager, fingerprintDialogFragmentTag: String) /** * Called when an unrecoverable error has been encountered and the operation is complete. @@ -47,7 +46,7 @@ void init(FingerprintManagerCompat fingerprintManager, FingerprintManagerCompat. * @param errorCode An integer identifying the error message * @param errString A human-readable error string that can be shown in UI */ - void onAuthenticationError(int errorCode, CharSequence errString); + fun onAuthenticationError(errorCode: Int, errString: CharSequence?) /** * Called when a recoverable error has been encountered during authentication. The help @@ -57,17 +56,17 @@ void init(FingerprintManagerCompat fingerprintManager, FingerprintManagerCompat. * @param helpCode An integer identifying the error message * @param helpString A human-readable string that can be shown in UI */ - void onAuthenticationHelp(int helpCode, CharSequence helpString); + fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence?) /** * Called when a fingerprint is recognized. * * @param result An object containing authentication-related data */ - void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result); + fun onAuthenticationSucceeded(result: FingerprintManagerCompat.AuthenticationResult?) /** * Called when a fingerprint is valid but not recognized. */ - void onAuthenticationFailed(); + fun onAuthenticationFailed() } diff --git a/surelock/src/main/java/com/smashingboxes/surelock/SurelockInvalidKeyException.java b/surelock/src/main/java/com/smashingboxes/surelock/SurelockInvalidKeyException.java deleted file mode 100644 index d46bd5e..0000000 --- a/surelock/src/main/java/com/smashingboxes/surelock/SurelockInvalidKeyException.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.smashingboxes.surelock; - -/** - * Created by Tyler McCraw on 4/3/17. - *- * KeyStore key was invalidated. This means you need to re-enroll the fingerprint - * via enrollFingerprintAndStore() or store() methods so that the value - * can be re-encrypted with a valid key. - *
- */ - -public class SurelockInvalidKeyException extends SurelockException { - - public SurelockInvalidKeyException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/surelock/src/main/java/com/smashingboxes/surelock/SurelockInvalidKeyException.kt b/surelock/src/main/java/com/smashingboxes/surelock/SurelockInvalidKeyException.kt new file mode 100644 index 0000000..9a4b967 --- /dev/null +++ b/surelock/src/main/java/com/smashingboxes/surelock/SurelockInvalidKeyException.kt @@ -0,0 +1,14 @@ +package com.smashingboxes.surelock + +/** + * Created by Tyler McCraw on 4/3/17. + * + * + * KeyStore key was invalidated. This means you need to re-enroll the fingerprint + * via enrollFingerprintAndStore() or store() methods so that the value + * can be re-encrypted with a valid key. + * + */ + +class SurelockInvalidKeyException(message: String, cause: Throwable?) : + SurelockException(message, cause) diff --git a/surelock/src/main/java/com/smashingboxes/surelock/SurelockMaterialDialog.java b/surelock/src/main/java/com/smashingboxes/surelock/SurelockMaterialDialog.java deleted file mode 100644 index 29713d2..0000000 --- a/surelock/src/main/java/com/smashingboxes/surelock/SurelockMaterialDialog.java +++ /dev/null @@ -1,219 +0,0 @@ -package com.smashingboxes.surelock; - -import android.app.DialogFragment; -import android.app.FragmentManager; -import android.content.Context; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.content.ContextCompat; -import android.support.v4.hardware.fingerprint.FingerprintManagerCompat; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.TextView; - -import com.mattprecious.swirl.SwirlView; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; - -/** - * Created by Nicholas Cook on 3/17/17. - *- * A login dialog which follows standard Material Design guidelines. It uses - * fingerprint APIs to authenticate the user, and falls back to password - * authentication if fingerprint is not available. - *
- */ - -public class SurelockMaterialDialog extends DialogFragment implements SurelockFragment { - - private static final String KEY_CIPHER_OP_MODE = "com.smashingboxes.surelock" + - ".SurelockMaterialDialog.KEY_CIPHER_OP_MODE"; - - private SwirlView swirlView; - private TextView messageView; - - private FingerprintManagerCompat fingerprintManager; - private FingerprintManagerCompat.CryptoObject cryptoObject; - private String keyForDecryption; - private byte[] valueToEncrypt; - private SurelockStorage storage; - private SurelockFingerprintListener listener; - private SurelockFingerprintUiHelper uiHelper; - private int cipherOperationMode; - - private static final long ERROR_TIMEOUT_MILLIS = 1600; - private static final long SUCCESS_DELAY_MILLIS = 1300; - - static SurelockMaterialDialog newInstance(int cipherOperationMode) { - - Bundle args = new Bundle(); - args.putInt(KEY_CIPHER_OP_MODE, cipherOperationMode); - - SurelockMaterialDialog fragment = new SurelockMaterialDialog(); - fragment.setArguments(args); - return fragment; - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - if (context instanceof SurelockFingerprintListener) { - listener = (SurelockFingerprintListener) context; - } else { - throw new RuntimeException(context.toString() + " must implement " + - "SurelockFingerprintListener"); - } - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - uiHelper = new SurelockFingerprintUiHelper(fingerprintManager, this); - - // Do not create a new Fragment when the Activity is re-created such as orientation changes. - setRetainInstance(true); - - if (savedInstanceState != null) { - cipherOperationMode = savedInstanceState.getInt(KEY_CIPHER_OP_MODE); - } else { - cipherOperationMode = getArguments().getInt(KEY_CIPHER_OP_MODE); - } - - setStyle(DialogFragment.STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog); - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle - savedInstanceState) { - getDialog().setTitle(R.string.sl_sign_in); - View view = inflater.inflate(R.layout.material_fingerprint_dialog, container, false); - Button cancelButton = (Button) view.findViewById(R.id.cancel_button); - cancelButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - dismiss(); - } - }); - swirlView = (SwirlView) view.findViewById(R.id.fingerprint_icon); - messageView = (TextView) view.findViewById(R.id.fingerprint_status); - return view; - } - - @Override - public void onResume() { - super.onResume(); - uiHelper.startListening(cryptoObject); - swirlView.setState(SwirlView.State.ON); - } - - @Override - public void show(FragmentManager manager, String tag) { - if (getDialog() == null || !getDialog().isShowing()) { - super.show(manager, tag); - } - } - - @Override - public void onPause() { - super.onPause(); - uiHelper.stopListening(); - } - - @Override - public void onDetach() { - super.onDetach(); - listener = null; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - outState.putInt(KEY_CIPHER_OP_MODE, cipherOperationMode); - super.onSaveInstanceState(outState); - } - - @Override - public void init(FingerprintManagerCompat fingerprintManager, FingerprintManagerCompat.CryptoObject - cryptoObject, @NonNull String key, SurelockStorage storage, byte[] valueToEncrypt) { - this.fingerprintManager = fingerprintManager; - this.cryptoObject = cryptoObject; - this.keyForDecryption = key; - this.storage = storage; - this.valueToEncrypt = valueToEncrypt; - } - - @Override - public void onAuthenticationError(int errorCode, CharSequence errString) { - showError(errString); - listener.onFingerprintError(errString); - dismiss(); - } - - @Override - public void onAuthenticationHelp(int helpCode, CharSequence helpString) { - showError(helpString); - listener.onFingerprintError(helpString); - } - - @Override - public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) { - swirlView.postDelayed(new Runnable() { - @Override - public void run() { - //TODO figure out a way to not make user have to run encryption/decryption - // themselves here - if (Cipher.ENCRYPT_MODE == cipherOperationMode) { - try { - final byte[] encryptedValue = cryptoObject.getCipher().doFinal - (valueToEncrypt); - storage.createOrUpdate(keyForDecryption, encryptedValue); - listener.onFingerprintEnrolled(); - } catch (IllegalBlockSizeException | BadPaddingException e) { - listener.onFingerprintError(e.getMessage()); - } - } else if (Cipher.DECRYPT_MODE == cipherOperationMode) { - byte[] encryptedValue = storage.get(keyForDecryption); - byte[] decryptedValue; - try { - decryptedValue = cryptoObject.getCipher().doFinal(encryptedValue); - listener.onFingerprintAuthenticated(decryptedValue); - } catch (BadPaddingException | IllegalBlockSizeException e) { - listener.onFingerprintError(e.getMessage()); - } - } - dismiss(); - } - }, SUCCESS_DELAY_MILLIS); - } - - @Override - public void onAuthenticationFailed() { - showError(messageView.getResources().getString(R.string.fingerprint_not_recognized)); - listener.onFingerprintError(null); - } - - private void showError(CharSequence error) { - swirlView.setState(SwirlView.State.ERROR); - messageView.setText(error); - messageView.setTextColor(ContextCompat.getColor(getActivity(), R.color.error_red)); - messageView.removeCallbacks(resetErrorTextRunnable); - messageView.postDelayed(resetErrorTextRunnable, ERROR_TIMEOUT_MILLIS); - } - - private Runnable resetErrorTextRunnable = new Runnable() { - @Override - public void run() { - if (isAdded()) { - messageView.setTextColor(ContextCompat.getColor(getActivity(), R.color.hint_grey)); - messageView.setText(getResources().getString(R.string.fingerprint_hint)); - swirlView.setState(SwirlView.State.ON); - } - } - }; -} diff --git a/surelock/src/main/java/com/smashingboxes/surelock/SurelockMaterialDialog.kt b/surelock/src/main/java/com/smashingboxes/surelock/SurelockMaterialDialog.kt new file mode 100644 index 0000000..0eb3b51 --- /dev/null +++ b/surelock/src/main/java/com/smashingboxes/surelock/SurelockMaterialDialog.kt @@ -0,0 +1,210 @@ +package com.smashingboxes.surelock + +import android.app.DialogFragment +import android.app.FragmentManager +import android.content.Context +import android.os.Bundle +import android.support.v4.content.ContextCompat +import android.support.v4.hardware.fingerprint.FingerprintManagerCompat +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView + +import com.mattprecious.swirl.SwirlView + +import javax.crypto.BadPaddingException +import javax.crypto.Cipher +import javax.crypto.IllegalBlockSizeException + +/** + * Created by Nicholas Cook on 3/17/17. + * + * + * A login dialog which follows standard Material Design guidelines. It uses + * fingerprint APIs to authenticate the user, and falls back to password + * authentication if fingerprint is not available. + * + */ + +class SurelockMaterialDialog : DialogFragment(), SurelockFragment { + + private var swirlView: SwirlView? = null + private var messageView: TextView? = null + + private var fingerprintManager: FingerprintManagerCompat? = null + private var cryptoObject: FingerprintManagerCompat.CryptoObject? = null + private var keyForDecryption: String? = null + private var valueToEncrypt: ByteArray? = null + private var storage: SurelockStorage? = null + private var listener: SurelockFingerprintListener? = null + private var uiHelper: SurelockFingerprintUiHelper? = null + private var cipherOperationMode: Int = 0 + + private val resetErrorTextRunnable = Runnable { + if (isAdded) { + messageView?.apply { + setTextColor(ContextCompat.getColor(activity, R.color.hint_grey)) + text = resources.getString(R.string.fingerprint_hint) + } + swirlView?.apply { + setState(SwirlView.State.ON) + } + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is SurelockFingerprintListener) { + listener = context + } else { + throw RuntimeException(context.toString() + " must implement " + + "SurelockFingerprintListener") + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + uiHelper = SurelockFingerprintUiHelper(fingerprintManager!!, this) + + // Do not create a new Fragment when the Activity is re-created such as orientation changes. + retainInstance = true + + cipherOperationMode = savedInstanceState?.getInt(KEY_CIPHER_OP_MODE) ?: arguments.getInt( + KEY_CIPHER_OP_MODE) + + setStyle(DialogFragment.STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + dialog.setTitle(R.string.sl_sign_in) + val view = inflater.inflate(R.layout.material_fingerprint_dialog, container, false) + val cancelButton = view.findViewById- * Persistence management interface required for Surelock - * to store encrypted objects based on fingerprint authentication - *
- */ - -public interface SurelockStorage { - - void createOrUpdate(String key, @NonNull byte[] objectToStore); - - @CheckResult - @Nullable - byte[] get(@NonNull String key); - - void remove(String key); - - void clearAll(); - - @CheckResult - @Nullable - Set