blob: fb92a8e04cb34849e1363a809333127043ccece6 [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.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();
}
}