/*
* Copyright 2012 The Stanford MobiSocial Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mobisocial.musubi.service;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import mobisocial.crypto.IBEncryptionScheme;
import mobisocial.crypto.IBHashedIdentity;
import mobisocial.crypto.IBHashedIdentity.Authority;
import mobisocial.crypto.IBIdentity;
import mobisocial.crypto.IBSignatureScheme;
import mobisocial.musubi.encoding.NeedsKey;
import mobisocial.musubi.identity.IdentityProvider;
import mobisocial.musubi.identity.IdentityProviderException;
import mobisocial.musubi.model.MEncryptionUserKey;
import mobisocial.musubi.model.MIdentity;
import mobisocial.musubi.model.MPendingIdentity;
import mobisocial.musubi.model.MSignatureUserKey;
import mobisocial.musubi.model.helpers.IdentitiesManager;
import mobisocial.musubi.model.helpers.PendingIdentityManager;
import mobisocial.musubi.model.helpers.UserKeyManager;
import android.content.Context;
import android.database.ContentObserver;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
public class KeyUpdateHandler extends ContentObserver {
public static final String TAG = "KeyUpdateHandler";
private final Context mContext;
private final SQLiteOpenHelper mHelper;
private final IdentitiesManager mIdentitiesManager;
private final IdentityProvider mIdentityProvider;
private final UserKeyManager mUserKeyManager;
private final PendingIdentityManager mPendingIdentityManager;
private final HashSet<IBHashedIdentity> mRequestedClaimKeys;
private final HashSet<IBHashedIdentity> mRequestedEncryptionKeys;
private final HashSet<IBHashedIdentity> mRequestedSignatureKeys;
HandlerThread mThread;
/*
* Support exponential backoff for retries. Only retry if there is a failure
* not related to authentication.
*
* Backoff ranges from 10 seconds to 30 minutes, and doubles on each successive
* failure until the maximum wait is reached.
*/
private static final long MINIMUM_BACKOFF = 10 * 1000;
private static final long MAXIMUM_BACKOFF = 30 * 60 * 1000;
private final HashMap<IBHashedIdentity, Long> mSigBackoff;
private final HashMap<IBHashedIdentity, Long> mEncBackoff;
private final HashMap<IBHashedIdentity, Long> mClaimBackoff;
public static KeyUpdateHandler newInstance(Context context, SQLiteOpenHelper dbh, IdentityProvider identityProvider) {
HandlerThread thread = new HandlerThread("KeyUpdateThread");
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
return new KeyUpdateHandler(context, dbh, thread, identityProvider);
}
private KeyUpdateHandler(Context context, SQLiteOpenHelper dbh, HandlerThread thread, IdentityProvider identityProvider) {
super(new Handler(thread.getLooper()));
mThread = thread;
mContext = context;
mHelper = dbh;
mIdentitiesManager = new IdentitiesManager(mHelper);
mIdentityProvider = identityProvider;
mUserKeyManager = new UserKeyManager(mIdentityProvider.getEncryptionScheme(),
mIdentityProvider.getSignatureScheme(), mHelper);
mPendingIdentityManager = new PendingIdentityManager(mHelper);
mRequestedClaimKeys = new HashSet<IBHashedIdentity>();
mRequestedEncryptionKeys = new HashSet<IBHashedIdentity>();
mRequestedSignatureKeys = new HashSet<IBHashedIdentity>();
mSigBackoff = new HashMap<IBHashedIdentity, Long>();
mEncBackoff = new HashMap<IBHashedIdentity, Long>();
mClaimBackoff = new HashMap<IBHashedIdentity, Long>();
context.getContentResolver().registerContentObserver(MusubiService.NETWORK_CHANGED, false,
new ResetBackOffAndReconnectIfNotConnected(new Handler(thread.getLooper())));
}
public class ResetBackOffAndReconnectIfNotConnected extends ContentObserver {
Handler mHandler;
public ResetBackOffAndReconnectIfNotConnected(Handler handler) {
super(handler);
mHandler = handler;
}
@Override
public void onChange(boolean selfChange) {
synchronized(mSigBackoff) {
mSigBackoff.clear();
}
synchronized (mEncBackoff) {
mEncBackoff.clear();
}
synchronized (mClaimBackoff) {
mClaimBackoff.clear();
}
KeyUpdateHandler.this.dispatchChange(false);
}
}
@Override
public void onChange(boolean selfChange) {
Log.i(TAG, "KeyUpdateHandler onChange reached");
// Determine which identities the user currently owns
List<MIdentity> ids = mIdentitiesManager.getOwnedIdentities();
Set<IBIdentity> idsToUpdate = new HashSet<IBIdentity>();
// Convert MIdentity objects to IBIdentity objects
// TODO: Might want to use MMyAccount here
for (MIdentity id : ids) {
IBIdentity ident = IdentitiesManager.toIBIdentity(id,
IdentitiesManager.computeTemporalFrameFromPrincipal(id.principal_));
if (ident.authority_ == Authority.PhoneNumber) {
ident = new IBIdentity(ident.authority_, ident.principal_, 0L);
}
idsToUpdate.add(ident);
Log.i(TAG, "Identity principal: " + id.principal_);
}
// Also get identities to notify with the 2-stage process
List<MPendingIdentity> pids = mPendingIdentityManager.getUnnotifiedIdentities();
for (MPendingIdentity pid : pids) {
MIdentity mid = mIdentitiesManager.getIdentityForId(pid.identityId_);
IBIdentity ident = IdentitiesManager.toIBIdentity(mid, 0L);
idsToUpdate.add(ident);
Log.i(TAG, "Identity to notify: " + mid.principal_);
}
// Contact the IBE server for new keys and save if valid
for (IBIdentity ident : idsToUpdate) {
MIdentity id = mIdentitiesManager.getIdentityForIBHashedIdentity(ident);
//local identities can't be refreshed.
if(id.type_ == Authority.Local)
continue;
assert(id != null);
boolean hasBoth = true;
try {
mUserKeyManager.getEncryptionKey(id, ident);
} catch (NeedsKey.Encryption e) {
requestEncryptionKey(ident);
hasBoth = false;
}
try {
mUserKeyManager.getSignatureKey(id, ident);
} catch (NeedsKey.Signature e) {
requestSignatureKey(ident);
hasBoth = false;
}
// Remove this pending identity if there is no work required
if (hasBoth) {
mPendingIdentityManager.deleteIdentity(id.id_, ident.temporalFrame_);
}
}
}
public void initiateIdClaim(IBHashedIdentity hid) {
if(hid.authority_ == Authority.Local) {
Log.e(TAG, "requesting key for local identity");
return;
}
synchronized (mRequestedClaimKeys) {
mRequestedClaimKeys.add(hid);
}
new Handler(mThread.getLooper()).post(new Runnable() {
@Override
public void run() {
for(;;) {
IBHashedIdentity hid;
synchronized (mRequestedClaimKeys) {
Iterator<IBHashedIdentity> it = mRequestedClaimKeys.iterator();
if(!it.hasNext())
break;
hid = it.next();
}
// Only attempt to send a request if the true principal is known
IBIdentity ident = mIdentitiesManager.getIBIdentityForIBHashedIdentity(hid);
if (ident != null) {
MIdentity mid = mIdentitiesManager.getIdentityForIBHashedIdentity(hid);
MPendingIdentity pid = mPendingIdentityManager.lookupIdentity(
mid.id_, hid.temporalFrame_);
if (pid == null) {
pid = mPendingIdentityManager.fillPendingIdentity(
mid.id_, hid.temporalFrame_);
mPendingIdentityManager.insertIdentity(pid);
}
if (!pid.notified_) {
boolean success = mIdentityProvider.initiateTwoPhaseClaim(
ident, pid.key_, pid.requestId_);
// If something went wrong, try again
if (!success) {
long backoff = updateBackoffForIdentity(hid, mClaimBackoff);
Log.i(TAG, "Claim request failed for " + mid.principal_ +
", retrying in " + backoff + " msec");
requestClaimAfterDelay(hid, backoff);
}
else {
synchronized (mClaimBackoff) {
mClaimBackoff.remove(hid);
}
}
}
else {
Log.w(TAG, "Tried to notify an already-notified identity");
}
}
synchronized (mRequestedClaimKeys) {
mRequestedClaimKeys.remove(hid);
}
}
}
});
}
public void requestSignatureKey(IBHashedIdentity hid) {
if(hid.authority_ == Authority.Local) {
Log.e(TAG, "requesting key for local identity");
return;
}
synchronized (mRequestedSignatureKeys) {
mRequestedSignatureKeys.add(hid);
}
new Handler(mThread.getLooper()).post(new Runnable() {
@Override
public void run() {
boolean addedNewKeys = false;
for(;;) {
IBHashedIdentity hid;
synchronized (mRequestedSignatureKeys) {
Iterator<IBHashedIdentity> it = mRequestedSignatureKeys.iterator();
if(!it.hasNext())
break;
hid = it.next();
}
// Retrieve this specific signature key if possible
MIdentity id = mIdentitiesManager.getIdentityForIBHashedIdentity(hid);
IBIdentity ibid = new IBIdentity(hid.authority_, id.principal_, hid.temporalFrame_);
try {
IBSignatureScheme.UserKey sKey = mIdentityProvider.syncGetSignatureKey(ibid);
assert(sKey != null);
Log.d(TAG, "Adding signature user key for " + id.principal_);
MSignatureUserKey key = new MSignatureUserKey();
key.identityId_ = id.id_;
key.when_ = hid.temporalFrame_;
key.userKey_ = sKey.key_;
mUserKeyManager.insertSignatureUserKey(key);
synchronized (mSigBackoff) {
mSigBackoff.remove(hid);
}
addedNewKeys = true;
} catch (IdentityProviderException.NeedsRetry e) {
// Should try to get the key again later
long backoff = updateBackoffForIdentity(hid, mSigBackoff);
Log.i(TAG, "Signature key fetch failed for " + id.principal_ +
", retrying in " + backoff + " msec");
requestSignatureKeyAfterDelay(hid, backoff);
} catch (IdentityProviderException.TwoPhase e) {
Log.i(TAG, "Two-phase verification needed.");
initiateIdClaim(hid);
} catch (IdentityProviderException e) {
Log.i(TAG, "Server unable to obtain key for " + id.principal_);
}
synchronized (mRequestedSignatureKeys) {
mRequestedSignatureKeys.remove(hid);
}
}
if (addedNewKeys) {
mContext.getContentResolver().notifyChange(MusubiService.PLAIN_OBJ_READY, null);
}
}
});
}
public void requestEncryptionKey(IBHashedIdentity hid) {
if(hid.authority_ == Authority.Local) {
Log.e(TAG, "requesting key for local identity");
return;
}
synchronized (mRequestedEncryptionKeys) {
mRequestedEncryptionKeys.add(hid);
}
new Handler(mThread.getLooper()).post(new Runnable() {
@Override
public void run() {
boolean addedNewKeys = false;
for(;;) {
IBHashedIdentity hid;
synchronized (mRequestedEncryptionKeys) {
Iterator<IBHashedIdentity> it = mRequestedEncryptionKeys.iterator();
if(!it.hasNext())
break;
hid = it.next();
}
// Retrieve this specific encryption key if possible
MIdentity id = mIdentitiesManager.getIdentityForIBHashedIdentity(hid);
IBIdentity ibid = new IBIdentity(hid.authority_, id.principal_, hid.temporalFrame_);
try {
IBEncryptionScheme.UserKey eKey = mIdentityProvider.syncGetEncryptionKey(ibid);
assert(eKey != null);
Log.d(TAG, "Adding encryption user key for " + id.principal_);
MEncryptionUserKey key = new MEncryptionUserKey();
key.identityId_ = id.id_;
key.when_ = hid.temporalFrame_;
key.userKey_ = eKey.key_;
mUserKeyManager.insertEncryptionUserKey(key);
synchronized (mEncBackoff) {
mEncBackoff.remove(hid);
}
addedNewKeys = true;
} catch (IdentityProviderException.NeedsRetry e) {
// Should try to get the key again later
long backoff = updateBackoffForIdentity(hid, mEncBackoff);
Log.i(TAG, "Encryption key fetch failed for " + id.principal_ +
", retrying in " + backoff + " msec");
requestEncryptionKeyAfterDelay(hid, backoff);
} catch (IdentityProviderException.TwoPhase e) {
Log.i(TAG, "Two-phase verification needed.");
initiateIdClaim(hid);
} catch (IdentityProviderException e) {
Log.i(TAG, "Server unable to obtain key for " + id.principal_);
}
synchronized (mRequestedEncryptionKeys) {
mRequestedEncryptionKeys.remove(hid);
}
}
if (addedNewKeys) {
Log.d(TAG, "Notifying new key");
mContext.getContentResolver().notifyChange(MusubiService.ENCODED_RECEIVED, null);
}
}
});
}
private long updateBackoffForIdentity(IBHashedIdentity hid,
HashMap<IBHashedIdentity, Long> backoffMap) {
long backoff = MINIMUM_BACKOFF;
synchronized (backoffMap) {
// Use a bounded exponential backoff
if (backoffMap.containsKey(hid)) {
backoff = backoffMap.get(hid).longValue() * 2;
backoff = (backoff > MAXIMUM_BACKOFF) ? MAXIMUM_BACKOFF : backoff;
}
backoffMap.put(hid, backoff);
}
return backoff;
}
private void requestSignatureKeyAfterDelay(IBHashedIdentity hid, long delayMillis) {
final IBHashedIdentity ident = hid;
new Handler(mThread.getLooper()).postDelayed(new Runnable() {
@Override
public void run() {
requestSignatureKey(ident);
}
}, delayMillis);
}
private void requestEncryptionKeyAfterDelay(IBHashedIdentity hid, long delayMillis) {
final IBHashedIdentity ident = hid;
new Handler(mThread.getLooper()).postDelayed(new Runnable() {
@Override
public void run() {
requestEncryptionKey(ident);
}
}, delayMillis);
}
private void requestClaimAfterDelay(IBHashedIdentity hid, long delayMillis) {
final IBHashedIdentity ident = hid;
new Handler(mThread.getLooper()).postDelayed(new Runnable() {
@Override
public void run() {
initiateIdClaim(ident);
}
}, delayMillis);
}
}