blob: 7edb1c5962c3881f0f01499b2c0e72932c84637d [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.apps.account_manager;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.ProgressDialog;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Base64;
import android.widget.Toast;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.io.CharStreams;
import com.google.common.util.concurrent.ListenableFuture;
import org.joda.time.Duration;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.interfaces.ECPublicKey;
import io.v.android.v23.V;
import io.v.v23.VFutures;
import io.v.v23.context.VContext;
import io.v.v23.security.BlessingPattern;
import io.v.v23.security.BlessingStore;
import io.v.v23.security.Blessings;
import io.v.v23.security.Caveat;
import io.v.v23.security.CryptoUtil;
import io.v.v23.security.VPrincipal;
import io.v.v23.security.VSecurity;
import io.v.v23.verror.VException;
import io.v.x.ref.services.identity.OAuthBlesserClient;
import io.v.x.ref.services.identity.OAuthBlesserClientFactory;
/**
* Creates a new Vanadium account, using the Google accounts present on the
* device.
*/
// TODO: Change to BlessingCreationActivity.
public class AccountActivity extends AccountAuthenticatorActivity {
public static final String TAG = "AccountActivity";
private static final int REQUEST_CODE_PICK_ACCOUNTS = 1000;
private static final int REQUEST_CODE_USER_APPROVAL = 1001;
private static final String OAUTH_PROFILE = "https://www.googleapis.com/auth/userinfo.email";
private static final String OAUTH_SCOPE = "oauth2:" + OAUTH_PROFILE;
public static final String GOOGLE_ACCOUNT = "GOOGLE_ACCOUNT";
private VContext mBaseContext = null;
private String mAccountName = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_account);
mBaseContext = V.init(this);
Intent intent = getIntent();
if (intent == null) {
replyWithError("Intent not found.");
return;
}
// See if the caller wants to use a specific Google account to create the Vanadium account.
// If null or empty string is passed, the user will be prompted to choose the Google
// account to use.
mAccountName = intent.getStringExtra(GOOGLE_ACCOUNT);
if (mAccountName != null && !mAccountName.isEmpty()) {
getIdentity();
return;
}
Intent chooseIntent = AccountManager.newChooseAccountIntent(
null, null, new String[]{"com.google"}, false, null, null, null, null);
startActivityForResult(chooseIntent, REQUEST_CODE_PICK_ACCOUNTS);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_PICK_ACCOUNTS) {
if (resultCode != RESULT_OK) {
replyWithError("User didn't pick account.");
return;
}
mAccountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
getIdentity();
} else if (requestCode == REQUEST_CODE_USER_APPROVAL) {
if (resultCode != RESULT_OK) {
replyWithError("User didn't give proposed permissions.");
return;
}
getIdentity();
}
super.onActivityResult(requestCode, resultCode, data);
}
private void getIdentity() {
if (mAccountName == null || mAccountName.isEmpty()) {
replyWithError("Empty account name.");
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(mAccountName)) {
account = accounts[i];
}
}
if (account == null) {
replyWithError("Couldn't find Google account with name: " + mAccountName);
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());
} catch (OperationCanceledException e) {
replyWithError("Authorization cancelled: " + e.getMessage());
} catch (IOException e) {
replyWithError("Unexpected error: " + e.getMessage());
}
}
}
private class BlessingFetcher extends AsyncTask<String, Void, Blessings> {
ProgressDialog progressDialog = new ProgressDialog(AccountActivity.this);
String errorMsg = null;
@Override
protected void onPreExecute() {
progressDialog.setMessage("Fetching Vanadium Identity...");
progressDialog.show();
}
@Override
protected Blessings doInBackground(String... tokens) {
if (tokens.length != 1) {
errorMsg = "Empty OAuth token.";
return null;
}
try {
URL url = new URL("https://dev.v.io/auth/blessing-root");
JSONObject object = new JSONObject(CharStreams.toString(
new InputStreamReader(url.openConnection().getInputStream(),
Charsets.US_ASCII)));
String publicKey = object.get("publicKey").toString();
byte[] base64DecodedKey = Base64.decode(
publicKey.getBytes(), Base64.URL_SAFE);
ECPublicKey ecPublicKey = CryptoUtil.decodeECPublicKey(base64DecodedKey);
JSONArray namesArray = (JSONArray) object.get("names");
for (int i = 0; i < namesArray.length(); i++) {
String name = namesArray.getString(i);
V.getPrincipal(mBaseContext).roots()
.add(ecPublicKey, new BlessingPattern(name));
}
OAuthBlesserClient blesser =
OAuthBlesserClientFactory.getOAuthBlesserClient(Constants.IDENTITY_DEV_V_IO_U_GOOGLE);
VContext ctx = mBaseContext.withTimeout(new Duration(20000)); // 20s
ListenableFuture<OAuthBlesserClient.BlessUsingAccessTokenWithCaveatsOut> reply =
blesser.blessUsingAccessTokenWithCaveats(ctx, tokens[0],
ImmutableList.<Caveat>of(VSecurity.newUnconstrainedUseCaveat()));
Blessings blessing = VFutures.sync(reply).blessing;
if (blessing == null || blessing.getCertificateChains() == null ||
blessing.getCertificateChains().size() <= 0) {
errorMsg = "Received empty blessing from Vanadium identity servers.";
return null;
}
if (blessing.getCertificateChains().size() > 1) {
errorMsg = "Received more than one blessing from Vanadium identity servers.";
return null;
}
return blessing;
} catch (VException e) {
errorMsg = e.getMessage();
return null;
} catch (MalformedURLException e) {
errorMsg = e.getMessage();
return null;
} catch (JSONException e) {
errorMsg = e.getMessage();
return null;
} catch (IOException e) {
errorMsg = e.getMessage();
return null;
}
}
@Override
protected void onPostExecute(Blessings blessing) {
progressDialog.dismiss();
if (blessing == null) { // Indicates an error
replyWithError("Couldn't get identity from Vanadium identity servers: " + errorMsg);
return;
}
replyWithSuccess(blessing);
}
}
private void replyWithError(String error) {
android.util.Log.e(TAG, "Error creating account: " + error);
setResult(RESULT_CANCELED);
String text = "Couldn't create blessing: " + error;
Toast.makeText(this, text, Toast.LENGTH_LONG).show();
finish();
}
private void replyWithSuccess(Blessings blessing) {
enforceAccountExists();
// Store the obtained blessing from identity server.
try {
VPrincipal principal = V.getPrincipal(mBaseContext);
BlessingStore blessingStore = principal.blessingStore();
blessingStore.set(blessing, new BlessingPattern(blessing.toString()));
} catch (VException e) {
replyWithError("Couldn't store obtained blessing: " + e.getMessage());
}
setResult(RESULT_OK);
Toast.makeText(this, "Success.", Toast.LENGTH_SHORT).show();
finish();
}
private void enforceAccountExists() {
if (AccountManager.get(this).getAccountsByType(Constants.ACCOUNT_TYPE).length <= 0) {
String name = "Vanadium";
Account account = new Account(name, getResources().getString(
R.string.authenticator_account_type));
AccountManager am = AccountManager.get(this);
am.addAccountExplicitly(account, null, null);
}
}
}