diff --git a/app/src/main/java/me/impy/aegis/AuthActivity.java b/app/src/main/java/me/impy/aegis/AuthActivity.java index b1322a67..ddf641d9 100644 --- a/app/src/main/java/me/impy/aegis/AuthActivity.java +++ b/app/src/main/java/me/impy/aegis/AuthActivity.java @@ -1,38 +1,84 @@ package me.impy.aegis; +import android.Manifest; +import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.hardware.fingerprint.FingerprintManager; +import android.os.Build; +import android.support.v4.app.ActivityCompat; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; -import android.text.Editable; import android.view.View; import android.widget.Button; import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.TextView; import java.lang.reflect.UndeclaredThrowableException; +import javax.crypto.BadPaddingException; import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; import javax.crypto.SecretKey; import me.impy.aegis.crypto.CryptoUtils; +import me.impy.aegis.crypto.KeyStoreHandle; import me.impy.aegis.crypto.MasterKey; +import me.impy.aegis.crypto.slots.FingerprintSlot; import me.impy.aegis.crypto.slots.PasswordSlot; import me.impy.aegis.crypto.slots.Slot; import me.impy.aegis.crypto.slots.SlotCollection; +import me.impy.aegis.finger.FingerprintUiHelper; +import me.impy.aegis.helpers.AuthHelper; -public class AuthActivity extends AppCompatActivity { +public class AuthActivity extends AppCompatActivity implements FingerprintUiHelper.Callback { public static final int RESULT_OK = 0; public static final int RESULT_EXCEPTION = 1; private EditText textPassword; + private SlotCollection slots; + private LinearLayout boxFingerprint; + private ImageView imgFingerprint; + private TextView textFingerprint; + private FingerprintUiHelper fingerHelper; + private Cipher fingerCipher; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_auth); textPassword = (EditText) findViewById(R.id.text_password); + boxFingerprint = (LinearLayout) findViewById(R.id.box_fingerprint); + imgFingerprint = (ImageView) findViewById(R.id.img_fingerprint); + textFingerprint = (TextView) findViewById(R.id.text_fingerprint); Intent intent = getIntent(); - final SlotCollection slots = (SlotCollection) intent.getSerializableExtra("slots"); + slots = (SlotCollection) intent.getSerializableExtra("slots"); + + // only show the fingerprint controls if the api version is new enough, permission is granted, a scanner is found and a fingerprint slot is found + Context context = getApplicationContext(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + FingerprintManager manager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE); + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.USE_FINGERPRINT) == PackageManager.PERMISSION_GRANTED && manager.isHardwareDetected()) { + if (slots.has(FingerprintSlot.class)) { + try { + KeyStoreHandle handle = new KeyStoreHandle(); + if (handle.keyExists()) { + SecretKey key = handle.getKey(); + fingerCipher = Slot.createCipher(key, Cipher.DECRYPT_MODE); + fingerHelper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this); + boxFingerprint.setVisibility(View.VISIBLE); + } + } catch (Exception e) { + throw new UndeclaredThrowableException(e); + } + } + } + } Button button = (Button) findViewById(R.id.button_decrypt); button.setOnClickListener(new View.OnClickListener() { @@ -42,11 +88,13 @@ public class AuthActivity extends AppCompatActivity { try { if (slots.has(PasswordSlot.class)) { PasswordSlot slot = slots.find(PasswordSlot.class); - char[] password = getPassword(true); + char[] password = AuthHelper.getPassword(textPassword, true); SecretKey key = slot.deriveKey(password); CryptoUtils.zero(password); Cipher cipher = Slot.createCipher(key, Cipher.DECRYPT_MODE); masterKey = MasterKey.decryptSlot(slot, cipher); + } else { + throw new RuntimeException(); } } catch (Exception e) { // TODO: feedback @@ -54,31 +102,57 @@ public class AuthActivity extends AppCompatActivity { } // send the master key back to the main activity - Intent result = new Intent(); - result.putExtra("key", masterKey); - setResult(RESULT_OK, result); - finish(); + setKey(masterKey); } }); } + private void setKey(MasterKey key) { + // send the master key back to the main activity + Intent result = new Intent(); + result.putExtra("key", key); + setResult(RESULT_OK, result); + finish(); + } + @Override public void onBackPressed() { // ignore back button presses } - private char[] getPassword(boolean clear) { - char[] password = getEditTextChars(textPassword); - if (clear) { - textPassword.getText().clear(); + @Override + public void onResume() { + super.onResume(); + + if (fingerHelper != null) { + fingerHelper.startListening(new FingerprintManager.CryptoObject(fingerCipher)); } - return password; } - private static char[] getEditTextChars(EditText text) { - Editable editable = text.getText(); - char[] chars = new char[editable.length()]; - editable.getChars(0, editable.length(), chars, 0); - return chars; + @Override + public void onPause() { + super.onPause(); + + if (fingerHelper != null) { + fingerHelper.stopListening(); + } + } + + @Override + public void onAuthenticated() { + MasterKey key; + FingerprintSlot slot = slots.find(FingerprintSlot.class); + try { + key = new MasterKey(slot.getKey(fingerCipher)); + } catch (Exception e) { + throw new UndeclaredThrowableException(e); + } + + setKey(key); + } + + @Override + public void onError() { + } } diff --git a/app/src/main/java/me/impy/aegis/CustomAuthenticatedSlide.java b/app/src/main/java/me/impy/aegis/CustomAuthenticatedSlide.java index 5e2b52cf..d8861601 100644 --- a/app/src/main/java/me/impy/aegis/CustomAuthenticatedSlide.java +++ b/app/src/main/java/me/impy/aegis/CustomAuthenticatedSlide.java @@ -1,42 +1,57 @@ package me.impy.aegis; +import android.content.Context; import android.content.Intent; +import android.hardware.fingerprint.FingerprintManager; import android.os.Bundle; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; -import android.text.Editable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; import com.github.paolorotolo.appintro.ISlidePolicy; import com.github.paolorotolo.appintro.ISlideSelectionListener; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; -import java.util.Arrays; +import java.lang.reflect.UndeclaredThrowableException; import javax.crypto.Cipher; -import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import me.impy.aegis.crypto.CryptoUtils; +import me.impy.aegis.crypto.KeyStoreHandle; +import me.impy.aegis.crypto.slots.FingerprintSlot; import me.impy.aegis.crypto.slots.PasswordSlot; import me.impy.aegis.crypto.slots.Slot; +import me.impy.aegis.finger.FingerprintUiHelper; +import me.impy.aegis.helpers.AuthHelper; -public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy, ISlideSelectionListener { +public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiHelper.Callback, ISlidePolicy, ISlideSelectionListener { private int cryptType; private EditText textPassword; private EditText textPasswordConfirm; private int bgColor; + private LinearLayout boxFingerprint; + private ImageView imgFingerprint; + private TextView textFingerprint; + private FingerprintUiHelper fingerHelper; + private KeyStoreHandle storeHandle; + private Cipher fingerCipher; + private boolean fingerAuthenticated; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.fragment_authenticated_slide, container, false); textPassword = (EditText) view.findViewById(R.id.text_password); textPasswordConfirm = (EditText) view.findViewById(R.id.text_password_confirm); + boxFingerprint = (LinearLayout) view.findViewById(R.id.box_fingerprint); + imgFingerprint = (ImageView) view.findViewById(R.id.img_fingerprint); + textFingerprint = (TextView) view.findViewById(R.id.text_fingerprint); view.findViewById(R.id.main).setBackgroundColor(bgColor); return view; } @@ -50,46 +65,17 @@ public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy, return cryptType; } - public Cipher getCipher(PasswordSlot slot, int mode) - throws InvalidKeySpecException, NoSuchAlgorithmException, - InvalidKeyException, NoSuchPaddingException { - char[] password = getPassword(true); - byte[] salt = CryptoUtils.generateSalt(); - SecretKey key = slot.deriveKey(password, salt, CryptoUtils.CRYPTO_SCRYPT_N, CryptoUtils.CRYPTO_SCRYPT_r, CryptoUtils.CRYPTO_SCRYPT_p); - CryptoUtils.zero(password); - - return Slot.createCipher(key, mode); - } - - private char[] getPassword(boolean clear) { - char[] password = getEditTextChars(textPassword); - if (clear) { - textPassword.getText().clear(); - } - return password; - } - - private static char[] getEditTextChars(EditText text) { - Editable editable = text.getText(); - char[] chars = new char[editable.length()]; - editable.getChars(0, editable.length(), chars, 0); - return chars; - } - - @Override - public void onSlideSelected() { - Intent intent = getActivity().getIntent(); - cryptType = intent.getIntExtra("cryptType", 1337); - - switch(cryptType) { - case CustomAuthenticationSlide.CRYPT_TYPE_NONE: - break; - case CustomAuthenticationSlide.CRYPT_TYPE_PASS: - break; - case CustomAuthenticationSlide.CRYPT_TYPE_FINGER: - break; - default: - throw new RuntimeException(); + public Cipher getCipher(Slot slot) throws Exception { + if (slot instanceof PasswordSlot) { + char[] password = AuthHelper.getPassword(textPassword, true); + byte[] salt = CryptoUtils.generateSalt(); + SecretKey key = ((PasswordSlot)slot).deriveKey(password, salt, CryptoUtils.CRYPTO_SCRYPT_N, CryptoUtils.CRYPTO_SCRYPT_r, CryptoUtils.CRYPTO_SCRYPT_p); + CryptoUtils.zero(password); + return Slot.createCipher(key, Cipher.ENCRYPT_MODE); + } else if (slot instanceof FingerprintSlot) { + return fingerCipher; + } else { + throw new RuntimeException(); } } @@ -97,8 +83,57 @@ public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy, bgColor = color; } + @Override + public void onSlideSelected() { + Intent intent = getActivity().getIntent(); + cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID); + + switch(cryptType) { + case CustomAuthenticationSlide.CRYPT_TYPE_NONE: + case CustomAuthenticationSlide.CRYPT_TYPE_PASS: + break; + case CustomAuthenticationSlide.CRYPT_TYPE_FINGER: + boxFingerprint.setVisibility(View.VISIBLE); + + SecretKey key; + try { + if (storeHandle == null) { + storeHandle = new KeyStoreHandle(); + } + // TODO: consider regenerating the key if it exists + if (!storeHandle.keyExists()) { + key = storeHandle.generateKey(true); + } else { + key = storeHandle.getKey(); + } + } catch (Exception e) { + throw new UndeclaredThrowableException(e); + } + + if (fingerHelper == null) { + FingerprintManager fingerManager = (FingerprintManager) getContext().getSystemService(Context.FINGERPRINT_SERVICE); + fingerHelper = new FingerprintUiHelper(fingerManager, imgFingerprint, textFingerprint, this); + } + + try { + fingerCipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE); + } catch (Exception e) { + throw new UndeclaredThrowableException(e); + } + fingerHelper.startListening(new FingerprintManager.CryptoObject(fingerCipher)); + break; + default: + throw new RuntimeException(); + } + } + @Override public void onSlideDeselected() { + if (fingerHelper != null) { + fingerAuthenticated = false; + boxFingerprint.setVisibility(View.INVISIBLE); + fingerHelper.stopListening(); + } } @Override @@ -106,15 +141,13 @@ public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy, switch(cryptType) { case CustomAuthenticationSlide.CRYPT_TYPE_NONE: return true; - case CustomAuthenticationSlide.CRYPT_TYPE_PASS: - char[] password = getEditTextChars(textPassword); - char[] passwordConfirm = getEditTextChars(textPasswordConfirm); - boolean equal = password.length != 0 && Arrays.equals(password, passwordConfirm); - CryptoUtils.zero(password); - CryptoUtils.zero(passwordConfirm); - return equal; case CustomAuthenticationSlide.CRYPT_TYPE_FINGER: - return false; + if (!fingerAuthenticated) { + return false; + } + // intentional fallthrough + case CustomAuthenticationSlide.CRYPT_TYPE_PASS: + return AuthHelper.arePasswordsEqual(textPassword, textPasswordConfirm); default: throw new RuntimeException(); } @@ -122,10 +155,29 @@ public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy, @Override public void onUserIllegallyRequestedNextPage() { + String message; + if (!AuthHelper.arePasswordsEqual(textPassword, textPasswordConfirm)) { + message = "Passwords should be equal and non-empty"; + } else if (!fingerAuthenticated) { + message = "Register your fingerprint"; + } else { + return; + } + View view = getView(); if (view != null) { - Snackbar snackbar = Snackbar.make(getView(), "Passwords should be equal and non-empty", Snackbar.LENGTH_LONG); + Snackbar snackbar = Snackbar.make(getView(), message, Snackbar.LENGTH_LONG); snackbar.show(); } } + + @Override + public void onAuthenticated() { + fingerAuthenticated = true; + } + + @Override + public void onError() { + + } } diff --git a/app/src/main/java/me/impy/aegis/CustomAuthenticationSlide.java b/app/src/main/java/me/impy/aegis/CustomAuthenticationSlide.java index ca2599ad..3e59ebe0 100644 --- a/app/src/main/java/me/impy/aegis/CustomAuthenticationSlide.java +++ b/app/src/main/java/me/impy/aegis/CustomAuthenticationSlide.java @@ -13,11 +13,9 @@ import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.LinearLayout; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.TextView; -import android.widget.Toast; import com.github.paolorotolo.appintro.ISlidePolicy; @@ -53,9 +51,6 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy break; case R.id.rb_fingerprint: id = CRYPT_TYPE_FINGER; - // TODO: remove this - group.clearCheck(); - Toast.makeText(context, "Fingerprint is not supported yet", Toast.LENGTH_SHORT).show(); break; default: throw new RuntimeException(); diff --git a/app/src/main/java/me/impy/aegis/IntroActivity.java b/app/src/main/java/me/impy/aegis/IntroActivity.java index d12aea47..b48ee94f 100644 --- a/app/src/main/java/me/impy/aegis/IntroActivity.java +++ b/app/src/main/java/me/impy/aegis/IntroActivity.java @@ -15,6 +15,7 @@ import javax.crypto.Cipher; import me.impy.aegis.crypto.CryptResult; import me.impy.aegis.crypto.MasterKey; +import me.impy.aegis.crypto.slots.FingerprintSlot; import me.impy.aegis.crypto.slots.PasswordSlot; import me.impy.aegis.crypto.slots.SlotCollection; import me.impy.aegis.db.Database; @@ -107,13 +108,13 @@ public class IntroActivity extends AppIntro { } } + SlotCollection slots = databaseFile.getSlots(); if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) { try { // encrypt the master key with a key derived from the user's password // and add it to the list of slots - SlotCollection slots = databaseFile.getSlots(); PasswordSlot slot = new PasswordSlot(); - Cipher cipher = authenticatedSlide.getCipher(slot, Cipher.ENCRYPT_MODE); + Cipher cipher = authenticatedSlide.getCipher(slot); masterKey.encryptSlot(slot, cipher); slots.add(slot); } catch (Exception e) { @@ -122,8 +123,18 @@ public class IntroActivity extends AppIntro { } } - if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_FINGER) { - // TODO + if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_FINGER) { + try { + // encrypt the master key with the fingerprint key + // and add it to the list of slots + FingerprintSlot slot = new FingerprintSlot(); + Cipher cipher = authenticatedSlide.getCipher(slot); + masterKey.encryptSlot(slot, cipher); + slots.add(slot); + } catch (Exception e) { + setException(e); + return; + } } // finally, save the database diff --git a/app/src/main/java/me/impy/aegis/crypto/slots/Slot.java b/app/src/main/java/me/impy/aegis/crypto/slots/Slot.java index 11b8d2ec..20a70605 100644 --- a/app/src/main/java/me/impy/aegis/crypto/slots/Slot.java +++ b/app/src/main/java/me/impy/aegis/crypto/slots/Slot.java @@ -25,7 +25,7 @@ public abstract class Slot implements Serializable { // getKey decrypts the encrypted master key in this slot with the given key and returns it. public SecretKey getKey(Cipher cipher) throws BadPaddingException, IllegalBlockSizeException { byte[] decryptedKeyBytes = cipher.doFinal(_encryptedMasterKey); - SecretKey decryptedKey = new SecretKeySpec(decryptedKeyBytes, CryptoUtils.CRYPTO_CIPHER_RAW); + SecretKey decryptedKey = new SecretKeySpec(decryptedKeyBytes, CryptoUtils.CRYPTO_CIPHER_AEAD); CryptoUtils.zero(decryptedKeyBytes); return decryptedKey; } diff --git a/app/src/main/java/me/impy/aegis/finger/FingerprintUiHelper.java b/app/src/main/java/me/impy/aegis/finger/FingerprintUiHelper.java index f4f4cbe5..88a10e0a 100644 --- a/app/src/main/java/me/impy/aegis/finger/FingerprintUiHelper.java +++ b/app/src/main/java/me/impy/aegis/finger/FingerprintUiHelper.java @@ -1,4 +1,5 @@ -package me.impy.aegis.finger; +// This file was originally taken from https://github.com/googlesamples/android-FingerprintDialog/blob/2feb02945ae220ebd1bc2c2b620a1d43e30daea8/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintUiHelper.java +// It has been modified to suit Aegis' needs /* * Copyright (C) 2015 The Android Open Source Project @@ -16,6 +17,8 @@ package me.impy.aegis.finger; * limitations under the License */ +package me.impy.aegis.finger; + import android.hardware.fingerprint.FingerprintManager; import android.os.Build; import android.os.CancellationSignal; @@ -109,8 +112,6 @@ public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallba public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) { mErrorTextView.removeCallbacks(mResetErrorTextRunnable); mIcon.setImageResource(R.drawable.ic_fingerprint_success); - mErrorTextView.setTextColor( - mErrorTextView.getResources().getColor(R.color.success_color, null)); mErrorTextView.setText( mErrorTextView.getResources().getString(R.string.fingerprint_success)); mIcon.postDelayed(new Runnable() { @@ -124,8 +125,6 @@ public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallba private void showError(CharSequence error) { mIcon.setImageResource(R.drawable.ic_fingerprint_error); mErrorTextView.setText(error); - mErrorTextView.setTextColor( - mErrorTextView.getResources().getColor(R.color.warning_color, null)); mErrorTextView.removeCallbacks(mResetErrorTextRunnable); mErrorTextView.postDelayed(mResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS); } @@ -133,8 +132,6 @@ public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallba private Runnable mResetErrorTextRunnable = new Runnable() { @Override public void run() { - mErrorTextView.setTextColor( - mErrorTextView.getResources().getColor(R.color.hint_color, null)); mErrorTextView.setText( mErrorTextView.getResources().getString(R.string.fingerprint_hint)); mIcon.setImageResource(R.drawable.ic_fp_40px); @@ -147,4 +144,4 @@ public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallba void onError(); } -} \ No newline at end of file +} diff --git a/app/src/main/java/me/impy/aegis/helpers/AuthHelper.java b/app/src/main/java/me/impy/aegis/helpers/AuthHelper.java new file mode 100644 index 00000000..df7f17bd --- /dev/null +++ b/app/src/main/java/me/impy/aegis/helpers/AuthHelper.java @@ -0,0 +1,38 @@ +package me.impy.aegis.helpers; + +import android.text.Editable; +import android.widget.EditText; + +import java.util.Arrays; + +import me.impy.aegis.crypto.CryptoUtils; + +public class AuthHelper { + private AuthHelper() { + } + + public static char[] getPassword(EditText text, boolean clear) { + char[] password = getEditTextChars(text); + if (clear) { + text.getText().clear(); + } + return password; + } + + public static char[] getEditTextChars(EditText text) { + Editable editable = text.getText(); + char[] chars = new char[editable.length()]; + editable.getChars(0, editable.length(), chars, 0); + return chars; + } + + public static boolean arePasswordsEqual(EditText text1, EditText text2) { + char[] password = getEditTextChars(text1); + char[] passwordConfirm = getEditTextChars(text2); + boolean equal = password.length != 0 && Arrays.equals(password, passwordConfirm); + CryptoUtils.zero(password); + CryptoUtils.zero(passwordConfirm); + return equal; + } + +} diff --git a/app/src/main/res/layout/activity_auth.xml b/app/src/main/res/layout/activity_auth.xml index 39bba448..7fec3096 100644 --- a/app/src/main/res/layout/activity_auth.xml +++ b/app/src/main/res/layout/activity_auth.xml @@ -8,8 +8,8 @@ tools:context="me.impy.aegis.AuthActivity"> @@ -40,5 +40,26 @@ android:layout_height="wrap_content" android:text="Decrypt" /> + + + + + + diff --git a/app/src/main/res/layout/fragment_authenticated_slide.xml b/app/src/main/res/layout/fragment_authenticated_slide.xml index 65f0c990..be01fb9b 100644 --- a/app/src/main/res/layout/fragment_authenticated_slide.xml +++ b/app/src/main/res/layout/fragment_authenticated_slide.xml @@ -17,7 +17,7 @@ + + + + + + + + + + + + +