mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-14 05:52:52 +00:00
Merge branch 'master' of https://github.com/alexbakker/Aegis
This commit is contained in:
commit
5dd00391fe
21 changed files with 573 additions and 317 deletions
|
@ -16,12 +16,16 @@ import android.widget.EditText;
|
|||
import android.widget.ImageView;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import me.impy.aegis.crypto.CryptoUtils;
|
||||
import me.impy.aegis.crypto.KeyInfo;
|
||||
import me.impy.aegis.crypto.KeyInfoException;
|
||||
import me.impy.aegis.db.DatabaseEntry;
|
||||
import me.impy.aegis.encoding.Base32;
|
||||
import me.impy.aegis.helpers.AuthHelper;
|
||||
import me.impy.aegis.helpers.SpinnerHelper;
|
||||
|
||||
public class EditProfileActivity extends AegisActivity {
|
||||
private boolean _isNew = false;
|
||||
private boolean _edited = false;
|
||||
private KeyProfile _profile;
|
||||
|
||||
|
@ -40,52 +44,66 @@ public class EditProfileActivity extends AegisActivity {
|
|||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_edit_profile);
|
||||
|
||||
_profile = (KeyProfile) getIntent().getSerializableExtra("KeyProfile");
|
||||
|
||||
ActionBar bar = getSupportActionBar();
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_close);
|
||||
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.setImageDrawable(_profile.getDrawable());
|
||||
|
||||
DatabaseEntry entry = _profile.getEntry();
|
||||
_textName = findViewById(R.id.text_name);
|
||||
_textName.setText(entry.getName());
|
||||
_textName.addTextChangedListener(watcher);
|
||||
|
||||
_textIssuer = findViewById(R.id.text_issuer);
|
||||
_textIssuer.setText(entry.getInfo().getIssuer());
|
||||
_textIssuer.addTextChangedListener(watcher);
|
||||
|
||||
_textPeriod = findViewById(R.id.text_period);
|
||||
_textPeriod.setText(Integer.toString(entry.getInfo().getPeriod()));
|
||||
_textPeriod.addTextChangedListener(watcher);
|
||||
|
||||
_textSecret = findViewById(R.id.text_secret);
|
||||
_textSecret.setText(Base32.encodeOriginal(entry.getInfo().getSecret()));
|
||||
_textSecret.addTextChangedListener(watcher);
|
||||
byte[] secretBytes = entry.getInfo().getSecret();
|
||||
if (secretBytes != null) {
|
||||
char[] secretChars = Base32.encode(secretBytes);
|
||||
_textSecret.setText(secretChars, 0, secretChars.length);
|
||||
}
|
||||
|
||||
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.setOnTouchListener(_selectedListener);
|
||||
_spinnerType.setOnItemSelectedListener(_selectedListener);
|
||||
|
||||
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.setOnTouchListener(_selectedListener);
|
||||
_spinnerAlgo.setOnItemSelectedListener(_selectedListener);
|
||||
|
||||
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.setOnTouchListener(_selectedListener);
|
||||
_spinnerDigits.setOnItemSelectedListener(_selectedListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -129,14 +147,31 @@ public class EditProfileActivity extends AegisActivity {
|
|||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_edit, menu);
|
||||
if (_isNew) {
|
||||
menu.findItem(R.id.action_delete).setVisible(false);
|
||||
}
|
||||
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() {
|
||||
return false;
|
||||
finish(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean onSave() {
|
||||
if (_textSecret.length() == 0) {
|
||||
onError("Secret is a required field.");
|
||||
return false;
|
||||
}
|
||||
|
||||
int period;
|
||||
try {
|
||||
period = Integer.parseInt(_textPeriod.getText().toString());
|
||||
|
@ -159,17 +194,22 @@ public class EditProfileActivity extends AegisActivity {
|
|||
DatabaseEntry entry = _profile.getEntry();
|
||||
entry.setName(_textName.getText().toString());
|
||||
KeyInfo info = entry.getInfo();
|
||||
info.setIssuer(_textIssuer.getText().toString());
|
||||
info.setSecret(Base32.decode(_textSecret.getText().toString()));
|
||||
info.setPeriod(period);
|
||||
info.setDigits(digits);
|
||||
info.setAlgorithm(algo);
|
||||
info.setType(type);
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("KeyProfile", _profile);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
try {
|
||||
char[] secret = AuthHelper.getEditTextChars(_textSecret);
|
||||
info.setSecret(secret);
|
||||
CryptoUtils.zero(secret);
|
||||
info.setIssuer(_textIssuer.getText().toString());
|
||||
info.setPeriod(period);
|
||||
info.setDigits(digits);
|
||||
info.setAlgorithm(algo);
|
||||
info.setType(type);
|
||||
} catch (KeyInfoException e) {
|
||||
onError("The entered info is incorrect: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
finish(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -185,7 +225,7 @@ public class EditProfileActivity extends AegisActivity {
|
|||
_edited = true;
|
||||
}
|
||||
|
||||
private TextWatcher watcher = new TextWatcher() {
|
||||
private TextWatcher _textListener = new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
onFieldEdited();
|
||||
|
|
|
@ -13,6 +13,10 @@ public class KeyProfile implements Serializable {
|
|||
private String _code;
|
||||
private DatabaseEntry _entry;
|
||||
|
||||
public KeyProfile() {
|
||||
this(new DatabaseEntry());
|
||||
}
|
||||
|
||||
public KeyProfile(DatabaseEntry entry) {
|
||||
_entry = entry;
|
||||
}
|
||||
|
@ -35,7 +39,7 @@ public class KeyProfile implements Serializable {
|
|||
|
||||
public TextDrawable getDrawable() {
|
||||
String name = _entry.getName();
|
||||
if (name == null) {
|
||||
if (name == null || name.length() <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileHolder> im
|
|||
}
|
||||
|
||||
public void removeKey(KeyProfile profile) {
|
||||
profile = getKeyByID(profile.getEntry().getID());
|
||||
int position = _keyProfiles.indexOf(profile);
|
||||
_keyProfiles.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
|
@ -47,12 +48,16 @@ public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileHolder> im
|
|||
}
|
||||
|
||||
public void replaceKey(KeyProfile newProfile) {
|
||||
for (KeyProfile oldProfile : _keyProfiles) {
|
||||
if (oldProfile.getEntry().getID() == newProfile.getEntry().getID()) {
|
||||
int position = _keyProfiles.indexOf(oldProfile);
|
||||
_keyProfiles.set(position, newProfile);
|
||||
notifyItemChanged(position);
|
||||
return;
|
||||
KeyProfile oldProfile = getKeyByID(newProfile.getEntry().getID());
|
||||
int position = _keyProfiles.indexOf(oldProfile);
|
||||
_keyProfiles.set(position, newProfile);
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
|
||||
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");
|
||||
|
|
|
@ -6,18 +6,20 @@ import android.content.ClipboardManager;
|
|||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Rect;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.support.design.widget.BottomSheetDialog;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.getbase.floatingactionbutton.FloatingActionsMenu;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
|
@ -32,13 +34,14 @@ import me.impy.aegis.util.ByteInputStream;
|
|||
|
||||
public class MainActivity extends AegisActivity implements KeyProfileView.Listener {
|
||||
// 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_EDIT_KEYINFO = 2;
|
||||
private static final int CODE_DO_INTRO = 3;
|
||||
private static final int CODE_DECRYPT = 4;
|
||||
private static final int CODE_IMPORT = 5;
|
||||
private static final int CODE_PREFERENCES = 6;
|
||||
private static final int CODE_ENTER_KEYINFO = 3;
|
||||
private static final int CODE_DO_INTRO = 4;
|
||||
private static final int CODE_DECRYPT = 5;
|
||||
private static final int CODE_IMPORT = 6;
|
||||
private static final int CODE_PREFERENCES = 7;
|
||||
|
||||
// permission request codes
|
||||
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 Menu _menu;
|
||||
private FloatingActionsMenu _fabMenu;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
@ -60,8 +64,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
|
||||
// set up the main view
|
||||
setContentView(R.layout.activity_main);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
|
||||
// set up the key profile view
|
||||
_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));
|
||||
|
||||
// set up the floating action button
|
||||
FloatingActionButton fab = findViewById(R.id.fab);
|
||||
fab.setEnabled(true);
|
||||
fab.setOnClickListener(view -> onGetKeyInfo());
|
||||
_fabMenu = findViewById(R.id.fab);
|
||||
findViewById(R.id.fab_enter).setOnClickListener(view -> {
|
||||
_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
|
||||
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
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
|
@ -133,8 +159,8 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
}
|
||||
|
||||
switch (requestCode) {
|
||||
case CODE_GET_KEYINFO:
|
||||
onGetKeyInfoResult(resultCode, data);
|
||||
case CODE_SCAN_KEYINFO:
|
||||
onScanKeyInfoResult(resultCode, data);
|
||||
break;
|
||||
case CODE_ADD_KEYINFO:
|
||||
onAddKeyInfoResult(resultCode, data);
|
||||
|
@ -142,6 +168,9 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
case CODE_EDIT_KEYINFO:
|
||||
onEditKeyInfoResult(resultCode, data);
|
||||
break;
|
||||
case CODE_ENTER_KEYINFO:
|
||||
onEnterKeyInfoResult(resultCode, data);
|
||||
break;
|
||||
case CODE_DO_INTRO:
|
||||
onDoIntroResult(resultCode, data);
|
||||
break;
|
||||
|
@ -172,7 +201,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
onImport();
|
||||
break;
|
||||
case CODE_PERM_CAMERA:
|
||||
onGetKeyInfo();
|
||||
onScanKeyInfo();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -297,7 +326,12 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
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)) {
|
||||
return;
|
||||
}
|
||||
|
@ -305,7 +339,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
startScanActivity();
|
||||
}
|
||||
|
||||
private void onGetKeyInfoResult(int resultCode, Intent data) {
|
||||
private void onScanKeyInfoResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
KeyProfile keyProfile = (KeyProfile)data.getSerializableExtra("KeyProfile");
|
||||
Intent intent = new Intent(this, AddProfileActivity.class);
|
||||
|
@ -324,17 +358,29 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
|
||||
private void onEditKeyInfoResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
// 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
|
||||
KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile");
|
||||
try {
|
||||
_db.replaceKey(profile.getEntry());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(this, "An error occurred while trying to update an entry", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
if (!data.getBooleanExtra("delete", false)) {
|
||||
// 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
|
||||
try {
|
||||
_db.replaceKey(profile.getEntry());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(this, "An error occurred while trying to update an entry", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
_keyProfileView.replaceKey(profile);
|
||||
saveDatabase();
|
||||
} else {
|
||||
deleteProfile(profile);
|
||||
}
|
||||
_keyProfileView.replaceKey(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() {
|
||||
Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class);
|
||||
startActivityForResult(scannerActivity, CODE_GET_KEYINFO);
|
||||
startActivityForResult(scannerActivity, CODE_SCAN_KEYINFO);
|
||||
}
|
||||
|
||||
private boolean doShortcutActions() {
|
||||
|
|
|
@ -3,15 +3,17 @@ package me.impy.aegis.crypto;
|
|||
import android.net.Uri;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
|
||||
import me.impy.aegis.encoding.Base32;
|
||||
import me.impy.aegis.encoding.Base32Exception;
|
||||
|
||||
public class KeyInfo implements Serializable {
|
||||
private String _type;
|
||||
private String _type = "totp";
|
||||
private byte[] _secret;
|
||||
private String _accountName;
|
||||
private String _issuer;
|
||||
private long _counter;
|
||||
private String _accountName = "";
|
||||
private String _issuer = "";
|
||||
private long _counter = 0;
|
||||
private String _algorithm = "SHA1";
|
||||
private int _digits = 6;
|
||||
private int _period = 30;
|
||||
|
@ -24,7 +26,7 @@ public class KeyInfo implements Serializable {
|
|||
builder.appendQueryParameter("digits", Integer.toString(_digits));
|
||||
builder.appendQueryParameter("period", Integer.toString(_period));
|
||||
builder.appendQueryParameter("algorithm", _algorithm);
|
||||
builder.appendQueryParameter("secret", Base32.encodeOriginal(_secret));
|
||||
builder.appendQueryParameter("secret", new String(Base32.encode(_secret)));
|
||||
if (_type.equals("hotp")) {
|
||||
builder.appendQueryParameter("counter", Long.toString(_counter));
|
||||
}
|
||||
|
@ -44,30 +46,25 @@ public class KeyInfo implements Serializable {
|
|||
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);
|
||||
if (!url.getScheme().equals("otpauth")) {
|
||||
throw new Exception("unsupported protocol");
|
||||
throw new KeyInfoException("unsupported protocol");
|
||||
}
|
||||
|
||||
KeyInfo info = new KeyInfo();
|
||||
|
||||
// only 'totp' and 'hotp' are supported
|
||||
info._type = url.getHost();
|
||||
if (info._type.equals("totp") && info._type.equals("hotp")) {
|
||||
throw new Exception("unsupported type");
|
||||
}
|
||||
info.setType(url.getHost());
|
||||
|
||||
// 'secret' is a required parameter
|
||||
String secret = url.getQueryParameter("secret");
|
||||
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
|
||||
String path = url.getPath();
|
||||
String label = path != null ? path.substring(1) : "";
|
||||
String label = path != null && path.length() > 0 ? path.substring(1) : "";
|
||||
|
||||
if (label.contains(":")) {
|
||||
// a label can only contain one colon
|
||||
|
@ -75,40 +72,40 @@ public class KeyInfo implements Serializable {
|
|||
String[] strings = label.split(":");
|
||||
|
||||
if (strings.length == 2) {
|
||||
info._issuer = strings[0];
|
||||
info._accountName = strings[1];
|
||||
info.setIssuer(strings[0]);
|
||||
info.setAccountName(strings[1]);
|
||||
} else {
|
||||
// at this point, just dump the whole thing into the accountName
|
||||
info._accountName = label;
|
||||
info.setAccountName(label);
|
||||
}
|
||||
} else {
|
||||
// label only contains the account name
|
||||
// grab the issuer's info from the 'issuer' parameter if it's present
|
||||
String issuer = url.getQueryParameter("issuer");
|
||||
info._issuer = issuer != null ? issuer : "";
|
||||
info._accountName = label;
|
||||
info.setIssuer(issuer != null ? issuer : "");
|
||||
info.setAccountName(label);
|
||||
}
|
||||
|
||||
// just use the defaults if these parameters aren't set
|
||||
String algorithm = url.getQueryParameter("algorithm");
|
||||
if (algorithm != null) {
|
||||
info._algorithm = algorithm;
|
||||
info.setAlgorithm(algorithm);
|
||||
}
|
||||
String period = url.getQueryParameter("period");
|
||||
if (period != null) {
|
||||
info._period = Integer.parseInt(period);
|
||||
info.setPeriod(Integer.parseInt(period));
|
||||
}
|
||||
String digits = url.getQueryParameter("digits");
|
||||
if (digits != null) {
|
||||
info._digits = Integer.parseInt(digits);
|
||||
info.setDigits(Integer.parseInt(digits));
|
||||
}
|
||||
|
||||
// 'counter' is required if the type is 'hotp'
|
||||
String counter = url.getQueryParameter("counter");
|
||||
if (counter != null) {
|
||||
info._counter = Long.parseLong(counter);
|
||||
} else if (info._type.equals("hotp")) {
|
||||
throw new Exception("'counter' was not set which is required for 'hotp'");
|
||||
info.setCounter(Long.parseLong(counter));
|
||||
} else if (info.getType().equals("hotp")) {
|
||||
throw new KeyInfoException("'counter' was not set which is required for 'hotp'");
|
||||
}
|
||||
|
||||
return info;
|
||||
|
@ -117,56 +114,119 @@ public class KeyInfo implements Serializable {
|
|||
public String getType() {
|
||||
return _type;
|
||||
}
|
||||
|
||||
public byte[] getSecret() {
|
||||
return _secret;
|
||||
}
|
||||
|
||||
public String getAccountName() {
|
||||
return _accountName;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return _issuer;
|
||||
}
|
||||
|
||||
public String getAlgorithm(boolean java) {
|
||||
if (java) {
|
||||
return "Hmac" + _algorithm;
|
||||
}
|
||||
return _algorithm;
|
||||
}
|
||||
|
||||
public int getDigits() {
|
||||
return _digits;
|
||||
}
|
||||
|
||||
public long getCounter() {
|
||||
return _counter;
|
||||
}
|
||||
|
||||
public int getPeriod() {
|
||||
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();
|
||||
}
|
||||
|
||||
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) {
|
||||
_secret = secret;
|
||||
}
|
||||
|
||||
public void setAccountName(String accountName) {
|
||||
_accountName = accountName;
|
||||
}
|
||||
|
||||
public void setIssuer(String 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")) {
|
||||
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 void setDigits(int digits) {
|
||||
|
||||
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));
|
||||
}
|
||||
_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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
20
app/src/main/java/me/impy/aegis/crypto/KeyInfoException.java
Normal file
20
app/src/main/java/me/impy/aegis/crypto/KeyInfoException.java
Normal 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());
|
||||
}
|
||||
}
|
|
@ -66,17 +66,13 @@ public class Database {
|
|||
}
|
||||
|
||||
public void removeKey(DatabaseEntry entry) {
|
||||
entry = getKeyByID(entry.getID());
|
||||
_entries.remove(entry);
|
||||
}
|
||||
|
||||
public void replaceKey(DatabaseEntry newEntry) {
|
||||
for (DatabaseEntry oldEntry : _entries) {
|
||||
if (oldEntry.getID() == newEntry.getID()) {
|
||||
_entries.set(_entries.indexOf(oldEntry), newEntry);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("no entry found with the same id");
|
||||
DatabaseEntry oldEntry = getKeyByID(newEntry.getID());
|
||||
_entries.set(_entries.indexOf(oldEntry), newEntry);
|
||||
}
|
||||
|
||||
public void swapKeys(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
|
@ -86,4 +82,13 @@ public class Database {
|
|||
public List<DatabaseEntry> getKeys() {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,10 @@ public class DatabaseEntry implements Serializable {
|
|||
private String _icon = "";
|
||||
private KeyInfo _info;
|
||||
|
||||
public DatabaseEntry() {
|
||||
this(new KeyInfo());
|
||||
}
|
||||
|
||||
public DatabaseEntry(KeyInfo info) {
|
||||
_info = info;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package me.impy.aegis.encoding;
|
||||
|
||||
// modified for use in Aegis
|
||||
|
||||
/* (PD) 2001 The Bitzi Corporation
|
||||
* Please see http://bitzi.com/publicdomain for more info.
|
||||
*
|
||||
|
@ -20,7 +22,9 @@ package me.impy.aegis.encoding;
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import me.impy.aegis.crypto.CryptoUtils;
|
||||
|
||||
/**
|
||||
* 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'
|
||||
};
|
||||
|
||||
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.
|
||||
*
|
||||
|
@ -62,10 +56,10 @@ public class Base32 {
|
|||
* @return Encoded byte array <code>bytes</code> as a String.
|
||||
*
|
||||
*/
|
||||
static public String encodeOriginal(final byte[] bytes) {
|
||||
int i = 0, index = 0, digit = 0;
|
||||
public static char[] encode(final byte[] bytes) {
|
||||
int i = 0, index = 0, digit = 0, j = 0;
|
||||
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) {
|
||||
currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign
|
||||
|
@ -90,10 +84,12 @@ public class Base32 {
|
|||
if (index == 0)
|
||||
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
|
||||
* @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;
|
||||
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++) {
|
||||
lookup = base32.charAt(i) - '0';
|
||||
|
||||
/* 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;
|
||||
}
|
||||
for (i = 0, index = 0, offset = 0; i < base32.length; i++) {
|
||||
lookup = base32[i] - '0';
|
||||
digit = decodeDigit(lookup);
|
||||
|
||||
if (index <= 3) {
|
||||
index = (index + 5) % 8;
|
||||
|
@ -144,4 +129,20 @@ public class Base32 {
|
|||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package me.impy.aegis.encoding;
|
||||
|
||||
public class Base32Exception extends Exception {
|
||||
public Base32Exception(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
import me.impy.aegis.crypto.KeyInfo;
|
||||
import me.impy.aegis.crypto.KeyInfoException;
|
||||
import me.impy.aegis.db.DatabaseEntry;
|
||||
import me.impy.aegis.util.ByteInputStream;
|
||||
|
||||
|
@ -40,7 +41,8 @@ public class FreeOTPImporter extends DatabaseImporter {
|
|||
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<>();
|
||||
|
||||
parser.require(XmlPullParser.START_TAG, null, "map");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue