0
0
mirror of https://github.com/schwabe/ics-openvpn.git synced 2024-09-20 12:02:28 +02:00

Add external API with security.

This commit is contained in:
Arne Schwabe 2013-04-06 19:46:07 +02:00
parent ad2256b6fe
commit bde3a3f780
12 changed files with 640 additions and 5 deletions

View File

@ -18,8 +18,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.blinkt.openvpn"
android:versionCode="65"
android:versionName="0.5.36a" >
android:versionCode="66"
android:versionName="0.5.36b" >
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -82,8 +82,13 @@
</service>
<activity
android:name=".api.GrantPermissionsActivity"
android:permission="de.blinkt.openvpn.REMOTE_API" >
android:name=".api.GrantPermissionsActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<activity
android:name=".api.ConfirmDialog" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
@ -152,6 +157,7 @@
android:targetActivity=".LaunchVPN" >
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity-alias>
@ -164,4 +170,4 @@
tools:ignore="ExportedContentProvider" />
</application>
</manifest>
</manifest>

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="3mm">
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageView android:id="@+id/icon"
android:layout_width="@android:dimen/app_icon_size"
android:layout_height="@android:dimen/app_icon_size"
android:paddingRight="1mm"/>
<TextView android:id="@+id/prompt"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="18sp"/>
</LinearLayout>
<TextView android:id="@+id/warning"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="1mm"
android:paddingBottom="1mm"
android:text="@string/remote_warning"
android:textSize="18sp"/>
<CheckBox android:id="@+id/check"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/remote_trust"
android:textSize="20sp"
android:checked="false"/>
</LinearLayout>
</ScrollView>

View File

@ -277,5 +277,9 @@
<string name="rdn_prefix">RDN prefix</string>
<string name="tls_remote_deprecated">tls-remote (DEPRECATED)</string>
<string name="help_translate">You can help translating by visiting http://crowdin.net/project/ics-openvpn/invite</string>
<!-- Dialog title to identify the request from a VPN application. [CHAR LIMIT=60] -->
<string name="prompt">%1$s attempts to control %2$s</string>
<string name="remote_warning">By proceeding, you are giving the application permission to completely control OpenVPN for Android and to intercept all network traffic. <b> Do NOT accept unless you trust the application. </b> Otherwise, you run the risk of having your data compromised by malicious software."</string>
<string name="remote_trust">I trust this application.</string>
</resources>

View File

@ -0,0 +1 @@
parcelable APIVpnProfile;

View File

@ -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];
}
};
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -0,0 +1,12 @@
package de.blinkt.openvpn.api;
import android.os.RemoteException;
public class SecurityRemoteException extends RemoteException {
/**
*
*/
private static final long serialVersionUID = 1L;
}