Add an activity to manage database key slots

This commit is contained in:
Alexander Bakker 2018-02-09 17:31:07 +01:00
parent d5f796ca87
commit c24b691a26
24 changed files with 798 additions and 33 deletions

View file

@ -1,6 +1,5 @@
package me.impy.aegis;
import android.content.DialogInterface;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
@ -98,12 +97,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
builder.setTitle("Decryption error");
builder.setMessage("Master key integrity check failed for every slot. Make sure you didn't mistype your password.");
builder.setCancelable(false);
builder.setPositiveButton("OK",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
builder.setPositiveButton(android.R.string.ok, null);
builder.create().show();
}

View file

@ -0,0 +1,79 @@
package me.impy.aegis;
import android.app.Dialog;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.view.View;
import android.widget.TextView;
import com.mattprecious.swirl.SwirlView;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import me.impy.aegis.crypto.KeyStoreHandle;
import me.impy.aegis.crypto.slots.FingerprintSlot;
import me.impy.aegis.crypto.slots.Slot;
import me.impy.aegis.helpers.FingerprintHelper;
import me.impy.aegis.helpers.FingerprintUiHelper;
public class FingerprintDialogFragment extends SlotDialogFragment implements FingerprintUiHelper.Callback {
private Cipher _cipher;
private FingerprintUiHelper _helper;
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
View view = getActivity().getLayoutInflater().inflate(R.layout.dialog_fingerprint, null);
TextView textFingerprint = view.findViewById(R.id.text_fingerprint);
SwirlView imgFingerprint = view.findViewById(R.id.img_fingerprint);
FingerprintManager manager = FingerprintHelper.getManager(getContext());
try {
KeyStoreHandle handle = new KeyStoreHandle();
SecretKey key = handle.getKey();
_cipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE);
_helper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
} catch (Exception e) {
throw new RuntimeException(e);
}
return new AlertDialog.Builder(getActivity())
.setTitle("Register a new fingerprint")
.setView(view)
.setNegativeButton(android.R.string.cancel, null)
.create();
}
@Override
public void onResume() {
super.onResume();
if (_helper != null) {
_helper.startListening(new FingerprintManager.CryptoObject(_cipher));
}
}
@Override
public void onPause() {
super.onPause();
if (_helper != null) {
_helper.stopListening();
}
}
@Override
public void onAuthenticated() {
FingerprintSlot slot = new FingerprintSlot();
getListener().onSlotResult(slot, _cipher);
dismiss();
}
@Override
public void onError() {
}
}

View file

