summaryrefslogtreecommitdiff
path: root/src/de/blinkt/openvpn/api
diff options
context:
space:
mode:
authorArne Schwabe <arne@rfc2549.org>2013-04-06 19:46:07 +0200
committerArne Schwabe <arne@rfc2549.org>2013-04-06 19:46:07 +0200
commitbde3a3f780cc668619076df96147b76be1c4ab64 (patch)
tree0c47b7894df388844ef3b9a806711b704c227b3d /src/de/blinkt/openvpn/api
parentad2256b6fe9c211d06321d99590cb457427d8e7d (diff)
Add external API with security.
Diffstat (limited to 'src/de/blinkt/openvpn/api')
-rw-r--r--src/de/blinkt/openvpn/api/APIVpnProfile.aidl1
-rw-r--r--src/de/blinkt/openvpn/api/APIVpnProfile.java56
-rw-r--r--src/de/blinkt/openvpn/api/ConfirmDialog.java123
-rw-r--r--src/de/blinkt/openvpn/api/ExternalAppDatabase.java58
-rw-r--r--src/de/blinkt/openvpn/api/ExternalOpenVPNService.java238
-rw-r--r--src/de/blinkt/openvpn/api/GrantPermissionsActivity.java26
-rw-r--r--src/de/blinkt/openvpn/api/IOpenVPNAPIService.aidl41
-rw-r--r--src/de/blinkt/openvpn/api/IOpenVPNStatusCallback.aidl13
-rw-r--r--src/de/blinkt/openvpn/api/SecurityRemoteException.java12
9 files changed, 568 insertions, 0 deletions
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<APIVpnProfile> CREATOR
+ = new Parcelable.Creator<APIVpnProfile>() {
+ 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<String> allowedapps = getExtAppList();
+
+ return allowedapps.contains(packagename);
+
+ }
+
+ Set<String> getExtAppList() {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
+ Set<String> allowedapps = prefs.getStringSet(PREFERENCES_KEY, new HashSet<String>());
+ return allowedapps;
+ }
+
+ void addApp(String packagename)
+ {
+ Set<String> allowedapps = getExtAppList();
+ allowedapps.add(packagename);
+ saveExtAppList(allowedapps);
+ }
+
+ private void saveExtAppList( Set<String> allowedapps) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
+ Editor prefedit = prefs.edit();
+ prefedit.putStringSet(PREFERENCES_KEY, allowedapps);
+ prefedit.apply();
+ }
+
+ void clearAllApiApps() {
+ saveExtAppList(new HashSet<String>());
+ }
+
+ public void removeApp(String packagename) {
+ Set<String> 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<IOpenVPNStatusCallback> mCallbacks =
+ new RemoteCallbackList<IOpenVPNStatusCallback>();
+
+ 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<APIVpnProfile> getProfiles() throws RemoteException {
+ checkOpenVPNPermission();
+
+ ProfileManager pm = ProfileManager.getInstance(getBaseContext());
+
+ List<APIVpnProfile> profiles = new LinkedList<APIVpnProfile>();
+
+ 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<N; i++) {
+ try {
+ mCallbacks.getBroadcastItem(i).newStatus(state,logmessage);
+ } catch (RemoteException e) {
+ // The RemoteCallbackList will take care of removing
+ // the dead object for us.
+ }
+ }
+ mCallbacks.finishBroadcast();
+ }
+
+
+
+} \ No newline at end of file
diff --git a/src/de/blinkt/openvpn/api/GrantPermissionsActivity.java b/src/de/blinkt/openvpn/api/GrantPermissionsActivity.java
new file mode 100644
index 00000000..659ec24b
--- /dev/null
+++ b/src/de/blinkt/openvpn/api/GrantPermissionsActivity.java
@@ -0,0 +1,26 @@
+package de.blinkt.openvpn.api;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.VpnService;
+
+public class GrantPermissionsActivity extends Activity {
+ private static final int VPN_PREPARE = 0;
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ Intent i= VpnService.prepare(this);
+ if(i==null)
+ onActivityResult(VPN_PREPARE, RESULT_OK, null);
+ else
+ startActivityForResult(i, VPN_PREPARE);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ setResult(resultCode);
+ finish();
+ }
+}
diff --git a/src/de/blinkt/openvpn/api/IOpenVPNAPIService.aidl b/src/de/blinkt/openvpn/api/IOpenVPNAPIService.aidl
new file mode 100644
index 00000000..c1e32eac
--- /dev/null
+++ b/src/de/blinkt/openvpn/api/IOpenVPNAPIService.aidl
@@ -0,0 +1,41 @@
+// IOpenVPNAPIService.aidl
+package de.blinkt.openvpn.api;
+
+import de.blinkt.openvpn.api.APIVpnProfile;
+import de.blinkt.openvpn.api.IOpenVPNStatusCallback;
+
+import android.content.Intent;
+import android.os.ParcelFileDescriptor;
+
+interface IOpenVPNAPIService {
+ List<APIVpnProfile> 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;
+
+}