From d40daba0d379b30d5951972ed34dc71d347127d7 Mon Sep 17 00:00:00 2001 From: Nathaniel McCallum Date: Thu, 17 Oct 2013 23:49:53 -0400 Subject: [PATCH] Enable manual token entry --- res/layout/manual.xml | 162 ++++++++++++++++++ res/menu/main.xml | 2 +- res/values/strings.xml | 30 +++- .../fedorahosted/freeotp/AddTokenDialog.java | 96 +++++++++++ .../freeotp/AddTokenSecretTextWatcher.java | 27 +++ .../freeotp/AddTokenTextWatcher.java | 47 +++++ .../fedorahosted/freeotp/MainActivity.java | 102 ++++++----- src/org/fedorahosted/freeotp/Token.java | 13 +- .../fedorahosted/freeotp/TokenAdapter.java | 3 +- 9 files changed, 427 insertions(+), 55 deletions(-) create mode 100644 res/layout/manual.xml create mode 100644 src/org/fedorahosted/freeotp/AddTokenDialog.java create mode 100644 src/org/fedorahosted/freeotp/AddTokenSecretTextWatcher.java create mode 100644 src/org/fedorahosted/freeotp/AddTokenTextWatcher.java diff --git a/res/layout/manual.xml b/res/layout/manual.xml new file mode 100644 index 0000000..4a94da6 --- /dev/null +++ b/res/layout/manual.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/menu/main.xml b/res/menu/main.xml index cbe751c..5fc3474 100644 --- a/res/menu/main.xml +++ b/res/menu/main.xml @@ -27,5 +27,5 @@ android:orderInCategory="100" android:showAsAction="always" android:icon="@drawable/scan" - android:title="@string/action_add" /> + android:title="@string/add" /> diff --git a/res/values/strings.xml b/res/values/strings.xml index c003408..48d2b62 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,8 +1,8 @@ FreeOTP - Add - The scanned token data was invalid! + Add + The token specified was invalid! Are you sure you want to remove this token?\n\nNOTE: This will NOT disable two-factor authentication on the server.\n\n Delete Yes @@ -10,4 +10,30 @@ Install Barcode Scanner? Barcode Scanner is required. Would you like to install it? No OTP keys installed. + Add Token + Scan QR Code + Interval + Counter + ID + Secret + Type + Algorithm + Digits + + + Time-based (TOTP) + Counter-based (HOTP) + + + + 6 + 8 + + + + MD5 + SHA1 + SHA256 + SHA512 + diff --git a/src/org/fedorahosted/freeotp/AddTokenDialog.java b/src/org/fedorahosted/freeotp/AddTokenDialog.java new file mode 100644 index 0000000..c6f781f --- /dev/null +++ b/src/org/fedorahosted/freeotp/AddTokenDialog.java @@ -0,0 +1,96 @@ +package org.fedorahosted.freeotp; + +import java.util.Locale; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.net.Uri; +import android.view.View; +import android.widget.AdapterView; +import android.widget.EditText; +import android.widget.RadioButton; +import android.widget.Spinner; +import android.widget.TextView; + +public abstract class AddTokenDialog extends AlertDialog { + private final int SHA1_OFFSET = 1; + private final int TOTP_OFFSET = 0; + + public AddTokenDialog(Context ctx) { + super(ctx); + + setTitle(R.string.add_token); + setView(getLayoutInflater().inflate(R.layout.manual, null)); + + setButton(BUTTON_NEGATIVE, ctx.getString(android.R.string.cancel), new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } + }); + + setButton(BUTTON_POSITIVE, ctx.getString(R.string.add), new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // Get the fields + String issuer = Uri.encode(((EditText) findViewById(R.id.issuer)).getText().toString()); + String id = Uri.encode(((EditText) findViewById(R.id.id)).getText().toString()); + String secret = Uri.encode(((EditText) findViewById(R.id.secret)).getText().toString()); + String type = ((Spinner) findViewById(R.id.type)).getSelectedItemId() == TOTP_OFFSET ? "totp" : "hotp"; + String algorithm = ((Spinner) findViewById(R.id.algorithm)).getSelectedItem().toString().toLowerCase(Locale.US); + int interval = Integer.parseInt(((EditText) findViewById(R.id.interval)).getText().toString()); + int digits = ((RadioButton) findViewById(R.id.digits6)).isChecked() ? 6 : 8; + + // Create the URI + String uri = String.format(Locale.US, "otpauth://%s/%s:%s?secret=%s&algorithm=%s&digits=%d", + type, issuer, id, secret, algorithm, digits); + if (type.equals("totp")) + uri = uri.concat(String.format("&period=%d", interval)); + else + uri = uri.concat(String.format("&counter=%d", interval)); + + // Add the token + addToken(uri); + } + }); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + // Disable the Add button + getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + + // Set constraints on when the Add button is enabled + ((EditText) findViewById(R.id.issuer)).addTextChangedListener(new AddTokenTextWatcher(this)); + ((EditText) findViewById(R.id.id)).addTextChangedListener(new AddTokenTextWatcher(this)); + ((EditText) findViewById(R.id.secret)).addTextChangedListener(new AddTokenSecretTextWatcher(this)); + ((EditText) findViewById(R.id.interval)).addTextChangedListener(new AddTokenTextWatcher(this)); + + // Select the default algorithm + ((Spinner) findViewById(R.id.algorithm)).setSelection(SHA1_OFFSET); + + // Setup the Interval / Counter toggle + ((Spinner) findViewById(R.id.type)).setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (position == 0) { + ((TextView) findViewById(R.id.interval_label)).setText(R.string.interval); + ((EditText) findViewById(R.id.interval)).setText("30"); + } else { + ((TextView) findViewById(R.id.interval_label)).setText(R.string.counter); + ((EditText) findViewById(R.id.interval)).setText("0"); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + + } + + public abstract void addToken(String uri); +} diff --git a/src/org/fedorahosted/freeotp/AddTokenSecretTextWatcher.java b/src/org/fedorahosted/freeotp/AddTokenSecretTextWatcher.java new file mode 100644 index 0000000..eaf335d --- /dev/null +++ b/src/org/fedorahosted/freeotp/AddTokenSecretTextWatcher.java @@ -0,0 +1,27 @@ +package org.fedorahosted.freeotp; + +import android.app.AlertDialog; +import android.text.Editable; + +public class AddTokenSecretTextWatcher extends AddTokenTextWatcher { + public AddTokenSecretTextWatcher(AlertDialog dialog) { + super(dialog); + } + + @Override + public void afterTextChanged(Editable s) { + super.afterTextChanged(s); + + if (s.length() == 0) + return; + + boolean haveData = false; + for (int i = s.length() - 1; i >= 0; i--) { + char c = s.charAt(i); + if (c != '=') + haveData = true; + else if (haveData) + s.delete(i, i + 1); + } + } +} diff --git a/src/org/fedorahosted/freeotp/AddTokenTextWatcher.java b/src/org/fedorahosted/freeotp/AddTokenTextWatcher.java new file mode 100644 index 0000000..5102ddd --- /dev/null +++ b/src/org/fedorahosted/freeotp/AddTokenTextWatcher.java @@ -0,0 +1,47 @@ +package org.fedorahosted.freeotp; + +import android.app.AlertDialog; +import android.text.Editable; +import android.text.TextWatcher; +import android.widget.Button; +import android.widget.EditText; + +public class AddTokenTextWatcher implements TextWatcher { + private final AlertDialog dialog; + + public AddTokenTextWatcher(AlertDialog dialog) { + this.dialog = dialog; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + Button b = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + + b.setEnabled(false); + + if (((EditText) dialog.findViewById(R.id.issuer)).getText().length() == 0) + return; + + if (((EditText) dialog.findViewById(R.id.id)).getText().length() == 0) + return; + + if (((EditText) dialog.findViewById(R.id.secret)).getText().length() == 0 || + ((EditText) dialog.findViewById(R.id.secret)).getText().length() % 8 != 0) + return; + + if (((EditText) dialog.findViewById(R.id.interval)).getText().length() == 0) + return; + + b.setEnabled(true); + } + + @Override + public void afterTextChanged(Editable s) { + + } +} diff --git a/src/org/fedorahosted/freeotp/MainActivity.java b/src/org/fedorahosted/freeotp/MainActivity.java index d7bb6fb..9a31a94 100644 --- a/src/org/fedorahosted/freeotp/MainActivity.java +++ b/src/org/fedorahosted/freeotp/MainActivity.java @@ -38,7 +38,6 @@ package org.fedorahosted.freeotp; -import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.List; @@ -48,6 +47,7 @@ import android.app.AlertDialog; import android.app.ListActivity; import android.content.ActivityNotFoundException; import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -97,42 +97,61 @@ public class MainActivity extends ListActivity { menu.findItem(R.id.action_add).setOnMenuItemClickListener(new OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { - Intent i = new Intent(ACTION_SCAN); - i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); - i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - i.addCategory(Intent.CATEGORY_DEFAULT); - i.putExtra("SCAN_MODE", "QR_CODE_MODE"); - i.putExtra("SAVE_HISTORY", false); - - String pkg = findAppPackage(i); - if (pkg != null) { - i.setPackage(pkg); - startActivityForResult(i, 0); - return false; - } - - new AlertDialog.Builder(MainActivity.this) - .setTitle(R.string.install_title) - .setMessage(R.string.install_message) - .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - Uri uri = Uri.parse("market://details?id=" + PROVIDERS.get(0)); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - try { - startActivity(intent); - } catch (ActivityNotFoundException e) { - e.printStackTrace(); - } + AlertDialog ad = new AddTokenDialog(MainActivity.this) { + @Override + public void addToken(String uri) { + try { + ta.add(MainActivity.this, uri); + } catch (TokenUriInvalidException e) { + Toast.makeText(MainActivity.this, R.string.invalid_token, Toast.LENGTH_SHORT).show(); + e.printStackTrace(); } - }) - .setNegativeButton(R.string.no, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { + } + }; + + ad.setButton(AlertDialog.BUTTON_NEUTRAL, getString(R.string.scan_qr_code), new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent i = new Intent(ACTION_SCAN); + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + i.addCategory(Intent.CATEGORY_DEFAULT); + i.putExtra("SCAN_MODE", "QR_CODE_MODE"); + i.putExtra("SAVE_HISTORY", false); + + String pkg = findAppPackage(i); + if (pkg != null) { + i.setPackage(pkg); + startActivityForResult(i, 0); return; } - }) - .create().show(); + + new AlertDialog.Builder(MainActivity.this) + .setTitle(R.string.install_title) + .setMessage(R.string.install_message) + .setPositiveButton(R.string.yes, new OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + Uri uri = Uri.parse("market://details?id=" + PROVIDERS.get(0)); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + try { + startActivity(intent); + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + } + } + }) + .setNegativeButton(R.string.no, new OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + return; + } + }) + .create().show(); + } + }); + + ad.show(); return false; } @@ -141,18 +160,15 @@ public class MainActivity extends ListActivity { return true; } - @Override + @Override public void onActivityResult(int requestCode, int resultCode, Intent intent) { - if (resultCode == RESULT_OK) { - try { + if (resultCode == RESULT_OK) { + try { ta.add(this, intent.getStringExtra("SCAN_RESULT")); - } catch (NoSuchAlgorithmException e) { - Toast.makeText(this, R.string.token_scan_invalid, Toast.LENGTH_SHORT).show(); - e.printStackTrace(); } catch (TokenUriInvalidException e) { - Toast.makeText(this, R.string.token_scan_invalid, Toast.LENGTH_SHORT).show(); + Toast.makeText(this, R.string.invalid_token, Toast.LENGTH_SHORT).show(); e.printStackTrace(); } - } - } + } + } } diff --git a/src/org/fedorahosted/freeotp/Token.java b/src/org/fedorahosted/freeotp/Token.java index a42d36f..ca51633 100644 --- a/src/org/fedorahosted/freeotp/Token.java +++ b/src/org/fedorahosted/freeotp/Token.java @@ -67,15 +67,13 @@ public class Token { tokens.add(new Token(prefs.getString(key, null))); } catch (TokenUriInvalidException e) { e.printStackTrace(); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); } } return tokens; } - private Token(Uri uri) throws TokenUriInvalidException, NoSuchAlgorithmException { + private Token(Uri uri) throws TokenUriInvalidException { if (!uri.getScheme().equals("otpauth")) throw new TokenUriInvalidException(); @@ -105,10 +103,11 @@ public class Token { if (algo == null) algo = "sha1"; algo = algo.toUpperCase(Locale.US); - if (!algo.equals("SHA1") && !algo.equals("SHA256") && - !algo.equals("SHA512") && !algo.equals("MD5")) + try { + Mac.getInstance("Hmac" + algo); + } catch (NoSuchAlgorithmException e1) { throw new TokenUriInvalidException(); - Mac.getInstance("Hmac" + algo); + } try { String d = uri.getQueryParameter("digits"); @@ -194,7 +193,7 @@ public class Token { return ""; } - public Token(String uri) throws TokenUriInvalidException, NoSuchAlgorithmException { + public Token(String uri) throws TokenUriInvalidException { this(Uri.parse(uri)); } diff --git a/src/org/fedorahosted/freeotp/TokenAdapter.java b/src/org/fedorahosted/freeotp/TokenAdapter.java index 56ceebc..5ad3310 100644 --- a/src/org/fedorahosted/freeotp/TokenAdapter.java +++ b/src/org/fedorahosted/freeotp/TokenAdapter.java @@ -22,7 +22,6 @@ package org.fedorahosted.freeotp; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -200,7 +199,7 @@ public class TokenAdapter extends BaseAdapter { } } - public void add(Context ctx, String uri) throws NoSuchAlgorithmException, TokenUriInvalidException { + public void add(Context ctx, String uri) throws TokenUriInvalidException { Token t = new Token(uri); t.save(ctx); tokens.add(t); -- 2.43.2