Java tutorial
/* * * Copyright (C) 2015 Orange Labs * * 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 com.orange.oidc.secproxy_service; import java.math.BigInteger; import java.security.KeyFactory; import java.security.PublicKey; import java.security.Security; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPrivateKey; import java.security.spec.RSAPrivateKeySpec; import java.security.spec.RSAPublicKeySpec; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Locale; import org.json.JSONException; import org.json.JSONObject; import com.orange.oidc.secproxy_service.IRemoteListenerToken; import com.orange.oidc.secproxy_service.IRemoteService; import com.orange.oidc.secproxy_service.IRemoteServiceInternal; import com.orange.oidc.secproxy_service.R; import com.orange.oidc.secproxy_service.MySecureProxy.RsaKeyProxy; import android.app.Notification; import android.app.NotificationManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.PackageManager; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; import android.preference.PreferenceManager; import android.util.Log; import android.widget.Toast; /** * Openid Connect proxy service class * */ public class Service extends android.app.Service { protected static final String TAG = Service.class.getName(); final static String EMPTY = ""; static { Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1); } int idList = 0; class RemoteListenerToken { IRemoteListenerToken listener; String id; OpenidConnectParams ocp; RemoteListenerToken(IRemoteListenerToken r) { listener = r; idList++; id = "" + idList; } }; List<RemoteListenerToken> RemoteListenerTokenList = new ArrayList<RemoteListenerToken>(); public static Service theService = null; private static SecureProxy secureProxy; public Service() { // android.os.Debug.waitForDebugger(); System.setProperty("http.keepAlive", "false"); if (theService == null) { theService = this; // init secure storage secureProxy = new MySecureProxy(); // init Http HttpOpenidConnect.secureProxy = secureProxy; } } // private final IRemoteServiceBinder mBinder = new IRemoteServiceBinder(); public class ServiceBinder extends Binder { Service getService() { return Service.this; } } private final IRemoteService.Stub mBinder = new IRemoteService.Stub() { @Override public boolean getTokensWithOidcProxy(IRemoteListenerToken listener, String serverUrl, String client_id, String scope, String state, String nonce) throws RemoteException { // check access if (!checkCallingSignature()) return false; // check parameters if (isEmpty(serverUrl) || isEmpty(client_id) || isEmpty(scope)) { // invalid parameters return false; } // android.os.Debug.waitForDebugger(); Logd(TAG, "getTokensWithOidcProxy begin"); showNotProtectedNotifIcon(); if (!serverUrl.endsWith("/")) serverUrl += "/"; // TazTag : disables specific scope // scope = sortScope(scope+" secure_proxy"); scope = sortScope(scope); OpenidConnectParams ocp = new OpenidConnectParams(serverUrl, client_id, scope, secureProxy.getRedirectUri(), state, nonce, null, null, null); Logd(TAG, "ocp: " + ocp.toString()); RemoteListenerToken rl; synchronized (RemoteListenerTokenList) { rl = new RemoteListenerToken(listener); rl.ocp = ocp; RemoteListenerTokenList.add(rl); } // launch request HttpOpenidConnect hc = new HttpOpenidConnect(ocp); if (!hc.getTokens(Service.this, rl.id, null)) { setClientTokens(rl.id, null); } Logd(TAG, "getTokensWithOidcProxy end"); return true; } @Override public String webFinger(String userInput, String serverUrl) { // check access if (!checkCallingSignature()) return null; // check parameters if (isEmpty(serverUrl) || isEmpty(userInput)) return null; try { return HttpOpenidConnect.webfinger(userInput, serverUrl); } catch (Exception e) { } return null; } @Override public String getUserInfo(String serverUrl, String access_token) { // check access if (!checkCallingSignature()) return null; // check parameters if (isEmpty(serverUrl) || isEmpty(access_token)) return null; // do the job return HttpOpenidConnect.getUserInfo(serverUrl, access_token); } // Logout from the idp @Override public boolean logout(String serverUrl) { // check access if (!checkCallingSignature()) return false; // check parameters if (isEmpty(serverUrl)) return false; // do the job return HttpOpenidConnect.logout(serverUrl); } private boolean FORCE_CHECK = true; // check calling process signature, if not valid return false // possibility of hack private boolean checkCallingSignature() { if (FORCE_CHECK) return true; // get package name String pName = getPackageManager().getNameForUid(Binder.getCallingUid()); X509Certificate certCalling = PackInfo.getCertificate(Service.this, pName); X509Certificate certProxy = PackInfo.getCertificate(Service.this, Service.this.getPackageName()); Logd(TAG, "certCalling : " + pName); Logd(TAG, "" + certCalling.toString()); Logd(TAG, "certProxy : " + Service.this.getPackageName()); Logd(TAG, "" + certProxy.toString()); //certCalling. try { certCalling.verify(RsaKeyRootCA.pubRsaKey); } catch (Exception e) { Logd(TAG, "checkCallingSignature failed"); return false; } // uid are not the same Logd(TAG, "checkCallingSignature success"); return true; } }; void resetCookies() { // init intent Intent intent = new Intent(Intent.ACTION_VIEW); intent.setClass(Service.this, WebViewActivity.class); intent.putExtra("resetcookies", "true"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NO_ANIMATION); // launch webview startActivity(intent); } long getSecretPathThreshold() { SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); long l = sharedPrefs.getLong("threshold", 80); return l; } void setSecretPathThreshold(long val) { SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); Editor editor = sharedPrefs.edit(); editor.putLong("threshold", val); editor.commit(); Logd(TAG, "threshold: " + val); } // internal remote service for internal webview private final IRemoteServiceInternal.Stub mBinderInternal = new IRemoteServiceInternal.Stub() { private void setTokensRedirect(String id, HttpOpenidConnect hc, String redirect) { // android.os.Debug.waitForDebugger(); // check caller uid if (checkCallingUid() == false) { // caller are not from inside this app Logd(TAG, "setTokensRedirect not from inside this app"); return; } RemoteListenerToken rl = null; synchronized (RemoteListenerTokenList) { for (int i = RemoteListenerTokenList.size() - 1; i >= 0; i--) { RemoteListenerToken r = RemoteListenerTokenList.get(i); if (r.id.compareTo(id) == 0) { rl = r; RemoteListenerTokenList.remove(i); break; } } } if (rl != null) { try { String tokens = hc.doRedirect(redirect); JSONObject jObject = null; try { Logd(TAG, "tokens: " + tokens); jObject = new JSONObject(tokens); } catch (JSONException je) { // check if it is JWE tokens = secureProxy.decryptJWE(tokens); Logd(TAG, "doRedirect JWE: " + tokens); jObject = new JSONObject(tokens); } boolean user_cancel = false; String userCancel = getFromJS(jObject, "cancel"); if (userCancel != null && userCancel.equalsIgnoreCase("true")) { user_cancel = true; } // put id_token and refresh_token in Secure Storage if (user_cancel == false) { String access_token = getFromJS(jObject, "access_token"); // String token_type = getFromJS( jObject, "token_type" ); String refresh_token = getFromJS(jObject, "refresh_token"); String expires_in = getFromJS(jObject, "expires_in"); String id_token = getFromJS(jObject, "id_token"); rl.ocp.m_server_scope = getFromJS(jObject, "scope"); rl.listener.handleTokenResponseWithOidcProxy(id_token, access_token, false); return; } else { rl.listener.handleTokenResponseWithOidcProxy(EMPTY, EMPTY, true); return; } } catch (Exception e) { e.printStackTrace(); } // if here, then nothing to notify if (rl.listener != null) { try { rl.listener.handleTokenResponseWithOidcProxy(EMPTY, EMPTY, false); } catch (RemoteException e) { e.printStackTrace(); } } } } @Override public void cancel(String id, boolean user) throws RemoteException { // check caller uid if (checkCallingUid() == false) { // caller are not from inside this app Logd(TAG, "setTokens not from inside this app"); return; } RemoteListenerToken rl = null; synchronized (RemoteListenerTokenList) { for (int i = RemoteListenerTokenList.size() - 1; i >= 0; i--) { RemoteListenerToken r = RemoteListenerTokenList.get(i); if (r.id.compareTo(id) == 0) { rl = r; RemoteListenerTokenList.remove(i); break; } } } if (rl != null) { rl.listener.handleTokenResponseWithOidcProxy(EMPTY, EMPTY, user); } // hideNotifIcon(); } @Override public void doRedirect(String id, String redirect) { // check caller uid if (checkCallingUid() == false) { // caller are not from inside this app Logd(TAG, "doRedirect not from inside this app"); return; } Logd(TAG, "doRedirect begin"); if (id == null || id.length() == 0) { Logd(TAG, "doRedirect end no ID"); // hideNotifIcon(); return; } OpenidConnectParams ocp = null; // android.os.Debug.waitForDebugger(); synchronized (RemoteListenerTokenList) { for (int i = RemoteListenerTokenList.size() - 1; i >= 0; i--) { RemoteListenerToken r = RemoteListenerTokenList.get(i); if (r.id.compareTo(id) == 0) { ocp = new OpenidConnectParams(r.ocp); break; } } } if (ocp != null) { HttpOpenidConnect hc = new HttpOpenidConnect(ocp); try { setTokensRedirect(id, hc, redirect); return; } catch (Exception e) { e.printStackTrace(); } } // if error, set null response try { cancel(id, false); } catch (Exception e) { e.printStackTrace(); } Logd(TAG, "doRedirect end"); // hideNotifIcon(); } @Override public void resetCookies() throws RemoteException { // check caller uid if (checkCallingUid() == false) { // caller are not from inside this app Logd(TAG, "resetCookies not from inside this app"); return; } // clear cookies // android.webkit.CookieManager.getInstance().removeAllCookie(); } // check calling process uid for possibility of hack, // if different from current service uid then return false private boolean checkCallingUid() { // check uid if (android.os.Process.myUid() == Binder.getCallingUid()) { return true; } // uid are not the same return false; } }; @Override public void onCreate() { super.onCreate(); } // service termination @Override public void onDestroy() { hideNotifIcon(); super.onDestroy(); } // service starting @Override public int onStartCommand(Intent intent, int flags, int startId) { return Service.START_NOT_STICKY; } // new connection to service @Override public IBinder onBind(Intent intent) { // Select the interface to return. If your service only implements // a single interface, you can just return it here without checking // the Intent. if (IRemoteService.class.getName().equals(intent.getAction())) { showProtectedNotifIcon(); Logd(TAG, "onBind Service"); return mBinder; } if (IRemoteServiceInternal.class.getName().equals(intent.getAction())) { Logd(TAG, "onBind ServiceInternal"); return mBinderInternal; } return null; } void setClientTokens(String id, String tokens) { Logd(TAG, "setClientTokens id:" + id); Logd(TAG, "setClientTokens tokens:" + tokens); RemoteListenerToken rl = null; synchronized (RemoteListenerTokenList) { for (int i = RemoteListenerTokenList.size() - 1; i >= 0; i--) { RemoteListenerToken r = RemoteListenerTokenList.get(i); if (r.id.compareTo(id) == 0) { rl = r; RemoteListenerTokenList.remove(i); break; } } } if (rl != null) { JSONObject jObject = null; try { if (tokens != null) jObject = new JSONObject(tokens); if (jObject != null) { String access_token = getFromJS(jObject, "access_token"); // String token_type = getFromJS( jObject, "token_type" ); String refresh_token = getFromJS(jObject, "refresh_token"); String expires_in = getFromJS(jObject, "expires_in"); String id_token = getFromJS(jObject, "id_token"); rl.ocp.m_server_scope = getFromJS(jObject, "scope"); boolean user_cancel = false; String userCancel = getFromJS(jObject, "cancel"); if (userCancel != null && userCancel.equalsIgnoreCase("true")) { user_cancel = true; } rl.listener.handleTokenResponseWithOidcProxy(id_token, access_token, user_cancel); return; } } catch (Exception e) { e.printStackTrace(); } } if (rl != null && rl.listener != null) { try { rl.listener.handleTokenResponseWithOidcProxy(EMPTY, EMPTY, false); } catch (Exception e) { e.printStackTrace(); } } } // get a string from a json object String getFromJS(JSONObject jo, String name) { Log.d(TAG, "getFromJS " + name); if (jo != null) { try { return jo.getString(name); } catch (Exception e) { e.printStackTrace(); } } return null; } String sortScope(String scope) { // sort scope in alphabetical order if (scope != null) { scope = scope.toLowerCase(Locale.getDefault()); // offline_access is mandatory if (!scope.contains("offline_access")) { // TazTag : disables specific scope // scope += " offline_access"; } String scopes[] = scope.split("\\ "); if (scopes != null) { Arrays.sort(scopes, new Comparator<String>() { @Override public int compare(String s1, String s2) { return s1.compareToIgnoreCase(s2); } }); scope = null; // filter null or empty strings for (int i = 0; i < scopes.length; i++) { if (scopes[i] != null && scopes[i].length() > 0) { if (scope == null) scope = scopes[i]; else scope += (" " + scopes[i]); } } } } return scope; } void toast(final String msg, final int duration) { new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() { @Override public void run() { android.widget.Toast.makeText(theService, msg, duration == 0 ? android.widget.Toast.LENGTH_SHORT : android.widget.Toast.LENGTH_LONG) .show(); } }); } private void showProtectedNotifIcon() { showNotification(true); } private void showNotProtectedNotifIcon() { showNotification(false); } private void showNotification(boolean bProtect) { Logd(TAG, "show protected icon " + bProtect); // this is it, we'll build the notification! // in the addAction method, if you don't want any icon, just set the first param to 0 Notification mNotification = null; if (bProtect) { mNotification = new Notification.Builder(this) .setContentTitle("SECURE OIDC PROXY").setContentText("privacy protected") .setSmallIcon(R.drawable.masked_on).setAutoCancel(false).build(); } else { mNotification = new Notification.Builder(this) .setContentTitle("SECURE OIDC PROXY").setContentText("privacy not protected") .setSmallIcon(R.drawable.masked_off).setAutoCancel(false).build(); } // to make it non clearable mNotification.flags |= Notification.FLAG_NO_CLEAR; NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); // If you want to hide the notification after it was selected, do the code below // myNotification.flags |= Notification.FLAG_AUTO_CANCEL; notificationManager.notify(0, mNotification); } private void hideNotifIcon() { Logd(TAG, "hideNotifIcon"); if (Context.NOTIFICATION_SERVICE != null) { String ns = Context.NOTIFICATION_SERVICE; NotificationManager nMgr = (NotificationManager) getApplicationContext().getSystemService(ns); nMgr.cancel(0); } } void Logd(String tag, String msg) { if (tag != null && msg != null) Log.d(tag, msg); } // RSA key used by the Secure Proxy // here for demo only, must be stored in a secure element final static class RsaKeyRootCA { // RSA 512 // static private String _rsaNs = "174534779388221555027193756296996728577785706931882582500865462191276991828908964783626120005370658240337053195460241456112314652761908126414596521509072923990652465127860275450913335454113631551447701718552251290927858625416498836268258006496356478412330739471783248956660708147504866504092613087913865419941"; static private String _rsaNs = "137615577516537198807817310937859988960161111436453997102401245051154742289581142838915071683987098078139191437791365924492551201544861234198101701401783768985281927492472872690006772264611369208880786262064142563461367635639697166679608227153800046014276971655300012833498776331150965310640667300049423086361"; static private String _rsaEs = "65537"; static public PublicKey pubRsaKey; } // init the key from the big numbers above static { BigInteger rsaN = null; BigInteger rsaE = null; try { rsaN = new BigInteger(RsaKeyRootCA._rsaNs); rsaE = new BigInteger(RsaKeyRootCA._rsaEs); } catch (Exception e) { e.printStackTrace(); } RSAPublicKeySpec pubRsaSpec = new RSAPublicKeySpec(rsaN, rsaE); RsaKeyRootCA.pubRsaKey = null; try { KeyFactory keyfact = KeyFactory.getInstance("RSA", "SC"); RsaKeyRootCA.pubRsaKey = keyfact.generatePublic(pubRsaSpec); } catch (Exception e) { e.printStackTrace(); } } // return true is string is null or empty, false otherwise private static boolean isEmpty(String s) { if (s == null || s.length() == 0) return true; return false; } }