| // 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.libs.security; |
| |
| import android.app.Activity; |
| import android.app.FragmentTransaction; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.os.Bundle; |
| import android.app.Fragment; |
| import android.os.Looper; |
| import android.preference.PreferenceManager; |
| import android.util.Log; |
| |
| import com.google.common.util.concurrent.AsyncFunction; |
| import com.google.common.util.concurrent.Futures; |
| import com.google.common.util.concurrent.ListenableFuture; |
| import com.google.common.util.concurrent.SettableFuture; |
| |
| import java.security.interfaces.ECPublicKey; |
| import java.util.UUID; |
| |
| import io.v.android.impl.google.services.blessing.BlessingActivity; |
| import io.v.android.v23.V; |
| import io.v.v23.VFutures; |
| import io.v.v23.context.VContext; |
| import io.v.v23.security.Blessings; |
| import io.v.v23.security.Constants; |
| import io.v.v23.security.VPrincipal; |
| import io.v.v23.security.VSecurity; |
| import io.v.v23.verror.VException; |
| import io.v.v23.vom.VomUtil; |
| |
| /** |
| * Manages {@link Blessings} for a given Android application, persisting them in its |
| * shared preferences. |
| * <p> |
| * This class is thread-safe. |
| */ |
| public class BlessingsManager extends Fragment { |
| private static String TAG = "BlessingsManager"; |
| |
| private static final int REQUEST_CODE_MINT_BLESSINGS = 1000; |
| private static final String STATE_SAVED = "STATE_SAVED"; |
| |
| private static SettableFuture<Blessings> mintFuture; |
| |
| private VContext mBaseContext; |
| private boolean mWasDestroyed = false; |
| private String mPrefKey; // may be null if wasDestroyed == true |
| private String mGoogleAccount; // may be null if wasDestroyed == true |
| |
| /** |
| * Returns a new {@link ListenableFuture} whose result are the {@link Blessings} found in |
| * {@link SharedPreferences} under the given key. |
| * <p> |
| * If no {@link Blessings} are found, mints a new set of {@link Blessings} and stores them |
| * in {@link SharedPreferences} under the provided key. |
| * <p> |
| * This method may start an activity to handle the creation of new blessings, if needed. |
| * Hence, you should be prepared that your activity may be stopped and re-started. |
| * <p> |
| * This method is re-entrant: if blessings need to be minted, multiple concurrent invocations |
| * of this method will result in only the last invocation's future ever being invoked. This |
| * means that it's safe to call this method from any of the android's lifecycle methods |
| * (e.g., onCreate(), onStart(), onResume()). |
| * <p> |
| * This method must be invoked on the UI thread. |
| * |
| * @param context Vanadium context |
| * @param activity android {@link Activity} requesting blessings |
| * @param key a key under which the blessings are stored |
| * @param setAsDefault if true, the returned {@link Blessings} will be set as default |
| * blessings for the app |
| * @return a new {@link ListenableFuture} whose result are the blessings |
| * persisted under the given key |
| */ |
| public static ListenableFuture<Blessings> getBlessings(VContext context, |
| final Activity activity, String key, boolean setAsDefault) { |
| if (Looper.myLooper() != Looper.getMainLooper()) { |
| return Futures.immediateFailedFuture(new VException("getBlessings() must be invoked " + |
| "on the UI thread")); |
| } |
| try { |
| Blessings blessings = readBlessings(context, activity, key, setAsDefault); |
| if (blessings != null) { |
| return Futures.immediateFuture(blessings); |
| } |
| } catch (VException e) { |
| Log.e(TAG, "Malformed blessings in SharedPreferences. Minting new blessings: " + |
| e.getMessage()); |
| } |
| return mintBlessings(context, activity, key, setAsDefault); |
| } |
| |
| /** |
| * Returns {@link Blessings} found in {@link SharedPreferences} under the given key. |
| * <p> |
| * Unlike {@link #getBlessings}, if no {@link Blessings} are found this method won't mint a new |
| * set of {@link Blessings}; instead, {@code null} value is returned. |
| * |
| * @param context Vanadium context |
| * @param androidContext android {@link Context} requesting blessings |
| * @param key a key under which the blessings are stored |
| * @param setAsDefault if true, the returned {@link Blessings}, if non-{@code null}, will be |
| * set as default blessings for the app |
| * @return {@link Blessings} found in {@link SharedPreferences} under the given |
| * key or {@code null} if no blessings are found |
| * @throws VException if the blessings are found in {@link SharedPreferences} but they |
| * are invalid |
| */ |
| public static Blessings readBlessings(VContext context, Context androidContext, String key, |
| boolean setAsDefault) throws VException { |
| String blessingsVom = |
| PreferenceManager.getDefaultSharedPreferences(androidContext).getString(key, ""); |
| if (blessingsVom == null || blessingsVom.isEmpty()) { |
| return null; |
| } |
| Blessings blessings = (Blessings) VomUtil.decodeFromString(blessingsVom, Blessings.class); |
| if (blessings == null) { |
| throw new VException("Couldn't decode blessings: got null blessings"); |
| } |
| // TODO(spetrovic): validate the blessings and fail if they aren't valid |
| return setAsDefault ? |
| VFutures.sync(wrapWithSetAsDefault( |
| context, androidContext, Futures.immediateFuture(blessings))) |
| : blessings; |
| } |
| |
| /** |
| * Mints a new set of {@link Blessings} that are persisted in {@link SharedPreferences} under |
| * the provided key. |
| * <p> |
| * If {@code googleAccount} is non-{@code null} and non-empty, mints the blessings using |
| * that account; otherwise, prompts the user to pick one of the installed Google accounts |
| * (if there is more than one installed). |
| * <p> |
| * This method will start an activity to handle the creation of new blessings. Hence, you |
| * should be prepared that your activity will be stopped and re-started, at the minimum. |
| * <p> |
| * This method is re-entrant: if invoked the 2nd time while the 1st invocation is still |
| * pending, the future associated with the 2nd invocation will overwrite the 1st future: the |
| * 1st future will never be invoked. |
| * <p> |
| * This method must be invoked on the UI thread. |
| * |
| * @param activity android {@link Activity} requesting blessings |
| * @param key a key in {@link SharedPreferences} under which the newly minted |
| * blessings are persisted |
| * @param googleAccount a Google account to use to mint the blessings; if {@code null} or |
| * empty, user will be prompted to pick one of the installed Google |
| * accounts, if there is more than one installed |
| * @param setAsDefault if true, the returned {@link Blessings} will be set as default |
| * blessings for the app |
| * @return a new {@link ListenableFuture} whose result are the newly minted |
| * {@link Blessings} |
| */ |
| public static ListenableFuture<Blessings> mintBlessings(VContext ctx, |
| final Activity activity, String key, String googleAccount, boolean setAsDefault) { |
| if (Looper.myLooper() != Looper.getMainLooper()) { |
| return Futures.immediateFailedFuture(new VException("mintBlessings() must be invoked " + |
| "on the UI thread")); |
| } |
| if (mintFuture != null) { |
| // Mint already in progress, which means that the invoking activity has been |
| // destroyed and then recreated. Register the new future to be invoked on completion |
| // of that mint. Note that it is safe and desirable to override the old future |
| // as it's invocation would be handled by a destroyed activity. |
| mintFuture = SettableFuture.create(); |
| return setAsDefault ? wrapWithSetAsDefault(ctx, activity, mintFuture) : mintFuture; |
| } |
| mintFuture = SettableFuture.create(); |
| FragmentTransaction transaction = activity.getFragmentManager().beginTransaction(); |
| BlessingsManager fragment = new BlessingsManager(); |
| fragment.mPrefKey = key; |
| fragment.mGoogleAccount = googleAccount; |
| transaction.add(fragment, UUID.randomUUID().toString()); |
| transaction.commit(); // this will invoke the fragment's onCreate() immediately. |
| return setAsDefault ? wrapWithSetAsDefault(ctx, activity, mintFuture) : mintFuture; |
| } |
| |
| /** |
| * A shortcut for {@link #mintBlessings(VContext, Activity, String, String, boolean)}} with |
| * empty Google account, causing the user to be prompted to pick one of the installed Google |
| * accounts (if there is more than one installed). |
| */ |
| public static ListenableFuture<Blessings> mintBlessings( |
| VContext ctx, Activity activity, final String key, boolean setAsDefault) { |
| return mintBlessings(ctx, activity, key, "", setAsDefault); |
| } |
| |
| public BlessingsManager() {} |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| mBaseContext = V.init(getActivity()); |
| // onCreate() being called with non-null savedInstanceState is an indicator that the |
| // fragment (and the containing activity) have been destroyed since originally |
| // created, as onCreate() wouldn't be called again, otherwise. |
| mWasDestroyed = savedInstanceState != null; |
| if (!mWasDestroyed) { |
| // Start the intent to fetch the blessings. |
| ECPublicKey pubKey = V.getPrincipal(mBaseContext).publicKey(); |
| Intent intent = new Intent(getActivity(), BlessingActivity.class); |
| intent.putExtra(BlessingActivity.EXTRA_PUBLIC_KEY, pubKey); |
| if (mGoogleAccount != null && !mGoogleAccount.isEmpty()) { |
| intent.putExtra(BlessingActivity.EXTRA_GOOGLE_ACCOUNT, mGoogleAccount); |
| } |
| intent.putExtra(BlessingActivity.EXTRA_PREF_KEY, mPrefKey); |
| startActivityForResult(intent, REQUEST_CODE_MINT_BLESSINGS); |
| } |
| } |
| |
| @Override |
| public void onDestroy() { |
| super.onDestroy(); |
| mBaseContext.cancel(); |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle savedInstanceState) { |
| super.onSaveInstanceState(savedInstanceState); |
| // Just write something into the bundle (see onCreate() above). |
| savedInstanceState.putBoolean(STATE_SAVED, true); |
| } |
| |
| @Override |
| public void onActivityResult(int requestCode, int resultCode, Intent data) { |
| switch (requestCode) { |
| case REQUEST_CODE_MINT_BLESSINGS: { |
| if (mintFuture == null) { // shouldn't really happen |
| break; |
| } |
| SettableFuture<Blessings> future = mintFuture; |
| mintFuture = null; |
| // Extract VOM-encoded blessings. |
| if (data == null) { |
| future.setException(new VException("NULL blessing response")); |
| break; |
| } |
| if (resultCode != Activity.RESULT_OK) { |
| future.setException(new VException("Error getting blessing: " + |
| data.getStringExtra(BlessingActivity.EXTRA_ERROR))); |
| break; |
| } |
| byte[] blessingsVom = data.getByteArrayExtra(BlessingActivity.EXTRA_REPLY); |
| if (blessingsVom == null || blessingsVom.length <= 0) { |
| future.setException(new VException("Got null blessings.")); |
| break; |
| } |
| // VOM-Decode blessings. |
| try { |
| Blessings blessings = |
| (Blessings) VomUtil.decode(blessingsVom, Blessings.class); |
| future.set(blessings); |
| } catch (VException e) { |
| future.setException(e); |
| } |
| break; |
| } |
| } |
| // Remove this fragment from the invoking activity. |
| FragmentTransaction transaction = getActivity().getFragmentManager().beginTransaction(); |
| transaction.remove(this); |
| transaction.commit(); |
| super.onActivityResult(requestCode, resultCode, data); |
| } |
| |
| private static ListenableFuture<Blessings> wrapWithSetAsDefault(final VContext ctx, |
| final Context context, ListenableFuture<Blessings> future) { |
| return Futures.transform(future, new AsyncFunction<Blessings, Blessings>() { |
| @Override |
| public ListenableFuture<Blessings> apply(Blessings blessings) throws Exception { |
| if (ctx.isCanceled()) { |
| return Futures.immediateFailedFuture( |
| new VException("Vanadium context canceled")); |
| } |
| // Update local state with the new blessings. |
| try { |
| VPrincipal p = V.getPrincipal(ctx); |
| p.blessingStore().setDefaultBlessings(blessings); |
| p.blessingStore().set(blessings, Constants.ALL_PRINCIPALS); |
| VSecurity.addToRoots(p, blessings); |
| return Futures.immediateFuture(blessings); |
| } catch (VException e) { |
| return Futures.immediateFailedFuture(e); |
| } |
| } |
| }); |
| } |
| } |