| // 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(); |
| } |
| } |