This commit is contained in:
Michael Schättgen 2018-01-02 20:29:41 +01:00
commit 5dd00391fe
21 changed files with 573 additions and 317 deletions

View file

@ -40,5 +40,6 @@ dependencies {
compile 'com.mattprecious.swirl:swirl:1.0.0' compile 'com.mattprecious.swirl:swirl:1.0.0'
compile 'com.madgag.spongycastle:core:1.56.0.0' compile 'com.madgag.spongycastle:core:1.56.0.0'
compile 'com.github.apl-devs:appintro:v4.2.2' compile 'com.github.apl-devs:appintro:v4.2.2'
compile 'com.getbase:floatingactionbutton:1.10.1'
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
} }

View file

@ -39,7 +39,7 @@
<activity <activity
android:name=".EditProfileActivity" android:name=".EditProfileActivity"
android:label="Edit profile" android:label="Edit profile"
android:theme="@style/AppTheme.TransparentActionBar"> android:theme="@style/AppTheme.Default.TransparentActionBar">
</activity> </activity>
<activity <activity
android:name=".IntroActivity" android:name=".IntroActivity"
@ -48,7 +48,8 @@
</activity> </activity>
<activity <activity
android:name=".AuthActivity" android:name=".AuthActivity"
android:launchMode="singleTop"> android:launchMode="singleTop"
android:theme="@style/AppTheme.Default.NoActionBar">
</activity> </activity>
<activity android:name=".PreferencesActivity"> <activity android:name=".PreferencesActivity">
</activity> </activity>

View file

@ -16,12 +16,16 @@ import android.widget.EditText;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.Spinner; import android.widget.Spinner;
import me.impy.aegis.crypto.CryptoUtils;
import me.impy.aegis.crypto.KeyInfo; import me.impy.aegis.crypto.KeyInfo;
import me.impy.aegis.crypto.KeyInfoException;
import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.encoding.Base32; import me.impy.aegis.encoding.Base32;
import me.impy.aegis.helpers.AuthHelper;
import me.impy.aegis.helpers.SpinnerHelper; import me.impy.aegis.helpers.SpinnerHelper;
public class EditProfileActivity extends AegisActivity { public class EditProfileActivity extends AegisActivity {
private boolean _isNew = false;
private boolean _edited = false; private boolean _edited = false;
private KeyProfile _profile; private KeyProfile _profile;
@ -40,52 +44,66 @@ public class EditProfileActivity extends AegisActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit_profile); setContentView(R.layout.activity_edit_profile);
_profile = (KeyProfile) getIntent().getSerializableExtra("KeyProfile");
ActionBar bar = getSupportActionBar(); ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close); bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true); bar.setDisplayHomeAsUpEnabled(true);
// if the intent doesn't contain a KeyProfile, create a new one
_profile = (KeyProfile) getIntent().getSerializableExtra("KeyProfile");
if (_profile == null) {
_isNew = true;
_profile = new KeyProfile();
setTitle("Add profile");
}
_textName = findViewById(R.id.text_name);
_textIssuer = findViewById(R.id.text_issuer);
_textPeriod = findViewById(R.id.text_period);
_textSecret = findViewById(R.id.text_secret);
_spinnerType = findViewById(R.id.spinner_type);
SpinnerHelper.fillSpinner(this, _spinnerType, R.array.otp_types_array);
_spinnerAlgo = findViewById(R.id.spinner_algo);
SpinnerHelper.fillSpinner(this, _spinnerAlgo, R.array.otp_algo_array);
_spinnerDigits = findViewById(R.id.spinner_digits);
SpinnerHelper.fillSpinner(this, _spinnerDigits, R.array.otp_digits_array);
updateFields();
_textName.addTextChangedListener(_textListener);
_textIssuer.addTextChangedListener(_textListener);
_textPeriod.addTextChangedListener(_textListener);
_textSecret.addTextChangedListener(_textListener);
_spinnerType.setOnTouchListener(_selectedListener);
_spinnerType.setOnItemSelectedListener(_selectedListener);
_spinnerAlgo.setOnTouchListener(_selectedListener);
_spinnerAlgo.setOnItemSelectedListener(_selectedListener);
_spinnerDigits.setOnTouchListener(_selectedListener);
_spinnerDigits.setOnItemSelectedListener(_selectedListener);
}
private void updateFields() {
DatabaseEntry entry = _profile.getEntry();
ImageView imageView = findViewById(R.id.profile_drawable); ImageView imageView = findViewById(R.id.profile_drawable);
imageView.setImageDrawable(_profile.getDrawable()); imageView.setImageDrawable(_profile.getDrawable());
DatabaseEntry entry = _profile.getEntry();
_textName = findViewById(R.id.text_name);
_textName.setText(entry.getName()); _textName.setText(entry.getName());
_textName.addTextChangedListener(watcher);
_textIssuer = findViewById(R.id.text_issuer);
_textIssuer.setText(entry.getInfo().getIssuer()); _textIssuer.setText(entry.getInfo().getIssuer());
_textIssuer.addTextChangedListener(watcher);
_textPeriod = findViewById(R.id.text_period);
_textPeriod.setText(Integer.toString(entry.getInfo().getPeriod())); _textPeriod.setText(Integer.toString(entry.getInfo().getPeriod()));
_textPeriod.addTextChangedListener(watcher);
_textSecret = findViewById(R.id.text_secret); byte[] secretBytes = entry.getInfo().getSecret();
_textSecret.setText(Base32.encodeOriginal(entry.getInfo().getSecret())); if (secretBytes != null) {
_textSecret.addTextChangedListener(watcher); char[] secretChars = Base32.encode(secretBytes);
_textSecret.setText(secretChars, 0, secretChars.length);
}
String type = entry.getInfo().getType(); String type = entry.getInfo().getType();
_spinnerType = findViewById(R.id.spinner_type);
SpinnerHelper.fillSpinner(this, _spinnerType, R.array.otp_types_array);
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type), false); _spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type), false);
_spinnerType.setOnTouchListener(_selectedListener);
_spinnerType.setOnItemSelectedListener(_selectedListener);
String algo = entry.getInfo().getAlgorithm(false); String algo = entry.getInfo().getAlgorithm(false);
_spinnerAlgo = findViewById(R.id.spinner_algo);
SpinnerHelper.fillSpinner(this, _spinnerAlgo, R.array.otp_algo_array);
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false); _spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
_spinnerAlgo.setOnTouchListener(_selectedListener);
_spinnerAlgo.setOnItemSelectedListener(_selectedListener);
String digits = Integer.toString(entry.getInfo().getDigits()); String digits = Integer.toString(entry.getInfo().getDigits());
_spinnerDigits = findViewById(R.id.spinner_digits);
SpinnerHelper.fillSpinner(this, _spinnerDigits, R.array.otp_digits_array);
_spinnerDigits.setSelection(getStringResourceIndex(R.array.otp_digits_array, digits), false); _spinnerDigits.setSelection(getStringResourceIndex(R.array.otp_digits_array, digits), false);
_spinnerDigits.setOnTouchListener(_selectedListener);
_spinnerDigits.setOnItemSelectedListener(_selectedListener);
} }
@Override @Override
@ -129,14 +147,31 @@ public class EditProfileActivity extends AegisActivity {
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_edit, menu); getMenuInflater().inflate(R.menu.menu_edit, menu);
if (_isNew) {
menu.findItem(R.id.action_delete).setVisible(false);
}
return true; return true;
} }
private void finish(boolean delete) {
Intent intent = new Intent();
intent.putExtra("KeyProfile", _profile);
intent.putExtra("delete", delete);
setResult(RESULT_OK, intent);
finish();
}
private boolean onDelete() { private boolean onDelete() {
return false; finish(true);
return true;
} }
private boolean onSave() { private boolean onSave() {
if (_textSecret.length() == 0) {
onError("Secret is a required field.");
return false;
}
int period; int period;
try { try {
period = Integer.parseInt(_textPeriod.getText().toString()); period = Integer.parseInt(_textPeriod.getText().toString());
@ -159,17 +194,22 @@ public class EditProfileActivity extends AegisActivity {
DatabaseEntry entry = _profile.getEntry(); DatabaseEntry entry = _profile.getEntry();
entry.setName(_textName.getText().toString()); entry.setName(_textName.getText().toString());
KeyInfo info = entry.getInfo(); KeyInfo info = entry.getInfo();
try {
char[] secret = AuthHelper.getEditTextChars(_textSecret);
info.setSecret(secret);
CryptoUtils.zero(secret);
info.setIssuer(_textIssuer.getText().toString()); info.setIssuer(_textIssuer.getText().toString());
info.setSecret(Base32.decode(_textSecret.getText().toString()));
info.setPeriod(period); info.setPeriod(period);
info.setDigits(digits); info.setDigits(digits);
info.setAlgorithm(algo); info.setAlgorithm(algo);
info.setType(type); info.setType(type);
} catch (KeyInfoException e) {
onError("The entered info is incorrect: " + e.getMessage());
return false;
}
Intent intent = new Intent(); finish(false);
intent.putExtra("KeyProfile", _profile);
setResult(RESULT_OK, intent);
finish();
return true; return true;
} }
@ -185,7 +225,7 @@ public class EditProfileActivity extends AegisActivity {
_edited = true; _edited = true;
} }
private TextWatcher watcher = new TextWatcher() { private TextWatcher _textListener = new TextWatcher() {
@Override @Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { public void beforeTextChanged(CharSequence s, int start, int count, int after) {
onFieldEdited(); onFieldEdited();

View file

@ -13,6 +13,10 @@ public class KeyProfile implements Serializable {
private String _code; private String _code;
private DatabaseEntry _entry; private DatabaseEntry _entry;
public KeyProfile() {
this(new DatabaseEntry());
}
public KeyProfile(DatabaseEntry entry) { public KeyProfile(DatabaseEntry entry) {
_entry = entry; _entry = entry;
} }
@ -35,7 +39,7 @@ public class KeyProfile implements Serializable {
public TextDrawable getDrawable() { public TextDrawable getDrawable() {
String name = _entry.getName(); String name = _entry.getName();
if (name == null) { if (name == null || name.length() <= 1) {
return null; return null;
} }

View file

@ -36,6 +36,7 @@ public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileHolder> im
} }
public void removeKey(KeyProfile profile) { public void removeKey(KeyProfile profile) {
profile = getKeyByID(profile.getEntry().getID());
int position = _keyProfiles.indexOf(profile); int position = _keyProfiles.indexOf(profile);
_keyProfiles.remove(position); _keyProfiles.remove(position);
notifyItemRemoved(position); notifyItemRemoved(position);
@ -47,12 +48,16 @@ public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileHolder> im
} }
public void replaceKey(KeyProfile newProfile) { public void replaceKey(KeyProfile newProfile) {
for (KeyProfile oldProfile : _keyProfiles) { KeyProfile oldProfile = getKeyByID(newProfile.getEntry().getID());
if (oldProfile.getEntry().getID() == newProfile.getEntry().getID()) {
int position = _keyProfiles.indexOf(oldProfile); int position = _keyProfiles.indexOf(oldProfile);
_keyProfiles.set(position, newProfile); _keyProfiles.set(position, newProfile);
notifyItemChanged(position); notifyItemChanged(position);
return; }
private KeyProfile getKeyByID(long id) {
for (KeyProfile profile : _keyProfiles) {
if (profile.getEntry().getID() == id) {
return profile;
} }
} }
throw new AssertionError("no key profile found with the same id"); throw new AssertionError("no key profile found with the same id");

View file

@ -6,18 +6,20 @@ import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.graphics.Rect;
import android.media.MediaScannerConnection; import android.media.MediaScannerConnection;
import android.support.design.widget.BottomSheetDialog; import android.support.design.widget.BottomSheetDialog;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.Toast; import android.widget.Toast;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.UndeclaredThrowableException; import java.lang.reflect.UndeclaredThrowableException;
@ -32,13 +34,14 @@ import me.impy.aegis.util.ByteInputStream;
public class MainActivity extends AegisActivity implements KeyProfileView.Listener { public class MainActivity extends AegisActivity implements KeyProfileView.Listener {
// activity request codes // activity request codes
private static final int CODE_GET_KEYINFO = 0; private static final int CODE_SCAN_KEYINFO = 0;
private static final int CODE_ADD_KEYINFO = 1; private static final int CODE_ADD_KEYINFO = 1;
private static final int CODE_EDIT_KEYINFO = 2; private static final int CODE_EDIT_KEYINFO = 2;
private static final int CODE_DO_INTRO = 3; private static final int CODE_ENTER_KEYINFO = 3;
private static final int CODE_DECRYPT = 4; private static final int CODE_DO_INTRO = 4;
private static final int CODE_IMPORT = 5; private static final int CODE_DECRYPT = 5;
private static final int CODE_PREFERENCES = 6; private static final int CODE_IMPORT = 6;
private static final int CODE_PREFERENCES = 7;
// permission request codes // permission request codes
private static final int CODE_PERM_EXPORT = 0; private static final int CODE_PERM_EXPORT = 0;
@ -51,6 +54,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
private boolean _nightMode = false; private boolean _nightMode = false;
private Menu _menu; private Menu _menu;
private FloatingActionsMenu _fabMenu;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -60,8 +64,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
// set up the main view // set up the main view
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(findViewById(R.id.toolbar));
setSupportActionBar(toolbar);
// set up the key profile view // set up the key profile view
_keyProfileView = (KeyProfileView) getSupportFragmentManager().findFragmentById(R.id.key_profiles); _keyProfileView = (KeyProfileView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
@ -69,9 +72,15 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
_keyProfileView.setShowIssuer(_app.getPreferences().getBoolean("pref_issuer", false)); _keyProfileView.setShowIssuer(_app.getPreferences().getBoolean("pref_issuer", false));
// set up the floating action button // set up the floating action button
FloatingActionButton fab = findViewById(R.id.fab); _fabMenu = findViewById(R.id.fab);
fab.setEnabled(true); findViewById(R.id.fab_enter).setOnClickListener(view -> {
fab.setOnClickListener(view -> onGetKeyInfo()); _fabMenu.collapse();
onEnterKeyInfo();
});
findViewById(R.id.fab_scan).setOnClickListener(view -> {
_fabMenu.collapse();
onScanKeyInfo();
});
// skip this part if this is the not initial startup and the database has been unlocked // skip this part if this is the not initial startup and the database has been unlocked
if (!_app.isRunning() && _db.isLocked()) { if (!_app.isRunning() && _db.isLocked()) {
@ -106,6 +115,23 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
} }
} }
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
// collapse the fab menu on touch
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (_fabMenu.isExpanded()) {
Rect rect = new Rect();
_fabMenu.getGlobalVisibleRect(rect);
if (!rect.contains((int) event.getRawX(), (int) event.getRawY())) {
_fabMenu.collapse();
}
}
}
return super.dispatchTouchEvent(event);
}
@Override @Override
protected void onNewIntent(Intent intent) { protected void onNewIntent(Intent intent) {
super.onNewIntent(intent); super.onNewIntent(intent);
@ -133,8 +159,8 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
} }
switch (requestCode) { switch (requestCode) {
case CODE_GET_KEYINFO: case CODE_SCAN_KEYINFO:
onGetKeyInfoResult(resultCode, data); onScanKeyInfoResult(resultCode, data);
break; break;
case CODE_ADD_KEYINFO: case CODE_ADD_KEYINFO:
onAddKeyInfoResult(resultCode, data); onAddKeyInfoResult(resultCode, data);
@ -142,6 +168,9 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
case CODE_EDIT_KEYINFO: case CODE_EDIT_KEYINFO:
onEditKeyInfoResult(resultCode, data); onEditKeyInfoResult(resultCode, data);
break; break;
case CODE_ENTER_KEYINFO:
onEnterKeyInfoResult(resultCode, data);
break;
case CODE_DO_INTRO: case CODE_DO_INTRO:
onDoIntroResult(resultCode, data); onDoIntroResult(resultCode, data);
break; break;
@ -172,7 +201,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
onImport(); onImport();
break; break;
case CODE_PERM_CAMERA: case CODE_PERM_CAMERA:
onGetKeyInfo(); onScanKeyInfo();
break; break;
} }
} }
@ -297,7 +326,12 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
saveDatabase(); saveDatabase();
} }
private void onGetKeyInfo() { private void onEnterKeyInfo() {
Intent intent = new Intent(this, EditProfileActivity.class);
startActivityForResult(intent, CODE_ENTER_KEYINFO);
}
private void onScanKeyInfo() {
if (!PermissionHelper.request(this, CODE_PERM_CAMERA, Manifest.permission.CAMERA)) { if (!PermissionHelper.request(this, CODE_PERM_CAMERA, Manifest.permission.CAMERA)) {
return; return;
} }
@ -305,7 +339,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
startScanActivity(); startScanActivity();
} }
private void onGetKeyInfoResult(int resultCode, Intent data) { private void onScanKeyInfoResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
KeyProfile keyProfile = (KeyProfile)data.getSerializableExtra("KeyProfile"); KeyProfile keyProfile = (KeyProfile)data.getSerializableExtra("KeyProfile");
Intent intent = new Intent(this, AddProfileActivity.class); Intent intent = new Intent(this, AddProfileActivity.class);
@ -324,9 +358,10 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
private void onEditKeyInfoResult(int resultCode, Intent data) { private void onEditKeyInfoResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile");
if (!data.getBooleanExtra("delete", false)) {
// this profile has been serialized/deserialized and is no longer the same instance it once was // this profile has been serialized/deserialized and is no longer the same instance it once was
// to deal with this, the replaceKey functions are used // to deal with this, the replaceKey functions are used
KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile");
try { try {
_db.replaceKey(profile.getEntry()); _db.replaceKey(profile.getEntry());
} catch (Exception e) { } catch (Exception e) {
@ -336,6 +371,17 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
} }
_keyProfileView.replaceKey(profile); _keyProfileView.replaceKey(profile);
saveDatabase(); saveDatabase();
} else {
deleteProfile(profile);
}
}
}
private void onEnterKeyInfoResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile");
addKey(profile);
saveDatabase();
} }
} }
@ -395,7 +441,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
private void startScanActivity() { private void startScanActivity() {
Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class); Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class);
startActivityForResult(scannerActivity, CODE_GET_KEYINFO); startActivityForResult(scannerActivity, CODE_SCAN_KEYINFO);
} }
private boolean doShortcutActions() { private boolean doShortcutActions() {

View file

@ -3,15 +3,17 @@ package me.impy.aegis.crypto;
import android.net.Uri; import android.net.Uri;
import java.io.Serializable; import java.io.Serializable;
import java.util.Arrays;
import me.impy.aegis.encoding.Base32; import me.impy.aegis.encoding.Base32;
import me.impy.aegis.encoding.Base32Exception;
public class KeyInfo implements Serializable { public class KeyInfo implements Serializable {
private String _type; private String _type = "totp";
private byte[] _secret; private byte[] _secret;
private String _accountName; private String _accountName = "";
private String _issuer; private String _issuer = "";
private long _counter; private long _counter = 0;
private String _algorithm = "SHA1"; private String _algorithm = "SHA1";
private int _digits = 6; private int _digits = 6;
private int _period = 30; private int _period = 30;
@ -24,7 +26,7 @@ public class KeyInfo implements Serializable {
builder.appendQueryParameter("digits", Integer.toString(_digits)); builder.appendQueryParameter("digits", Integer.toString(_digits));
builder.appendQueryParameter("period", Integer.toString(_period)); builder.appendQueryParameter("period", Integer.toString(_period));
builder.appendQueryParameter("algorithm", _algorithm); builder.appendQueryParameter("algorithm", _algorithm);
builder.appendQueryParameter("secret", Base32.encodeOriginal(_secret)); builder.appendQueryParameter("secret", new String(Base32.encode(_secret)));
if (_type.equals("hotp")) { if (_type.equals("hotp")) {
builder.appendQueryParameter("counter", Long.toString(_counter)); builder.appendQueryParameter("counter", Long.toString(_counter));
} }
@ -44,30 +46,25 @@ public class KeyInfo implements Serializable {
return p - (System.currentTimeMillis() % p); return p - (System.currentTimeMillis() % p);
} }
public static KeyInfo fromURL(String s) throws Exception { public static KeyInfo fromURL(String s) throws KeyInfoException {
final Uri url = Uri.parse(s); final Uri url = Uri.parse(s);
if (!url.getScheme().equals("otpauth")) { if (!url.getScheme().equals("otpauth")) {
throw new Exception("unsupported protocol"); throw new KeyInfoException("unsupported protocol");
} }
KeyInfo info = new KeyInfo(); KeyInfo info = new KeyInfo();
info.setType(url.getHost());
// only 'totp' and 'hotp' are supported
info._type = url.getHost();
if (info._type.equals("totp") && info._type.equals("hotp")) {
throw new Exception("unsupported type");
}
// 'secret' is a required parameter // 'secret' is a required parameter
String secret = url.getQueryParameter("secret"); String secret = url.getQueryParameter("secret");
if (secret == null) { if (secret == null) {
throw new Exception("'secret' is not set"); throw new KeyInfoException("'secret' is not set");
} }
info._secret = Base32.decode(secret); info.setSecret(secret.toCharArray());
// provider info used to disambiguate accounts // provider info used to disambiguate accounts
String path = url.getPath(); String path = url.getPath();
String label = path != null ? path.substring(1) : ""; String label = path != null && path.length() > 0 ? path.substring(1) : "";
if (label.contains(":")) { if (label.contains(":")) {
// a label can only contain one colon // a label can only contain one colon
@ -75,40 +72,40 @@ public class KeyInfo implements Serializable {
String[] strings = label.split(":"); String[] strings = label.split(":");
if (strings.length == 2) { if (strings.length == 2) {
info._issuer = strings[0]; info.setIssuer(strings[0]);
info._accountName = strings[1]; info.setAccountName(strings[1]);
} else { } else {
// at this point, just dump the whole thing into the accountName // at this point, just dump the whole thing into the accountName
info._accountName = label; info.setAccountName(label);
} }
} else { } else {
// label only contains the account name // label only contains the account name
// grab the issuer's info from the 'issuer' parameter if it's present // grab the issuer's info from the 'issuer' parameter if it's present
String issuer = url.getQueryParameter("issuer"); String issuer = url.getQueryParameter("issuer");
info._issuer = issuer != null ? issuer : ""; info.setIssuer(issuer != null ? issuer : "");
info._accountName = label; info.setAccountName(label);
} }
// just use the defaults if these parameters aren't set // just use the defaults if these parameters aren't set
String algorithm = url.getQueryParameter("algorithm"); String algorithm = url.getQueryParameter("algorithm");
if (algorithm != null) { if (algorithm != null) {
info._algorithm = algorithm; info.setAlgorithm(algorithm);
} }
String period = url.getQueryParameter("period"); String period = url.getQueryParameter("period");
if (period != null) { if (period != null) {
info._period = Integer.parseInt(period); info.setPeriod(Integer.parseInt(period));
} }
String digits = url.getQueryParameter("digits"); String digits = url.getQueryParameter("digits");
if (digits != null) { if (digits != null) {
info._digits = Integer.parseInt(digits); info.setDigits(Integer.parseInt(digits));
} }
// 'counter' is required if the type is 'hotp' // 'counter' is required if the type is 'hotp'
String counter = url.getQueryParameter("counter"); String counter = url.getQueryParameter("counter");
if (counter != null) { if (counter != null) {
info._counter = Long.parseLong(counter); info.setCounter(Long.parseLong(counter));
} else if (info._type.equals("hotp")) { } else if (info.getType().equals("hotp")) {
throw new Exception("'counter' was not set which is required for 'hotp'"); throw new KeyInfoException("'counter' was not set which is required for 'hotp'");
} }
return info; return info;
@ -117,56 +114,119 @@ public class KeyInfo implements Serializable {
public String getType() { public String getType() {
return _type; return _type;
} }
public byte[] getSecret() { public byte[] getSecret() {
return _secret; return _secret;
} }
public String getAccountName() { public String getAccountName() {
return _accountName; return _accountName;
} }
public String getIssuer() { public String getIssuer() {
return _issuer; return _issuer;
} }
public String getAlgorithm(boolean java) { public String getAlgorithm(boolean java) {
if (java) { if (java) {
return "Hmac" + _algorithm; return "Hmac" + _algorithm;
} }
return _algorithm; return _algorithm;
} }
public int getDigits() { public int getDigits() {
return _digits; return _digits;
} }
public long getCounter() { public long getCounter() {
return _counter; return _counter;
} }
public int getPeriod() { public int getPeriod() {
return _period; return _period;
} }
public void setType(String type) { public boolean isTypeValid(String type) {
return type.equals("totp") || type.equals("hotp");
}
public void setType(String type) throws KeyInfoException {
type = type.toLowerCase();
if (!isTypeValid(type)) {
throw new KeyInfoException(String.format("unsupported otp type: %s", type));
}
_type = type.toLowerCase(); _type = type.toLowerCase();
} }
public void setSecret(char[] base32) throws KeyInfoException {
byte[] secret;
try {
secret = Base32.decode(base32);
} catch (Base32Exception e) {
throw new KeyInfoException("bad secret", e);
}
setSecret(secret);
}
public void setSecret(byte[] secret) { public void setSecret(byte[] secret) {
_secret = secret; _secret = secret;
} }
public void setAccountName(String accountName) { public void setAccountName(String accountName) {
_accountName = accountName; _accountName = accountName;
} }
public void setIssuer(String issuer) { public void setIssuer(String issuer) {
_issuer = issuer; _issuer = issuer;
} }
public void setAlgorithm(String algorithm) {
public boolean isAlgorithmValid(String algorithm) {
return algorithm.equals("SHA1") || algorithm.equals("SHA256") || algorithm.equals("SHA512");
}
public void setAlgorithm(String algorithm) throws KeyInfoException {
if (algorithm.startsWith("Hmac")) { if (algorithm.startsWith("Hmac")) {
algorithm = algorithm.substring(4); algorithm = algorithm.substring(4);
} }
_algorithm = algorithm.toUpperCase(); algorithm = algorithm.toUpperCase();
if (!isAlgorithmValid(algorithm)) {
throw new KeyInfoException(String.format("unsupported algorithm: %s", algorithm));
}
_algorithm = algorithm;
}
public boolean isDigitsValid(int digits) {
return digits == 6 || digits == 8;
}
public void setDigits(int digits) throws KeyInfoException {
if (!isDigitsValid(digits)) {
throw new KeyInfoException(String.format("unsupported amount of digits: %d", digits));
} }
public void setDigits(int digits) {
_digits = digits; _digits = digits;
} }
public void setCounter(long count) {
public boolean isCounterValid(long count) {
return count >= 0;
}
public void setCounter(long count) throws KeyInfoException {
if (!isCounterValid(count)) {
throw new KeyInfoException(String.format("bad count: %d", count));
}
_counter = count; _counter = count;
} }
public void setPeriod(int period) {
public boolean isPeriodValid(int period) {
return period > 0;
}
public void setPeriod(int period) throws KeyInfoException {
if (!isPeriodValid(period)) {
throw new KeyInfoException(String.format("bad period: %d", period));
}
_period = period; _period = period;
} }
} }

View file

@ -0,0 +1,20 @@
package me.impy.aegis.crypto;
public class KeyInfoException extends Exception {
public KeyInfoException(String message) {
super(message);
}
public KeyInfoException(String message, Throwable cause) {
super(message, cause);
}
@Override
public String getMessage() {
Throwable cause = getCause();
if (cause == null) {
return super.getMessage();
}
return String.format("%s (%s)", super.getMessage(), cause.getMessage());
}
}

View file

@ -66,17 +66,13 @@ public class Database {
} }
public void removeKey(DatabaseEntry entry) { public void removeKey(DatabaseEntry entry) {
entry = getKeyByID(entry.getID());
_entries.remove(entry); _entries.remove(entry);
} }
public void replaceKey(DatabaseEntry newEntry) { public void replaceKey(DatabaseEntry newEntry) {
for (DatabaseEntry oldEntry : _entries) { DatabaseEntry oldEntry = getKeyByID(newEntry.getID());
if (oldEntry.getID() == newEntry.getID()) {
_entries.set(_entries.indexOf(oldEntry), newEntry); _entries.set(_entries.indexOf(oldEntry), newEntry);
return;
}
}
throw new AssertionError("no entry found with the same id");
} }
public void swapKeys(DatabaseEntry entry1, DatabaseEntry entry2) { public void swapKeys(DatabaseEntry entry1, DatabaseEntry entry2) {
@ -86,4 +82,13 @@ public class Database {
public List<DatabaseEntry> getKeys() { public List<DatabaseEntry> getKeys() {
return Collections.unmodifiableList(_entries); return Collections.unmodifiableList(_entries);
} }
private DatabaseEntry getKeyByID(long id) {
for (DatabaseEntry entry : _entries) {
if (entry.getID() == id) {
return entry;
}
}
throw new AssertionError("no entry found with the same id");
}
} }

View file

@ -13,6 +13,10 @@ public class DatabaseEntry implements Serializable {
private String _icon = ""; private String _icon = "";
private KeyInfo _info; private KeyInfo _info;
public DatabaseEntry() {
this(new KeyInfo());
}
public DatabaseEntry(KeyInfo info) { public DatabaseEntry(KeyInfo info) {
_info = info; _info = info;
} }

View file

@ -1,5 +1,7 @@
package me.impy.aegis.encoding; package me.impy.aegis.encoding;
// modified for use in Aegis
/* (PD) 2001 The Bitzi Corporation /* (PD) 2001 The Bitzi Corporation
* Please see http://bitzi.com/publicdomain for more info. * Please see http://bitzi.com/publicdomain for more info.
* *
@ -20,7 +22,9 @@ package me.impy.aegis.encoding;
* limitations under the License. * limitations under the License.
*/ */
import java.io.UnsupportedEncodingException; import java.util.Arrays;
import me.impy.aegis.crypto.CryptoUtils;
/** /**
* Base32 - encodes and decodes RFC3548 Base32 * Base32 - encodes and decodes RFC3548 Base32
@ -45,16 +49,6 @@ public class Base32 {
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL' 0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL'
}; };
public static byte[] encode(byte[] data) throws UnsupportedEncodingException {
String lower = encodeOriginal(data).toLowerCase();
return lower.getBytes("US-ASCII");
}
public static byte[] decodeModified(String data) {
return decode(data.replace('8', 'L').replace('9', 'O'));
}
/** /**
* Encodes byte array to Base32 String. * Encodes byte array to Base32 String.
* *
@ -62,10 +56,10 @@ public class Base32 {
* @return Encoded byte array <code>bytes</code> as a String. * @return Encoded byte array <code>bytes</code> as a String.
* *
*/ */
static public String encodeOriginal(final byte[] bytes) { public static char[] encode(final byte[] bytes) {
int i = 0, index = 0, digit = 0; int i = 0, index = 0, digit = 0, j = 0;
int currByte, nextByte; int currByte, nextByte;
StringBuffer base32 = new StringBuffer((bytes.length + 7) * 8 / 5); char[] base32 = new char[(bytes.length + 7) * 8 / 5];
while (i < bytes.length) { while (i < bytes.length) {
currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign
@ -90,10 +84,12 @@ public class Base32 {
if (index == 0) if (index == 0)
i++; i++;
} }
base32.append(base32Chars.charAt(digit)); base32[j++] = base32Chars.charAt(digit);
} }
return base32.toString(); char[] res = Arrays.copyOf(base32, j);
CryptoUtils.zero(base32);
return res;
} }
/** /**
@ -102,24 +98,13 @@ public class Base32 {
* @param base32 * @param base32
* @return Decoded <code>base32</code> String as a raw byte array. * @return Decoded <code>base32</code> String as a raw byte array.
*/ */
static public byte[] decode(final String base32) { public static byte[] decode(final char[] base32) throws Base32Exception {
int i, index, lookup, offset, digit; int i, index, lookup, offset, digit;
byte[] bytes = new byte[base32.length() * 5 / 8]; byte[] bytes = new byte[base32.length * 5 / 8];
for (i = 0, index = 0, offset = 0; i < base32.length(); i++) { for (i = 0, index = 0, offset = 0; i < base32.length; i++) {
lookup = base32.charAt(i) - '0'; lookup = base32[i] - '0';
digit = decodeDigit(lookup);
/* Skip chars outside the lookup table */
if (lookup < 0 || lookup >= base32Lookup.length) {
continue;
}
digit = base32Lookup[lookup];
/* If this digit is not in the table, ignore it */
if (digit == 0xFF) {
continue;
}
if (index <= 3) { if (index <= 3) {
index = (index + 5) % 8; index = (index + 5) % 8;
@ -144,4 +129,20 @@ public class Base32 {
} }
return bytes; return bytes;
} }
private static int decodeDigit(int c) throws Base32Exception {
/* Skip chars outside the lookup table */
if (c < 0 || c >= base32Lookup.length) {
throw new Base32Exception("char not found in base32 lookup table");
}
int digit = base32Lookup[c];
/* If this digit is not in the table, ignore it */
if (digit == 0xFF) {
throw new Base32Exception("char not found in base32 lookup table");
}
return digit;
}
} }

View file

@ -0,0 +1,7 @@
package me.impy.aegis.encoding;
public class Base32Exception extends Exception {
public Base32Exception(String message) {
super(message);
}
}

View file

@ -13,6 +13,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import me.impy.aegis.crypto.KeyInfo; import me.impy.aegis.crypto.KeyInfo;
import me.impy.aegis.crypto.KeyInfoException;
import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.util.ByteInputStream; import me.impy.aegis.util.ByteInputStream;
@ -40,7 +41,8 @@ public class FreeOTPImporter extends DatabaseImporter {
return "FreeOTP"; return "FreeOTP";
} }
private static List<DatabaseEntry> parse(XmlPullParser parser) throws IOException, XmlPullParserException, JSONException { private static List<DatabaseEntry> parse(XmlPullParser parser)
throws IOException, XmlPullParserException, JSONException, KeyInfoException {
List<Entry> entries = new ArrayList<>(); List<Entry> entries = new ArrayList<>();
parser.require(XmlPullParser.START_TAG, null, "map"); parser.require(XmlPullParser.START_TAG, null, "map");

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#B2000000"/>
<padding
android:left="16dp"
android:top="4dp"
android:right="16dp"
android:bottom="4dp"/>
<corners
android:radius="2dp"/>
</shape>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z" />
</vector>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M4,4H10V10H4V4M20,4V10H14V4H20M14,15H16V13H14V11H16V13H18V11H20V13H18V15H20V18H18V20H16V18H13V20H11V16H14V15M16,15V18H18V15H16M4,20V14H10V20H4M6,6V8H8V6H6M16,6V8H18V6H16M6,16V18H8V16H6M4,11H6V13H4V11M9,11H13V15H11V13H9V11M11,6H13V10H11V6M2,2V6H0V2A2,2 0 0,1 2,0H6V2H2M22,0A2,2 0 0,1 24,2V6H22V2H18V0H22M2,18V22H6V24H2A2,2 0 0,1 0,22V18H2M22,22V18H24V22A2,2 0 0,1 22,24H18V22H22Z" />
</vector>

View file

@ -1,35 +1,36 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"
android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true">
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="200dp"
android:layout_weight="0.66" android:background="@color/colorPrimary">
android:background="@color/colorPrimary"
android:orientation="horizontal">
<ImageView <ImageView
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="100dp" android:layout_height="100dp"
android:id="@+id/profile_drawable" android:id="@+id/profile_drawable"
android:layout_centerVertical="true" android:layout_centerInParent="true"/>
android:layout_centerHorizontal="true"/>
</RelativeLayout> </RelativeLayout>
<TableLayout <TableLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:layout_weight="0.33"
android:background="?attr/background" android:background="?attr/background"
android:orientation="vertical" android:orientation="vertical"
android:stretchColumns="1" android:stretchColumns="1"
@ -81,24 +82,25 @@
<LinearLayout android:layout_column="1" <LinearLayout android:layout_column="1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:orientation="horizontal"
android:gravity="center_vertical">
<Spinner <Spinner
android:id="@+id/spinner_type" android:id="@+id/spinner_type"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="3" android:layout_weight="4"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"/> style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
<Spinner <Spinner
android:id="@+id/spinner_algo" android:id="@+id/spinner_algo"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="3" android:layout_weight="4"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"/> style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
<Spinner <Spinner
android:id="@+id/spinner_digits" android:id="@+id/spinner_digits"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="2" android:layout_weight="3"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"/> style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
</LinearLayout> </LinearLayout>
@ -170,7 +172,7 @@
app:hintEnabled="false" app:hintEnabled="false"
app:passwordToggleEnabled="true"> app:passwordToggleEnabled="true">
<EditText <android.support.design.widget.TextInputEditText
android:id="@+id/text_secret" android:id="@+id/text_secret"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -182,4 +184,5 @@
</TableLayout> </TableLayout>
</LinearLayout> </LinearLayout>
</android.support.design.widget.CoordinatorLayout> </ScrollView>
</LinearLayout>

View file

@ -29,13 +29,38 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior"/> app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<!-- note: the fab should always be the last element to be sure it's displayed on top --> <!-- note: the fab should always be the last element to be sure it's displayed on top -->
<android.support.design.widget.FloatingActionButton <com.getbase.floatingactionbutton.FloatingActionsMenu
android:id="@+id/fab" android:id="@+id/fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin" android:layout_margin="@dimen/fab_margin"
android:src="@drawable/ic_add_black_24dp" android:src="@drawable/ic_add_black_24dp"
android:tint="@color/background"/> app:fab_addButtonColorNormal="@color/colorAccent"
app:fab_addButtonColorPressed="@color/colorAccent"
app:fab_labelStyle="@style/fab_label_style"
app:fab_labelsPosition="left">
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_scan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_title="Scan QR code"
app:fab_size="mini"
app:fab_colorNormal="@color/background"
app:fab_colorPressed="@color/background"
app:fab_icon="@drawable/ic_qrcode_scan"/>
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_enter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_title="Enter manually"
app:fab_size="mini"
app:fab_colorNormal="@color/background"
app:fab_colorPressed="@color/background"
app:fab_icon="@drawable/ic_create_black_24dp"/>
</com.getbase.floatingactionbutton.FloatingActionsMenu>
</android.support.design.widget.CoordinatorLayout> </android.support.design.widget.CoordinatorLayout>

View file

@ -14,7 +14,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginRight="16dp" android:layout_marginRight="16dp"
android:src="@drawable/ic_delete_black_24dp"/> android:src="@drawable/ic_content_copy_black_24dp"/>
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -5,6 +5,7 @@
<color name="colorHeaderSuccess">#12b600</color> <color name="colorHeaderSuccess">#12b600</color>
<color name="colorPrimaryLight">#5472d3</color> <color name="colorPrimaryLight">#5472d3</color>
<color name="colorAccent">#FF5252</color> <color name="colorAccent">#FF5252</color>
<color name="colorAccentPressed">#FF5252</color>
<color name="primary_text">#212121</color> <color name="primary_text">#212121</color>
<color name="secondary_text">#434343</color> <color name="secondary_text">#434343</color>
<color name="extra_info_text">#8e8e8e</color> <color name="extra_info_text">#8e8e8e</color>

View file

@ -30,7 +30,7 @@
</style> </style>
<style name="Theme.Intro" parent="Theme.AppCompat.NoActionBar"> <style name="Theme.Intro" parent="Theme.AppCompat.NoActionBar">
<item name="swirl_ridgeColor">@color/colorSwirlPrimary</item> <item name="swirl_ridgeColor">@color/primary_text_inverted</item>
<item name="swirl_errorColor">@color/colorSwirlError</item> <item name="swirl_errorColor">@color/colorSwirlError</item>
<item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item> <item name="android:windowIsTranslucent">true</item>
@ -94,5 +94,9 @@
<item name="windowActionBarOverlay">true</item> <item name="windowActionBarOverlay">true</item>
</style> </style>
<style name="fab_label_style">
<item name="android:background">@drawable/fab_label_background</item>
<item name="android:textColor">#FFFFFF</item>
</style>
</resources> </resources>