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 getKeys() { - return Collections.unmodifiableSet(new LinkedHashSet<>(getPrefs().getAll().keySet())); - } -} diff --git a/surelock/src/main/java/com/smashingboxes/surelock/SharedPreferencesStorage.kt b/surelock/src/main/java/com/smashingboxes/surelock/SharedPreferencesStorage.kt new file mode 100644 index 0000000..3a6650d --- /dev/null +++ b/surelock/src/main/java/com/smashingboxes/surelock/SharedPreferencesStorage.kt @@ -0,0 +1,60 @@ +package com.smashingboxes.surelock + +import android.content.Context +import android.content.SharedPreferences +import android.util.Base64 +import java.util.* + +/** + * Created by Tyler McCraw on 3/5/17. + * + * + * Storage mechanism used by Surelock in order to + * persist encrypted objects in SharedPreferences by default + * + */ + +class SharedPreferencesStorage +/** + * Create a new SurelockStorage which uses SharedPreferences for persistence + * + * @param context context + * @param prefsName Desired preferences file. + */ +(private val context: Context, private val prefsName: String) : SurelockStorage { + + private var preferences: SharedPreferences? = null + + private val prefs: SharedPreferences + @Synchronized get() { + if (preferences == null) { + preferences = context.getSharedPreferences(prefsName, Context.MODE_PRIVATE) + } + return preferences!! + } + + override fun createOrUpdate(key: String, objectToStore: ByteArray) { + val encodedString = Base64.encodeToString(objectToStore, Base64.DEFAULT) + prefs.edit().putString(key, encodedString).apply() + } + + override fun get(key: String): ByteArray? { + val byteString = prefs.getString(key, null) + var decodedBytes: ByteArray? = null + if (!byteString.isNullOrEmpty()) { + decodedBytes = Base64.decode(byteString, Base64.DEFAULT) + } + return decodedBytes + } + + override fun remove(key: String) { + prefs.edit().remove(key).apply() + } + + override fun clearAll() { + prefs.edit().clear().apply() + } + + override val keys: Set? + get() = Collections.unmodifiableSet(LinkedHashSet(prefs.all.keys)) +} diff --git a/surelock/src/main/java/com/smashingboxes/surelock/Surelock.java b/surelock/src/main/java/com/smashingboxes/surelock/Surelock.java deleted file mode 100644 index 05fa888..0000000 --- a/surelock/src/main/java/com/smashingboxes/surelock/Surelock.java +++ /dev/null @@ -1,596 +0,0 @@ -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.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.StyleRes; -import android.support.v4.hardware.fingerprint.FingerprintManagerCompat; -import android.text.TextUtils; -import android.util.Log; -import android.widget.Toast; - -import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -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.KeySpec; -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) -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(R.id.fingerprint_icon) as SwirlView + statusTextView = fragmentView.findViewById(R.id.fingerprint_status) as TextView + val fallbackButton = fragmentView.findViewById(R.id.fallback_button) as Button + fallbackButton.setOnClickListener { dismiss() } + + val fallbackButtonText = attrs.getString(R.styleable + .SurelockDefaultDialog_sl_fallback_button_text) + val fallbackButtonColor = attrs.getColor(R.styleable + .SurelockDefaultDialog_sl_fallback_button_background, 0) + val fallbackButtonTextColor = attrs.getColor(R.styleable + .SurelockDefaultDialog_sl_fallback_button_text_color, 0) + fallbackButton.text = fallbackButtonText + if (fallbackButtonColor != 0) { + fallbackButton.setBackgroundColor(fallbackButtonColor) + } + if (fallbackButtonTextColor != 0) { + fallbackButton.setTextColor(fallbackButtonTextColor) + } + + val titleBar = fragmentView.findViewById(R.id.sl_title_bar) as TextView + val titleBarText = attrs.getString(R.styleable.SurelockDefaultDialog_sl_title_bar_text) + titleBar.text = titleBarText + val titleBarColor = attrs.getColor(R.styleable + .SurelockDefaultDialog_sl_title_bar_background, 0) + val 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 fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE) + return dialog + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is SurelockFingerprintListener) { + listener = context + } else { + throw RuntimeException( + context.toString() + " must implement SurelockFingerprintListener") + } + } + + override fun onResume() { + super.onResume() + uiHelper?.startListening(cryptoObject!!) + iconView?.setState(SwirlView.State.ON) + } + + override fun show(fragmentManager: FragmentManager, fingerprintDialogFragmentTag: String) { + if (dialog == null || !dialog.isShowing) { + super.show(fragmentManager, fingerprintDialogFragmentTag) + } + } + + override fun onPause() { + super.onPause() + uiHelper?.stopListening() + } + + override fun onDetach() { + super.onDetach() + listener = null + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putInt(KEY_CIPHER_OP_MODE, cipherOperationMode) + outState.putInt(KEY_STYLE_ID, styleId) + super.onSaveInstanceState(outState) + } + + override fun onAuthenticationSucceeded(result: FingerprintManagerCompat.AuthenticationResult?) { + iconView?.postDelayed({ + //TODO figure out a way to not make user have to run encryption/decryption themselves here + if (Cipher.ENCRYPT_MODE == cipherOperationMode) { + try { + val encryptedValue = cryptoObject?.cipher?.doFinal(valueToEncrypt) + keyForDecryption?.let { + if (encryptedValue != null) { + storage?.createOrUpdate(it, encryptedValue) + listener?.onFingerprintEnrolled() + } + } + } catch (e: IllegalBlockSizeException) { + listener?.onFingerprintError(e.message) + } catch (e: BadPaddingException) { + listener?.onFingerprintError(e.message) + } + + } else if (Cipher.DECRYPT_MODE == cipherOperationMode) { + val encryptedValue = storage?.get(keyForDecryption!!) + val decryptedValue: ByteArray + try { + decryptedValue = cryptoObject?.cipher?.doFinal(encryptedValue) ?: ByteArray(0) + listener?.onFingerprintAuthenticated(decryptedValue) + } catch (e: BadPaddingException) { + listener?.onFingerprintError(e.message) + } catch (e: IllegalBlockSizeException) { + listener?.onFingerprintError(e.message) + } + + } + dismiss() + }, SUCCESS_DELAY_MILLIS) + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { + showError(errString) + listener?.onFingerprintError(errString) + dismiss() + } + + override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence?) { + showError(helpString) + listener?.onFingerprintError(helpString) + } + + override fun onAuthenticationFailed() { + showError(statusTextView!!.resources.getString(R.string.fingerprint_not_recognized)) + listener?.onFingerprintError(null) + } + + private fun showError(error: CharSequence?) { + iconView?.setState(SwirlView.State.ERROR) + statusTextView?.apply { + text = error + setTextColor(ContextCompat.getColor(activity, R.color.error_red)) + removeCallbacks(resetErrorTextRunnable) + postDelayed(resetErrorTextRunnable, ERROR_TIMEOUT_MILLIS) + } + } + + companion object { + + private val KEY_CIPHER_OP_MODE = "com.smashingboxes.surelock.SurelockDefaultDialog.KEY_CIPHER_OP_MODE" + private val KEY_STYLE_ID = "com.smashingboxes.surelock.KEY_STYLE_ID" + + private val ERROR_TIMEOUT_MILLIS: Long = 1600 + private val SUCCESS_DELAY_MILLIS: Long = 1300 //TODO make these configurable via attrs + + internal fun newInstance(cipherOperationMode: Int, + @StyleRes styleId: Int): SurelockDefaultDialog { + val args = Bundle() + args.putInt(KEY_CIPHER_OP_MODE, cipherOperationMode) + args.putInt(KEY_STYLE_ID, styleId) + + return SurelockDefaultDialog().apply { + arguments = args + } + } + } +} diff --git a/surelock/src/main/java/com/smashingboxes/surelock/SurelockException.java b/surelock/src/main/java/com/smashingboxes/surelock/SurelockException.java deleted file mode 100644 index 2572455..0000000 --- a/surelock/src/main/java/com/smashingboxes/surelock/SurelockException.java +++ /dev/null @@ -1,16 +0,0 @@ -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 - *

- */ - -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(R.id.cancel_button) as Button + cancelButton.setOnClickListener { dismiss() } + swirlView = view.findViewById(R.id.fingerprint_icon) as SwirlView + messageView = view.findViewById(R.id.fingerprint_status) as TextView + return view + } + + override fun onResume() { + super.onResume() + cryptoObject?.let { + uiHelper?.startListening(it) + } + swirlView?.setState(SwirlView.State.ON) + } + + override fun show(fragmentManager: FragmentManager, fingerprintDialogFragmentTag: String) { + if (dialog == null || !dialog.isShowing) { + super.show(fragmentManager, fingerprintDialogFragmentTag) + } + } + + override fun onPause() { + super.onPause() + uiHelper?.stopListening() + } + + override fun onDetach() { + super.onDetach() + listener = null + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putInt(KEY_CIPHER_OP_MODE, cipherOperationMode) + super.onSaveInstanceState(outState) + } + + override fun init(fingerprintManager: FingerprintManagerCompat, + cryptoObject: FingerprintManagerCompat.CryptoObject, key: String, + storage: SurelockStorage, valueToEncrypt: ByteArray?) { + this.fingerprintManager = fingerprintManager + this.cryptoObject = cryptoObject + this.keyForDecryption = key + this.storage = storage + this.valueToEncrypt = valueToEncrypt + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { + showError(errString) + listener?.onFingerprintError(errString) + dismiss() + } + + override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence?) { + showError(helpString) + listener?.onFingerprintError(helpString) + } + + override fun onAuthenticationSucceeded(result: FingerprintManagerCompat.AuthenticationResult?) { + swirlView?.postDelayed({ + //TODO figure out a way to not make user have to run encryption/decryption + // themselves here + if (Cipher.ENCRYPT_MODE == cipherOperationMode) { + try { + val encryptedValue = cryptoObject?.cipher?.doFinal(valueToEncrypt) + keyForDecryption?.let { + if (encryptedValue != null) { + storage?.createOrUpdate(it, encryptedValue) + listener?.onFingerprintEnrolled() + } + } + } catch (e: IllegalBlockSizeException) { + listener?.onFingerprintError(e.message) + } catch (e: BadPaddingException) { + listener?.onFingerprintError(e.message) + } + + } else if (Cipher.DECRYPT_MODE == cipherOperationMode) { + val encryptedValue = storage?.get(keyForDecryption!!) + val decryptedValue: ByteArray + try { + decryptedValue = cryptoObject?.cipher?.doFinal(encryptedValue) ?: ByteArray(0) + listener?.onFingerprintAuthenticated(decryptedValue) + } catch (e: BadPaddingException) { + listener?.onFingerprintError(e.message) + } catch (e: IllegalBlockSizeException) { + listener?.onFingerprintError(e.message) + } + + } + dismiss() + }, SUCCESS_DELAY_MILLIS) + } + + override fun onAuthenticationFailed() { + showError(messageView?.resources?.getString(R.string.fingerprint_not_recognized) ?: "") + listener?.onFingerprintError(null) + } + + private fun showError(error: CharSequence?) { + swirlView?.setState(SwirlView.State.ERROR) + messageView?.apply { + text = error + setTextColor(ContextCompat.getColor(activity, R.color.error_red)) + removeCallbacks(resetErrorTextRunnable) + postDelayed(resetErrorTextRunnable, ERROR_TIMEOUT_MILLIS) + } + } + + companion object { + + private val KEY_CIPHER_OP_MODE = "com.smashingboxes.surelock" + ".SurelockMaterialDialog.KEY_CIPHER_OP_MODE" + + private val ERROR_TIMEOUT_MILLIS: Long = 1600 + private val SUCCESS_DELAY_MILLIS: Long = 1300 + + internal fun newInstance(cipherOperationMode: Int): SurelockMaterialDialog { + + val args = Bundle() + args.putInt(KEY_CIPHER_OP_MODE, cipherOperationMode) + + return SurelockMaterialDialog().apply { + arguments = args + } + } + } +} diff --git a/surelock/src/main/java/com/smashingboxes/surelock/SurelockStorage.java b/surelock/src/main/java/com/smashingboxes/surelock/SurelockStorage.java deleted file mode 100644 index d8882e2..0000000 --- a/surelock/src/main/java/com/smashingboxes/surelock/SurelockStorage.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.smashingboxes.surelock; - -import android.support.annotation.CheckResult; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.util.Set; - -/** - * Created by Tyler McCraw on 3/5/17. - *

- * 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 getKeys(); -} diff --git a/surelock/src/main/java/com/smashingboxes/surelock/SurelockStorage.kt b/surelock/src/main/java/com/smashingboxes/surelock/SurelockStorage.kt new file mode 100644 index 0000000..c808b90 --- /dev/null +++ b/surelock/src/main/java/com/smashingboxes/surelock/SurelockStorage.kt @@ -0,0 +1,27 @@ +package com.smashingboxes.surelock + +import android.support.annotation.CheckResult + +/** + * Created by Tyler McCraw on 3/5/17. + * + * + * Persistence management interface required for Surelock + * to store encrypted objects based on fingerprint authentication + * + */ + +interface SurelockStorage { + + @get:CheckResult + val keys: Set? + + fun createOrUpdate(key: String, objectToStore: ByteArray) + + @CheckResult + operator fun get(key: String): ByteArray? + + fun remove(key: String) + + fun clearAll() +}