From 9de0f3510454921c8c406561c0dc8646716a9a1d Mon Sep 17 00:00:00 2001 From: Impyy Date: Sun, 13 Nov 2016 18:21:00 +0100 Subject: [PATCH] Replaced the sqlcipher database with a custom one Still a WIP. This change was made to allow us to use Android's SecretKey API. Which in turn allows us to store our secret keys in the Android KeyStore. You can read about the database file format here: doc/db.md --- Makefile | 18 ++ app/build.gradle | 1 - app/src/main/AndroidManifest.xml | 2 + .../main/java/me/impy/aegis/KeyProfile.java | 2 - .../java/me/impy/aegis/KeyProfileAdapter.java | 4 - .../main/java/me/impy/aegis/MainActivity.java | 264 +++++++++++++---- .../me/impy/aegis/crypto/CryptParameters.java | 6 + .../me/impy/aegis/crypto/CryptResult.java | 6 + .../me/impy/aegis/crypto/CryptoUtils.java | 88 +++++- .../aegis/crypto/DerivationParameters.java | 6 + .../me/impy/aegis/crypto/KeyStoreHandle.java | 53 ++++ .../main/java/me/impy/aegis/db/Database.java | 127 ++++---- .../java/me/impy/aegis/db/DatabaseEntry.java | 27 ++ .../java/me/impy/aegis/db/DatabaseFile.java | 247 ++++++++++++++++ .../java/me/impy/aegis/db/DatabaseHelper.java | 38 --- ...ngerprintAuthenticationDialogFragment.java | 277 ++++++++++++++++++ .../aegis/finger/FingerprintUiHelper.java | 150 ++++++++++ app/src/main/res/drawable-hdpi/ic_fp_40px.png | Bin 0 -> 7011 bytes app/src/main/res/drawable-hdpi/tile.9.png | Bin 0 -> 196 bytes app/src/main/res/drawable-mdpi/ic_fp_40px.png | Bin 0 -> 4001 bytes .../main/res/drawable-nodpi/android_robot.png | Bin 0 -> 8288 bytes .../main/res/drawable-xhdpi/ic_fp_40px.png | Bin 0 -> 10524 bytes .../main/res/drawable-xxhdpi/ic_fp_40px.png | Bin 0 -> 18565 bytes .../main/res/drawable-xxxhdpi/ic_fp_40px.png | Bin 0 -> 16535 bytes .../res/drawable/ic_fingerprint_error.xml | 28 ++ .../res/drawable/ic_fingerprint_success.xml | 28 ++ .../res/layout/fingerprint_dialog_backup.xml | 76 +++++ .../layout/fingerprint_dialog_container.xml | 63 ++++ .../res/layout/fingerprint_dialog_content.xml | 56 ++++ app/src/main/res/values/colors.xml | 4 + app/src/main/res/values/strings.xml | 24 ++ doc/db.md | 119 ++++++++ 32 files changed, 1551 insertions(+), 163 deletions(-) create mode 100644 Makefile create mode 100644 app/src/main/java/me/impy/aegis/crypto/CryptParameters.java create mode 100644 app/src/main/java/me/impy/aegis/crypto/CryptResult.java create mode 100644 app/src/main/java/me/impy/aegis/crypto/DerivationParameters.java create mode 100644 app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java create mode 100644 app/src/main/java/me/impy/aegis/db/DatabaseEntry.java create mode 100644 app/src/main/java/me/impy/aegis/db/DatabaseFile.java delete mode 100644 app/src/main/java/me/impy/aegis/db/DatabaseHelper.java create mode 100644 app/src/main/java/me/impy/aegis/finger/FingerprintAuthenticationDialogFragment.java create mode 100644 app/src/main/java/me/impy/aegis/finger/FingerprintUiHelper.java create mode 100644 app/src/main/res/drawable-hdpi/ic_fp_40px.png create mode 100644 app/src/main/res/drawable-hdpi/tile.9.png create mode 100644 app/src/main/res/drawable-mdpi/ic_fp_40px.png create mode 100644 app/src/main/res/drawable-nodpi/android_robot.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_fp_40px.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_fp_40px.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_fp_40px.png create mode 100644 app/src/main/res/drawable/ic_fingerprint_error.xml create mode 100644 app/src/main/res/drawable/ic_fingerprint_success.xml create mode 100644 app/src/main/res/layout/fingerprint_dialog_backup.xml create mode 100644 app/src/main/res/layout/fingerprint_dialog_container.xml create mode 100644 app/src/main/res/layout/fingerprint_dialog_content.xml create mode 100644 doc/db.md diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..aef16d2e --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +# pandoc format parameters taken from https://github.com/TokTok/spec/blob/54ee53de924210295e311d10c63527c47468305c/pandoc.mk +FORMAT := markdown_github +FORMAT := $(FORMAT)-hard_line_breaks +FORMAT := $(FORMAT)-native_spans +FORMAT := $(FORMAT)-raw_html +FORMAT := $(FORMAT)+tex_math_dollars +FORMAT := $(FORMAT)-tex_math_single_backslash + +PANDOC_ARGS := \ + --no-tex-ligatures \ + --atx-headers \ + --columns=79 \ + +all: + printf "nope" + +format: + pandoc $(PANDOC_ARGS) -f $(FORMAT) -t $(FORMAT) doc/db.md -o doc/db.md \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index d5cea5bd..69275baa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,6 +39,5 @@ dependencies { compile 'com.android.support:support-v4:25.0.0' compile 'com.android.support:recyclerview-v7:25.0.0' compile 'com.yarolegovich:lovely-dialog:1.0.4' - compile 'net.zetetic:android-database-sqlcipher:3.5.3@aar' testCompile 'junit:junit:4.12' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 15250fb9..a104e2c8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,8 @@ + + another.Order){ return -1; diff --git a/app/src/main/java/me/impy/aegis/KeyProfileAdapter.java b/app/src/main/java/me/impy/aegis/KeyProfileAdapter.java index b136fc8e..b790fecf 100644 --- a/app/src/main/java/me/impy/aegis/KeyProfileAdapter.java +++ b/app/src/main/java/me/impy/aegis/KeyProfileAdapter.java @@ -1,11 +1,9 @@ package me.impy.aegis; import android.animation.ObjectAnimator; -import android.content.ClipData; import android.content.SharedPreferences; import android.os.Handler; import android.preference.PreferenceManager; -import android.provider.ContactsContract; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; @@ -24,8 +22,6 @@ import java.util.Comparator; import java.util.List; import me.impy.aegis.crypto.OTP; -import me.impy.aegis.db.Database; -import me.impy.aegis.helpers.ItemClickListener; import me.impy.aegis.helpers.ItemTouchHelperAdapter; public class KeyProfileAdapter extends RecyclerView.Adapter implements ItemTouchHelperAdapter { diff --git a/app/src/main/java/me/impy/aegis/MainActivity.java b/app/src/main/java/me/impy/aegis/MainActivity.java index b2b5bbd0..c8b6e8f0 100644 --- a/app/src/main/java/me/impy/aegis/MainActivity.java +++ b/app/src/main/java/me/impy/aegis/MainActivity.java @@ -1,10 +1,8 @@ package me.impy.aegis; -import android.app.FragmentManager; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ShortcutInfo; @@ -12,6 +10,7 @@ import android.content.pm.ShortcutManager; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Build; +import android.hardware.fingerprint.FingerprintManager; import android.preference.PreferenceManager; import android.support.design.widget.BottomSheetDialog; import android.support.design.widget.FloatingActionButton; @@ -26,31 +25,43 @@ import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.widget.Button; import android.widget.LinearLayout; import android.widget.Toast; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Objects; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.SecretKey; + +import me.impy.aegis.crypto.CryptParameters; +import me.impy.aegis.crypto.CryptResult; import me.impy.aegis.crypto.CryptoUtils; +import me.impy.aegis.crypto.DerivationParameters; +import me.impy.aegis.crypto.KeyStoreHandle; import me.impy.aegis.crypto.OTP; import me.impy.aegis.db.Database; +import me.impy.aegis.db.DatabaseFile; +import me.impy.aegis.finger.FingerprintAuthenticationDialogFragment; import me.impy.aegis.helpers.SimpleItemTouchHelperCallback; -public class MainActivity extends AppCompatActivity { +public class MainActivity extends AppCompatActivity { static final int GET_KEYINFO = 1; static final int ADD_KEYINFO = 2; RecyclerView rvKeyProfiles; KeyProfileAdapter mKeyProfileAdapter; - ArrayList mKeyProfiles; + ArrayList mKeyProfiles = new ArrayList<>(); Database database; + DatabaseFile databaseFile; + boolean blockSave = false; boolean nightMode = false; int clickedItemPosition = -1; @@ -59,19 +70,16 @@ public class MainActivity extends AppCompatActivity { super.onCreate(savedInstanceState); SharedPreferences prefs = this.getSharedPreferences("me.impy.aegis", Context.MODE_PRIVATE); - if(!prefs.getBoolean("passedIntro", false)) - { + if (!prefs.getBoolean("passedIntro", false)) { Intent intro = new Intent(this, IntroActivity.class); startActivity(intro); } SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - if(sharedPreferences.getBoolean("pref_night_mode", false)) - { + if (sharedPreferences.getBoolean("pref_night_mode", false)) { nightMode = true; setTheme(R.style.AppTheme_Dark_NoActionBar); - } else - { + } else { setPreferredTheme(); } @@ -80,43 +88,24 @@ public class MainActivity extends AppCompatActivity { setSupportActionBar(toolbar); initializeAppShortcuts(); FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); + fab.setEnabled(false); fab.setOnClickListener(view -> { + blockSave = true; Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class); startActivityForResult(scannerActivity, GET_KEYINFO); }); - char[] password = "test".toCharArray(); - database = Database.createInstance(getApplicationContext(), "keys.db", password); - CryptoUtils.zero(password); - - mKeyProfiles = new ArrayList<>(); - rvKeyProfiles = (RecyclerView) findViewById(R.id.rvKeyProfiles); LinearLayoutManager mLayoutManager = new LinearLayoutManager(this); rvKeyProfiles.setLayoutManager(mLayoutManager); - final Context context = this.getApplicationContext(); - //EditProfileBottomSheetdialog bottomSheetDialog = EditProfileBottomSheetdialog.getInstance(); - mKeyProfileAdapter = new KeyProfileAdapter(mKeyProfiles); mKeyProfileAdapter.setOnItemClickListener((position, v) -> { clickedItemPosition = position; InitializeBottomSheet().show(); }); - - //View dialogView = bottomSheetDialog.getView(); - //LinearLayout copyLayout = (LinearLayout)dialogView.findViewById(R.id.copy_button); - /*copyLayout.setOnClickListener(view -> { - ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("text/plain", mKeyProfiles.get(clickedItemPosition).Code); - clipboard.setPrimaryClip(clip); - - Toast.makeText(context, "Code successfully copied to the clipboard", Toast.LENGTH_SHORT).show(); - });*/ - mKeyProfileAdapter.setOnLongItemClickListener((position, v) -> { - }); ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(mKeyProfileAdapter); @@ -131,17 +120,13 @@ public class MainActivity extends AppCompatActivity { } }; Collections.sort(mKeyProfiles, comparator); - - try { - mKeyProfiles.addAll(database.getKeys()); - mKeyProfileAdapter.notifyDataSetChanged(); - } catch (Exception e) { - e.printStackTrace(); - } + + loadDatabase(null); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { + blockSave = false; if (requestCode == GET_KEYINFO) { if (resultCode == RESULT_OK) { final KeyProfile keyProfile = (KeyProfile)data.getSerializableExtra("KeyProfile"); @@ -153,7 +138,7 @@ public class MainActivity extends AppCompatActivity { } else if (requestCode == ADD_KEYINFO) { if (resultCode == RESULT_OK) { - final KeyProfile keyProfile = (KeyProfile)data.getSerializableExtra("KeyProfile"); + final KeyProfile keyProfile = (KeyProfile) data.getSerializableExtra("KeyProfile"); String otp; try { @@ -165,14 +150,17 @@ public class MainActivity extends AppCompatActivity { keyProfile.Order = mKeyProfiles.size() + 1; keyProfile.Code = otp; - mKeyProfiles.add(keyProfile); - mKeyProfileAdapter.notifyDataSetChanged(); - try { database.addKey(keyProfile); } catch (Exception e) { e.printStackTrace(); + // TODO: feedback + return; } + + mKeyProfiles.add(keyProfile); + mKeyProfileAdapter.notifyDataSetChanged(); + saveDatabase(true, null); } } } @@ -186,18 +174,84 @@ public class MainActivity extends AppCompatActivity { @Override protected void onPause() { - for(int i = 0; i < mKeyProfiles.size(); i++) - { - try { - database.updateKey(mKeyProfiles.get(i)); - } catch (Exception e) { - e.printStackTrace(); + if (!blockSave) { + // update order of keys + for (int i = 0; i < mKeyProfiles.size(); i++) { + try { + database.updateKey(mKeyProfiles.get(i)); + } catch (Exception e) { + e.printStackTrace(); + } } + + saveDatabase(false, null); } super.onPause(); } + private void promptFingerPrint(FingerprintAuthenticationDialogFragment.Action action, Cipher cipher) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + FingerprintAuthenticationDialogFragment fragment = new FingerprintAuthenticationDialogFragment(); + fragment.setCryptoObject(new FingerprintManager.CryptoObject(cipher)); + fragment.setStage(FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT); + fragment.setAction(action); + fragment.show(getFragmentManager(), ""); + } + } + + public void onAuthenticated(FingerprintAuthenticationDialogFragment.Action action, FingerprintManager.CryptoObject obj) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Cipher cipher = obj.getCipher(); + switch (action) { + case SAVE: + saveDatabase(false, cipher); + break; + case LOAD: + loadDatabase(cipher); + break; + } + } + } + + private void saveDatabase(boolean allowPrompt, Cipher cipher) { + try { + byte[] bytes = database.serialize(); + CryptParameters cryptParams = null; + DerivationParameters derParams = null; + + switch (databaseFile.getLevel()) { + case DatabaseFile.SEC_LEVEL_DERIVED: + // TODO + break; + case DatabaseFile.SEC_LEVEL_KEYSTORE: + if (cipher == null) { + KeyStoreHandle keyStore = new KeyStoreHandle(); + SecretKey key = keyStore.getKey(); + cipher = CryptoUtils.createCipher(key, Cipher.ENCRYPT_MODE); + } + + CryptResult result = CryptoUtils.encrypt(bytes, cipher); + bytes = result.Data; + cryptParams = result.Parameters; + break; + } + + databaseFile.setContent(bytes); + databaseFile.setCryptParameters(cryptParams); + databaseFile.setDerivationParameters(derParams); + databaseFile.save(); + } catch (IllegalBlockSizeException e) { + // TODO: is there a way to catch "Key user not authenticated" specifically aside from checking the exception message? + if (causeIsKeyUserNotAuthenticated(e) && allowPrompt && cipher != null) { + promptFingerPrint(FingerprintAuthenticationDialogFragment.Action.SAVE, cipher); + } + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + } + private BottomSheetDialog InitializeBottomSheet() { View bottomSheetView = getLayoutInflater ().inflate (R.layout.bottom_sheet_edit_profile, null); @@ -242,7 +296,13 @@ public class MainActivity extends AppCompatActivity { .setTitle("Delete entry") .setMessage("Are you sure you want to delete this profile?") .setPositiveButton(android.R.string.yes, (dialog, which) -> { - database.removeKey(profile); + try { + database.removeKey(profile); + } catch (Exception e) { + e.printStackTrace(); + //TODO: feedback + return; + } mKeyProfiles.remove(clickedItemPosition); mKeyProfileAdapter.notifyItemRemoved(clickedItemPosition); }) @@ -335,4 +395,108 @@ public class MainActivity extends AppCompatActivity { startActivity(new Intent(this, this.getClass())); } } + + private void loadDatabase(Cipher cipher) { + try { + databaseFile = DatabaseFile.load(getApplicationContext()); + } catch (IOException e) { + // this file doesn't exist yet + try { + // TODO: prompt for security settings (level, auth, etc) + database = new Database(); + databaseFile = new DatabaseFile(getApplicationContext()); + databaseFile.setLevel(DatabaseFile.SEC_LEVEL_KEYSTORE); + + if (databaseFile.getLevel() == DatabaseFile.SEC_LEVEL_KEYSTORE) { + KeyStoreHandle store = new KeyStoreHandle(); + if (!store.keyExists()) { + store.generateKey(true); + } + } + } catch (Exception ex) { + e.printStackTrace(); + return; + } + } catch (Exception e) { + // something else went wrong + e.printStackTrace(); + return; + } + + if (database == null) { + byte[] content = databaseFile.getContent(); + switch (databaseFile.getLevel()) { + case DatabaseFile.SEC_LEVEL_NONE: + try { + Database temp = new Database(); + temp.deserialize(content); + database = temp; + } catch (Exception e) { + // TODO: handle corrupt database + e.printStackTrace(); + return; + } + break; + case DatabaseFile.SEC_LEVEL_DERIVED: + // TODO: prompt for pin/pass + /*CryptParameters cryptParams = dbFile.getCryptParameters(); + DerivationParameters derParams = dbFile.getDerivationParameters(); + SecretKey key = CryptoUtils.deriveKey("password".toCharArray(), derParams.Salt, (int)derParams.IterationCount);*/ + + break; + case DatabaseFile.SEC_LEVEL_KEYSTORE: + // TODO: prompt for fingerprint if auth is required + try { + CryptParameters params = databaseFile.getCryptParameters(); + + if (cipher == null) { + KeyStoreHandle store = new KeyStoreHandle(); + SecretKey key = store.getKey(); + cipher = CryptoUtils.createCipher(key, Cipher.DECRYPT_MODE, params.Nonce); + } + + CryptResult result = null; + //try { + result = CryptoUtils.decrypt(content, cipher, params); + //} catch (Exception e) { + // // we probably need to authenticate ourselves + // promptFingerPrint(1, cipher); + //} + if (result != null) { + database = new Database(); + database.deserialize(result.Data); + } + } catch (IllegalBlockSizeException e) { + if (causeIsKeyUserNotAuthenticated(e) && cipher != null) { + promptFingerPrint(FingerprintAuthenticationDialogFragment.Action.LOAD, cipher); + } + e.printStackTrace(); + return; + } catch (Exception e) { + e.printStackTrace(); + return; + } + break; + default: + // TODO: handle unknown security level + return; + } + } + + try { + mKeyProfiles.addAll(database.getKeys()); + mKeyProfileAdapter.notifyDataSetChanged(); + } catch (Exception e) { + e.printStackTrace(); + return; + } + + FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); + fab.setEnabled(true); + } + + private boolean causeIsKeyUserNotAuthenticated(Exception e) { + // TODO: is there a way to catch "Key user not authenticated" specifically aside from checking the exception message? + return e.getCause().getMessage().equals("Key user not authenticated"); + } } diff --git a/app/src/main/java/me/impy/aegis/crypto/CryptParameters.java b/app/src/main/java/me/impy/aegis/crypto/CryptParameters.java new file mode 100644 index 00000000..4c3140da --- /dev/null +++ b/app/src/main/java/me/impy/aegis/crypto/CryptParameters.java @@ -0,0 +1,6 @@ +package me.impy.aegis.crypto; + +public class CryptParameters { + public byte[] Nonce; + public byte[] Tag; +} \ No newline at end of file diff --git a/app/src/main/java/me/impy/aegis/crypto/CryptResult.java b/app/src/main/java/me/impy/aegis/crypto/CryptResult.java new file mode 100644 index 00000000..661c2c3e --- /dev/null +++ b/app/src/main/java/me/impy/aegis/crypto/CryptResult.java @@ -0,0 +1,6 @@ +package me.impy.aegis.crypto; + +public class CryptResult { + public CryptParameters Parameters; + public byte[] Data; +} diff --git a/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java b/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java index 6d798253..3719d6dc 100644 --- a/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java +++ b/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java @@ -1,9 +1,93 @@ package me.impy.aegis.crypto; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; import java.util.Arrays; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; + public class CryptoUtils { - private CryptoUtils() { + public static final byte CRYPTO_TAG_SIZE = 16; + public static final byte CRYPTO_KEY_SIZE = 32; + public static final byte CRYPTO_NONCE_SIZE = 12; + public static final byte CRYPTO_SALT_SIZE = 32; + public static final String CRYPTO_CIPHER = "AES/GCM/NoPadding"; + public static final String CRYPTO_DERIVE_ALGO = "PBKDF2WithHmacSHA256"; + + public static SecretKey deriveKey(char[] password, byte[] salt, int iterations) throws NoSuchAlgorithmException, InvalidKeySpecException { + SecretKeyFactory factory = SecretKeyFactory.getInstance(CRYPTO_DERIVE_ALGO); + KeySpec spec = new PBEKeySpec(password, salt, iterations, CRYPTO_KEY_SIZE * 8); + return factory.generateSecret(spec); + } + + public static Cipher createCipher(SecretKey key, int opmode) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException { + byte[] nonce = generateNonce(); + return createCipher(key, opmode, nonce); + } + + public static Cipher createCipher(SecretKey key, int opmode, byte[] nonce) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException { + GCMParameterSpec spec = new GCMParameterSpec(CRYPTO_TAG_SIZE * 8, nonce); + Cipher cipher = Cipher.getInstance(CRYPTO_CIPHER); + cipher.init(opmode, key, spec); + return cipher; + } + + public static CryptResult encrypt(byte[] data, Cipher cipher) throws BadPaddingException, IllegalBlockSizeException { + // split off the tag to store it separately + byte[] result = cipher.doFinal(data); + byte[] tag = Arrays.copyOfRange(result, result.length - CRYPTO_TAG_SIZE, result.length); + byte[] encrypted = Arrays.copyOfRange(result, 0, result.length - CRYPTO_TAG_SIZE); + + return new CryptResult() {{ + Parameters = new CryptParameters() {{ + Nonce = cipher.getIV(); + Tag = tag; + }}; + Data = encrypted; + }}; + } + + public static CryptResult decrypt(byte[] encrypted, Cipher cipher, CryptParameters params) throws IOException, BadPaddingException, IllegalBlockSizeException { + // append the tag to the ciphertext + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + stream.write(encrypted); + stream.write(params.Tag); + + encrypted = stream.toByteArray(); + byte[] decrypted = cipher.doFinal(encrypted); + + return new CryptResult() {{ + Parameters = params; + Data = decrypted; + }}; + } + + public static byte[] generateSalt() { + return generateRandomBytes(CRYPTO_KEY_SIZE); + } + + public static byte[] generateNonce() { + return generateRandomBytes(CRYPTO_NONCE_SIZE); + } + + private static byte[] generateRandomBytes(int length) { + SecureRandom random = new SecureRandom(); + byte[] data = new byte[length]; + random.nextBytes(data); + return data; } public static void zero(char[] data) { @@ -11,6 +95,6 @@ public class CryptoUtils { } public static void zero(byte[] data) { - Arrays.fill(data, (byte)0); + Arrays.fill(data, (byte) 0); } } diff --git a/app/src/main/java/me/impy/aegis/crypto/DerivationParameters.java b/app/src/main/java/me/impy/aegis/crypto/DerivationParameters.java new file mode 100644 index 00000000..e81111db --- /dev/null +++ b/app/src/main/java/me/impy/aegis/crypto/DerivationParameters.java @@ -0,0 +1,6 @@ +package me.impy.aegis.crypto; + +public class DerivationParameters { + public long IterationCount; + public byte[] Salt; +} diff --git a/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java b/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java new file mode 100644 index 00000000..3374fca0 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java @@ -0,0 +1,53 @@ +package me.impy.aegis.crypto; + +import android.os.Build; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +public class KeyStoreHandle { + private final KeyStore keyStore; + private static final String KEY_NAME = "AegisKey"; + private static final String STORE_NAME = "AndroidKeyStore"; + + public KeyStoreHandle() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { + keyStore = KeyStore.getInstance(STORE_NAME); + keyStore.load(null); + } + + public boolean keyExists() throws KeyStoreException { + return keyStore.containsAlias(KEY_NAME); + } + + public SecretKey generateKey(boolean authRequired) throws Exception { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, STORE_NAME); + generator.init(new KeyGenParameterSpec.Builder(KEY_NAME, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setUserAuthenticationRequired(authRequired) + .setRandomizedEncryptionRequired(false) + .setKeySize(CryptoUtils.CRYPTO_KEY_SIZE * 8) + //.setUserAuthenticationValidityDurationSeconds(30); + .build()); + + return generator.generateKey(); + } else { + throw new Exception("Symmetric KeyStore keys are not supported in this version of Android"); + } + } + + public SecretKey getKey() throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { + return (SecretKey) keyStore.getKey(KEY_NAME, null); + } +} diff --git a/app/src/main/java/me/impy/aegis/db/Database.java b/app/src/main/java/me/impy/aegis/db/Database.java index d9746e92..c6e9bc65 100644 --- a/app/src/main/java/me/impy/aegis/db/Database.java +++ b/app/src/main/java/me/impy/aegis/db/Database.java @@ -1,99 +1,94 @@ package me.impy.aegis.db; -import android.content.Context; - -import net.sqlcipher.Cursor; -import net.sqlcipher.database.SQLiteDatabase; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import java.io.UnsupportedEncodingException; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.List; import me.impy.aegis.KeyProfile; import me.impy.aegis.crypto.KeyInfo; public class Database { - private static Database instance; - private static Boolean libsLoaded = false; - private SQLiteDatabase db; + private static final int version = 1; + private List entries = new ArrayList<>(); - private Database(Context context, String filename, char[] password) { - DatabaseHelper helper = new DatabaseHelper(context, filename); - db = helper.getWritableDatabase(password); - } - - public static Database createInstance(Context context, String filename, char[] password) { - // load the sqlcipher library, once - if (!libsLoaded) { - SQLiteDatabase.loadLibs(context); - libsLoaded = true; + public byte[] serialize() throws JSONException, UnsupportedEncodingException { + JSONArray array = new JSONArray(); + for (DatabaseEntry e : entries) { + array.put(e.serialize()); } - if (instance == null) { - instance = new Database(context, filename, password); - } + JSONObject obj = new JSONObject(); + obj.put("version", version); + obj.put("entries", array); - return instance; + return obj.toString().getBytes("UTF-8"); + } + + public void deserialize(byte[] data) throws Exception { + JSONObject obj = new JSONObject(new String(data, "UTF-8")); + + // TODO: support different version deserialization providers + int ver = obj.getInt("version"); + if (ver != version) { + throw new Exception("Unsupported version"); + } + + JSONArray array = obj.getJSONArray("entries"); + for (int i = 0; i < array.length(); i++) { + DatabaseEntry e = new DatabaseEntry(); + e.deserialize(array.getJSONObject(i)); + entries.add(e); + } } - // adds a key to the database and returns it's ID public void addKey(KeyProfile profile) throws Exception { - db.execSQL("insert into otp (name, url) values (?, ?)", - new Object[]{ profile.Name, profile.Info.getURL() }); - profile.ID = getLastID(db, "otp"); + DatabaseEntry e = new DatabaseEntry(); + e.Name = profile.Name; + e.URL = profile.Info.getURL(); + e.Order = profile.Order; + e.ID = entries.size() + 1; + profile.ID = e.ID; + entries.add(e); } public void updateKey(KeyProfile profile) throws Exception { - db.execSQL("update otp set name=?, url=?, 'order'=? where id=?", - new Object[]{ profile.Name, profile.Info.getURL(), profile.Order, profile.ID }); + DatabaseEntry e = findEntry(profile); + e.Name = profile.Name; + e.URL = profile.Info.getURL(); + e.Order = profile.Order; } - public void removeKey(KeyProfile profile) { - db.execSQL("delete from otp where id=?", new Object[]{ profile.ID }); + public void removeKey(KeyProfile profile) throws Exception { + DatabaseEntry e = findEntry(profile); + entries.remove(e); } public List getKeys() throws Exception { List list = new ArrayList<>(); - Cursor cursor = db.rawQuery("select * from otp order by 'order' desc", null); - try { - while (cursor.moveToNext()) { - KeyProfile profile = new KeyProfile(); - profile.ID = cursor.getInt(cursor.getColumnIndexOrThrow("id")); - profile.Name = cursor.getString(cursor.getColumnIndexOrThrow("name")); - profile.Order = cursor.getInt(cursor.getColumnIndexOrThrow("order")); - String url = cursor.getString(cursor.getColumnIndexOrThrow("url")); - profile.Info = KeyInfo.FromURL(url); - - list.add(profile); - } - Collections.sort(list, new Comparator() { - @Override - public int compare(KeyProfile a, KeyProfile b) { - return b.compareTo(a); - } - }); - return list; - } finally { - cursor.close(); + for (DatabaseEntry e : entries) { + KeyProfile profile = new KeyProfile(); + profile.Name = e.Name; + profile.Info = KeyInfo.FromURL(e.URL); + profile.Order = e.Order; + profile.ID = e.ID; + list.add(profile); } + + return list; } - public void close() { - db.close(); - } - - private int getLastID(SQLiteDatabase db, String table) throws Exception { - Cursor cursor = db.rawQuery(String.format("select id from %s order by id desc limit 1", table), null); - try { - if (!cursor.moveToFirst()) { - throw new Exception("no items in the table, this should not happen here"); + private DatabaseEntry findEntry(KeyProfile profile) throws Exception { + for (DatabaseEntry e : entries) { + if (e.ID == profile.ID) { + return e; } - - return cursor.getInt(cursor.getColumnIndexOrThrow("id")); - } finally { - cursor.close(); } + + throw new Exception("Key doesn't exist"); } -} +} \ No newline at end of file diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java b/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java new file mode 100644 index 00000000..19d1616d --- /dev/null +++ b/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java @@ -0,0 +1,27 @@ +package me.impy.aegis.db; + +import org.json.JSONException; +import org.json.JSONObject; + +public class DatabaseEntry { + public int ID; + public String Name; + public String URL; + public int Order; + + public JSONObject serialize() throws JSONException { + JSONObject obj = new JSONObject(); + obj.put("id", ID); + obj.put("name", Name); + obj.put("url", URL); + obj.put("order", Order); + return obj; + } + + public void deserialize(JSONObject obj) throws JSONException { + ID = obj.getInt("id"); + Name = obj.getString("name"); + URL = obj.getString("url"); + Order = obj.getInt("order"); + } +} diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseFile.java b/app/src/main/java/me/impy/aegis/db/DatabaseFile.java new file mode 100644 index 00000000..fb9d722d --- /dev/null +++ b/app/src/main/java/me/impy/aegis/db/DatabaseFile.java @@ -0,0 +1,247 @@ +package me.impy.aegis.db; + +import android.content.Context; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.Arrays; + +import me.impy.aegis.crypto.CryptParameters; +import me.impy.aegis.crypto.CryptoUtils; +import me.impy.aegis.crypto.DerivationParameters; + +public class DatabaseFile { + public static final byte SEC_LEVEL_NONE = 0x00; + public static final byte SEC_LEVEL_DERIVED = 0x01; + public static final byte SEC_LEVEL_KEYSTORE = 0x02; + private static final byte bSectionEncryptionParameters = 0x00; + private static final byte bSectionDerivationParameters = 0x01; + private static final byte bSectionEnd = (byte) 0xFF; + private static final byte bVersion = 1; + private static final String dbFilename = "aegis.db"; + + private final byte[] bHeader; + private final Context context; + + private byte level; + private byte[] content; + private CryptParameters cryptParameters; + private DerivationParameters derivationParameters; + + public DatabaseFile(Context ctx) throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException { + context = ctx; + bHeader = "AEGIS".getBytes("US_ASCII"); + } + + public byte[] serialize() throws IOException { + CryptParameters cryptParams = getCryptParameters(); + DerivationParameters derParams = getDerivationParameters(); + byte[] content = getContent(); + byte level = getLevel(); + + // this is dumb, java doesn't provide an endianness-aware data stream + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + DataOutputStream stream = new DataOutputStream(byteStream); + stream.write(bHeader); + stream.write(bVersion); + stream.write(level); + + // we assume that all of the needed params for the security level are set + // if that's not the case, a NullPointerException will be thrown. + switch (level) { + case SEC_LEVEL_DERIVED: + ByteBuffer paramBuffer = newBuffer(/* iterations */ 8 + CryptoUtils.CRYPTO_SALT_SIZE); + paramBuffer.putLong(derParams.IterationCount); + paramBuffer.put(derParams.Salt); + writeSection(stream, bSectionDerivationParameters, paramBuffer.array()); + // intentional fallthrough + case SEC_LEVEL_KEYSTORE: + paramBuffer = newBuffer(CryptoUtils.CRYPTO_NONCE_SIZE + CryptoUtils.CRYPTO_TAG_SIZE); + paramBuffer.put(cryptParams.Nonce); + paramBuffer.put(cryptParams.Tag); + writeSection(stream, bSectionEncryptionParameters, paramBuffer.array()); + break; + } + + writeSection(stream, bSectionEnd, null); + stream.write(content); + return byteStream.toByteArray(); + } + + public void deserialize(byte[] data) throws Exception { + ByteBuffer buffer = newBuffer(data); + + byte[] header = new byte[bHeader.length]; + buffer.get(header); + if (!Arrays.equals(header, bHeader)) { + throw new Exception("Bad header"); + } + + // TODO: support different version deserialization providers + byte version = buffer.get(); + if (version != bVersion) { + throw new Exception("Unsupported version"); + } + + byte level = buffer.get(); + if (level > SEC_LEVEL_KEYSTORE) { + throw new Exception("Unsupported security level"); + } + setLevel(level); + + CryptParameters cryptParams = null; + DerivationParameters derParams = null; + + for (section s = readSection(buffer); s.ID != bSectionEnd; s = readSection(buffer)) { + ByteBuffer sBuff = newBuffer(s.Data); + switch (s.ID) { + case bSectionEncryptionParameters: + assertLength(s.Data, CryptoUtils.CRYPTO_NONCE_SIZE + CryptoUtils.CRYPTO_TAG_SIZE); + + byte[] nonce = new byte[CryptoUtils.CRYPTO_NONCE_SIZE]; + byte[] tag = new byte[CryptoUtils.CRYPTO_TAG_SIZE]; + sBuff.get(nonce); + sBuff.get(tag); + + cryptParams = new CryptParameters() {{ + Nonce = nonce; + Tag = tag; + }}; + break; + case bSectionDerivationParameters: + assertLength(s.Data, /* iterations */ 8 + CryptoUtils.CRYPTO_SALT_SIZE); + + long iterations = sBuff.getLong(); + byte[] salt = new byte[CryptoUtils.CRYPTO_SALT_SIZE]; + sBuff.get(salt); + + derParams = new DerivationParameters() {{ + IterationCount = iterations; + Salt = salt; + }}; + break; + } + } + + if ((level == SEC_LEVEL_DERIVED && (cryptParams == null || derParams == null)) + || (level == SEC_LEVEL_KEYSTORE && cryptParams == null)) { + throw new Exception("Security level parameters missing"); + } + + setCryptParameters(cryptParams); + setDerivationParameters(derParams); + + byte[] content = new byte[buffer.remaining()]; + buffer.get(content); + setContent(content); + } + + public void save() throws IOException { + byte[] data = serialize(); + + FileOutputStream file = context.openFileOutput(dbFilename, Context.MODE_PRIVATE); + file.write(data); + file.close(); + } + + public static DatabaseFile load(Context context) throws Exception { + FileInputStream file = context.openFileInput(dbFilename); + byte[] data = new byte[(int) file.getChannel().size()]; + file.read(data); + file.close(); + + DatabaseFile db = new DatabaseFile(context); + db.deserialize(data); + return db; + } + + private static void writeSection(DataOutputStream stream, byte id, byte[] data) throws IOException { + stream.write(id); + + ByteBuffer buffer = newBuffer(/* sizeof uint32_t */ 4); + if (data == null) { + buffer.putInt(0); + } else { + buffer.putInt(data.length); + } + stream.write(buffer.array()); + + if (data != null) { + stream.write(data); + } + } + + private static section readSection(ByteBuffer buffer) { + section s = new section(); + s.ID = buffer.get(); + + int len = buffer.getInt(); + s.Data = new byte[len]; + buffer.get(s.Data); + + return s; + } + + private static ByteBuffer newBuffer(byte[] data) { + ByteBuffer buffer = ByteBuffer.wrap(data); + buffer.order(ByteOrder.LITTLE_ENDIAN); + return buffer; + } + + private static ByteBuffer newBuffer(int size) { + ByteBuffer buffer = ByteBuffer.allocate(size); + buffer.order(ByteOrder.LITTLE_ENDIAN); + return buffer; + } + + private static void assertLength(byte[] bytes, int length) throws Exception { + if (bytes.length != length) { + throw new Exception("Bad length"); + } + } + + public byte[] getContent() { + return content; + } + + public void setContent(byte[] content) { + this.content = content; + } + + public CryptParameters getCryptParameters() { + return cryptParameters; + } + + public void setCryptParameters(CryptParameters parameters) { + this.cryptParameters = parameters; + } + + public DerivationParameters getDerivationParameters() { + return derivationParameters; + } + + public void setDerivationParameters(DerivationParameters derivationParameters) { + this.derivationParameters = derivationParameters; + } + + public byte getLevel() { + return level; + } + + public void setLevel(byte level) { + this.level = level; + } + + private static class section { + public byte ID; + public byte[] Data; + } +} diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseHelper.java b/app/src/main/java/me/impy/aegis/db/DatabaseHelper.java deleted file mode 100644 index 4b999ec2..00000000 --- a/app/src/main/java/me/impy/aegis/db/DatabaseHelper.java +++ /dev/null @@ -1,38 +0,0 @@ -package me.impy.aegis.db; - -import android.content.Context; - -import net.sqlcipher.database.SQLiteDatabase; -import net.sqlcipher.database.SQLiteOpenHelper; - -import java.io.File; - -import me.impy.aegis.crypto.KeyInfo; -import me.impy.aegis.encoding.Base32; - -public class DatabaseHelper extends SQLiteOpenHelper { - // NOTE: increment this every time the schema is changed - public static final int Version = 1; - - private static final String queryCreateOTPTable = - "create table otp (" + - "id integer primary key autoincrement, " + - "name varchar not null, " + - "url varchar not null, " + - "'order' integer)"; - - public DatabaseHelper(Context context, String filename) { - super(context, filename, null, Version); - } - - public void onCreate(SQLiteDatabase db) { - db.execSQL(queryCreateOTPTable); - } - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - //db.execSQL(SQL_DELETE_ENTRIES); - //onCreate(db); - } - public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { - onUpgrade(db, oldVersion, newVersion); - } -} diff --git a/app/src/main/java/me/impy/aegis/finger/FingerprintAuthenticationDialogFragment.java b/app/src/main/java/me/impy/aegis/finger/FingerprintAuthenticationDialogFragment.java new file mode 100644 index 00000000..fca10304 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/finger/FingerprintAuthenticationDialogFragment.java @@ -0,0 +1,277 @@ +package me.impy.aegis.finger; + +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +import android.app.Activity; +import android.app.DialogFragment; +import android.content.SharedPreferences; +import android.hardware.fingerprint.FingerprintManager; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.annotation.RequiresApi; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; + +import me.impy.aegis.MainActivity; +import me.impy.aegis.R; + +/** + * A dialog which uses fingerprint APIs to authenticate the user, and falls back to password + * authentication if fingerprint is not available. + */ +@RequiresApi(api = Build.VERSION_CODES.M) +public class FingerprintAuthenticationDialogFragment extends DialogFragment + implements TextView.OnEditorActionListener, FingerprintUiHelper.Callback { + + private Button mCancelButton; + private Button mSecondDialogButton; + private View mFingerprintContent; + private View mBackupContent; + private EditText mPassword; + private CheckBox mUseFingerprintFutureCheckBox; + private TextView mPasswordDescriptionTextView; + private TextView mNewFingerprintEnrolledTextView; + + private Stage mStage = Stage.FINGERPRINT; + private Action mAction; + + private FingerprintManager.CryptoObject mCryptoObject; + private FingerprintUiHelper mFingerprintUiHelper; + private MainActivity mActivity; + + private InputMethodManager mInputMethodManager; + private SharedPreferences mSharedPreferences; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Do not create a new Fragment when the Activity is re-created such as orientation changes. + setRetainInstance(true); + setStyle(DialogFragment.STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + getDialog().setTitle(getString(R.string.sign_in)); + View v = inflater.inflate(R.layout.fingerprint_dialog_container, container, false); + mCancelButton = (Button) v.findViewById(R.id.cancel_button); + mCancelButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + dismiss(); + } + }); + + mSecondDialogButton = (Button) v.findViewById(R.id.second_dialog_button); + mSecondDialogButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mStage == Stage.FINGERPRINT) { + goToBackup(); + } else { + verifyPassword(); + } + } + }); + mFingerprintContent = v.findViewById(R.id.fingerprint_container); + mBackupContent = v.findViewById(R.id.backup_container); + mPassword = (EditText) v.findViewById(R.id.password); + mPassword.setOnEditorActionListener(this); + mPasswordDescriptionTextView = (TextView) v.findViewById(R.id.password_description); + mUseFingerprintFutureCheckBox = (CheckBox) + v.findViewById(R.id.use_fingerprint_in_future_check); + mNewFingerprintEnrolledTextView = (TextView) + v.findViewById(R.id.new_fingerprint_enrolled_description); + mFingerprintUiHelper = new FingerprintUiHelper( + mActivity.getSystemService(FingerprintManager.class), + (ImageView) v.findViewById(R.id.fingerprint_icon), + (TextView) v.findViewById(R.id.fingerprint_status), this); + updateStage(); + + // If fingerprint authentication is not available, switch immediately to the backup + // (password) screen. + if (!mFingerprintUiHelper.isFingerprintAuthAvailable()) { + goToBackup(); + } + return v; + } + + @Override + public void onResume() { + super.onResume(); + if (mStage == Stage.FINGERPRINT) { + mFingerprintUiHelper.startListening(mCryptoObject); + } + } + + public void setStage(Stage stage) { + mStage = stage; + } + + public void setAction(Action action) { mAction = action; } + + @Override + public void onPause() { + super.onPause(); + mFingerprintUiHelper.stopListening(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mActivity = (MainActivity) activity; + mInputMethodManager = mActivity.getSystemService(InputMethodManager.class); + mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mActivity); + } + + /** + * Sets the crypto object to be passed in when authenticating with fingerprint. + */ + public void setCryptoObject(FingerprintManager.CryptoObject cryptoObject) { + mCryptoObject = cryptoObject; + } + + /** + * Switches to backup (password) screen. This either can happen when fingerprint is not + * available or the user chooses to use the password authentication method by pressing the + * button. This can also happen when the user had too many fingerprint attempts. + */ + private void goToBackup() { + mStage = Stage.PASSWORD; + updateStage(); + mPassword.requestFocus(); + + // Show the keyboard. + mPassword.postDelayed(mShowKeyboardRunnable, 500); + + // Fingerprint is not used anymore. Stop listening for it. + mFingerprintUiHelper.stopListening(); + } + + /** + * Checks whether the current entered password is correct, and dismisses the the dialog and + * let's the activity know about the result. + */ + private void verifyPassword() { + if (!checkPassword(mPassword.getText().toString())) { + return; + } + if (mStage == Stage.NEW_FINGERPRINT_ENROLLED) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(getString(R.string.use_fingerprint_to_authenticate_key), + mUseFingerprintFutureCheckBox.isChecked()); + editor.apply(); + + if (mUseFingerprintFutureCheckBox.isChecked()) { + // Re-create the key so that fingerprints including new ones are validated. + //mActivity.createKey(MainActivity.DEFAULT_KEY_NAME, true); + mStage = Stage.FINGERPRINT; + } + } + mPassword.setText(""); + //mActivity.onPurchased(, null); + dismiss(); + } + + /** + * @return true if {@code password} is correct, false otherwise + */ + private boolean checkPassword(String password) { + // Assume the password is always correct. + // In the real world situation, the password needs to be verified in the server side. + return password.length() > 0; + } + + private final Runnable mShowKeyboardRunnable = new Runnable() { + @Override + public void run() { + mInputMethodManager.showSoftInput(mPassword, 0); + } + }; + + private void updateStage() { + switch (mStage) { + case FINGERPRINT: + mCancelButton.setText(R.string.cancel); + mSecondDialogButton.setText(R.string.use_password); + mFingerprintContent.setVisibility(View.VISIBLE); + mBackupContent.setVisibility(View.GONE); + break; + case NEW_FINGERPRINT_ENROLLED: + // Intentional fall through + case PASSWORD: + mCancelButton.setText(R.string.cancel); + mSecondDialogButton.setText(R.string.ok); + mFingerprintContent.setVisibility(View.GONE); + mBackupContent.setVisibility(View.VISIBLE); + if (mStage == Stage.NEW_FINGERPRINT_ENROLLED) { + mPasswordDescriptionTextView.setVisibility(View.GONE); + mNewFingerprintEnrolledTextView.setVisibility(View.VISIBLE); + mUseFingerprintFutureCheckBox.setVisibility(View.VISIBLE); + } + break; + } + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_GO) { + verifyPassword(); + return true; + } + return false; + } + + @Override + public void onAuthenticated() { + // Callback from FingerprintUiHelper. Let the activity know that authentication was + // successful. + mActivity.onAuthenticated(mAction, mCryptoObject); + dismiss(); + } + + @Override + public void onError() { + goToBackup(); + } + + /** + * Enumeration to indicate which authentication method the user is trying to authenticate with. + */ + public enum Stage { + FINGERPRINT, + NEW_FINGERPRINT_ENROLLED, + PASSWORD + } + + public enum Action { + LOAD, + SAVE + } +} \ No newline at end of file diff --git a/app/src/main/java/me/impy/aegis/finger/FingerprintUiHelper.java b/app/src/main/java/me/impy/aegis/finger/FingerprintUiHelper.java new file mode 100644 index 00000000..e51be546 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/finger/FingerprintUiHelper.java @@ -0,0 +1,150 @@ +package me.impy.aegis.finger; + +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +import android.hardware.fingerprint.FingerprintManager; +import android.os.Build; +import android.os.CancellationSignal; +import android.support.annotation.RequiresApi; +import android.widget.ImageView; +import android.widget.TextView; + +import me.impy.aegis.R; + +/** + * Small helper class to manage text/icon around fingerprint authentication UI. + */ +@RequiresApi(api = Build.VERSION_CODES.M) +public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallback { + + private static final long ERROR_TIMEOUT_MILLIS = 1600; + private static final long SUCCESS_DELAY_MILLIS = 1300; + + private final FingerprintManager mFingerprintManager; + private final ImageView mIcon; + private final TextView mErrorTextView; + private final Callback mCallback; + private CancellationSignal mCancellationSignal; + + private boolean mSelfCancelled; + + /** + * Constructor for {@link FingerprintUiHelper}. + */ + FingerprintUiHelper(FingerprintManager fingerprintManager, + ImageView icon, TextView errorTextView, Callback callback) { + mFingerprintManager = fingerprintManager; + mIcon = icon; + mErrorTextView = errorTextView; + mCallback = callback; + } + + public boolean isFingerprintAuthAvailable() { + // The line below prevents the false positive inspection from Android Studio + // noinspection ResourceType + return mFingerprintManager.isHardwareDetected() + && mFingerprintManager.hasEnrolledFingerprints(); + } + + public void startListening(FingerprintManager.CryptoObject cryptoObject) { + if (!isFingerprintAuthAvailable()) { + return; + } + mCancellationSignal = new CancellationSignal(); + mSelfCancelled = false; + // The line below prevents the false positive inspection from Android Studio + // noinspection ResourceType + mFingerprintManager + .authenticate(cryptoObject, mCancellationSignal, 0 /* flags */, this, null); + mIcon.setImageResource(R.drawable.ic_fp_40px); + } + + public void stopListening() { + if (mCancellationSignal != null) { + mSelfCancelled = true; + mCancellationSignal.cancel(); + mCancellationSignal = null; + } + } + + @Override + public void onAuthenticationError(int errMsgId, CharSequence errString) { + if (!mSelfCancelled) { + showError(errString); + mIcon.postDelayed(new Runnable() { + @Override + public void run() { + mCallback.onError(); + } + }, ERROR_TIMEOUT_MILLIS); + } + } + + @Override + public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { + showError(helpString); + } + + @Override + public void onAuthenticationFailed() { + showError(mIcon.getResources().getString( + R.string.fingerprint_not_recognized)); + } + + @Override + 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() { + @Override + public void run() { + mCallback.onAuthenticated(); + } + }, SUCCESS_DELAY_MILLIS); + } + + 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); + } + + 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); + } + }; + + public interface Callback { + + void onAuthenticated(); + + void onError(); + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_fp_40px.png b/app/src/main/res/drawable-hdpi/ic_fp_40px.png new file mode 100644 index 0000000000000000000000000000000000000000..48ebd8ad737272d78d0ab9141c47c84f463fa3bf GIT binary patch literal 7011 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/tile.9.png b/app/src/main/res/drawable-hdpi/tile.9.png new file mode 100644 index 0000000000000000000000000000000000000000..135862883e26eddce2b19db021adf62e10357ad0 GIT binary patch literal 196 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-nodpi/android_robot.png b/app/src/main/res/drawable-nodpi/android_robot.png new file mode 100644 index 0000000000000000000000000000000000000000..40bf934bb5867e7d1ffa8e8cc705c4b7f1ba5c25 GIT binary patch literal 8288 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 LZIvn|>+t^oPe^CY literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_fp_40px.png b/app/src/main/res/drawable-xhdpi/ic_fp_40px.png new file mode 100644 index 0000000000000000000000000000000000000000..e1c9590bbfbcf220b08879f2f9e560a4f285f4ec GIT binary patch literal 10524 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 O0000 + + + + + diff --git a/app/src/main/res/drawable/ic_fingerprint_success.xml b/app/src/main/res/drawable/ic_fingerprint_success.xml new file mode 100644 index 00000000..261f3e7f --- /dev/null +++ b/app/src/main/res/drawable/ic_fingerprint_success.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/app/src/main/res/layout/fingerprint_dialog_backup.xml b/app/src/main/res/layout/fingerprint_dialog_backup.xml new file mode 100644 index 00000000..7c0cebc6 --- /dev/null +++ b/app/src/main/res/layout/fingerprint_dialog_backup.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fingerprint_dialog_container.xml b/app/src/main/res/layout/fingerprint_dialog_container.xml new file mode 100644 index 00000000..53c8e9af --- /dev/null +++ b/app/src/main/res/layout/fingerprint_dialog_container.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + +