--- /dev/null
+<?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. 'GEZDGNBV')"
+ 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
android:orderInCategory="100"
android:showAsAction="always"
android:icon="@drawable/scan"
- android:title="@string/action_add" />
+ android:title="@string/add" />
</menu>
<?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>
--- /dev/null
+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);
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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) {
+
+ }
+}
package org.fedorahosted.freeotp;
-import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
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;
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;
}
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();
}
- }
- }
+ }
+ }
}
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();
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");
return "";
}
- public Token(String uri) throws TokenUriInvalidException, NoSuchAlgorithmException {
+ public Token(String uri) throws TokenUriInvalidException {
this(Uri.parse(uri));
}
package org.fedorahosted.freeotp;
-import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
}
}
- 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);