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. --- AndroidManifest.xml | 16 +- res/layout/api_confirm.xml | 57 +++++ res/values/strings.xml | 4 + 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 ++ 12 files changed, 640 insertions(+), 5 deletions(-) create mode 100644 res/layout/api_confirm.xml 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 diff --git a/AndroidManifest.xml b/AndroidManifest.xml index b01e92d8..b8d231d4 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -18,8 +18,8 @@ + android:versionCode="66" + android:versionName="0.5.36b" > @@ -82,8 +82,13 @@ + android:name=".api.GrantPermissionsActivity"> + + + + + @@ -152,6 +157,7 @@ android:targetActivity=".LaunchVPN" > + @@ -164,4 +170,4 @@ tools:ignore="ExportedContentProvider" /> - + \ No newline at end of file diff --git a/res/layout/api_confirm.xml b/res/layout/api_confirm.xml new file mode 100644 index 00000000..bad20cc0 --- /dev/null +++ b/res/layout/api_confirm.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index cdc42288..3e969f18 100755 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -277,5 +277,9 @@ RDN prefix tls-remote (DEPRECATED) You can help translating by visiting http://crowdin.net/project/ics-openvpn/invite + + %1$s attempts to control %2$s + By proceeding, you are giving the application permission to completely control OpenVPN for Android and to intercept all network traffic. Do NOT accept unless you trust the application. Otherwise, you run the risk of having your data compromised by malicious software." + I trust this application. \ No newline at end of file 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