@ -26,6 +26,7 @@ import java.lang.reflect.UndeclaredThrowableException;
import java.util.List;
import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.crypto.slots.SlotCollection;
import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.db.DatabaseManager;
import me.impy.aegis.helpers.PermissionHelper;
@ -42,6 +43,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
private static final int CODE_DECRYPT = 5;
private static final int CODE_IMPORT = 6;
private static final int CODE_PREFERENCES = 7;
private static final int CODE_SLOTS = 8;
// permission request codes
private static final int CODE_PERM_EXPORT = 0;
@ -183,6 +185,8 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
case CODE_PREFERENCES:
onPreferencesResult(resultCode, data);
break;
case CODE_SLOTS:
onSlotManagerResult(resultCode, data);
}
}
@ -206,6 +210,16 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
}
}
private void onSlotManagerResult(int resultCode, Intent data) {
if (resultCode != RESULT_OK) {
return;
}
SlotCollection slots = (SlotCollection) data.getSerializableExtra("slots");
_db.getFile().setSlots(slots);
saveDatabase();
}
private void onPreferencesResult(int resultCode, Intent data) {
// refresh the entire key profile list if needed
if (data.getBooleanExtra("needsRefresh", false)) {
@ -219,6 +233,20 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
case PreferencesActivity.ACTION_EXPORT:
onExport();
break;
case PreferencesActivity.ACTION_SLOTS:
MasterKey masterKey;
try {
masterKey = _db.getMasterKey();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "An error occurred while trying to obtain the database key", Toast.LENGTH_SHORT).show();
break;
}
Intent intent = new Intent(this, SlotManagerActivity.class);
intent.putExtra("masterKey", masterKey);
intent.putExtra("slots", _db.getFile().getSlots());
startActivityForResult(intent, CODE_SLOTS);
break;
}
}
@ -554,8 +582,9 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
Intent preferencesActivity = new Intent(this, PreferencesActivity.class);
startActivityForResult(preferencesActivity, CODE_PREFERENCES);
Intent intent = new Intent(this, PreferencesActivity.class);
intent.putExtra("encrypted", _db.getFile().isEncrypted());
startActivityForResult(intent, CODE_PREFERENCES);
return true;
case R.id.action_import:
if (PermissionHelper.request(this, CODE_PERM_IMPORT, Manifest.permission.READ_EXTERNAL_STORAGE)) {

View file

@ -0,0 +1,82 @@
package me.impy.aegis;
import android.app.Dialog;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import javax.crypto.Cipher;
import me.impy.aegis.crypto.slots.PasswordSlot;
import me.impy.aegis.crypto.slots.Slot;
import me.impy.aegis.helpers.AuthHelper;
public class PasswordDialogFragment extends SlotDialogFragment {
private Button _buttonOK;
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
View view = getActivity().getLayoutInflater().inflate(R.layout.dialog_password, null);
EditText textPassword = view.findViewById(R.id.text_password);
EditText textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
AlertDialog alert = new AlertDialog.Builder(getActivity())
.setTitle("Enter a new password")
.setView(view)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
alert.setOnShowListener(dialog -> {
_buttonOK = alert.getButton(AlertDialog.BUTTON_POSITIVE);
_buttonOK.setEnabled(false);
// replace the default listener
_buttonOK.setOnClickListener(v -> {
if (!AuthHelper.arePasswordsEqual(textPassword, textPasswordConfirm)) {
return;
}
AuthHelper.clearPassword(textPasswordConfirm);
char[] password = AuthHelper.getPassword(textPassword, true);
PasswordSlot slot = new PasswordSlot();
DerivationTask task = new DerivationTask(getContext(), key -> {
Cipher cipher;
try {
cipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE);
} catch (Exception e) {
getListener().onException(e);
dialog.cancel();
return;
}
getListener().onSlotResult(slot, cipher);
dialog.dismiss();
});
task.execute(new DerivationTask.Params() {{
Slot = slot;
Password = password;
}});
});
});
TextWatcher watcher = new TextWatcher() {
public void onTextChanged(CharSequence c, int start, int before, int count) {
boolean equal = AuthHelper.arePasswordsEqual(textPassword, textPasswordConfirm);
_buttonOK.setEnabled(equal);
}
public void beforeTextChanged(CharSequence c, int start, int count, int after) { }
public void afterTextChanged(Editable c) { }
};
textPassword.addTextChangedListener(watcher);
textPasswordConfirm.addTextChangedListener(watcher);
return alert;
}
}

View file

@ -9,12 +9,15 @@ import android.widget.Toast;
public class PreferencesActivity extends AegisActivity {
public static final int ACTION_EXPORT = 0;
public static final int ACTION_SLOTS = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getFragmentManager().beginTransaction().replace(android.R.id.content, new PreferencesFragment()).commit();
PreferencesFragment fragment = new PreferencesFragment();
fragment.setArguments(getIntent().getExtras());
getFragmentManager().beginTransaction().replace(android.R.id.content, fragment).commit();
}
@Override
@ -65,6 +68,17 @@ public class PreferencesActivity extends AegisActivity {
}
});
Preference slotsPreference = findPreference("pref_slots");
slotsPreference.setEnabled(getArguments().getBoolean("encrypted"));
slotsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
_result.putExtra("action", ACTION_SLOTS);
finish();
return true;
}
});
Preference issuerPreference = findPreference("pref_issuer");
issuerPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override

View file

