From bde3a3f780cc668619076df96147b76be1c4ab64 Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Sat, 6 Apr 2013 19:46:07 +0200 Subject: Add external API with security. --- src/de/blinkt/openvpn/api/APIVpnProfile.aidl | 1 + src/de/blinkt/openvpn/api/APIVpnProfile.java | 56 +++++ src/de/blinkt/openvpn/api/ConfirmDialog.java | 123 +++++++++++ src/de/blinkt/openvpn/api/ExternalAppDatabase.java | 58 +++++ .../blinkt/openvpn/api/ExternalOpenVPNService.java | 238 +++++++++++++++++++++ .../openvpn/api/GrantPermissionsActivity.java | 26 +++ src/de/blinkt/openvpn/api/IOpenVPNAPIService.aidl | 41 ++++ .../blinkt/openvpn/api/IOpenVPNStatusCallback.aidl | 13 ++ .../openvpn/api/SecurityRemoteException.java | 12 ++ 9 files changed, 568 insertions(+) create mode 100644 src/de/blinkt/openvpn/api/APIVpnProfile.aidl create mode 100644 src/de/blinkt/openvpn/api/APIVpnProfile.java create mode 100644 src/de/blinkt/openvpn/api/ConfirmDialog.java create mode 100644 src/de/blinkt/openvpn/api/ExternalAppDatabase.java create mode 100644 src/de/blinkt/openvpn/api/ExternalOpenVPNService.java create mode 100644 src/de/blinkt/openvpn/api/GrantPermissionsActivity.java create mode 100644 src/de/blinkt/openvpn/api/IOpenVPNAPIService.aidl create mode 100644 src/de/blinkt/openvpn/api/IOpenVPNStatusCallback.aidl create mode 100644 src/de/blinkt/openvpn/api/SecurityRemoteException.java (limited to 'src/de') diff --git a/src/de/blinkt/openvpn/api/APIVpnProfile.aidl b/src/de/blinkt/openvpn/api/APIVpnProfile.aidl new file mode 100644 index 00000000..16cc85bc --- /dev/null +++ b/src/de/blinkt/openvpn/api/APIVpnProfile.aidl @@ -0,0 +1 @@ +parcelable APIVpnProfile; \ No newline at end of file diff --git a/src/de/blinkt/openvpn/api/APIVpnProfile.java b/src/de/blinkt/openvpn/api/APIVpnProfile.java new file mode 100644 index 00000000..5445913a --- /dev/null +++ b/src/de/blinkt/openvpn/api/APIVpnProfile.java @@ -0,0 +1,56 @@ +package de.blinkt.openvpn.api; + +import android.os.Parcel; +import android.os.Parcelable; + +public class APIVpnProfile implements Parcelable { + + public final String mUUID; + public final String mName; + public final boolean mUserEditable; + + + + public APIVpnProfile(Parcel in) { + mUUID = in.readString(); + mName = in.readString(); + if(in.readInt()==0) + mUserEditable=false; + else + mUserEditable=true; + } + + public APIVpnProfile(String uuidString, String name, boolean userEditable) { + mUUID=uuidString; + mName = name; + mUserEditable=userEditable; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mUUID); + dest.writeString(mName); + if(mUserEditable) + dest.writeInt(0); + else + dest.writeInt(1); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public APIVpnProfile createFromParcel(Parcel in) { + return new APIVpnProfile(in); + } + + public APIVpnProfile[] newArray(int size) { + return new APIVpnProfile[size]; + } + }; + + +} diff --git a/src/de/blinkt/openvpn/api/ConfirmDialog.java b/src/de/blinkt/openvpn/api/ConfirmDialog.java new file mode 100644 index 00000000..f72f4921 --- /dev/null +++ b/src/de/blinkt/openvpn/api/ConfirmDialog.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 de.blinkt.openvpn.api; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; +import android.content.DialogInterface; +import android.content.DialogInterface.OnShowListener; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.TextView; +import de.blinkt.openvpn.R; + + +public class ConfirmDialog extends Activity implements +CompoundButton.OnCheckedChangeListener, DialogInterface.OnClickListener { + private static final String TAG = "OpenVPNVpnConfirm"; + + private String mPackage; + + private Button mButton; + + private AlertDialog mAlert; + + @Override + protected void onResume() { + super.onResume(); + try { + mPackage = getCallingPackage(); + if (mPackage==null) { + finish(); + return; + } + + + PackageManager pm = getPackageManager(); + ApplicationInfo app = pm.getApplicationInfo(mPackage, 0); + + View view = View.inflate(this, R.layout.api_confirm, null); + ((ImageView) view.findViewById(R.id.icon)).setImageDrawable(app.loadIcon(pm)); + ((TextView) view.findViewById(R.id.prompt)).setText( + getString(R.string.prompt, app.loadLabel(pm), getString(R.string.app))); + ((CompoundButton) view.findViewById(R.id.check)).setOnCheckedChangeListener(this); + + + Builder builder = new AlertDialog.Builder(this); + + builder.setView(view); + + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setTitle(android.R.string.dialog_alert_title); + builder.setPositiveButton(android.R.string.ok,this); + builder.setNegativeButton(android.R.string.cancel,this); + + mAlert = builder.create(); + + mAlert.setOnShowListener (new OnShowListener() { + + @Override + public void onShow(DialogInterface dialog) { + // TODO Auto-generated method stub + mButton = mAlert.getButton(DialogInterface.BUTTON_POSITIVE); + mButton.setEnabled(false); + + } + }); + + //setCloseOnTouchOutside(false); + + mAlert.show(); + + } catch (Exception e) { + Log.e(TAG, "onResume", e); + finish(); + } + } + + @Override + public void onBackPressed() { + } + + @Override + public void onCheckedChanged(CompoundButton button, boolean checked) { + mButton.setEnabled(checked); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + + if (which == DialogInterface.BUTTON_POSITIVE) { + ExternalAppDatabase extapps = new ExternalAppDatabase(this); + extapps.addApp(mPackage); + setResult(RESULT_OK); + finish(); + } + + if (which == DialogInterface.BUTTON_NEGATIVE) { + finish(); + } + } + +} + diff --git a/src/de/blinkt/openvpn/api/ExternalAppDatabase.java b/src/de/blinkt/openvpn/api/ExternalAppDatabase.java new file mode 100644 index 00000000..ca348152 --- /dev/null +++ b/src/de/blinkt/openvpn/api/ExternalAppDatabase.java @@ -0,0 +1,58 @@ +package de.blinkt.openvpn.api; + +import java.util.HashSet; +import java.util.Set; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.preference.PreferenceManager; + +public class ExternalAppDatabase { + + Context mContext; + + public ExternalAppDatabase(Context c) { + mContext =c; + } + + private final String PREFERENCES_KEY = "PREFERENCES_KEY"; + + boolean isAllowed(String packagename) { + Set allowedapps = getExtAppList(); + + return allowedapps.contains(packagename); + + } + + Set getExtAppList() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); + Set allowedapps = prefs.getStringSet(PREFERENCES_KEY, new HashSet()); + return allowedapps; + } + + void addApp(String packagename) + { + Set allowedapps = getExtAppList(); + allowedapps.add(packagename); + saveExtAppList(allowedapps); + } + + private void saveExtAppList( Set allowedapps) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); + Editor prefedit = prefs.edit(); + prefedit.putStringSet(PREFERENCES_KEY, allowedapps); + prefedit.apply(); + } + + void clearAllApiApps() { + saveExtAppList(new HashSet()); + } + + public void removeApp(String packagename) { + Set allowedapps = getExtAppList(); + allowedapps.remove(packagename); + saveExtAppList(allowedapps); + } + +} diff --git a/src/de/blinkt/openvpn/api/ExternalOpenVPNService.java b/src/de/blinkt/openvpn/api/ExternalOpenVPNService.java new file mode 100644 index 00000000..459bf790 --- /dev/null +++ b/src/de/blinkt/openvpn/api/ExternalOpenVPNService.java @@ -0,0 +1,238 @@ +package de.blinkt.openvpn.api; + +import java.io.IOException; +import java.io.StringReader; +import java.util.LinkedList; +import java.util.List; + +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.VpnService; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.ConfigParser; +import de.blinkt.openvpn.core.ConfigParser.ConfigParseError; +import de.blinkt.openvpn.core.OpenVPN; +import de.blinkt.openvpn.core.OpenVPN.ConnectionStatus; +import de.blinkt.openvpn.core.OpenVPN.StateListener; +import de.blinkt.openvpn.core.OpenVpnService; +import de.blinkt.openvpn.core.OpenVpnService.LocalBinder; +import de.blinkt.openvpn.core.ProfileManager; +import de.blinkt.openvpn.core.VPNLaunchHelper; + +public class ExternalOpenVPNService extends Service implements StateListener { + + final RemoteCallbackList mCallbacks = + new RemoteCallbackList(); + + private OpenVpnService mService; + private ExternalAppDatabase mExtAppDb; + + + private ServiceConnection mConnection = new ServiceConnection() { + + + @Override + public void onServiceConnected(ComponentName className, + IBinder service) { + // We've bound to LocalService, cast the IBinder and get LocalService instance + LocalBinder binder = (LocalBinder) service; + mService = binder.getService(); + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + mService =null; + } + + }; + + @Override + public void onCreate() { + super.onCreate(); + OpenVPN.addStateListener(this); + mExtAppDb = new ExternalAppDatabase(this); + + Intent intent = new Intent(getBaseContext(), OpenVpnService.class); + intent.setAction(OpenVpnService.START_SERVICE); + + bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + } + + private final IOpenVPNAPIService.Stub mBinder = new IOpenVPNAPIService.Stub() { + private boolean checkOpenVPNPermission() throws SecurityRemoteException{ + PackageManager pm = getPackageManager(); + + for (String apppackage:mExtAppDb.getExtAppList()) { + ApplicationInfo app; + try { + app = pm.getApplicationInfo(apppackage, 0); + if (Binder.getCallingUid() == app.uid) { + return true; + } + } catch (NameNotFoundException e) { + // App not found. Remove it from the list + mExtAppDb.removeApp(apppackage); + e.printStackTrace(); + } + + } + throw new SecurityException("Unauthorized OpenVPN API Caller"); + } + + @Override + public List getProfiles() throws RemoteException { + checkOpenVPNPermission(); + + ProfileManager pm = ProfileManager.getInstance(getBaseContext()); + + List profiles = new LinkedList(); + + for(VpnProfile vp: pm.getProfiles()) + profiles.add(new APIVpnProfile(vp.getUUIDString(),vp.mName,vp.mUserEditable)); + + return profiles; + } + + @Override + public void startProfile(String profileUUID) throws RemoteException { + checkOpenVPNPermission(); + + Intent shortVPNIntent = new Intent(Intent.ACTION_MAIN); + shortVPNIntent.setClass(getBaseContext(),de.blinkt.openvpn.LaunchVPN.class); + shortVPNIntent.putExtra(de.blinkt.openvpn.LaunchVPN.EXTRA_KEY,profileUUID); + shortVPNIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(shortVPNIntent); + } + + public void startVPN(String inlineconfig) throws RemoteException { + checkOpenVPNPermission(); + + ConfigParser cp = new ConfigParser(); + try { + cp.parseConfig(new StringReader(inlineconfig)); + VpnProfile vp = cp.convertProfile(); + if(vp.checkProfile(getApplicationContext()) != R.string.no_error_found) + throw new RemoteException(getString(vp.checkProfile(getApplicationContext()))); + + + ProfileManager.setTemporaryProfile(vp); + VPNLaunchHelper.startOpenVpn(vp, getBaseContext()); + + + } catch (IOException e) { + throw new RemoteException(e.getMessage()); + } catch (ConfigParseError e) { + throw new RemoteException(e.getMessage()); + } + } + + @Override + public boolean addVPNProfile(String name, String config) throws RemoteException { + checkOpenVPNPermission(); + + ConfigParser cp = new ConfigParser(); + try { + cp.parseConfig(new StringReader(config)); + VpnProfile vp = cp.convertProfile(); + vp.mName = name; + ProfileManager pm = ProfileManager.getInstance(getBaseContext()); + pm.addProfile(vp); + } catch (IOException e) { + e.printStackTrace(); + return false; + } catch (ConfigParseError e) { + e.printStackTrace(); + return false; + } + + return true; + } + + + @Override + public Intent prepare(String packagename) { + if (new ExternalAppDatabase(ExternalOpenVPNService.this).isAllowed(packagename)) + return null; + + Intent intent = new Intent(); + intent.setClass(ExternalOpenVPNService.this, ConfirmDialog.class); + return intent; + } + + @Override + public boolean hasPermission() throws RemoteException { + checkOpenVPNPermission(); + + return VpnService.prepare(ExternalOpenVPNService.this)==null; + } + + + @Override + public void registerStatusCallback(IOpenVPNStatusCallback cb) + throws RemoteException { + checkOpenVPNPermission(); + + if (cb != null) mCallbacks.register(cb); + + + } + + @Override + public void unregisterStatusCallback(IOpenVPNStatusCallback cb) + throws RemoteException { + checkOpenVPNPermission(); + + if (cb != null) mCallbacks.unregister(cb); + + } + + @Override + public void disconnect() throws RemoteException { + checkOpenVPNPermission(); + + mService.getManagement().stopVPN(); + } + }; + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public void onDestroy() { + super.onDestroy(); + mCallbacks.kill(); + unbindService(mConnection); + OpenVPN.removeStateListener(this); + } + + @Override + public void updateState(String state, String logmessage, int resid, ConnectionStatus level) { + // Broadcast to all clients the new value. + final int N = mCallbacks.beginBroadcast(); + for (int i=0; i getProfiles(); + + void startProfile (String profileUUID); + + /* Use a profile with all certificates etc. embedded */ + boolean addVPNProfile (String name, String config); + + /* start a profile using an config */ + void startVPN (String inlineconfig); + + /* This permission framework is used to avoid confused deputy style attack to the VPN + * calling this will give null if the app is allowed to use the frame and null otherwise */ + Intent prepare (String packagename); + + /* Tells the calling app wether we already have permission to avoid calling the activity/flicker */ + boolean hasPermission(); + + /* Disconnect the VPN */ + void disconnect(); + + /** + * Registers to receive OpenVPN Status Updates + */ + void registerStatusCallback(IOpenVPNStatusCallback cb); + + /** + * Remove a previously registered callback interface. + */ + void unregisterStatusCallback(IOpenVPNStatusCallback cb); + +} \ No newline at end of file diff --git a/src/de/blinkt/openvpn/api/IOpenVPNStatusCallback.aidl b/src/de/blinkt/openvpn/api/IOpenVPNStatusCallback.aidl new file mode 100644 index 00000000..11e369c2 --- /dev/null +++ b/src/de/blinkt/openvpn/api/IOpenVPNStatusCallback.aidl @@ -0,0 +1,13 @@ +package de.blinkt.openvpn.api; + +/** + * Example of a callback interface used by IRemoteService to send + * synchronous notifications back to its clients. Note that this is a + * one-way interface so the server does not block waiting for the client. + */ +oneway interface IOpenVPNStatusCallback { + /** + * Called when the service has a new status for you. + */ + void newStatus(String state, String message); +} diff --git a/src/de/blinkt/openvpn/api/SecurityRemoteException.java b/src/de/blinkt/openvpn/api/SecurityRemoteException.java new file mode 100644 index 00000000..e6011aa3 --- /dev/null +++ b/src/de/blinkt/openvpn/api/SecurityRemoteException.java @@ -0,0 +1,12 @@ +package de.blinkt.openvpn.api; + +import android.os.RemoteException; + +public class SecurityRemoteException extends RemoteException { + + /** + * + */ + private static final long serialVersionUID = 1L; + +} -- cgit v1.2.3