From e5fb1922d4a1c63211a48428591d50019c6ae0e4 Mon Sep 17 00:00:00 2001 From: Eric Atkin Date: Fri, 5 Aug 2016 01:30:00 -0600 Subject: [PATCH] Add GET_CONTENT and INSERT actions to MainActivity to support 3rd party app integration. This branch adds support for 3rd party app integration/automation using standard "otpauth://" URIs (https://github.com/google/google-authenticator/wiki/Key-Uri-Format). The GET_CONTENT intent action will return the current code for an existing token. The INSERT action will install a new token and return the current code (and the token secret to allow stateless operation by the intent sender). Both actions are subject to user confirmation. These two actions will allow a 3rd party app to alleviate the cubersome and error prone user process of being prompted for an OTP code, switching to FreeOTP, memorizing or copying the code to the clipboard, switch back to the original app, and finally entering the code. The 3rd party app should appropriately handle the exception cases of FreeOTP not being installed or the user denying the requested action. --- app/src/main/AndroidManifest.xml | 12 ++ .../fedorahosted/freeotp/MainActivity.java | 131 +++++++++++++++++- .../freeotp/TokenPersistence.java | 3 +- app/src/main/res/values/strings.xml | 10 ++ 4 files changed, 151 insertions(+), 5 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a2bf533c..22beee29 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -95,6 +95,18 @@ + + + + + + + + + + + + diff --git a/app/src/main/java/org/fedorahosted/freeotp/MainActivity.java b/app/src/main/java/org/fedorahosted/freeotp/MainActivity.java index f1cc81b2..11358f1c 100644 --- a/app/src/main/java/org/fedorahosted/freeotp/MainActivity.java +++ b/app/src/main/java/org/fedorahosted/freeotp/MainActivity.java @@ -40,10 +40,15 @@ import org.fedorahosted.freeotp.add.ScanActivity; import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.database.DataSetObserver; import android.net.Uri; import android.os.Bundle; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; @@ -51,6 +56,8 @@ import android.view.WindowManager.LayoutParams; import android.widget.GridView; +import com.google.gson.Gson; + public class MainActivity extends Activity implements OnMenuItemClickListener { private TokenAdapter mTokenAdapter; private DataSetObserver mDataSetObserver; @@ -132,8 +139,126 @@ public boolean onMenuItemClick(MenuItem item) { protected void onNewIntent(Intent intent) { super.onNewIntent(intent); - Uri uri = intent.getData(); - if (uri != null) - TokenPersistence.addWithToast(this, uri.toString()); + final Uri uri = intent.getData(); + if (uri != null) { + String action = intent.getAction(); + if (action.equals(Intent.ACTION_VIEW)) { + TokenPersistence.addWithToast(this, uri.toString()); + return; + } + try { + final Token token = new Token(uri); + final String key = token.getID(); + final Intent out = new Intent(); + out.putExtra("key", key); + String appname = intent.getStringExtra("appname"); + if (appname == null) { + appname = getString(R.string.default_appname); + } + final SharedPreferences prefs = getSharedPreferences(TokenPersistence.NAME, Context.MODE_PRIVATE); + switch (action) { + case Intent.ACTION_GET_CONTENT: + if (prefs.contains(key)) { + new AlertDialog.Builder(this) + .setTitle(R.string.attention) + .setMessage(appname + getString(R.string.request_code) + "\"" + key + "\"") + .setCancelable(false) + .setPositiveButton(R.string.allow, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + out.putExtra("currentCode", new Gson().fromJson(prefs.getString(key, null), Token.class).generateCodes().getCurrentCode()); + setResult(Activity.RESULT_OK, out); + finish(); + } + }) + .setNegativeButton(R.string.deny, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + setResult(Activity.RESULT_CANCELED, out); + finish(); + } + }) + .show() + ; + } else { + setResult(Activity.RESULT_CANCELED, out); + finish(); + } + break; + case Intent.ACTION_INSERT: + if (prefs.contains(key)) { + new AlertDialog.Builder(MainActivity.this) + .setTitle(R.string.error) + .setMessage(String.format(getString(R.string.token_already_exists), key)) + .setCancelable(false) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + setResult(Activity.RESULT_CANCELED, out); + finish(); + } + }) + .show() + ; + } else { + new AlertDialog.Builder(this) + .setTitle(R.string.attention) + .setMessage(appname + getString(R.string.request_install_token) + "\"" + key + "\"") + .setCancelable(false) + .setPositiveButton(R.string.allow, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + try { + new TokenPersistence(MainActivity.this).add(token); + out.putExtra("currentCode", token.generateCodes().getCurrentCode()); + out.putExtra("secret", uri.getQueryParameter("secret")); + setResult(Activity.RESULT_OK, out); + finish(); + } catch (Token.TokenUriInvalidException e) { + new AlertDialog.Builder(MainActivity.this) + .setTitle(R.string.error) + .setMessage(getString(R.string.bad_token_uri) + uri.toString()) + .setCancelable(false) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + setResult(Activity.RESULT_CANCELED, out); + finish(); + } + }) + .show() + ; + } + } + }) + .setNegativeButton(R.string.deny, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + setResult(Activity.RESULT_CANCELED, out); + finish(); + } + }) + .show() + ; + } + break; + default: + Log.e("LOG", "bad action: " + action); + } + } catch (Token.TokenUriInvalidException e) { + new AlertDialog.Builder(this) + .setTitle(R.string.error) + .setMessage(getString(R.string.bad_token_uri) + uri.toString()) + .setCancelable(false) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + finish(); + } + }) + .show() + ; + } + } } } diff --git a/app/src/main/java/org/fedorahosted/freeotp/TokenPersistence.java b/app/src/main/java/org/fedorahosted/freeotp/TokenPersistence.java index 3d450f90..8c4ba378 100644 --- a/app/src/main/java/org/fedorahosted/freeotp/TokenPersistence.java +++ b/app/src/main/java/org/fedorahosted/freeotp/TokenPersistence.java @@ -8,7 +8,6 @@ import android.content.Context; import android.content.SharedPreferences; -import android.net.Uri; import android.widget.Toast; import com.google.gson.Gson; @@ -16,7 +15,7 @@ import com.google.gson.reflect.TypeToken; public class TokenPersistence { - private static final String NAME = "tokens"; + protected static final String NAME = "tokens"; private static final String ORDER = "tokenOrder"; private final SharedPreferences prefs; private final Gson gson; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index de39f413..3f6b2970 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -38,6 +38,16 @@ This is your last chance: if you delete this token, it will be gone forever. It will not be disabled on the server. Delete this token? + Attention + Error + Allow + Deny + An unspecified app + " has requested a code for token " + " has requested to install a token for " + "Bad token uri: " + Token \"%s\" already exists + MD5 SHA1