@ -0,0 +1,70 @@
package me.impy.aegis;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import me.impy.aegis.crypto.slots.Slot;
public class SlotAdapter extends RecyclerView.Adapter<SlotHolder> {
private Listener _listener;
private ArrayList<Slot> _slots;
public SlotAdapter(Listener listener) {
_listener = listener;
_slots = new ArrayList<>();
}
public void addSlot(Slot slot) {
_slots.add(slot);
int position = getItemCount() - 1;
if (position == 0) {
notifyDataSetChanged();
} else {
notifyItemInserted(position);
}
}
public void updateSlot(Slot slot) {
notifyItemChanged(_slots.indexOf(slot));
}
public void removeSlot(Slot slot) {
int position = _slots.indexOf(slot);
_slots.remove(position);
notifyItemRemoved(position);
}
@Override
public SlotHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_slot, parent, false);
return new SlotHolder(view);
}
@Override
public void onBindViewHolder(SlotHolder holder, int position) {
holder.setData(_slots.get(position));
holder.setOnEditClickListener(v -> {
int position1 = holder.getAdapterPosition();
_listener.onEditSlot(_slots.get(position1));
});
holder.setOnDeleteClickListener(v -> {
int position12 = holder.getAdapterPosition();
_listener.onRemoveSlot(_slots.get(position12));
});
}
@Override
public int getItemCount() {
return _slots.size();
}
public interface Listener {
void onEditSlot(Slot slot);
void onRemoveSlot(Slot slot);
}
}

View file

@ -42,7 +42,6 @@ public class SlotCollectionTask<T extends Slot> extends ProgressDialogTask<SlotC
if (slot instanceof PasswordSlot) {
char[] password = (char[])params.Obj;
SecretKey key = ((PasswordSlot)slot).deriveKey(password);
CryptoUtils.zero(password);
Cipher cipher = Slot.createCipher(key, Cipher.DECRYPT_MODE);
masterKey = params.Slots.decrypt(slot, cipher);
} else if (slot instanceof FingerprintSlot) {
@ -63,6 +62,10 @@ public class SlotCollectionTask<T extends Slot> extends ProgressDialogTask<SlotC
return null;
} catch (Exception e) {
throw new UndeclaredThrowableException(e);
} finally {
if (params.Obj instanceof char[]) {
CryptoUtils.zero((char[]) params.Obj);
}
}
}

View file

@ -0,0 +1,32 @@
package me.impy.aegis;
import android.content.Context;
import android.support.v4.app.DialogFragment;
import javax.crypto.Cipher;
import me.impy.aegis.crypto.slots.Slot;
public class SlotDialogFragment extends DialogFragment {
private Listener _listener;
protected Listener getListener() {
return _listener;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
_listener = (Listener) context;
} catch (ClassCastException e) {
throw new ClassCastException(context.toString() + " must implement SlotDialogFragment.Listener");
}
}
public interface Listener {
void onSlotResult(Slot slot, Cipher cipher);
void onException(Exception e);
}
}

View file

@ -0,0 +1,52 @@
package me.impy.aegis;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import me.impy.aegis.crypto.slots.FingerprintSlot;
import me.impy.aegis.crypto.slots.PasswordSlot;
import me.impy.aegis.crypto.slots.RawSlot;
import me.impy.aegis.crypto.slots.Slot;
public class SlotHolder extends RecyclerView.ViewHolder {
private TextView _slotUsed;
private TextView _slotName;
private ImageView _slotImg;
private LinearLayout _buttonEdit;
private ImageView _buttonDelete;
public SlotHolder(final View view) {
super(view);
_slotUsed = view.findViewById(R.id.text_slot_used);
_slotName = view.findViewById(R.id.text_slot_name);
_slotImg = view.findViewById(R.id.img_slot);
_buttonEdit = view.findViewById(R.id.button_edit);
_buttonDelete = view.findViewById(R.id.button_delete);
}
public void setData(Slot slot) {
if (slot instanceof PasswordSlot) {
_slotName.setText("Password 1");
_slotImg.setImageResource(R.drawable.ic_create_black_24dp);
} else if (slot instanceof FingerprintSlot) {
_slotName.setText("Finger 1");
_slotImg.setImageResource(R.drawable.ic_fingerprint_black_24dp);
} else if (slot instanceof RawSlot) {
_slotName.setText("Raw 1");
_slotImg.setImageResource(R.drawable.ic_vpn_key_black_24dp);
} else {
throw new RuntimeException();
}
}
public void setOnEditClickListener(View.OnClickListener listener) {
_buttonEdit.setOnClickListener(listener);
}
public void setOnDeleteClickListener(View.OnClickListener listener) {
_buttonDelete.setOnClickListener(listener);
}
}

