/* * 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); } } }