]> Pileus Git - ~andy/freeotp/commitdiff
Enable manual token entry
authorNathaniel McCallum <npmccallum@redhat.com>
Fri, 18 Oct 2013 03:49:53 +0000 (23:49 -0400)
committerNathaniel McCallum <npmccallum@redhat.com>
Fri, 18 Oct 2013 03:49:53 +0000 (23:49 -0400)
res/layout/manual.xml [new file with mode: 0644]
res/menu/main.xml
res/values/strings.xml
src/org/fedorahosted/freeotp/AddTokenDialog.java [new file with mode: 0644]
src/org/fedorahosted/freeotp/AddTokenSecretTextWatcher.java [new file with mode: 0644]
src/org/fedorahosted/freeotp/AddTokenTextWatcher.java [new file with mode: 0644]
src/org/fedorahosted/freeotp/MainActivity.java
src/org/fedorahosted/freeotp/Token.java
src/org/fedorahosted/freeotp/TokenAdapter.java

diff --git a/res/layout/manual.xml b/res/layout/manual.xml
new file mode 100644 (file)
index 0000000..4a94da6
--- /dev/null
@@ -0,0 +1,162 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="16dp" >
+
+    <TableRow
+        android:layout_width="match_parent"
+        android:layout_height="48dp" >
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:gravity="center_vertical|right"
+            android:text="@string/interval"
+            android:paddingRight="8dp" />
+
+        <EditText
+            android:id="@+id/issuer"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:hint="jdoe@example.com"
+            android:textAppearance="?android:attr/textAppearanceSmallInverse" />
+    </TableRow>
+
+    <TableRow
+        android:layout_width="match_parent"
+        android:layout_height="48dp" >
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:gravity="center_vertical|right"
+            android:text="@string/id"
+            android:paddingRight="8dp" />
+
+        <EditText
+            android:id="@+id/id"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:hint="18c5d06cfcbd4927"
+            android:textAppearance="?android:attr/textAppearanceSmallInverse" />
+    </TableRow>
+
+    <TableRow
+        android:layout_width="match_parent"
+        android:layout_height="48dp" >
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:gravity="center_vertical|right"
+            android:text="@string/secret"
+            android:paddingRight="8dp" />
+
+        <EditText
+            android:id="@+id/secret"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:digits="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ234567="
+            android:hint="Base32 (ex. &apos;GEZDGNBV&apos;)"
+            android:textAppearance="?android:attr/textAppearanceSmall" />
+    </TableRow>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:layout_marginTop="12dp"
+        android:layout_marginBottom="4dp"
+        android:background="?android:attr/dividerHorizontal"
+        />
+
+    <TableRow
+        android:layout_width="match_parent"
+        android:layout_height="48dp" >
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:gravity="center_vertical|right"
+            android:text="@string/type"
+            android:paddingRight="8dp" />
+
+        <Spinner
+            android:id="@+id/type"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:entries="@array/token_types" />
+    </TableRow>
+
+    <TableRow
+        android:layout_width="match_parent"
+        android:layout_height="48dp" >
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:gravity="center_vertical|right"
+            android:text="@string/algorithm"
+            android:paddingRight="8dp" />
+
+        <Spinner
+            android:id="@+id/algorithm"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:entries="@array/algorithms" />
+    </TableRow>
+
+    <TableRow
+        android:layout_width="match_parent"
+        android:layout_height="48dp" >
+        <TextView
+            android:id="@+id/interval_label"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:gravity="center_vertical|right"
+            android:text="@string/interval"
+            android:paddingRight="8dp" />
+
+        <EditText
+            android:id="@+id/interval"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:inputType="number"
+            android:text="30" />
+    </TableRow>
+
+    <TableRow
+        android:layout_width="match_parent"
+        android:layout_height="48dp" >
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:gravity="center_vertical|right"
+            android:text="@string/digits"
+            android:paddingRight="8dp" />
+
+        <RadioGroup
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:orientation="horizontal" >
+            <RadioButton
+                android:id="@+id/digits6"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:text="6"
+                android:checked="true" />
+
+            <RadioButton
+                android:id="@+id/digits8"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:text="8" />
+        </RadioGroup>
+    </TableRow>
+</TableLayout>
\ No newline at end of file
index cbe751ca9abef95aeaf7224763a1a767e1ea9fb4..5fc34745d4405cccfd8c8762b2083c9ab09f8149 100644 (file)
@@ -27,5 +27,5 @@
         android:orderInCategory="100"
         android:showAsAction="always"
         android:icon="@drawable/scan"
-        android:title="@string/action_add" />
+        android:title="@string/add" />
 </menu>
index c0034084b833b0f345933223ab907d5c7fc270b3..48d2b625ee34a9b67c058a27dcc34cf85e8dbb12 100644 (file)
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <string name="app_name">FreeOTP</string>
-    <string name="action_add">Add</string>
-    <string name="token_scan_invalid">The scanned token data was invalid!</string>
+    <string name="add">Add</string>
+    <string name="invalid_token">The token specified was invalid!</string>
     <string name="delete_message">Are you sure you want to remove this token?\n\nNOTE: This will NOT disable two-factor authentication on the server.\n\n</string>
     <string name="delete">Delete</string>
     <string name="yes">Yes</string>
     <string name="install_title">Install Barcode Scanner?</string>
     <string name="install_message">Barcode Scanner is required. Would you like to install it?</string>
     <string name="no_keys">No OTP keys installed.</string>
+    <string name="add_token">Add Token</string>
+    <string name="scan_qr_code">Scan QR Code</string>
+    <string name="interval">Interval</string>
+    <string name="counter">Counter</string>
+    <string name="id">ID</string>
+    <string name="secret">Secret</string>
+    <string name="type">Type</string>
+    <string name="algorithm">Algorithm</string>
+    <string name="digits">Digits</string>
+
+    <string-array name="token_types">
+        <item>Time-based (TOTP)</item>
+        <item>Counter-based (HOTP)</item>
+    </string-array>
+
+    <string-array name="digits">
+        <item>6</item>
+        <item>8</item>
+    </string-array>
+
+    <string-array name="algorithms">
+        <item>MD5</item>
+        <item>SHA1</item>
+        <item>SHA256</item>
+        <item>SHA512</item>
+    </string-array>
 </resources>
diff --git a/src/org/fedorahosted/freeotp/AddTokenDialog.java b/src/org/fedorahosted/freeotp/AddTokenDialog.java
new file mode 100644 (file)
index 0000000..c6f781f
--- /dev/null
@@ -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 (file)
index 0000000..eaf335d
--- /dev/null
@@ -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 (file)
index 0000000..5102ddd
--- /dev/null
@@ -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) {
+
+       }
+}
index d7bb6fbc3f8b8d78404c8feb1e9da4c6f9b65b5b..9a31a94592d7353f6408a464bed48368b5ee5f43 100644 (file)
@@ -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();
                        }
-        }
-    }
+               }
+       }
 }
index a42d36f92ec4c6783c4dd030358b1e758f82604a..ca516337785424b6bb1a5875aa48cc0b039db955 100644 (file)
@@ -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));
        }
 
index 56ceebc0b9a2221ed6d295d3f8ad08da0009109a..5ad331020169baef856311629abf9fcabfab8f47 100644 (file)
@@ -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);