View file

@ -0,0 +1,156 @@
package me.impy.aegis;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import javax.crypto.Cipher;
import me.impy.aegis.crypto.MasterKey;
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.helpers.FingerprintHelper;
public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener, SlotDialogFragment.Listener {
private MasterKey _masterKey;
private SlotCollection _slots;
private SlotAdapter _adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// set up the view
setContentView(R.layout.activity_slots);
setSupportActionBar(findViewById(R.id.toolbar));
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true);
// only show the fingerprint option if we can get an instance of the fingerprint manager
// TODO: also hide the option if this device's fingerprint has already been registered
if (FingerprintHelper.getManager(this) != null) {
findViewById(R.id.button_add_fingerprint).setOnClickListener(view -> {
FingerprintDialogFragment dialog = new FingerprintDialogFragment();
dialog.show(getSupportFragmentManager(), null);
});
} else {
findViewById(R.id.button_add_fingerprint).setVisibility(View.GONE);
}
findViewById(R.id.button_add_password).setOnClickListener(view -> {
PasswordDialogFragment dialog = new PasswordDialogFragment();
dialog.show(getSupportFragmentManager(), null);
});
// set up the recycler view
_adapter = new SlotAdapter(this);
RecyclerView slotsView = findViewById(R.id.list_slots);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
slotsView.setLayoutManager(layoutManager);
slotsView.setAdapter(_adapter);
slotsView.setNestedScrollingEnabled(false);
// load the slots and masterKey
_masterKey = (MasterKey) getIntent().getSerializableExtra("masterKey");
_slots = (SlotCollection) getIntent().getSerializableExtra("slots");
for (Slot slot : _slots) {
_adapter.addSlot(slot);
}
}
private boolean onSave() {
Intent intent = new Intent();
intent.putExtra("slots", _slots);
setResult(RESULT_OK, intent);
finish();
return true;
}
@Override
protected void setPreferredTheme(boolean nightMode) {
if (nightMode) {
setTheme(R.style.AppTheme_Dark_NoActionBar);
} else {
setTheme(R.style.AppTheme_Default_NoActionBar);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
case R.id.action_save:
return onSave();
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_slots, menu);
return true;
}
@Override
public void onEditSlot(Slot slot) {
EditText textName = new EditText(this);
textName.setHint("Name");
new AlertDialog.Builder(this)
.setTitle("Edit slot name")
.setView(textName)
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
String name = textName.getText().toString();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
@Override
public void onRemoveSlot(Slot slot) {
if (slot instanceof PasswordSlot && _slots.findAll(PasswordSlot.class).size() <= 1) {
Toast.makeText(this, "You must have at least one password slot", Toast.LENGTH_SHORT).show();
return;
}
new AlertDialog.Builder(this)
.setTitle("Remove slot")
.setMessage("Are you sure you want to remove this slot?")
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
_slots.remove(slot);
_adapter.removeSlot(slot);
})
.setNegativeButton(android.R.string.no, null)
.show();
}
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
try {
_slots.encrypt(slot, _masterKey, cipher);
} catch (Exception e) {
onException(e);
return;
}
_slots.add(slot);
_adapter.addSlot(slot);
}
@Override
public void onException(Exception e) {
Toast.makeText(this, "An error occurred while trying to add a new slot: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}

View file

@ -167,6 +167,11 @@ public class DatabaseManager {
return _db.getKeys();
}
public MasterKey getMasterKey() throws Exception {
assertState(false, true);
return _key;
}
public DatabaseFile getFile() {
return _file;
}