| // 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.app.Activity; |
| import android.app.ProgressDialog; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.view.View; |
| import android.widget.AdapterView; |
| import android.widget.ArrayAdapter; |
| import android.widget.EditText; |
| import android.widget.LinearLayout; |
| import android.widget.Spinner; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import org.joda.time.DateTime; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.ObjectOutputStream; |
| import java.security.interfaces.ECPublicKey; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| import io.v.android.v23.V; |
| import io.v.v23.context.VContext; |
| import io.v.v23.security.Blessings; |
| import io.v.v23.security.Caveat; |
| import io.v.v23.security.VPrincipal; |
| import io.v.v23.security.VSecurity; |
| import io.v.v23.verror.VException; |
| import io.v.v23.vom.VomUtil; |
| |
| /** |
| * BlessActivity represents the blessing action. This activity expects as an input a blessee's |
| * identity and blesses that identity using a combination of user-selected: |
| * 1) local blessings, |
| * 2) caveats, and |
| * 3) extension. |
| * The activity subsequently attempts to log this blessing event; it does not fail if logging is |
| * unsuccessful. |
| */ |
| public class BlessActivity extends Activity implements AdapterView.OnItemSelectedListener { |
| public static final String TAG = "BlessActivity"; |
| |
| // Names of intent extras that BlessActivity is expecting to receive when invoked. |
| public static final String BLESSEE_PUBLIC_KEY = "BLESSEE_PUBLIC_KEY"; |
| public static final String BLESSEE_NAMES = "BLESSEE_NAMES"; |
| public static final String BLESSEE_EXTENSION = "BLESSEE_EXTENSION"; |
| public static final String BLESSEE_EXTENSION_MUTABLE = "BLESSEE_EXTENSION_MUTABLE"; |
| public static final String BLESS_WITH = "BLESS_WITH"; |
| |
| /* The logging scheme for remote blessings is as follows. ************************************** |
| * The SharedPreferences file LOG_REMOTE_PRINCIPALS stores the public keys of blessed |
| * principals. |
| * The SharedPreferences file LOG_REMOTE_BLESSINGS stores serialized data for the blessings |
| * that the principals were blessed with. |
| * |
| * LOG_REMOTE_PRINCIPALS is structured as follows: |
| * The NUM_REMOTE_PRINCIPALS_KEY has an int value n that corresponds to the number of |
| * principals blessed hereto by the account manager app. |
| * The public keys of these principals are keyed by |
| * REMOTE_PRINCIPAL_KEY_0 ... REMOTE_PRINCIPAL_KEY_(n-1), |
| * where REMOTE_PRINCIPAL_KEY is the static string defined below. |
| * Furthermore, the blessing names that the holder presented initially to get blessed |
| * are stored at REMOTE_PRINCIPAL_NAMES_KEY_0 ... REMOTE_PRINCIPAL_NAMES_KEY_(n-1). |
| * |
| * LOG_REMOTE_BLESSINGS contains the actual blessing data indexed as follows: |
| * The number of blessings given to a principal with public key - pk - is listed in |
| * LOG_REMOTE_PRINCIPALS. Let us say m blessings were given out. |
| * Only MAX_BLESSINGS_FOR_PRINCIPAL blessing events are maintained, so the |
| * string serializations of stored blessing events are keyed by |
| * pk_(m - MAX_BLESSINGS_FOR_PRINCIPAL) ... pk_(m-1), if m is greater than |
| * MAX_BLESSINGS_FOR_PRINCIPAL, and pk_0 ... pk_(m-1) otherwise. |
| * |
| * Rationale for the scheme: |
| * A seemingly simpler implementation would have maintained a map of the form: |
| * pk --> Set of Blessings |
| * in the shared preferences file; however this would make adding to the log take |
| * linear time, as the stored set could not be added to directly. Therefore, in order |
| * to add to the set one would need to copy the whole set, add to the copy, and then |
| * re-insert the new set in the map. |
| * Our scheme clearly cuts this time complexity down. |
| **********************************************************************************************/ |
| public static final String LOG_PRINCIPALS = "LOG_PRINCIPALS"; |
| public static final String LOG_BLESSINGS = "LOG_BLESSINGS"; |
| public static final String NUM_PRINCIPALS_KEY = "numRemotePrincipals"; |
| public static final String PRINCIPAL_KEY = "principal"; |
| public static final String PRINCIPAL_NAMES_KEY = "principalName"; |
| public static final int MAX_BLESSINGS_FOR_PRINCIPAL = 50; |
| |
| private static final int BLESSING_CHOOSING_REQUEST = 1; |
| |
| VContext mBaseContext = null; |
| Blessings mWithBlessings = null; |
| ECPublicKey mBlesseePublicKey = null; |
| String[] mBlesseeNames = null; |
| String mExtension = ""; |
| boolean mExtensionMutable = true; |
| ProgressDialog mDialog = null; |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| mBaseContext = V.init(this); |
| |
| Intent intent = getIntent(); |
| if (intent == null) { |
| replyWithError("Intent not found."); |
| return; |
| } |
| Bundle extras = intent.getExtras(); |
| if (extras == null) { |
| replyWithError("No extras received."); |
| return; |
| } |
| mBlesseeNames = intent.getStringArrayExtra(BLESSEE_NAMES); |
| if (mBlesseeNames == null || mBlesseeNames.length <= 0) { |
| mBlesseeNames = new String[]{"Principal: Not Recognized"}; |
| } |
| mBlesseePublicKey = (ECPublicKey) extras.get(BLESSEE_PUBLIC_KEY); |
| if (mBlesseePublicKey == null) { |
| replyWithError("No public key received"); |
| return; |
| } |
| mExtension = intent.getStringExtra(BLESSEE_EXTENSION); |
| if (mExtension == null) { |
| mExtension = ""; |
| } |
| mExtensionMutable = intent.getBooleanExtra(BLESSEE_EXTENSION_MUTABLE, true); |
| byte[] blessWithVom = intent.getByteArrayExtra(BLESS_WITH); |
| if (blessWithVom != null && blessWithVom.length > 0) { |
| handleSelectedBlessings(blessWithVom); |
| } else { |
| // No blessings provided: let the user pick which one to use. |
| Intent i = new Intent(this, BlessingChooserActivity.class); |
| startActivityForResult(i, BLESSING_CHOOSING_REQUEST); |
| } |
| } |
| |
| private void addCaveatView() { |
| // Create new caveat view. |
| LinearLayout caveatView = |
| (LinearLayout) getLayoutInflater().inflate(R.layout.caveat, null); |
| // Set the list of supported caveats. |
| Spinner spinner = (Spinner) caveatView.findViewById(R.id.caveat_spinner); |
| ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, |
| R.array.caveats_array, android.R.layout.simple_spinner_item); |
| adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); |
| spinner.setAdapter(adapter); |
| spinner.setSelection(0, false); |
| spinner.setOnItemSelectedListener(this); |
| // Add caveat view. |
| ((LinearLayout) findViewById(R.id.caveats)).addView(caveatView); |
| } |
| |
| private void removeCaveatView(View caveatView) { |
| ((LinearLayout) findViewById(R.id.caveats)).removeView(caveatView); |
| } |
| |
| public void onAccept(View view) { |
| try { |
| List<Caveat> caveats = getCaveats(); |
| String extension = getExtension(); |
| if (extension.isEmpty()) { |
| Toast.makeText(this, "Must enter non-empty extension.", Toast.LENGTH_LONG).show(); |
| return; |
| } else { |
| try { |
| log(VomUtil.encode(mWithBlessings, Blessings.class), caveats, extension); |
| } catch (Exception e) { |
| String msg = "Couldn't log blessing event: " + e.getMessage(); |
| android.util.Log.e(TAG, msg); |
| Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); |
| } |
| bless(extension, caveats); |
| } |
| } catch (VException e) { |
| replyWithError("Could not get caveats."); |
| } |
| } |
| private String encodePubKeyToString(ECPublicKey publicKey) throws IOException{ |
| ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); |
| ObjectOutputStream objOut = new ObjectOutputStream(byteOut); |
| objOut.writeObject(publicKey); |
| objOut.close(); |
| byte[] byteArray = byteOut.toByteArray(); |
| String stringEncoding = VomUtil.bytesToHexString(byteArray); |
| return stringEncoding; |
| } |
| |
| public void onDeny(View view) { |
| replyWithError("User denied blessing request."); |
| } |
| |
| @Override |
| public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) { |
| LinearLayout caveatView = (LinearLayout) parent.getParent(); |
| String caveatType = (String) parent.getItemAtPosition(pos); |
| if (caveatType.equals("None")) { |
| removeCaveatView(caveatView); |
| return; |
| } |
| if (caveatType.equals("Expiry")) { |
| LinearLayout expiryView = (LinearLayout) |
| getLayoutInflater().inflate(R.layout.expiry_caveat, null); |
| Spinner spinner = (Spinner) expiryView.findViewById(R.id.expiry_units_spinner); |
| ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, |
| R.array.time_units_array, android.R.layout.simple_spinner_item); |
| adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); |
| spinner.setAdapter(adapter); |
| caveatView.addView(expiryView); |
| } |
| addCaveatView(); |
| } |
| |
| @Override |
| public void onNothingSelected(AdapterView<?> parent) { |
| // Do nothing. |
| } |
| |
| private String getExtension() { |
| TextView extensionView = (EditText) findViewById(R.id.nameEditText); |
| return extensionView.getText().toString(); |
| } |
| |
| private List<Caveat> getCaveats() throws VException { |
| LinearLayout caveatsView = (LinearLayout) findViewById(R.id.caveats); |
| ArrayList<Caveat> ret = new ArrayList<Caveat>(); |
| for (int i = 0; i < caveatsView.getChildCount(); ++i) { |
| LinearLayout caveatView = (LinearLayout) caveatsView.getChildAt(i); |
| AdapterView<?> caveatTypeView = |
| (AdapterView<?>) caveatView.findViewById(R.id.caveat_spinner); |
| if (caveatTypeView == null) { |
| android.util.Log.e(TAG, "Null caveat type spinner!"); |
| continue; |
| } |
| String caveatType = (String) caveatTypeView.getSelectedItem(); |
| if (caveatType == null) { |
| android.util.Log.e(TAG, "Null caveat type!"); |
| continue; |
| } |
| Caveat caveat = null; |
| if (caveatType.equals("None")) { |
| continue; |
| } |
| if (caveatType.equals("Expiry")) { |
| if ((caveat = getExpiryCaveat(caveatView)) == null) { |
| android.util.Log.e(TAG, "Ignoring expiry caveat."); |
| continue; |
| } |
| } |
| ret.add(caveat); |
| } |
| // No caveats selected: add an unconstrained caveat. |
| if (ret.isEmpty()) { |
| ret.add(VSecurity.newUnconstrainedUseCaveat()); |
| } |
| return ret; |
| } |
| |
| private Caveat getExpiryCaveat(LinearLayout caveatView) { |
| EditText numberUnitsView = |
| (EditText) caveatView.findViewById(R.id.expiry_units); |
| AdapterView<?> unitsView = |
| (AdapterView<?>) caveatView.findViewById(R.id.expiry_units_spinner); |
| if (numberUnitsView == null || unitsView == null) { |
| android.util.Log.e(TAG, "Couldn't find expiry caveat views"); |
| return null; |
| } |
| String numberUnitsStr = numberUnitsView.getText().toString(); |
| String unitStr = (String) unitsView.getSelectedItem(); |
| int numberUnits = 0; |
| try { |
| numberUnits = Integer.decode(numberUnitsStr); |
| } catch (NumberFormatException e) { |
| android.util.Log.e(TAG, String.format("Can't parse expiry caveat units number %s: %s", |
| numberUnitsStr, e.getMessage())); |
| return null; |
| } |
| DateTime expiry = null; |
| if (unitStr.equals("Seconds")) { |
| expiry = DateTime.now().plusSeconds(numberUnits); |
| } else if (unitStr.equals("Minutes")) { |
| expiry = DateTime.now().plusMinutes(numberUnits); |
| } else if (unitStr.equals("Hours")) { |
| expiry = DateTime.now().plusHours(numberUnits); |
| } else if (unitStr.equals("Days")) { |
| expiry = DateTime.now().plusDays(numberUnits); |
| } else if (unitStr.equals("Years")) { |
| expiry = DateTime.now().plusYears(numberUnits); |
| } else { |
| android.util.Log.e(TAG, "Unrecognized expiry caveat unit: " + unitStr); |
| return null; |
| } |
| try { |
| return VSecurity.newExpiryCaveat(expiry); |
| } catch (VException e) { |
| android.util.Log.e(TAG, "Couldn't create expiry caveat: " + e.getMessage()); |
| return null; |
| } |
| } |
| |
| @Override |
| protected void onActivityResult(int requestCode, int resultCode, Intent data) { |
| switch (requestCode) { |
| case BLESSING_CHOOSING_REQUEST: |
| if (resultCode != RESULT_OK) { |
| replyWithError("Error choosing blessings: " + |
| data.getStringExtra(Constants.ERROR)); |
| return; |
| } |
| handleSelectedBlessings(data.getByteArrayExtra(Constants.REPLY)); |
| break; |
| default: |
| super.onActivityResult(requestCode, resultCode, data); |
| } |
| } |
| |
| private void handleSelectedBlessings(byte[] blessingsVom) { |
| if (blessingsVom == null || blessingsVom.length <= 0) { |
| replyWithError("No blessings selected."); |
| return; |
| } |
| try { |
| mWithBlessings = (Blessings) VomUtil.decode(blessingsVom, Blessings.class); |
| } catch (VException e) { |
| replyWithError("Couldn't deserialize blessings."); |
| return; |
| } |
| if (mWithBlessings == null || mWithBlessings.isEmpty()) { |
| replyWithError("Empty blessings provided."); |
| return; |
| } |
| display(); |
| } |
| |
| private void display() { |
| setContentView(R.layout.activity_bless); |
| |
| TextView blesseeNames = (TextView) findViewById(R.id.text_application); |
| String names = "Blessee Names:"; |
| for (String name : mBlesseeNames) { |
| names += "\n" + name; |
| } |
| blesseeNames.setText(names); |
| |
| String[] blessingNames = mWithBlessings.toString().split(","); |
| String blessingNamesToShow = ""; |
| for (String name: blessingNames) { |
| blessingNamesToShow += "\n" + name; |
| } |
| |
| TextView accountView = |
| (TextView) getLayoutInflater().inflate(R.layout.blessing_account, null); |
| accountView.setText(blessingNamesToShow); |
| ((LinearLayout) findViewById(R.id.blessing_accounts)).addView(accountView); |
| |
| try { |
| addCaveatView(); |
| } catch (Exception e) { |
| replyWithError("Failed to get caveats."); |
| } |
| |
| EditText extensionText = (EditText) findViewById(R.id.nameEditText); |
| extensionText.setText(mExtension); |
| extensionText.setEnabled(mExtensionMutable); |
| } |
| |
| private void bless(String extension, List<Caveat> caveats) { |
| mDialog = new ProgressDialog(this); |
| mDialog.setMessage("Creating blessings..."); |
| mDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); |
| mDialog.setIndeterminate(true); |
| mDialog.show(); |
| new BlesserAsyncTask(extension, caveats).execute(); |
| } |
| |
| /** |
| * Extends a set of Vanadium {@link Blessings} for the invoking activity. |
| */ |
| private class BlesserAsyncTask extends AsyncTask<Void, Void, byte[]> { |
| String mExtension = ""; |
| List<Caveat> mCaveats = null; |
| |
| BlesserAsyncTask(String extension, List<Caveat> caveats) { |
| mExtension = extension; |
| mCaveats = caveats; |
| } |
| |
| @Override |
| protected byte[] doInBackground(Void... arg) { |
| try { |
| VPrincipal principal = V.getPrincipal(mBaseContext); |
| Blessings blessings = |
| principal.bless(mBlesseePublicKey, mWithBlessings, mExtension, |
| mCaveats.get(0), |
| mCaveats.subList(1, mCaveats.size()).toArray(new Caveat[0])); |
| if (blessings == null) { |
| replyWithError("Got null blessing."); |
| return null; |
| } |
| if (blessings.getCertificateChains().size() <= 0) { |
| replyWithError("Got empty certificate chains."); |
| return null; |
| } |
| return VomUtil.encode(blessings, Blessings.class); |
| } catch (VException e) { |
| replyWithError("Couldn't bless: " + e.getMessage()); |
| return null; |
| } |
| } |
| |
| @Override |
| protected void onPostExecute(byte[] blessingsVom) { |
| super.onPostExecute(blessingsVom); |
| if (blessingsVom == null || blessingsVom.length == 0) { |
| replyWithError("Couldn't bless : got empty derived blessings"); |
| return; |
| } |
| |
| if (mDialog != null) { |
| mDialog.dismiss(); |
| } |
| replyWithSuccess(blessingsVom); |
| } |
| } |
| |
| private void log(byte[] blessingsVom, List<Caveat> caveats, String extension) throws Exception { |
| BlessingEvent newBlessingEvent = |
| new BlessingEvent(mBlesseeNames, blessingsVom, DateTime.now(), caveats, extension, |
| mBlesseePublicKey); |
| String pubKey = encodePubKeyToString(mBlesseePublicKey); |
| |
| SharedPreferences blessingsLog = getSharedPreferences(LOG_BLESSINGS, MODE_PRIVATE); |
| SharedPreferences.Editor blessingsLogEditor = blessingsLog.edit(); |
| SharedPreferences principalsLog = getSharedPreferences(LOG_PRINCIPALS, MODE_PRIVATE); |
| SharedPreferences.Editor principalsLogEditor = principalsLog.edit(); |
| |
| int numEvents = blessingsLog.getInt(pubKey, 0); |
| |
| // Save the newBlessingEvent in the LOG_REMOTE_BLESSINGS file. |
| String strNewEvent = newBlessingEvent.encodeToString(); |
| blessingsLogEditor.putString(pubKey + "_" + numEvents, strNewEvent); |
| blessingsLogEditor.putInt(pubKey, numEvents + 1); |
| if (!blessingsLogEditor.commit()) { |
| throw new Exception("Failed to commit log changes"); |
| } |
| |
| // Delete stale blessing entry, if there are too many entries for the principal. |
| String keyToDelete = pubKey + "_" + (numEvents - MAX_BLESSINGS_FOR_PRINCIPAL); |
| if (numEvents > MAX_BLESSINGS_FOR_PRINCIPAL && blessingsLog.contains(keyToDelete)) { |
| blessingsLogEditor.remove(keyToDelete); |
| blessingsLogEditor.apply(); |
| } |
| |
| // Record that the principal with the public key pubKey and names mBlesseeNames has been |
| // blessed in the LOG_REMOTE_PRINCIPALS file if this has not already been done. |
| if (numEvents <= 0) { |
| int numPrincipals = principalsLog.getInt(NUM_PRINCIPALS_KEY, 0); |
| String principalsKey = PRINCIPAL_KEY + "_" + numPrincipals; |
| String principalNamesKey = PRINCIPAL_NAMES_KEY + "_" + numPrincipals; |
| principalsLogEditor.putString(principalsKey, pubKey); |
| principalsLogEditor.putInt(NUM_PRINCIPALS_KEY, numPrincipals + 1); |
| |
| Set<String> blesseeNameSet = new HashSet<>(); |
| for (String name: mBlesseeNames) { |
| blesseeNameSet.add(name); |
| } |
| |
| principalsLogEditor.putStringSet(principalNamesKey, blesseeNameSet); |
| principalsLogEditor.apply(); |
| } |
| } |
| |
| private void replyWithError(String error) { |
| android.util.Log.e(TAG, "Error while blessing: " + error); |
| Intent intent = new Intent(); |
| intent.putExtra(Constants.ERROR, error); |
| setResult(RESULT_CANCELED, intent); |
| finish(); |
| } |
| |
| private void replyWithSuccess(byte[] blessingsVom) { |
| Intent intent = new Intent(); |
| intent.putExtra(Constants.REPLY, blessingsVom); |
| setResult(RESULT_OK, intent); |
| finish(); |
| } |
| } |