/*
* 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.identity;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import mobisocial.crypto.IBEncryptionScheme;
import mobisocial.crypto.IBHashedIdentity;
import mobisocial.crypto.IBHashedIdentity.Authority;
import mobisocial.crypto.IBIdentity;
import mobisocial.crypto.IBSignatureScheme;
import mobisocial.crypto.IBSignatureScheme.UserKey;
import mobisocial.musubi.App;
import mobisocial.musubi.model.MIdentity;
import mobisocial.musubi.model.MMyAccount;
import mobisocial.musubi.model.MPendingIdentity;
import mobisocial.musubi.model.PresenceAwareNotify;
import mobisocial.musubi.model.helpers.IdentitiesManager;
import mobisocial.musubi.model.helpers.MyAccountManager;
import mobisocial.musubi.model.helpers.PendingIdentityManager;
import mobisocial.musubi.ui.SettingsActivity;
import mobisocial.musubi.ui.fragments.AccountLinkDialog;
import mobisocial.musubi.util.CertifiedHttpClient;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Base64;
import android.util.Log;
import android.util.Pair;
public class AphidIdentityProvider implements IdentityProvider {
public static final String TAG = "AphidIdentityProvider";
private static final String ENCRYPTION_PUBLIC_PARAMETERS =
"BUV+jbo5aCVPJgdETzxaemL2WAQVDWRdYw9qlt8jl6LlMfdnGFkh1gEjjVnw4jEhVafxg+D4xIBO" +
"xHVe4SClyInvNa/EO9KkGnpkZI9MyKnwAB1YmKr/XSx34QC4TUncF7aGT95pAYsNZnkf0cZC7IX8" +
"8oZQGh+FUokAIMlbuAZfm4m+qqnMFOiYCTz/P4MBHHUcD9eZz9ZYWnzGf8TQaGZAu7UBIXxTZ453" +
"+7DzmLbhsi97c5s1XAIABM1AlQLFnpW0F0ErCeBh46BozB8Yojr/CxLq+Fda6QKtqMRMXIlCWxaF" +
"W1ItocmQ3ca/dZ5u2hVM5QQ3C0eXf/jOyln2sn3BIwwe3vpTlrAHcBZZ7/S5G2rsXByRIeMHr2gE" +
"GgJS9PV0q5zclv7jmSuXKwajI582G91K0pAe6YHwwhF1a3K5iYFFBFcQFXmYlFmj2TAmmohiMiaV" +
"bWk2WBPJStN6+ml1AjcqT0DtEOTdcJtkC4zv1hR86WBoCNIIhmWF3tOLswoPRHLqp7Qfijex75TJ" +
"MTxGGHK8fh6B0duHS7dNvhAUMB4VDVfJt0tq";
private static final String SIGNATURE_PUBLIC_PARAMETERS =
"I2rwl+saWhxnJmficrgH1ZK79/gFnozVJmJAUdCj/9dvdBGhAi+d9QEggAW8I7GisfcXg26nHJkm" +
"1YEDQxCJ8kQ6ptq1t//Yypsy4FaE2GWlAA==";
private static final String URL_SCHEME = "https";
private static final String SERVER_LOCATION = "aphid.musubi.us";
private static final String KEYS_PATH = "/ibe/keys.py";
private static final String CLAIM_PATH = "/ibe/claim.py";
private static final String SERVER_NAME_SIG = "sig_key";
private static final String SERVER_NAME_CRYPTO = "crypto_key";
private static final String SIGNIN_TEXT = "Sign-In Required";
private final Context mContext;
private IBEncryptionScheme mEncryptionScheme;
private IBSignatureScheme mSignatureScheme;
private IdentitiesManager mIdentitiesManager;
private PendingIdentityManager mPendingIdentityManager;
private Map<Pair<Authority, String>, String> mKnownTokens;
public AphidIdentityProvider(Context context) {
mContext = context;
mEncryptionScheme = new IBEncryptionScheme(
Base64.decode(ENCRYPTION_PUBLIC_PARAMETERS, Base64.DEFAULT)
);
mSignatureScheme = new IBSignatureScheme(
Base64.decode(SIGNATURE_PUBLIC_PARAMETERS, Base64.DEFAULT)
);
mIdentitiesManager = new IdentitiesManager(App.getDatabaseSource(mContext));
mPendingIdentityManager = new PendingIdentityManager(App.getDatabaseSource(mContext));
mKnownTokens = new HashMap<Pair<Authority, String>, String>();
}
public IBEncryptionScheme getEncryptionScheme() {
return mEncryptionScheme;
}
///Create a new instance of /aphididentityprovider and call this to get the encryption
//scheme so you can sign a challenge.
public IBSignatureScheme getSignatureScheme() {
return mSignatureScheme;
}
public UserKey syncGetSignatureKey(IBIdentity ident)
throws IdentityProviderException {
byte[] rawUserKey = getAphidResultForIdentity(ident, SERVER_NAME_SIG);
assert(rawUserKey != null);
return new UserKey(rawUserKey);
}
public mobisocial.crypto.IBEncryptionScheme.UserKey syncGetEncryptionKey(IBIdentity ident)
throws IdentityProviderException {
byte[] rawUserKey = getAphidResultForIdentity(ident, SERVER_NAME_CRYPTO);
assert(rawUserKey != null);
return new mobisocial.crypto.IBEncryptionScheme.UserKey(rawUserKey);
}
public UserKey syncGetSignatureKey(IBHashedIdentity hid)
throws IdentityProviderException {
IBIdentity ident = mIdentitiesManager.getIBIdentityForIBHashedIdentity(hid);
if(ident == null) {
throw new RuntimeException("you must know the real principal to request an aphid signature secret");
}
byte[] rawUserKey = getAphidResultForIdentity(ident, SERVER_NAME_SIG);
assert(rawUserKey != null);
return new UserKey(rawUserKey);
}
public mobisocial.crypto.IBEncryptionScheme.UserKey syncGetEncryptionKey(IBHashedIdentity hid)
throws IdentityProviderException {
IBIdentity ident = mIdentitiesManager.getIBIdentityForIBHashedIdentity(hid);
if(ident == null) {
throw new RuntimeException("you must know the real principal to request an aphid encryption secret");
}
byte[] rawUserKey = getAphidResultForIdentity(ident, SERVER_NAME_CRYPTO);
assert(rawUserKey != null);
return new mobisocial.crypto.IBEncryptionScheme.UserKey(rawUserKey);
}
/*
* Certain identities (e.g. phone numbers) require the server to solicit user response
*/
public boolean initiateTwoPhaseClaim(IBIdentity ident, String key, int requestId) {
// Send the request to Aphid
HttpClient http = new CertifiedHttpClient(mContext);
List<NameValuePair> qparams = new ArrayList<NameValuePair>();
qparams.add(new BasicNameValuePair("req", new Integer(requestId).toString()));
qparams.add(new BasicNameValuePair("type", new Integer(ident.authority_.ordinal()).toString()));
qparams.add(new BasicNameValuePair("uid", ident.principal_));
qparams.add(new BasicNameValuePair("time", new Long(ident.temporalFrame_).toString()));
qparams.add(new BasicNameValuePair("key", key));
try {
// Send the request
URI uri = URIUtils.createURI(URL_SCHEME, SERVER_LOCATION, -1, CLAIM_PATH,
URLEncodedUtils.format(qparams, "UTF-8"), null);
Log.d(TAG, "Aphid URI: " + uri.toString());
HttpGet httpGet = new HttpGet(uri);
HttpResponse response = http.execute(httpGet);
int code = response.getStatusLine().getStatusCode();
// Read the response
BufferedReader rd = new BufferedReader(new InputStreamReader(
response.getEntity().getContent()));
String responseStr = "";
String line = "";
while ((line = rd.readLine()) != null) {
responseStr += line;
}
Log.d(TAG, "Server response:" + responseStr);
// Only 200 should indicate that this worked
if (code == HttpURLConnection.HTTP_OK) {
// Mark as notified (suppress repeated texts)
MIdentity mid = mIdentitiesManager.getIdentityForIBHashedIdentity(ident);
if (mid != null) {
MPendingIdentity pendingIdent = mPendingIdentityManager.lookupIdentity(
mid.id_, ident.temporalFrame_, requestId);
if (pendingIdent == null) {
pendingIdent = mPendingIdentityManager.fillPendingIdentity(
mid.id_, ident.temporalFrame_);
mPendingIdentityManager.insertIdentity(pendingIdent);
}
pendingIdent.notified_ = true;
mPendingIdentityManager.updateIdentity(pendingIdent);
}
return true;
}
} catch (URISyntaxException e) {
Log.e(TAG, "URISyntaxException", e);
} catch (IOException e) {
Log.i(TAG, "Error claiming keys.");
}
return false;
}
public void setTokenForUser(Authority authority, String principal, String token) {
mKnownTokens.put(
new Pair<Authority, String>(authority, principal),
token
);
}
/*
* Send notifications when accounts cannot connect.
*/
private void sendNotification(String account) {
Intent launch = new Intent(mContext, SettingsActivity.class);
launch.putExtra(SettingsActivity.ACTION,
SettingsActivity.SettingsAction.ACCOUNT.toString());
PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0,
launch, PendingIntent.FLAG_CANCEL_CURRENT);
(new PresenceAwareNotify(mContext)).notify(SIGNIN_TEXT,
account + " account failed to connect", contentIntent);
}
/*
* Cache known Google tokens
*/
private void cacheGoogleTokens() throws IdentityProviderException {
SQLiteOpenHelper db = App.getDatabaseSource(mContext);
MyAccountManager am = new MyAccountManager(db);
MMyAccount[] accounts = am.getClaimedAccounts(AccountLinkDialog.ACCOUNT_TYPE_GOOGLE);
for (MMyAccount account : accounts) {
String gToken = null;
String googleAccount = account.accountName_;
if (googleAccount != null) {
try {
gToken = AccountLinkDialog.silentBlockForGoogleToken(mContext, googleAccount);
} catch (IOException e) {
// Connection errors should be treated differently from auth errors
throw new IdentityProviderException.NeedsRetry(
new IBIdentity(Authority.Email, googleAccount, 0));
}
Log.d(TAG, "Google account:" + googleAccount);
}
if (gToken != null) {
setTokenForUser(Authority.Email, googleAccount, gToken);
Log.d(TAG, "Google token:" + gToken);
}
else if (googleAccount != null && gToken == null) {
// Authentication failures should be reported
sendNotification("Google");
throw new IdentityProviderException.Auth(
new IBIdentity(Authority.Email, googleAccount, 0));
}
}
}
/*
* Cache the current Facebook token
*/
private void cacheCurrentFacebookToken() throws IdentityProviderException.Auth {
String fAccount = getFacebookAccount();
String fToken = null;
if (fAccount != null) {
fToken = AccountLinkDialog.getActiveFacebookToken(mContext);
Log.d(TAG, "Facebook account:" + fAccount);
}
if (fToken != null) {
setTokenForUser(Authority.Facebook, fAccount, fToken);
Log.d(TAG, "Facebook token:" + fToken);
}
else if (fAccount != null && fToken == null) {
// Authentication failures should be reported
sendNotification("Facebook");
throw new IdentityProviderException.Auth(
new IBIdentity(Authority.Facebook, fAccount, 0));
}
}
String getFacebookAccount() {
MyAccountManager am = new MyAccountManager(App.getDatabaseSource(mContext));
MMyAccount[] acc = am.getClaimedAccounts(AccountLinkDialog.ACCOUNT_TYPE_FACEBOOK);
if (acc.length > 0) {
MIdentity identity = mIdentitiesManager.getIdentityForId(acc[0].identityId_);
return identity.principal_;
}
return null;
}
private byte[] getAphidResultForIdentity(IBIdentity ident, String property)
throws IdentityProviderException {
Log.d(TAG, "Getting key for " + ident.principal_);
// Populate tokens from identity providers (only Google and Facebook for now)
try {
cacheGoogleTokens();
} catch (IdentityProviderException.Auth e) {
// No need to continue if this is our identity and token fetch failed
if (e.identity.equalsStable(ident)) {
throw new IdentityProviderException.Auth(ident);
}
} catch (IdentityProviderException.NeedsRetry e) {
if (e.identity.equalsStable(ident)) {
throw new IdentityProviderException.NeedsRetry(ident);
}
}
try {
cacheCurrentFacebookToken();
} catch (IdentityProviderException e) {
// No need to continue if this is our identity and token fetch failed
if (e.identity.equalsStable(ident)) {
throw new IdentityProviderException.Auth(ident);
}
}
String aphidType = null;
String aphidToken = null;
// Get a service-specific token if it exists
Pair<Authority, String> userProperties =
new Pair<Authority, String>(ident.authority_, ident.principal_);
if (mKnownTokens.containsKey(userProperties)) {
aphidToken = mKnownTokens.get(userProperties);
}
// The IBE server has its own identifiers for providers
switch (ident.authority_) {
case Facebook:
aphidType = "facebook";
break;
case Email:
if (mKnownTokens.containsKey(userProperties)) {
aphidType = "google";
}
break;
case PhoneNumber:
// Aphid doesn't return keys for a phone number without verification
throw new IdentityProviderException.TwoPhase(ident);
}
// Do not ask the server for identities we don't know how to handle
if (aphidType == null || aphidToken == null) {
throw new IdentityProviderException(ident);
}
// Bundle arguments as JSON
JSONObject jsonObj = new JSONObject();
try {
jsonObj.put("type", aphidType);
jsonObj.put("token", aphidToken);
jsonObj.put("starttime", ident.temporalFrame_);
} catch (JSONException e) {
Log.e(TAG, e.toString());
}
JSONArray userinfo = new JSONArray();
userinfo.put(jsonObj);
// Contact the server
try {
JSONObject resultObj = getAphidResult(userinfo);
if (resultObj == null) {
throw new IdentityProviderException.NeedsRetry(ident);
}
String encodedKey = resultObj.getString(property);
boolean hasError = resultObj.has("error");
if (!hasError) {
long temporalFrame = resultObj.getLong("time");
if (encodedKey != null && temporalFrame == ident.temporalFrame_) {
// Success!
return Base64.decode(encodedKey, Base64.DEFAULT);
}
else {
// Might have jumped the gun a little bit, so try again later
throw new IdentityProviderException.NeedsRetry(ident);
}
}
else {
// Aphid authentication error means Musubi has a bad token
String error = resultObj.getString("error");
if (error.contains("401")) {
// Authentication errors require user action
String accountType = Character.toString(
Character.toUpperCase(aphidType.charAt(0))
) + aphidType.substring(1);
sendNotification(accountType);
throw new IdentityProviderException.Auth(ident);
}
else {
// Other failures should be retried silently
throw new IdentityProviderException.NeedsRetry(ident);
}
}
} catch (IOException e) {
Log.e(TAG, e.toString());
} catch (JSONException e) {
Log.e(TAG, e.toString());
}
throw new IdentityProviderException.NeedsRetry(ident);
}
private JSONObject getAphidResult(JSONArray userinfo) throws IOException {
// Set up HTTP request
HttpClient http = new CertifiedHttpClient(mContext);
URI uri;
try {
uri = URIUtils.createURI(URL_SCHEME, SERVER_LOCATION, -1, KEYS_PATH, null, null);
} catch (URISyntaxException e) {
throw new IOException("Malformed URL", e);
}
HttpPost post = new HttpPost(uri);
List<NameValuePair> postData = new ArrayList<NameValuePair>();
postData.add(new BasicNameValuePair("userinfo", userinfo.toString()));
Log.d(TAG, "Server request: " + userinfo.toString());
// Send the request
post.setEntity(new UrlEncodedFormEntity(postData, HTTP.UTF_8));
HttpResponse response = http.execute(post);
// Read the response
BufferedReader rd = new BufferedReader(new InputStreamReader(
response.getEntity().getContent()));
String responseStr = "";
String line = "";
while ((line = rd.readLine()) != null) {
responseStr += line;
}
Log.d(TAG, "Server response:" + responseStr);
// Parse the response as JSON
try {
JSONArray arr = new JSONArray(responseStr);
if (arr.length() != 0) {
JSONObject object = arr.getJSONObject(0);
return object;
}
else {
return null;
}
} catch(JSONException e) {
throw new IOException("Bad JSON format", e);
}
}
}