blob: e766514a14e917539fb4d3ab5e91a339e26a450a [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.v.android.impl.google.services.blessing;
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Base64;
import android.util.Log;
import com.google.common.base.Preconditions;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.interfaces.ECPublicKey;
import io.v.android.v23.V;
import io.v.v23.context.VContext;
import io.v.v23.security.Caveat;
import io.v.v23.verror.VException;
import io.v.v23.vom.VomUtil;
/**
* Mints a new Vanadium blessing given the email address and the public key.
* <p>
* The provided email address must be already present on the phone.
*/
public class BlessingActivity extends Activity
implements ActivityCompat.OnRequestPermissionsResultCallback {
public static final String TAG = "BlessingActivity";
private static final int REQUEST_CODE_USER_APPROVAL = 1000;
private static final int REQUEST_CODE_PICK_ACCOUNT = 1001;
private static final String STATE_GOOGLE_ACCOUNT = "STATE_GOOGLE_ACCOUNT";
private static final String STATE_PUBLIC_KEY = "STATE_PUBLIC_KEY";
private static final String STATE_PREF_KEY = "STATE_PREF_KEY";
private static final String OAUTH_PROFILE = "https://www.googleapis.com/auth/userinfo.email";
private static final String OAUTH_SCOPE = "oauth2:" + OAUTH_PROFILE;
private static final int BASE64_FLAGS = Base64.URL_SAFE | Base64.NO_WRAP;
/**
* Serialized {@link ECPublicKey} of the invoking activity.
*/
public static final String EXTRA_PUBLIC_KEY = "PUBLIC_KEY";
/**
* If non-{@code null} and non-empty in the invoking intent, the activity will generate
* blessings based on the provided account, instead of prompting the user to choose
* an account.
*/
public static final String EXTRA_GOOGLE_ACCOUNT = "GOOGLE_ACCOUNT";
/**
* If non-{@code null} and non-empty in the invoking intent, this activity will store the
* generated blessings in default {@link SharedPreferences} under this key.
*/
public static final String EXTRA_PREF_KEY = "PREF_KEY";
/**
* If successful, VOM-encoded blessings will be stored (in byte array format) in the returned
* intent under the given key.
*/
public static final String EXTRA_REPLY = "REPLY";
/**
* If an error is encountered, the error string will be stored in the returned intent
* under the given key.
*/
public static final String EXTRA_ERROR = "ERROR";
private VContext mBaseContext;
private String mGoogleAccount = "";
private ECPublicKey mPublicKey = null;
private String mPrefKey = "";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBaseContext = V.init(this);
setFinishOnTouchOutside(false);
if (savedInstanceState != null) {
mGoogleAccount = savedInstanceState.getString(STATE_GOOGLE_ACCOUNT);
mPublicKey = (ECPublicKey) savedInstanceState.getSerializable(STATE_PUBLIC_KEY);
mPrefKey = savedInstanceState.getString(STATE_PREF_KEY);
return;
}
Intent intent = getIntent();
if (intent == null) {
replyWithError("Intent not found.");
return;
}
// Get the public key of the application invoking this activity.
mPublicKey = (ECPublicKey) intent.getSerializableExtra(EXTRA_PUBLIC_KEY);
if (mPublicKey == null) {
replyWithError("Empty blesee public key.");
return;
}
// Get the SharedPreferences key where the blessings are to be stored. If empty, blessings
// aren't stored in preferences.
if (intent.hasExtra(EXTRA_PREF_KEY)) {
mPrefKey = intent.getStringExtra(EXTRA_PREF_KEY);
}
// Get the google email address (if any).
mGoogleAccount = intent.getStringExtra(EXTRA_GOOGLE_ACCOUNT);
if (mGoogleAccount == null || mGoogleAccount.isEmpty()) {
// Prompt the user to choose the google account to use (if more than one).
Intent accountIntent = AccountManager.newChooseAccountIntent(
null, null, new String[]{"com.google"}, false, null, null, null, null);
startActivityForResult(accountIntent, REQUEST_CODE_PICK_ACCOUNT);
return;
}
getBlessing();
}
@Override
protected void onDestroy() {
super.onDestroy();
mBaseContext.cancel();
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putString(STATE_GOOGLE_ACCOUNT, mGoogleAccount);
savedInstanceState.putSerializable(STATE_PUBLIC_KEY, mPublicKey);
savedInstanceState.putString(STATE_PREF_KEY, mPrefKey);
super.onSaveInstanceState(savedInstanceState);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_CODE_PICK_ACCOUNT:
if (resultCode != RESULT_OK) {
replyWithError("User didn't pick account.");
return;
}
mGoogleAccount = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
if (mGoogleAccount == null || mGoogleAccount.isEmpty()) {
replyWithError("Empty Google email.");
return;
}
getBlessing();
break;
case REQUEST_CODE_USER_APPROVAL:
if (resultCode != RESULT_OK) {
replyWithError("User didn't give proposed permissions.");
return;
}
getBlessing();
break;
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String permission[], int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_USER_APPROVAL:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
getBlessing();
return;
}
replyWithError("User didn't give proposed permissions.");
break;
}
}
private void getBlessing() {
int permissionCheck = ContextCompat.checkSelfPermission(
this, Manifest.permission.GET_ACCOUNTS);
if (permissionCheck == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.GET_ACCOUNTS}, REQUEST_CODE_USER_APPROVAL);
return;
}
Account[] accounts = AccountManager.get(this).getAccountsByType("com.google");
Account account = null;
for (int i = 0; i < accounts.length; i++) {
if (accounts[i].name.equals(mGoogleAccount)) {
account = accounts[i];
}
}
if (account == null) {
replyWithError("Couldn't find Google account with name: " + mGoogleAccount);
return;
}
AccountManager.get(this).getAuthToken(
account,
OAUTH_SCOPE,
new Bundle(),
false,
new OnTokenAcquired(),
new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
replyWithError("Error getting auth token: " + msg.toString());
return true;
}
}));
}
class OnTokenAcquired implements AccountManagerCallback<Bundle> {
@Override
public void run(AccountManagerFuture<Bundle> result) {
try {
Bundle bundle = result.getResult();
Intent launch = (Intent) bundle.get(AccountManager.KEY_INTENT);
if (launch != null) { // Needs user approval.
// NOTE(spetrovic): The returned intent has the wrong flag value
// FLAG_ACTIVITY_NEW_TASK set, which results in the launched intent replying
// immediately with RESULT_CANCELED. Hence, we clear the flag here.
launch.setFlags(0);
startActivityForResult(launch, REQUEST_CODE_USER_APPROVAL);
return;
}
String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
(new BlessingFetcher()).execute(token);
} catch (AuthenticatorException e) {
replyWithError("Couldn't authorize: " + e.getMessage());
return;
} catch (OperationCanceledException e) {
replyWithError("Authorization cancelled: " + e.getMessage());
return;
} catch (IOException e) {
replyWithError("Unexpected error: " + e.getMessage());
return;
}
}
}
private class BlessingFetcher extends AsyncTask<String, Void, byte[]> {
ProgressDialog progressDialog = new ProgressDialog(BlessingActivity.this);
String errorMsg = null;
@Override
protected void onPreExecute() {
progressDialog.setMessage("Fetching Vanadium Blessing...");
progressDialog.show();
}
@Override
protected byte[] doInBackground(String... tokens) {
if (tokens.length != 1) {
errorMsg = "Empty OAuth token.";
return null;
}
try {
String publicKey = Base64.encodeToString(mPublicKey.getEncoded(), BASE64_FLAGS);
String token = tokens[0];
String caveats = Base64.encodeToString(
VomUtil.encode(new Caveat[]{}, Caveat[].class), BASE64_FLAGS);
String outputFormat = "base64vom";
URL url = new URL("https://dev.v.io/auth/google/bless" +
"?token=" + token +
"&public_key=" + publicKey +
"&caveats=" + caveats +
"&output_format=" + outputFormat);
byte[] base64EncodedBlessingVom = ByteStreams.toByteArray(
url.openConnection().getInputStream());
return Base64.decode(base64EncodedBlessingVom, BASE64_FLAGS);
} catch (MalformedURLException e) {
errorMsg = e.getMessage();
return null;
} catch (IOException e) {
errorMsg = e.getMessage();
return null;
} catch (VException e) {
errorMsg = e.getMessage();
return null;
}
}
@Override
protected void onPostExecute(byte[] blessingVom) {
progressDialog.dismiss();
if (blessingVom == null || blessingVom.length == 0) {
replyWithError("Couldn't get identity from Vanadium identity servers: " + errorMsg);
return;
}
replyWithSuccess(blessingVom);
}
}
private void updatePrefs(String key, String value) {
SharedPreferences.Editor editor =
PreferenceManager.getDefaultSharedPreferences(this).edit();
editor.putString(key, value);
editor.commit();
}
private void replyWithError(String error) {
Preconditions.checkArgument(error != null && !error.isEmpty());
Log.e(TAG, "Error while blessing: " + error);
Intent intent = new Intent();
intent.putExtra(EXTRA_ERROR, error);
setResult(RESULT_CANCELED, intent);
finish();
}
private void replyWithSuccess(byte[] blessingVom) {
Preconditions.checkArgument(blessingVom != null && blessingVom.length > 0);
// Store the result in preferences, if the caller asked for it.
if (!mPrefKey.isEmpty()) {
String hexBlessingsVom = VomUtil.bytesToHexString(blessingVom);
updatePrefs(mPrefKey, hexBlessingsVom);
}
// Prepare the return intent.
Intent intent = new Intent();
intent.putExtra(EXTRA_REPLY, blessingVom);
setResult(RESULT_OK, intent);
finish();
}
}