android: Migrate to the Gradle build system

This uses a manual way to trigger the NDK build (the default with
on-the-fly Android.mk files does not work for us).
This commit is contained in:
Tobias Brunner
2015-11-12 14:11:37 +01:00
parent 073761ec41
commit a50f3037ad
139 changed files with 327 additions and 71 deletions
+42
View File
@@ -0,0 +1,42 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 21
buildToolsVersion "22.0.1"
defaultConfig {
applicationId "org.strongswan.android"
minSdkVersion 15
targetSdkVersion 22
versionCode 28
versionName "1.5.0"
}
sourceSets.main {
jni.srcDirs = [] // disables the default ndk-build call (with on-the-fly Android.mk files)
jniLibs.srcDir 'src/main/libs'
}
task buildNative(type: Exec) {
workingDir 'src/main/jni'
commandLine "${android.ndkDirectory}/ndk-build", '-j', Runtime.runtime.availableProcessors()
}
task cleanNative(type: Exec) {
workingDir 'src/main/jni'
commandLine "${android.ndkDirectory}/ndk-build", 'clean'
}
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn buildNative
}
clean.dependsOn 'cleanNative'
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
}
@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012-2015 Tobias Brunner
Copyright (C) 2012 Giuliano Grassi
Copyright (C) 2012 Ralf Sager
Hochschule fuer Technik Rapperswil
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.strongswan.android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".logic.StrongSwanApplication"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/ApplicationTheme"
android:allowBackup="false" >
<activity
android:name=".ui.MainActivity"
android:label="@string/main_activity_name"
android:launchMode="singleTop" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="org.strongswan.android.action.START_PROFILE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ui.VpnProfileDetailActivity" >
</activity>
<activity
android:name=".ui.TrustedCertificatesActivity"
android:label="@string/trusted_certs_title" >
</activity>
<activity
android:name=".ui.LogActivity"
android:label="@string/log_title" >
</activity>
<activity
android:name=".ui.RemediationInstructionsActivity"
android:label="@string/remediation_instructions_title" >
</activity>
<activity
android:name=".ui.VpnProfileSelectActivity"
android:label="@string/strongswan_shortcut" >
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ui.TrustedCertificateImportActivity"
android:label="@string/import_certificate"
android:theme="@android:style/Theme.Holo.Dialog.NoActionBar" >
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/x-x509-ca-cert" />
<data android:mimeType="application/x-x509-server-cert" />
<data android:mimeType="application/x-pem-file" />
<data android:mimeType="application/pkix-cert" />
</intent-filter>
</activity>
<service
android:name=".logic.VpnStateService"
android:exported="false" >
</service>
<service
android:name=".logic.CharonVpnService"
android:exported="false"
android:permission="android.permission.BIND_VPN_SERVICE" >
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
<provider
android:name=".data.LogContentProvider"
android:authorities="org.strongswan.android.content.log"
android:exported="true" >
<!-- android:grantUriPermissions="true" combined with a custom permission does
not work (probably too many indirections with ACTION_SEND) so we secure
this provider with a custom ticketing system -->
</provider>
</application>
</manifest>
@@ -0,0 +1,149 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.data;
import java.io.File;
import java.io.FileNotFoundException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.concurrent.ConcurrentHashMap;
import org.strongswan.android.logic.CharonVpnService;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.provider.OpenableColumns;
public class LogContentProvider extends ContentProvider
{
private static final String AUTHORITY = "org.strongswan.android.content.log";
/* an Uri is valid for 30 minutes */
private static final long URI_VALIDITY = 30 * 60 * 1000;
private static ConcurrentHashMap<Uri, Long> mUris = new ConcurrentHashMap<Uri, Long>();
private File mLogFile;
public LogContentProvider()
{
}
@Override
public boolean onCreate()
{
mLogFile = new File(getContext().getFilesDir(), CharonVpnService.LOG_FILE);
return true;
}
/**
* The log file can only be accessed by Uris created with this method
* @return null if failed to create the Uri
*/
public static Uri createContentUri()
{
SecureRandom random;
try
{
random = SecureRandom.getInstance("SHA1PRNG");
}
catch (NoSuchAlgorithmException e)
{
return null;
}
Uri uri = Uri.parse("content://" + AUTHORITY + "/" + random.nextLong());
mUris.put(uri, SystemClock.uptimeMillis());
return uri;
}
@Override
public String getType(Uri uri)
{
/* MIME type for our log file */
return "text/plain";
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder)
{
/* this is called by apps to find out the name and size of the file.
* since we only provide a single file this is simple to implement */
if (projection == null || projection.length < 1)
{
return null;
}
Long timestamp = mUris.get(uri);
if (timestamp == null)
{ /* don't check the validity as this information is not really private */
return null;
}
MatrixCursor cursor = new MatrixCursor(projection, 1);
if (OpenableColumns.DISPLAY_NAME.equals(cursor.getColumnName(0)))
{
cursor.newRow().add(CharonVpnService.LOG_FILE);
}
else if (OpenableColumns.SIZE.equals(cursor.getColumnName(0)))
{
cursor.newRow().add(mLogFile.length());
}
else
{
return null;
}
return cursor;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException
{
Long timestamp = mUris.get(uri);
if (timestamp != null)
{
long elapsed = SystemClock.uptimeMillis() - timestamp;
if (elapsed > 0 && elapsed < URI_VALIDITY)
{ /* we fail if clock wrapped, should happen rarely though */
return ParcelFileDescriptor.open(mLogFile, ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_ONLY);
}
mUris.remove(uri);
}
return super.openFile(uri, mode);
}
@Override
public Uri insert(Uri uri, ContentValues values)
{
/* not supported */
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs)
{
/* not supported */
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs)
{
/* not supported */
return 0;
}
}
@@ -0,0 +1,170 @@
/*
* Copyright (C) 2012-2015 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.data;
public class VpnProfile implements Cloneable
{
/* While storing this as EnumSet would be nicer this simplifies storing it in a database */
public static final int SPLIT_TUNNELING_BLOCK_IPV4 = 1;
public static final int SPLIT_TUNNELING_BLOCK_IPV6 = 2;
private String mName, mGateway, mUsername, mPassword, mCertificate, mUserCertificate;
private Integer mMTU, mPort, mSplitTunneling;
private VpnType mVpnType;
private long mId = -1;
public long getId()
{
return mId;
}
public void setId(long id)
{
this.mId = id;
}
public String getName()
{
return mName;
}
public void setName(String name)
{
this.mName = name;
}
public String getGateway()
{
return mGateway;
}
public void setGateway(String gateway)
{
this.mGateway = gateway;
}
public VpnType getVpnType()
{
return mVpnType;
}
public void setVpnType(VpnType type)
{
this.mVpnType = type;
}
public String getUsername()
{
return mUsername;
}
public void setUsername(String username)
{
this.mUsername = username;
}
public String getPassword()
{
return mPassword;
}
public void setPassword(String password)
{
this.mPassword = password;
}
public String getCertificateAlias()
{
return mCertificate;
}
public void setCertificateAlias(String alias)
{
this.mCertificate = alias;
}
public String getUserCertificateAlias()
{
return mUserCertificate;
}
public void setUserCertificateAlias(String alias)
{
this.mUserCertificate = alias;
}
public Integer getMTU()
{
return mMTU;
}
public void setMTU(Integer mtu)
{
this.mMTU = mtu;
}
public Integer getPort()
{
return mPort;
}
public void setPort(Integer port)
{
this.mPort = port;
}
public Integer getSplitTunneling()
{
return mSplitTunneling;
}
public void setSplitTunneling(Integer splitTunneling)
{
this.mSplitTunneling = splitTunneling;
}
@Override
public String toString()
{
return mName;
}
@Override
public boolean equals(Object o)
{
if (o != null && o instanceof VpnProfile)
{
return this.mId == ((VpnProfile)o).getId();
}
return false;
}
@Override
public VpnProfile clone()
{
try
{
return (VpnProfile)super.clone();
}
catch (CloneNotSupportedException e)
{
throw new AssertionError();
}
}
}
@@ -0,0 +1,308 @@
/*
* Copyright (C) 2012-2015 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.data;
import java.util.ArrayList;
import java.util.List;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.util.Log;
public class VpnProfileDataSource
{
private static final String TAG = VpnProfileDataSource.class.getSimpleName();
public static final String KEY_ID = "_id";
public static final String KEY_NAME = "name";
public static final String KEY_GATEWAY = "gateway";
public static final String KEY_VPN_TYPE = "vpn_type";
public static final String KEY_USERNAME = "username";
public static final String KEY_PASSWORD = "password";
public static final String KEY_CERTIFICATE = "certificate";
public static final String KEY_USER_CERTIFICATE = "user_certificate";
public static final String KEY_MTU = "mtu";
public static final String KEY_PORT = "port";
public static final String KEY_SPLIT_TUNNELING = "split_tunneling";
private DatabaseHelper mDbHelper;
private SQLiteDatabase mDatabase;
private final Context mContext;
private static final String DATABASE_NAME = "strongswan.db";
private static final String TABLE_VPNPROFILE = "vpnprofile";
private static final int DATABASE_VERSION = 7;
public static final String DATABASE_CREATE =
"CREATE TABLE " + TABLE_VPNPROFILE + " (" +
KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
KEY_NAME + " TEXT NOT NULL," +
KEY_GATEWAY + " TEXT NOT NULL," +
KEY_VPN_TYPE + " TEXT NOT NULL," +
KEY_USERNAME + " TEXT," +
KEY_PASSWORD + " TEXT," +
KEY_CERTIFICATE + " TEXT," +
KEY_USER_CERTIFICATE + " TEXT," +
KEY_MTU + " INTEGER," +
KEY_PORT + " INTEGER," +
KEY_SPLIT_TUNNELING + " INTEGER" +
");";
private static final String[] ALL_COLUMNS = new String[] {
KEY_ID,
KEY_NAME,
KEY_GATEWAY,
KEY_VPN_TYPE,
KEY_USERNAME,
KEY_PASSWORD,
KEY_CERTIFICATE,
KEY_USER_CERTIFICATE,
KEY_MTU,
KEY_PORT,
KEY_SPLIT_TUNNELING,
};
private static class DatabaseHelper extends SQLiteOpenHelper
{
public DatabaseHelper(Context context)
{
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase database)
{
database.execSQL(DATABASE_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
{
Log.w(TAG, "Upgrading database from version " + oldVersion +
" to " + newVersion);
if (oldVersion < 2)
{
db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_USER_CERTIFICATE +
" TEXT;");
}
if (oldVersion < 3)
{
db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_VPN_TYPE +
" TEXT DEFAULT '';");
}
if (oldVersion < 4)
{ /* remove NOT NULL constraint from username column */
updateColumns(db);
}
if (oldVersion < 5)
{
db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_MTU +
" INTEGER;");
}
if (oldVersion < 6)
{
db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_PORT +
" INTEGER;");
}
if (oldVersion < 7)
{
db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_SPLIT_TUNNELING +
" INTEGER;");
}
}
private void updateColumns(SQLiteDatabase db)
{
db.beginTransaction();
try
{
db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " RENAME TO tmp_" + TABLE_VPNPROFILE + ";");
db.execSQL(DATABASE_CREATE);
StringBuilder insert = new StringBuilder("INSERT INTO " + TABLE_VPNPROFILE + " SELECT ");
SQLiteQueryBuilder.appendColumns(insert, ALL_COLUMNS);
db.execSQL(insert.append(" FROM tmp_" + TABLE_VPNPROFILE + ";").toString());
db.execSQL("DROP TABLE tmp_" + TABLE_VPNPROFILE + ";");
db.setTransactionSuccessful();
}
finally
{
db.endTransaction();
}
}
}
/**
* Construct a new VPN profile data source. The context is used to
* open/create the database.
* @param context context used to access the database
*/
public VpnProfileDataSource(Context context)
{
this.mContext = context;
}
/**
* Open the VPN profile data source. The database is automatically created
* if it does not yet exist. If that fails an exception is thrown.
* @return itself (allows to chain initialization calls)
* @throws SQLException if the database could not be opened or created
*/
public VpnProfileDataSource open() throws SQLException
{
if (mDbHelper == null)
{
mDbHelper = new DatabaseHelper(mContext);
mDatabase = mDbHelper.getWritableDatabase();
}
return this;
}
/**
* Close the data source.
*/
public void close()
{
if (mDbHelper != null)
{
mDbHelper.close();
mDbHelper = null;
}
}
/**
* Insert the given VPN profile into the database. On success the Id of
* the object is updated and the object returned.
*
* @param profile the profile to add
* @return the added VPN profile or null, if failed
*/
public VpnProfile insertProfile(VpnProfile profile)
{
ContentValues values = ContentValuesFromVpnProfile(profile);
long insertId = mDatabase.insert(TABLE_VPNPROFILE, null, values);
if (insertId == -1)
{
return null;
}
profile.setId(insertId);
return profile;
}
/**
* Updates the given VPN profile in the database.
* @param profile the profile to update
* @return true if update succeeded, false otherwise
*/
public boolean updateVpnProfile(VpnProfile profile)
{
long id = profile.getId();
ContentValues values = ContentValuesFromVpnProfile(profile);
return mDatabase.update(TABLE_VPNPROFILE, values, KEY_ID + " = " + id, null) > 0;
}
/**
* Delete the given VPN profile from the database.
* @param profile the profile to delete
* @return true if deleted, false otherwise
*/
public boolean deleteVpnProfile(VpnProfile profile)
{
long id = profile.getId();
return mDatabase.delete(TABLE_VPNPROFILE, KEY_ID + " = " + id, null) > 0;
}
/**
* Get a single VPN profile from the database.
* @param id the ID of the VPN profile
* @return the profile or null, if not found
*/
public VpnProfile getVpnProfile(long id)
{
VpnProfile profile = null;
Cursor cursor = mDatabase.query(TABLE_VPNPROFILE, ALL_COLUMNS,
KEY_ID + "=" + id, null, null, null, null);
if (cursor.moveToFirst())
{
profile = VpnProfileFromCursor(cursor);
}
cursor.close();
return profile;
}
/**
* Get a list of all VPN profiles stored in the database.
* @return list of VPN profiles
*/
public List<VpnProfile> getAllVpnProfiles()
{
List<VpnProfile> vpnProfiles = new ArrayList<VpnProfile>();
Cursor cursor = mDatabase.query(TABLE_VPNPROFILE, ALL_COLUMNS, null, null, null, null, null);
cursor.moveToFirst();
while (!cursor.isAfterLast())
{
VpnProfile vpnProfile = VpnProfileFromCursor(cursor);
vpnProfiles.add(vpnProfile);
cursor.moveToNext();
}
cursor.close();
return vpnProfiles;
}
private VpnProfile VpnProfileFromCursor(Cursor cursor)
{
VpnProfile profile = new VpnProfile();
profile.setId(cursor.getLong(cursor.getColumnIndex(KEY_ID)));
profile.setName(cursor.getString(cursor.getColumnIndex(KEY_NAME)));
profile.setGateway(cursor.getString(cursor.getColumnIndex(KEY_GATEWAY)));
profile.setVpnType(VpnType.fromIdentifier(cursor.getString(cursor.getColumnIndex(KEY_VPN_TYPE))));
profile.setUsername(cursor.getString(cursor.getColumnIndex(KEY_USERNAME)));
profile.setPassword(cursor.getString(cursor.getColumnIndex(KEY_PASSWORD)));
profile.setCertificateAlias(cursor.getString(cursor.getColumnIndex(KEY_CERTIFICATE)));
profile.setUserCertificateAlias(cursor.getString(cursor.getColumnIndex(KEY_USER_CERTIFICATE)));
profile.setMTU(getInt(cursor, cursor.getColumnIndex(KEY_MTU)));
profile.setPort(getInt(cursor, cursor.getColumnIndex(KEY_PORT)));
profile.setSplitTunneling(getInt(cursor, cursor.getColumnIndex(KEY_SPLIT_TUNNELING)));
return profile;
}
private ContentValues ContentValuesFromVpnProfile(VpnProfile profile)
{
ContentValues values = new ContentValues();
values.put(KEY_NAME, profile.getName());
values.put(KEY_GATEWAY, profile.getGateway());
values.put(KEY_VPN_TYPE, profile.getVpnType().getIdentifier());
values.put(KEY_USERNAME, profile.getUsername());
values.put(KEY_PASSWORD, profile.getPassword());
values.put(KEY_CERTIFICATE, profile.getCertificateAlias());
values.put(KEY_USER_CERTIFICATE, profile.getUserCertificateAlias());
values.put(KEY_MTU, profile.getMTU());
values.put(KEY_PORT, profile.getPort());
values.put(KEY_SPLIT_TUNNELING, profile.getSplitTunneling());
return values;
}
private Integer getInt(Cursor cursor, int columnIndex)
{
return cursor.isNull(columnIndex) ? null : cursor.getInt(columnIndex);
}
}
@@ -0,0 +1,94 @@
/*
* Copyright (C) 2012-2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.data;
import java.util.EnumSet;
public enum VpnType
{
/* the order here must match the items in R.array.vpn_types */
IKEV2_EAP("ikev2-eap", EnumSet.of(VpnTypeFeature.USER_PASS)),
IKEV2_CERT("ikev2-cert", EnumSet.of(VpnTypeFeature.CERTIFICATE)),
IKEV2_CERT_EAP("ikev2-cert-eap", EnumSet.of(VpnTypeFeature.USER_PASS, VpnTypeFeature.CERTIFICATE)),
IKEV2_EAP_TLS("ikev2-eap-tls", EnumSet.of(VpnTypeFeature.CERTIFICATE)),
IKEV2_BYOD_EAP("ikev2-byod-eap", EnumSet.of(VpnTypeFeature.USER_PASS, VpnTypeFeature.BYOD));
/**
* Features of a VPN type.
*/
public enum VpnTypeFeature
{
/** client certificate is required */
CERTIFICATE,
/** username and password are required */
USER_PASS,
/** enable BYOD features */
BYOD;
}
private String mIdentifier;
private EnumSet<VpnTypeFeature> mFeatures;
/**
* Enum which provides additional information about the supported VPN types.
*
* @param id identifier used to store and transmit this specific type
* @param features of the given VPN type
* @param certificate true if a client certificate is required
*/
VpnType(String id, EnumSet<VpnTypeFeature> features)
{
mIdentifier = id;
mFeatures = features;
}
/**
* The identifier used to store this value in the database
* @return identifier
*/
public String getIdentifier()
{
return mIdentifier;
}
/**
* Checks whether a feature is supported/required by this type of VPN.
*
* @return true if the feature is supported/required
*/
public boolean has(VpnTypeFeature feature)
{
return mFeatures.contains(feature);
}
/**
* Get the enum entry with the given identifier.
*
* @param identifier get the enum entry with this identifier
* @return the enum entry, or the default if not found
*/
public static VpnType fromIdentifier(String identifier)
{
for (VpnType type : VpnType.values())
{
if (identifier.equals(type.mIdentifier))
{
return type;
}
}
return VpnType.IKEV2_EAP;
}
}
@@ -0,0 +1,857 @@
/*
* Copyright (C) 2012-2015 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic;
import java.io.File;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.PrivateKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.strongswan.android.data.VpnProfile;
import org.strongswan.android.data.VpnProfileDataSource;
import org.strongswan.android.data.VpnType.VpnTypeFeature;
import org.strongswan.android.logic.VpnStateService.ErrorState;
import org.strongswan.android.logic.VpnStateService.State;
import org.strongswan.android.logic.imc.ImcState;
import org.strongswan.android.logic.imc.RemediationInstruction;
import org.strongswan.android.ui.MainActivity;
import org.strongswan.android.utils.SettingsWriter;
import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.VpnService;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.security.KeyChain;
import android.security.KeyChainException;
import android.system.OsConstants;
import android.util.Log;
public class CharonVpnService extends VpnService implements Runnable
{
private static final String TAG = CharonVpnService.class.getSimpleName();
public static final String LOG_FILE = "charon.log";
private String mLogFile;
private VpnProfileDataSource mDataSource;
private Thread mConnectionHandler;
private VpnProfile mCurrentProfile;
private volatile String mCurrentCertificateAlias;
private volatile String mCurrentUserCertificateAlias;
private VpnProfile mNextProfile;
private volatile boolean mProfileUpdated;
private volatile boolean mTerminate;
private volatile boolean mIsDisconnecting;
private VpnStateService mService;
private final Object mServiceLock = new Object();
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name)
{ /* since the service is local this is theoretically only called when the process is terminated */
synchronized (mServiceLock)
{
mService = null;
}
}
@Override
public void onServiceConnected(ComponentName name, IBinder service)
{
synchronized (mServiceLock)
{
mService = ((VpnStateService.LocalBinder)service).getService();
}
/* we are now ready to start the handler thread */
mConnectionHandler.start();
}
};
/**
* as defined in charonservice.h
*/
static final int STATE_CHILD_SA_UP = 1;
static final int STATE_CHILD_SA_DOWN = 2;
static final int STATE_AUTH_ERROR = 3;
static final int STATE_PEER_AUTH_ERROR = 4;
static final int STATE_LOOKUP_ERROR = 5;
static final int STATE_UNREACHABLE_ERROR = 6;
static final int STATE_GENERIC_ERROR = 7;
@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
if (intent != null)
{
Bundle bundle = intent.getExtras();
VpnProfile profile = null;
if (bundle != null)
{
profile = mDataSource.getVpnProfile(bundle.getLong(VpnProfileDataSource.KEY_ID));
if (profile != null)
{
String password = bundle.getString(VpnProfileDataSource.KEY_PASSWORD);
profile.setPassword(password);
}
}
setNextProfile(profile);
}
return START_NOT_STICKY;
}
@Override
public void onCreate()
{
mLogFile = getFilesDir().getAbsolutePath() + File.separator + LOG_FILE;
mDataSource = new VpnProfileDataSource(this);
mDataSource.open();
/* use a separate thread as main thread for charon */
mConnectionHandler = new Thread(this);
/* the thread is started when the service is bound */
bindService(new Intent(this, VpnStateService.class),
mServiceConnection, Service.BIND_AUTO_CREATE);
}
@Override
public void onRevoke()
{ /* the system revoked the rights grated with the initial prepare() call.
* called when the user clicks disconnect in the system's VPN dialog */
setNextProfile(null);
}
@Override
public void onDestroy()
{
mTerminate = true;
setNextProfile(null);
try
{
mConnectionHandler.join();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
if (mService != null)
{
unbindService(mServiceConnection);
}
mDataSource.close();
}
/**
* Set the profile that is to be initiated next. Notify the handler thread.
*
* @param profile the profile to initiate
*/
private void setNextProfile(VpnProfile profile)
{
synchronized (this)
{
this.mNextProfile = profile;
mProfileUpdated = true;
notifyAll();
}
}
@Override
public void run()
{
while (true)
{
synchronized (this)
{
try
{
while (!mProfileUpdated)
{
wait();
}
mProfileUpdated = false;
stopCurrentConnection();
if (mNextProfile == null)
{
setState(State.DISABLED);
if (mTerminate)
{
break;
}
}
else
{
mCurrentProfile = mNextProfile;
mNextProfile = null;
/* store this in a separate (volatile) variable to avoid
* a possible deadlock during deinitialization */
mCurrentCertificateAlias = mCurrentProfile.getCertificateAlias();
mCurrentUserCertificateAlias = mCurrentProfile.getUserCertificateAlias();
startConnection(mCurrentProfile);
mIsDisconnecting = false;
BuilderAdapter builder = new BuilderAdapter(mCurrentProfile.getName(), mCurrentProfile.getSplitTunneling());
if (initializeCharon(builder, mLogFile, mCurrentProfile.getVpnType().has(VpnTypeFeature.BYOD)))
{
Log.i(TAG, "charon started");
SettingsWriter writer = new SettingsWriter();
writer.setValue("global.language", Locale.getDefault().getLanguage());
writer.setValue("global.mtu", mCurrentProfile.getMTU());
writer.setValue("connection.type", mCurrentProfile.getVpnType().getIdentifier());
writer.setValue("connection.server", mCurrentProfile.getGateway());
writer.setValue("connection.port", mCurrentProfile.getPort());
writer.setValue("connection.username", mCurrentProfile.getUsername());
writer.setValue("connection.password", mCurrentProfile.getPassword());
initiate(writer.serialize());
}
else
{
Log.e(TAG, "failed to start charon");
setError(ErrorState.GENERIC_ERROR);
setState(State.DISABLED);
mCurrentProfile = null;
}
}
}
catch (InterruptedException ex)
{
stopCurrentConnection();
setState(State.DISABLED);
}
}
}
}
/**
* Stop any existing connection by deinitializing charon.
*/
private void stopCurrentConnection()
{
synchronized (this)
{
if (mCurrentProfile != null)
{
setState(State.DISCONNECTING);
mIsDisconnecting = true;
deinitializeCharon();
Log.i(TAG, "charon stopped");
mCurrentProfile = null;
}
}
}
/**
* Notify the state service about a new connection attempt.
* Called by the handler thread.
*
* @param profile currently active VPN profile
*/
private void startConnection(VpnProfile profile)
{
synchronized (mServiceLock)
{
if (mService != null)
{
mService.startConnection(profile);
}
}
}
/**
* Update the current VPN state on the state service. Called by the handler
* thread and any of charon's threads.
*
* @param state current state
*/
private void setState(State state)
{
synchronized (mServiceLock)
{
if (mService != null)
{
mService.setState(state);
}
}
}
/**
* Set an error on the state service. Called by the handler thread and any
* of charon's threads.
*
* @param error error state
*/
private void setError(ErrorState error)
{
synchronized (mServiceLock)
{
if (mService != null)
{
mService.setError(error);
}
}
}
/**
* Set the IMC state on the state service. Called by the handler thread and
* any of charon's threads.
*
* @param state IMC state
*/
private void setImcState(ImcState state)
{
synchronized (mServiceLock)
{
if (mService != null)
{
mService.setImcState(state);
}
}
}
/**
* Set an error on the state service. Called by the handler thread and any
* of charon's threads.
*
* @param error error state
*/
private void setErrorDisconnect(ErrorState error)
{
synchronized (mServiceLock)
{
if (mService != null)
{
if (!mIsDisconnecting)
{
mService.setError(error);
}
}
}
}
/**
* Updates the state of the current connection.
* Called via JNI by different threads (but not concurrently).
*
* @param status new state
*/
public void updateStatus(int status)
{
switch (status)
{
case STATE_CHILD_SA_DOWN:
if (!mIsDisconnecting)
{
setState(State.CONNECTING);
}
break;
case STATE_CHILD_SA_UP:
setState(State.CONNECTED);
break;
case STATE_AUTH_ERROR:
setErrorDisconnect(ErrorState.AUTH_FAILED);
break;
case STATE_PEER_AUTH_ERROR:
setErrorDisconnect(ErrorState.PEER_AUTH_FAILED);
break;
case STATE_LOOKUP_ERROR:
setErrorDisconnect(ErrorState.LOOKUP_FAILED);
break;
case STATE_UNREACHABLE_ERROR:
setErrorDisconnect(ErrorState.UNREACHABLE);
break;
case STATE_GENERIC_ERROR:
setErrorDisconnect(ErrorState.GENERIC_ERROR);
break;
default:
Log.e(TAG, "Unknown status code received");
break;
}
}
/**
* Updates the IMC state of the current connection.
* Called via JNI by different threads (but not concurrently).
*
* @param value new state
*/
public void updateImcState(int value)
{
ImcState state = ImcState.fromValue(value);
if (state != null)
{
setImcState(state);
}
}
/**
* Add a remediation instruction to the VPN state service.
* Called via JNI by different threads (but not concurrently).
*
* @param xml XML text
*/
public void addRemediationInstruction(String xml)
{
for (RemediationInstruction instruction : RemediationInstruction.fromXml(xml))
{
synchronized (mServiceLock)
{
if (mService != null)
{
mService.addRemediationInstruction(instruction);
}
}
}
}
/**
* Function called via JNI to generate a list of DER encoded CA certificates
* as byte array.
*
* @return a list of DER encoded CA certificates
*/
private byte[][] getTrustedCertificates()
{
ArrayList<byte[]> certs = new ArrayList<byte[]>();
TrustedCertificateManager certman = TrustedCertificateManager.getInstance();
try
{
String alias = this.mCurrentCertificateAlias;
if (alias != null)
{
X509Certificate cert = certman.getCACertificateFromAlias(alias);
if (cert == null)
{
return null;
}
certs.add(cert.getEncoded());
}
else
{
for (X509Certificate cert : certman.getAllCACertificates().values())
{
certs.add(cert.getEncoded());
}
}
}
catch (CertificateEncodingException e)
{
e.printStackTrace();
return null;
}
return certs.toArray(new byte[certs.size()][]);
}
/**
* Function called via JNI to get a list containing the DER encoded certificates
* of the user selected certificate chain (beginning with the user certificate).
*
* Since this method is called from a thread of charon's thread pool we are safe
* to call methods on KeyChain directly.
*
* @return list containing the certificates (first element is the user certificate)
* @throws InterruptedException
* @throws KeyChainException
* @throws CertificateEncodingException
*/
private byte[][] getUserCertificate() throws KeyChainException, InterruptedException, CertificateEncodingException
{
ArrayList<byte[]> encodings = new ArrayList<byte[]>();
X509Certificate[] chain = KeyChain.getCertificateChain(getApplicationContext(), mCurrentUserCertificateAlias);
if (chain == null || chain.length == 0)
{
return null;
}
for (X509Certificate cert : chain)
{
encodings.add(cert.getEncoded());
}
return encodings.toArray(new byte[encodings.size()][]);
}
/**
* Function called via JNI to get the private key the user selected.
*
* Since this method is called from a thread of charon's thread pool we are safe
* to call methods on KeyChain directly.
*
* @return the private key
* @throws InterruptedException
* @throws KeyChainException
* @throws CertificateEncodingException
*/
private PrivateKey getUserKey() throws KeyChainException, InterruptedException
{
return KeyChain.getPrivateKey(getApplicationContext(), mCurrentUserCertificateAlias);
}
/**
* Initialization of charon, provided by libandroidbridge.so
*
* @param builder BuilderAdapter for this connection
* @param logfile absolute path to the logfile
* @param boyd enable BYOD features
* @return TRUE if initialization was successful
*/
public native boolean initializeCharon(BuilderAdapter builder, String logfile, boolean byod);
/**
* Deinitialize charon, provided by libandroidbridge.so
*/
public native void deinitializeCharon();
/**
* Initiate VPN, provided by libandroidbridge.so
*/
public native void initiate(String config);
/**
* Adapter for VpnService.Builder which is used to access it safely via JNI.
* There is a corresponding C object to access it from native code.
*/
public class BuilderAdapter
{
private final String mName;
private final Integer mSplitTunneling;
private VpnService.Builder mBuilder;
private BuilderCache mCache;
private BuilderCache mEstablishedCache;
public BuilderAdapter(String name, Integer splitTunneling)
{
mName = name;
mSplitTunneling = splitTunneling;
mBuilder = createBuilder(name);
mCache = new BuilderCache(mSplitTunneling);
}
private VpnService.Builder createBuilder(String name)
{
VpnService.Builder builder = new CharonVpnService.Builder();
builder.setSession(mName);
/* even though the option displayed in the system dialog says "Configure"
* we just use our main Activity */
Context context = getApplicationContext();
Intent intent = new Intent(context, MainActivity.class);
PendingIntent pending = PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
builder.setConfigureIntent(pending);
return builder;
}
public synchronized boolean addAddress(String address, int prefixLength)
{
try
{
mCache.addAddress(address, prefixLength);
}
catch (IllegalArgumentException ex)
{
return false;
}
return true;
}
public synchronized boolean addDnsServer(String address)
{
try
{
mBuilder.addDnsServer(address);
mCache.recordAddressFamily(address);
}
catch (IllegalArgumentException ex)
{
return false;
}
return true;
}
public synchronized boolean addRoute(String address, int prefixLength)
{
try
{
mCache.addRoute(address, prefixLength);
}
catch (IllegalArgumentException ex)
{
return false;
}
return true;
}
public synchronized boolean addSearchDomain(String domain)
{
try
{
mBuilder.addSearchDomain(domain);
}
catch (IllegalArgumentException ex)
{
return false;
}
return true;
}
public synchronized boolean setMtu(int mtu)
{
try
{
mCache.setMtu(mtu);
}
catch (IllegalArgumentException ex)
{
return false;
}
return true;
}
public synchronized int establish()
{
ParcelFileDescriptor fd;
try
{
mCache.applyData(mBuilder);
fd = mBuilder.establish();
}
catch (Exception ex)
{
ex.printStackTrace();
return -1;
}
if (fd == null)
{
return -1;
}
/* now that the TUN device is created we don't need the current
* builder anymore, but we might need another when reestablishing */
mBuilder = createBuilder(mName);
mEstablishedCache = mCache;
mCache = new BuilderCache(mSplitTunneling);
return fd.detachFd();
}
public synchronized int establishNoDns()
{
ParcelFileDescriptor fd;
if (mEstablishedCache == null)
{
return -1;
}
try
{
Builder builder = createBuilder(mName);
mEstablishedCache.applyData(builder);
fd = builder.establish();
}
catch (Exception ex)
{
ex.printStackTrace();
return -1;
}
if (fd == null)
{
return -1;
}
return fd.detachFd();
}
}
/**
* Cache non DNS related information so we can recreate the builder without
* that information when reestablishing IKE_SAs
*/
public class BuilderCache
{
private final List<PrefixedAddress> mAddresses = new ArrayList<PrefixedAddress>();
private final List<PrefixedAddress> mRoutesIPv4 = new ArrayList<PrefixedAddress>();
private final List<PrefixedAddress> mRoutesIPv6 = new ArrayList<PrefixedAddress>();
private final int mSplitTunneling;
private int mMtu;
private boolean mIPv4Seen, mIPv6Seen;
public BuilderCache(Integer splitTunneling)
{
mSplitTunneling = splitTunneling != null ? splitTunneling : 0;
}
public void addAddress(String address, int prefixLength)
{
mAddresses.add(new PrefixedAddress(address, prefixLength));
recordAddressFamily(address);
}
public void addRoute(String address, int prefixLength)
{
try
{
if (isIPv6(address))
{
mRoutesIPv6.add(new PrefixedAddress(address, prefixLength));
}
else
{
mRoutesIPv4.add(new PrefixedAddress(address, prefixLength));
}
}
catch (UnknownHostException ex)
{
ex.printStackTrace();
}
}
public void setMtu(int mtu)
{
mMtu = mtu;
}
public void recordAddressFamily(String address)
{
try
{
if (isIPv6(address))
{
mIPv6Seen = true;
}
else
{
mIPv4Seen = true;
}
}
catch (UnknownHostException ex)
{
ex.printStackTrace();
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void applyData(VpnService.Builder builder)
{
for (PrefixedAddress address : mAddresses)
{
builder.addAddress(address.mAddress, address.mPrefix);
}
/* add routes depending on whether split tunneling is allowed or not,
* that is, whether we have to handle and block non-VPN traffic */
if ((mSplitTunneling & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) == 0)
{
if (mIPv4Seen)
{ /* split tunneling is used depending on the routes */
for (PrefixedAddress route : mRoutesIPv4)
{
builder.addRoute(route.mAddress, route.mPrefix);
}
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
{ /* allow traffic that would otherwise be blocked to bypass the VPN */
builder.allowFamily(OsConstants.AF_INET);
}
}
else if (mIPv4Seen)
{ /* only needed if we've seen any addresses. otherwise, traffic
* is blocked by default (we also install no routes in that case) */
builder.addRoute("0.0.0.0", 0);
}
/* same thing for IPv6 */
if ((mSplitTunneling & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6) == 0)
{
if (mIPv6Seen)
{
for (PrefixedAddress route : mRoutesIPv6)
{
builder.addRoute(route.mAddress, route.mPrefix);
}
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
{
builder.allowFamily(OsConstants.AF_INET6);
}
}
else if (mIPv6Seen)
{
builder.addRoute("::", 0);
}
builder.setMtu(mMtu);
}
private boolean isIPv6(String address) throws UnknownHostException
{
InetAddress addr = InetAddress.getByName(address);
if (addr instanceof Inet4Address)
{
return false;
}
else if (addr instanceof Inet6Address)
{
return true;
}
return false;
}
private class PrefixedAddress
{
public String mAddress;
public int mPrefix;
public PrefixedAddress(String address, int prefix)
{
this.mAddress = address;
this.mPrefix = prefix;
}
}
}
/*
* The libraries are extracted to /data/data/org.strongswan.android/...
* during installation. On newer releases most are loaded in JNI_OnLoad.
*/
static
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2)
{
System.loadLibrary("strongswan");
if (MainActivity.USE_BYOD)
{
System.loadLibrary("tncif");
System.loadLibrary("tnccs");
System.loadLibrary("imcv");
}
System.loadLibrary("hydra");
System.loadLibrary("charon");
System.loadLibrary("ipsec");
}
System.loadLibrary("androidbridge");
}
}
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2012-2015 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
public class NetworkManager extends BroadcastReceiver
{
private final Context mContext;
private boolean mRegistered;
public NetworkManager(Context context)
{
mContext = context;
}
public void Register()
{
mContext.registerReceiver(this, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}
public void Unregister()
{
mContext.unregisterReceiver(this);
}
public boolean isConnected()
{
ConnectivityManager cm = (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = null;
if (cm != null)
{
info = cm.getActiveNetworkInfo();
}
return info != null && info.isConnected();
}
@Override
public void onReceive(Context context, Intent intent)
{
networkChanged(!isConnected());
}
/**
* Notify the native parts about a network change
*
* @param disconnected true if no connection is available at the moment
*/
public native void networkChanged(boolean disconnected);
}
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic;
import java.security.Security;
import org.strongswan.android.security.LocalCertificateKeyStoreProvider;
import android.app.Application;
import android.content.Context;
public class StrongSwanApplication extends Application
{
private static Context mContext;
static {
Security.addProvider(new LocalCertificateKeyStoreProvider());
}
@Override
public void onCreate()
{
super.onCreate();
StrongSwanApplication.mContext = getApplicationContext();
}
/**
* Returns the current application context
* @return context
*/
public static Context getContext()
{
return StrongSwanApplication.mContext;
}
}
@@ -0,0 +1,242 @@
/*
* Copyright (C) 2012-2014 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import android.util.Log;
public class TrustedCertificateManager
{
private static final String TAG = TrustedCertificateManager.class.getSimpleName();
private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
private Hashtable<String, X509Certificate> mCACerts = new Hashtable<String, X509Certificate>();
private volatile boolean mReload;
private boolean mLoaded;
private final ArrayList<KeyStore> mKeyStores = new ArrayList<KeyStore>();
public enum TrustedCertificateSource
{
SYSTEM("system:"),
USER("user:"),
LOCAL("local:");
private final String mPrefix;
private TrustedCertificateSource(String prefix)
{
mPrefix = prefix;
}
private String getPrefix()
{
return mPrefix;
}
}
/**
* Private constructor to prevent instantiation from other classes.
*/
private TrustedCertificateManager()
{
for (String name : new String[] { "LocalCertificateStore", "AndroidCAStore" })
{
KeyStore store;
try
{
store = KeyStore.getInstance(name);
store.load(null,null);
mKeyStores.add(store);
}
catch (Exception e)
{
Log.e(TAG, "Unable to load KeyStore: " + name);
e.printStackTrace();
}
}
}
/**
* This is not instantiated until the first call to getInstance()
*/
private static class Singleton {
public static final TrustedCertificateManager mInstance = new TrustedCertificateManager();
}
/**
* Get the single instance of the CA certificate manager.
* @return CA certificate manager
*/
public static TrustedCertificateManager getInstance()
{
return Singleton.mInstance;
}
/**
* Invalidates the current load state so that the next call to load()
* will force a reload of the cached CA certificates.
* @return reference to itself
*/
public TrustedCertificateManager reset()
{
Log.d(TAG, "Force reload of cached CA certificates on next load");
this.mReload = true;
return this;
}
/**
* Ensures that the certificates are loaded but does not force a reload.
* As this takes a while if the certificates are not loaded yet it should
* be called asynchronously.
* @return reference to itself
*/
public TrustedCertificateManager load()
{
Log.d(TAG, "Ensure cached CA certificates are loaded");
this.mLock.writeLock().lock();
if (!this.mLoaded || this.mReload)
{
this.mReload = false;
loadCertificates();
}
this.mLock.writeLock().unlock();
return this;
}
/**
* Opens the CA certificate KeyStore and loads the cached certificates.
* The lock must be locked when calling this method.
*/
private void loadCertificates()
{
Log.d(TAG, "Load cached CA certificates");
Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>();
for (KeyStore store : this.mKeyStores)
{
fetchCertificates(certs, store);
}
this.mCACerts = certs;
this.mLoaded = true;
Log.d(TAG, "Cached CA certificates loaded");
}
/**
* Load all X.509 certificates from the given KeyStore.
* @param certs Hashtable to store certificates in
* @param store KeyStore to load certificates from
*/
private void fetchCertificates(Hashtable<String, X509Certificate> certs, KeyStore store)
{
try
{
Enumeration<String> aliases = store.aliases();
while (aliases.hasMoreElements())
{
String alias = aliases.nextElement();
Certificate cert;
cert = store.getCertificate(alias);
if (cert != null && cert instanceof X509Certificate)
{
certs.put(alias, (X509Certificate)cert);
}
}
}
catch (KeyStoreException ex)
{
ex.printStackTrace();
}
}
/**
* Retrieve the CA certificate with the given alias.
* @param alias alias of the certificate to get
* @return the certificate, null if not found
*/
public X509Certificate getCACertificateFromAlias(String alias)
{
X509Certificate certificate = null;
if (this.mLock.readLock().tryLock())
{
certificate = this.mCACerts.get(alias);
this.mLock.readLock().unlock();
}
else
{ /* if we cannot get the lock load it directly from the KeyStore,
* should be fast for a single certificate */
for (KeyStore store : this.mKeyStores)
{
try
{
Certificate cert = store.getCertificate(alias);
if (cert != null && cert instanceof X509Certificate)
{
certificate = (X509Certificate)cert;
break;
}
}
catch (KeyStoreException e)
{
e.printStackTrace();
}
}
}
return certificate;
}
/**
* Get all CA certificates (from all keystores).
* @return Hashtable mapping aliases to certificates
*/
@SuppressWarnings("unchecked")
public Hashtable<String, X509Certificate> getAllCACertificates()
{
Hashtable<String, X509Certificate> certs;
this.mLock.readLock().lock();
certs = (Hashtable<String, X509Certificate>)this.mCACerts.clone();
this.mLock.readLock().unlock();
return certs;
}
/**
* Get all certificates from the given source.
* @param source type to filter certificates
* @return Hashtable mapping aliases to certificates
*/
public Hashtable<String, X509Certificate> getCACertificates(TrustedCertificateSource source)
{
Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>();
this.mLock.readLock().lock();
for (String alias : this.mCACerts.keySet())
{
if (alias.startsWith(source.getPrefix()))
{
certs.put(alias, this.mCACerts.get(alias));
}
}
this.mLock.readLock().unlock();
return certs;
}
}
@@ -0,0 +1,360 @@
/*
* Copyright (C) 2012-2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import org.strongswan.android.data.VpnProfile;
import org.strongswan.android.logic.imc.ImcState;
import org.strongswan.android.logic.imc.RemediationInstruction;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
public class VpnStateService extends Service
{
private final List<VpnStateListener> mListeners = new ArrayList<VpnStateListener>();
private final IBinder mBinder = new LocalBinder();
private long mConnectionID = 0;
private Handler mHandler;
private VpnProfile mProfile;
private State mState = State.DISABLED;
private ErrorState mError = ErrorState.NO_ERROR;
private ImcState mImcState = ImcState.UNKNOWN;
private final LinkedList<RemediationInstruction> mRemediationInstructions = new LinkedList<RemediationInstruction>();
public enum State
{
DISABLED,
CONNECTING,
CONNECTED,
DISCONNECTING,
}
public enum ErrorState
{
NO_ERROR,
AUTH_FAILED,
PEER_AUTH_FAILED,
LOOKUP_FAILED,
UNREACHABLE,
GENERIC_ERROR,
}
/**
* Listener interface for bound clients that are interested in changes to
* this Service.
*/
public interface VpnStateListener
{
public void stateChanged();
}
/**
* Simple Binder that allows to directly access this Service class itself
* after binding to it.
*/
public class LocalBinder extends Binder
{
public VpnStateService getService()
{
return VpnStateService.this;
}
}
@Override
public void onCreate()
{
/* this handler allows us to notify listeners from the UI thread and
* not from the threads that actually report any state changes */
mHandler = new Handler();
}
@Override
public IBinder onBind(Intent intent)
{
return mBinder;
}
@Override
public void onDestroy()
{
}
/**
* Register a listener with this Service. We assume this is called from
* the main thread so no synchronization is happening.
*
* @param listener listener to register
*/
public void registerListener(VpnStateListener listener)
{
mListeners.add(listener);
}
/**
* Unregister a listener from this Service.
*
* @param listener listener to unregister
*/
public void unregisterListener(VpnStateListener listener)
{
mListeners.remove(listener);
}
/**
* Get the current VPN profile.
*
* @return profile
*/
public VpnProfile getProfile()
{ /* only updated from the main thread so no synchronization needed */
return mProfile;
}
/**
* Get the current connection ID. May be used to track which state
* changes have already been handled.
*
* Is increased when startConnection() is called.
*
* @return connection ID
*/
public long getConnectionID()
{ /* only updated from the main thread so no synchronization needed */
return mConnectionID;
}
/**
* Get the current state.
*
* @return state
*/
public State getState()
{ /* only updated from the main thread so no synchronization needed */
return mState;
}
/**
* Get the current error, if any.
*
* @return error
*/
public ErrorState getErrorState()
{ /* only updated from the main thread so no synchronization needed */
return mError;
}
/**
* Get the current IMC state, if any.
*
* @return imc state
*/
public ImcState getImcState()
{ /* only updated from the main thread so no synchronization needed */
return mImcState;
}
/**
* Get the remediation instructions, if any.
*
* @return read-only list of instructions
*/
public List<RemediationInstruction> getRemediationInstructions()
{ /* only updated from the main thread so no synchronization needed */
return Collections.unmodifiableList(mRemediationInstructions);
}
/**
* Disconnect any existing connection and shutdown the daemon, the
* VpnService is not stopped but it is reset so new connections can be
* started.
*/
public void disconnect()
{
/* as soon as the TUN device is created by calling establish() on the
* VpnService.Builder object the system binds to the service and keeps
* bound until the file descriptor of the TUN device is closed. thus
* calling stopService() here would not stop (destroy) the service yet,
* instead we call startService() with an empty Intent which shuts down
* the daemon (and closes the TUN device, if any) */
Context context = getApplicationContext();
Intent intent = new Intent(context, CharonVpnService.class);
context.startService(intent);
}
/**
* Update state and notify all listeners about the change. By using a Handler
* this is done from the main UI thread and not the initial reporter thread.
* Also, in doing the actual state change from the main thread, listeners
* see all changes and none are skipped.
*
* @param change the state update to perform before notifying listeners, returns true if state changed
*/
private void notifyListeners(final Callable<Boolean> change)
{
mHandler.post(new Runnable() {
@Override
public void run()
{
try
{
if (change.call())
{ /* otherwise there is no need to notify the listeners */
for (VpnStateListener listener : mListeners)
{
listener.stateChanged();
}
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
});
}
/**
* Called when a connection is started. Sets the currently active VPN
* profile, resets IMC and Error state variables, sets the State to
* CONNECTING, increases the connection ID, and notifies all listeners.
*
* May be called from threads other than the main thread.
*
* @param profile current profile
*/
public void startConnection(final VpnProfile profile)
{
notifyListeners(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception
{
VpnStateService.this.mConnectionID++;
VpnStateService.this.mProfile = profile;
VpnStateService.this.mState = State.CONNECTING;
VpnStateService.this.mError = ErrorState.NO_ERROR;
VpnStateService.this.mImcState = ImcState.UNKNOWN;
VpnStateService.this.mRemediationInstructions.clear();
return true;
}
});
}
/**
* Update the state and notify all listeners, if changed.
*
* May be called from threads other than the main thread.
*
* @param state new state
*/
public void setState(final State state)
{
notifyListeners(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception
{
if (VpnStateService.this.mState != state)
{
VpnStateService.this.mState = state;
return true;
}
return false;
}
});
}
/**
* Set the current error state and notify all listeners, if changed.
*
* May be called from threads other than the main thread.
*
* @param error error state
*/
public void setError(final ErrorState error)
{
notifyListeners(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception
{
if (VpnStateService.this.mError != error)
{
VpnStateService.this.mError = error;
return true;
}
return false;
}
});
}
/**
* Set the current IMC state and notify all listeners, if changed.
*
* Setting the state to UNKNOWN clears all remediation instructions.
*
* May be called from threads other than the main thread.
*
* @param error error state
*/
public void setImcState(final ImcState state)
{
notifyListeners(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception
{
if (state == ImcState.UNKNOWN)
{
VpnStateService.this.mRemediationInstructions.clear();
}
if (VpnStateService.this.mImcState != state)
{
VpnStateService.this.mImcState = state;
return true;
}
return false;
}
});
}
/**
* Add the given remediation instruction to the internal list. Listeners
* are not notified.
*
* Instructions are cleared if the IMC state is set to UNKNOWN.
*
* May be called from threads other than the main thread.
*
* @param instruction remediation instruction
*/
public void addRemediationInstruction(final RemediationInstruction instruction)
{
mHandler.post(new Runnable() {
@Override
public void run()
{
VpnStateService.this.mRemediationInstructions.add(instruction);
}
});
}
}
@@ -0,0 +1,99 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc;
import org.strongswan.android.logic.imc.attributes.Attribute;
import org.strongswan.android.logic.imc.attributes.AttributeType;
import org.strongswan.android.logic.imc.collectors.Collector;
import org.strongswan.android.logic.imc.collectors.DeviceIdCollector;
import org.strongswan.android.logic.imc.collectors.InstalledPackagesCollector;
import org.strongswan.android.logic.imc.collectors.PortFilterCollector;
import org.strongswan.android.logic.imc.collectors.ProductInformationCollector;
import org.strongswan.android.logic.imc.collectors.SettingsCollector;
import org.strongswan.android.logic.imc.collectors.StringVersionCollector;
import android.content.Context;
public class AndroidImc
{
private final Context mContext;
public AndroidImc(Context context)
{
mContext = context;
}
/**
* Get a measurement (the binary encoding of the requested attribute) for
* the given vendor specific attribute type.
*
* @param vendor vendor ID
* @param type vendor specific attribute type
* @return encoded attribute, or null if not available or failed
*/
public byte[] getMeasurement(int vendor, int type)
{
return getMeasurement(vendor, type, null);
}
/**
* Get a measurement (the binary encoding of the requested attribute) for
* the given vendor specific attribute type.
*
* @param vendor vendor ID
* @param type vendor specific attribute type
* @param args optional arguments for a measurement
* @return encoded attribute, or null if not available or failed
*/
public byte[] getMeasurement(int vendor, int type, String[] args)
{
AttributeType attributeType = AttributeType.fromValues(vendor, type);
Collector collector = null;
switch (attributeType)
{
case IETF_PRODUCT_INFORMATION:
collector = new ProductInformationCollector();
break;
case IETF_STRING_VERSION:
collector = new StringVersionCollector();
break;
case IETF_PORT_FILTER:
collector = new PortFilterCollector();
break;
case IETF_INSTALLED_PACKAGES:
collector = new InstalledPackagesCollector(mContext);
break;
case ITA_SETTINGS:
collector = new SettingsCollector(mContext, args);
break;
case ITA_DEVICE_ID:
collector = new DeviceIdCollector(mContext);
break;
default:
break;
}
if (collector != null)
{
Attribute attribute = collector.getMeasurement();
if (attribute != null)
{
return attribute.getEncoding();
}
}
return null;
}
}
@@ -0,0 +1,58 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc;
public enum ImcState
{
UNKNOWN(0),
ALLOW(1),
BLOCK(2),
ISOLATE(3);
private final int mValue;
private ImcState(int value)
{
mValue = value;
}
/**
* Get the numeric value of the IMC state.
* @return numeric value
*/
public int getValue()
{
return mValue;
}
/**
* Get the enum entry from a numeric value, if defined
*
* @param value numeric value
* @return the enum entry or null
*/
public static ImcState fromValue(int value)
{
for (ImcState state : ImcState.values())
{
if (state.mValue == value)
{
return state;
}
}
return null;
}
}
@@ -0,0 +1,273 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc;
import java.io.IOException;
import java.io.StringReader;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Xml;
public class RemediationInstruction implements Parcelable
{
private String mTitle;
private String mDescription;
private String mHeader;
private final List<String> mItems = new LinkedList<String>();
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeString(mTitle);
dest.writeString(mDescription);
dest.writeString(mHeader);
dest.writeStringList(mItems);
}
public static final Parcelable.Creator<RemediationInstruction> CREATOR = new Creator<RemediationInstruction>() {
@Override
public RemediationInstruction[] newArray(int size)
{
return new RemediationInstruction[size];
}
@Override
public RemediationInstruction createFromParcel(Parcel source)
{
return new RemediationInstruction(source);
}
};
private RemediationInstruction()
{
}
private RemediationInstruction(Parcel source)
{
mTitle = source.readString();
mDescription = source.readString();
mHeader = source.readString();
source.readStringList(mItems);
}
public String getTitle()
{
return mTitle;
}
private void setTitle(String title)
{
mTitle = title;
}
public String getDescription()
{
return mDescription;
}
private void setDescription(String description)
{
mDescription = description;
}
public String getHeader()
{
return mHeader;
}
private void setHeader(String header)
{
mHeader = header;
}
public List<String> getItems()
{
return Collections.unmodifiableList(mItems);
}
private void addItem(String item)
{
mItems.add(item);
}
/**
* Create a list of RemediationInstruction objects from the given XML data.
*
* @param xml XML data
* @return list of RemediationInstruction objects
*/
public static List<RemediationInstruction> fromXml(String xml)
{
List<RemediationInstruction> instructions = new LinkedList<RemediationInstruction>();
XmlPullParser parser = Xml.newPullParser();
try
{
parser.setInput(new StringReader(xml));
parser.nextTag();
readInstructions(parser, instructions);
}
catch (XmlPullParserException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}
return instructions;
}
/**
* Read a &lt;remediationinstructions&gt; element and store the extracted
* RemediationInstruction objects in the given list.
*
* @param parser
* @param instructions
* @throws XmlPullParserException
* @throws IOException
*/
private static void readInstructions(XmlPullParser parser, List<RemediationInstruction> instructions) throws XmlPullParserException, IOException
{
parser.require(XmlPullParser.START_TAG, null, "remediationinstructions");
while (parser.next() != XmlPullParser.END_TAG)
{
if (parser.getEventType() != XmlPullParser.START_TAG)
{
continue;
}
if (parser.getName().equals("instruction"))
{
RemediationInstruction instruction = new RemediationInstruction();
readInstruction(parser, instruction);
instructions.add(instruction);
}
else
{
skipTag(parser);
}
}
}
/**
* Read an &lt;instruction&gt; element and store the information in the
* given RemediationInstruction object.
*
* @param parser
* @param instruction
* @throws XmlPullParserException
* @throws IOException
*/
private static void readInstruction(XmlPullParser parser, RemediationInstruction instruction) throws XmlPullParserException, IOException
{
parser.require(XmlPullParser.START_TAG, null, "instruction");
while (parser.next() != XmlPullParser.END_TAG)
{
if (parser.getEventType() != XmlPullParser.START_TAG)
{
continue;
}
String name = parser.getName();
if (name.equals("title"))
{
instruction.setTitle(parser.nextText());
}
else if (name.equals("description"))
{
instruction.setDescription(parser.nextText());
}
else if (name.equals("itemsheader"))
{
instruction.setHeader(parser.nextText());
}
else if (name.equals("items"))
{
readItems(parser, instruction);
}
else
{
skipTag(parser);
}
}
}
/**
* Read all items of an &lt;items&gt; node and add them to the given
* RemediationInstruction object.
*
* @param parser
* @param instruction
* @throws XmlPullParserException
* @throws IOException
*/
private static void readItems(XmlPullParser parser, RemediationInstruction instruction) throws XmlPullParserException, IOException
{
while (parser.next() != XmlPullParser.END_TAG)
{
if (parser.getEventType() != XmlPullParser.START_TAG)
{
continue;
}
if (parser.getName().equals("item"))
{
instruction.addItem(parser.nextText());
}
else
{
skipTag(parser);
}
}
}
/**
* Skip the current tag and all child elements.
*
* @param parser
* @throws XmlPullParserException
* @throws IOException
*/
private static void skipTag(XmlPullParser parser) throws XmlPullParserException, IOException
{
int depth = 1;
parser.require(XmlPullParser.START_TAG, null, null);
while (depth != 0)
{
switch (parser.next())
{
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.attributes;
/**
* Interface to be implemented by attribute classes
*/
public interface Attribute
{
/**
* Returns the binary encoding of the attribute
* @return binary encoding
*/
public byte[] getEncoding();
}
@@ -0,0 +1,100 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Copyright (C) 2012 Christoph Buehler
* Copyright (C) 2012 Patrick Loetscher
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.attributes;
public enum AttributeType
{
/* IETF standard PA-TNC attribute types defined by RFC 5792 */
IETF_TESTING(PrivateEnterpriseNumber.IETF, 0),
IETF_ATTRIBUTE_REQUEST(PrivateEnterpriseNumber.IETF, 1),
IETF_PRODUCT_INFORMATION(PrivateEnterpriseNumber.IETF, 2),
IETF_NUMERIC_VERSION(PrivateEnterpriseNumber.IETF, 3),
IETF_STRING_VERSION(PrivateEnterpriseNumber.IETF, 4),
IETF_OPERATIONAL_STATUS(PrivateEnterpriseNumber.IETF, 5),
IETF_PORT_FILTER(PrivateEnterpriseNumber.IETF, 6),
IETF_INSTALLED_PACKAGES(PrivateEnterpriseNumber.IETF, 7),
IETF_PA_TNC_ERROR(PrivateEnterpriseNumber.IETF, 8),
IETF_ASSESSMENT_RESULT(PrivateEnterpriseNumber.IETF, 9),
IETF_REMEDIATION_INSTRUCTIONS(PrivateEnterpriseNumber.IETF, 10),
IETF_FORWARDING_ENABLED(PrivateEnterpriseNumber.IETF, 11),
IETF_FACTORY_DEFAULT_PWD_ENABLED(PrivateEnterpriseNumber.IETF, 12),
IETF_RESERVED(PrivateEnterpriseNumber.IETF, 0xffffffff),
/* ITA attributes */
ITA_SETTINGS(PrivateEnterpriseNumber.ITA, 4),
ITA_DEVICE_ID(PrivateEnterpriseNumber.ITA, 8);
private PrivateEnterpriseNumber mVendor;
private int mType;
/**
* Enum type for vendor specific attributes (defined in their namespace)
*
* @param vendor private enterprise number of vendor
* @param type vendor specific attribute type
*/
private AttributeType(PrivateEnterpriseNumber vendor, int type)
{
mVendor = vendor;
mType = type;
}
/**
* Get private enterprise number of vendor
*
* @return PEN
*/
public PrivateEnterpriseNumber getVendor()
{
return mVendor;
}
/**
* Get vendor specific type
*
* @return type
*/
public int getType()
{
return mType;
}
/**
* Get the enum entry from the given numeric values, if defined
*
* @param vendor vendor id
* @param type vendor specific type
* @return enum entry or null
*/
public static AttributeType fromValues(int vendor, int type)
{
PrivateEnterpriseNumber pen = PrivateEnterpriseNumber.fromValue(vendor);
if (pen == null)
{
return null;
}
for (AttributeType attr : AttributeType.values())
{
if (attr.mVendor == pen && attr.mType == type)
{
return attr;
}
}
return null;
}
}
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.attributes;
/**
* ITA Device ID attribute
*
* 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Device ID (Variable Length) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
public class DeviceIdAttribute implements Attribute
{
private String mDeviceId;
/**
* Set the device ID
* @param version version number
*/
public void setDeviceId(String deviceId)
{
this.mDeviceId = deviceId;
}
@Override
public byte[] getEncoding()
{
return mDeviceId.getBytes();
}
}
@@ -0,0 +1,67 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Copyright (C) 2012 Christoph Buehler
* Copyright (C) 2012 Patrick Loetscher
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.attributes;
import java.util.LinkedList;
import org.strongswan.android.utils.BufferedByteWriter;
import android.util.Pair;
/**
* PA-TNC Installed Packages attribute (see section 4.2.7 of RFC 5792)
*
* 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Reserved | Package Count |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Pkg Name Len | Package Name (Variable Length) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Version Len | Package Version Number (Variable Length) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
public class InstalledPackagesAttribute implements Attribute
{
private final short RESERVED = 0;
private final LinkedList<Pair<String, String>> mPackages = new LinkedList<Pair<String, String>>();
/**
* Add an installed package to this attribute.
* @param name name of the package
* @param version version number of the package
*/
public void addPackage(String name, String version)
{
mPackages.add(new Pair<String, String>(name, version));
}
@Override
public byte[] getEncoding()
{
BufferedByteWriter writer = new BufferedByteWriter();
writer.put16(RESERVED);
writer.put16((short)mPackages.size());
for (Pair<String, String> pair : mPackages)
{
writer.putLen8(pair.first.getBytes());
writer.putLen8(pair.second.getBytes());
}
return writer.toByteArray();
}
}
@@ -0,0 +1,65 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Copyright (C) 2012 Christoph Buehler
* Copyright (C) 2012 Patrick Loetscher
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.attributes;
import java.util.LinkedList;
import org.strongswan.android.logic.imc.collectors.Protocol;
import org.strongswan.android.utils.BufferedByteWriter;
import android.util.Pair;
/**
* PA-TNC Port Filter attribute (see section 4.2.6 of RFC 5792)
*
* 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Reserved |B| Protocol | Port Number |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Reserved |B| Protocol | Port Number |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
public class PortFilterAttribute implements Attribute
{
private final LinkedList<Pair<Protocol, Short>> mPorts = new LinkedList<Pair<Protocol, Short>>();
/**
* Add an open port with the given protocol and port number
* @param protocol transport protocol
* @param port port number
*/
public void addPort(Protocol protocol, short port)
{
mPorts.add(new Pair<Protocol, Short>(protocol, port));
}
@Override
public byte[] getEncoding()
{
BufferedByteWriter writer = new BufferedByteWriter();
for (Pair<Protocol, Short> port : mPorts)
{
/* we report open ports, so the BLOCKED flag is not set */
writer.put((byte)0);
writer.put(port.first.getValue());
writer.put16(port.second);
}
return writer.toByteArray();
}
}
@@ -0,0 +1,65 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.attributes;
public enum PrivateEnterpriseNumber
{
IETF(0x000000),
GOOGLE(0x002B79),
ITA(0x00902a),
UNASSIGNED(0xfffffe),
RESERVED(0xffffff);
private int mValue;
/**
* Enum for private enterprise numbers (PEN) as allocated by IANA
*
* @param value numeric value
*/
private PrivateEnterpriseNumber(int value)
{
mValue = value;
}
/**
* Get the numeric value of a PEN
*
* @return numeric value
*/
public int getValue()
{
return mValue;
}
/**
* Get the enum entry from a numeric value, if defined
*
* @param value numeric value
* @return the enum entry or null
*/
public static PrivateEnterpriseNumber fromValue(int value)
{
for (PrivateEnterpriseNumber pen : PrivateEnterpriseNumber.values())
{
if (pen.mValue == value)
{
return pen;
}
}
return null;
}
}
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Copyright (C) 2012 Christoph Buehler
* Copyright (C) 2012 Patrick Loetscher
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.attributes;
import org.strongswan.android.utils.BufferedByteWriter;
/**
* PA-TNC Product Information attribute (see section 4.2.2 of RFC 5792)
*
* 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Product Vendor ID | Product ID |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Product ID | Product Name (Variable Length) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
public class ProductInformationAttribute implements Attribute
{
private final String PRODUCT_NAME = "Android";
private final short PRODUCT_ID = 0;
@Override
public byte[] getEncoding()
{
BufferedByteWriter writer = new BufferedByteWriter();
writer.put24(PrivateEnterpriseNumber.GOOGLE.getValue());
writer.put16(PRODUCT_ID);
writer.put(PRODUCT_NAME.getBytes());
return writer.toByteArray();
}
}
@@ -0,0 +1,78 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Copyright (C) 2012 Christoph Buehler
* Copyright (C) 2012 Patrick Loetscher
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.attributes;
import java.util.LinkedList;
import org.strongswan.android.utils.BufferedByteWriter;
import android.util.Pair;
/**
* ITA Settings attribute
*
* 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Settings Count |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Name Length | Name (Variable Length) ~
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* ~ Name (Variable Length) ~
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Value Length | Value (Variable Length) ~
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* ~ Value (Variable Length) ~
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Name Length | Name (Variable Length) ~
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* ~ Name (Variable Length) ~
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Value Length | Value (Variable Length) ~
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* ~ Value (Variable Length) ~
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* ...........................
*/
public class SettingsAttribute implements Attribute
{
private final LinkedList<Pair<String, String>> mSettings = new LinkedList<Pair<String, String>>();
/**
* Add a setting to this attribute.
* @param name name of the setting
* @param value value of the setting
*/
public void addSetting(String name, String value)
{
mSettings.add(new Pair<String, String>(name, value));
}
@Override
public byte[] getEncoding()
{
BufferedByteWriter writer = new BufferedByteWriter();
writer.put32(mSettings.size());
for (Pair<String, String> pair : mSettings)
{
writer.putLen16(pair.first.getBytes());
writer.putLen16(pair.second.getBytes());
}
return writer.toByteArray();
}
}
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Copyright (C) 2012 Christoph Buehler
* Copyright (C) 2012 Patrick Loetscher
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.attributes;
import org.strongswan.android.utils.BufferedByteWriter;
/**
* PA-TNC String Version attribute (see section 4.2.4 of RFC 5792)
*
* 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Version Len | Product Version Number (Variable Length) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Build Num Len | Internal Build Number (Variable Length) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Config. Len | Configuration Version Number (Variable Length)|
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
public class StringVersionAttribute implements Attribute
{
private String mVersionNumber;
private String mBuildNumber;
/**
* Set the product version number
* @param version version number
*/
public void setProductVersionNumber(String version)
{
this.mVersionNumber = version;
}
/**
* Set the internal build number
* @param build build number
*/
public void setInternalBuildNumber(String build)
{
this.mBuildNumber = build;
}
@Override
public byte[] getEncoding()
{
BufferedByteWriter writer = new BufferedByteWriter();
writer.putLen8(mVersionNumber.getBytes());
writer.putLen8(mBuildNumber.getBytes());
/* we don't provide a configuration number */
writer.put((byte)0);
return writer.toByteArray();
}
}
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.collectors;
import org.strongswan.android.logic.imc.attributes.Attribute;
/**
* Interface for measurement collectors
*/
public interface Collector
{
/**
* This method shall return the result of a measurement, if available
* @return attribute or null
*/
public abstract Attribute getMeasurement();
}
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.collectors;
import org.strongswan.android.logic.imc.attributes.Attribute;
import org.strongswan.android.logic.imc.attributes.DeviceIdAttribute;
import android.content.ContentResolver;
import android.content.Context;
public class DeviceIdCollector implements Collector
{
private final ContentResolver mContentResolver;
public DeviceIdCollector(Context context)
{
mContentResolver = context.getContentResolver();
}
@Override
public Attribute getMeasurement()
{
String id = android.provider.Settings.Secure.getString(mContentResolver, "android_id");
if (id != null)
{
DeviceIdAttribute attribute = new DeviceIdAttribute();
attribute.setDeviceId(id);
return attribute;
}
return null;
}
}
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Copyright (C) 2012 Christoph Buehler
* Copyright (C) 2012 Patrick Loetscher
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.collectors;
import java.util.List;
import org.strongswan.android.logic.imc.attributes.Attribute;
import org.strongswan.android.logic.imc.attributes.InstalledPackagesAttribute;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
public class InstalledPackagesCollector implements Collector
{
private final PackageManager mPackageManager;
public InstalledPackagesCollector(Context context)
{
mPackageManager = context.getPackageManager();
}
@Override
public Attribute getMeasurement()
{
InstalledPackagesAttribute attribute = new InstalledPackagesAttribute();
List<PackageInfo> packages = mPackageManager.getInstalledPackages(0);
for (PackageInfo info : packages)
{
if ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 ||
info.packageName == null || info.versionName == null)
{ /* ignore packages installed in the system image */
continue;
}
attribute.addPackage(info.packageName, info.versionName);
}
return attribute;
}
}
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Copyright (C) 2012 Christoph Buehler
* Copyright (C) 2012 Patrick Loetscher
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.collectors;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.strongswan.android.logic.imc.attributes.Attribute;
import org.strongswan.android.logic.imc.attributes.PortFilterAttribute;
public class PortFilterCollector implements Collector
{
private static Pattern LISTEN = Pattern.compile("\\bLISTEN\\b");
private static Pattern PROTOCOL = Pattern.compile("\\b(tcp|udp)6?\\b");
private static Pattern PORT = Pattern.compile("[:]{1,3}(\\d{1,5})\\b(?!\\.)");
@Override
public Attribute getMeasurement()
{
PortFilterAttribute attribute = null;
try
{
Process netstat = Runtime.getRuntime().exec("netstat -n");
try
{
BufferedReader reader = new BufferedReader(new InputStreamReader(netstat.getInputStream()));
String line;
attribute = new PortFilterAttribute();
while ((line = reader.readLine()) != null)
{
if (!LISTEN.matcher(line).find())
{
continue;
}
Matcher protocolMatcher = PROTOCOL.matcher(line);
Matcher portMatcher = PORT.matcher(line);
if (protocolMatcher.find() && portMatcher.find())
{
Protocol protocol = Protocol.fromName(protocolMatcher.group());
if (protocol == null)
{
continue;
}
int port = Integer.parseInt(portMatcher.group(1));
attribute.addPort(protocol, (short)port);
}
}
}
finally
{
netstat.destroy();
}
}
catch (IOException e)
{
e.printStackTrace();
}
return attribute;
}
}
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Copyright (C) 2012 Christoph Buehler
* Copyright (C) 2012 Patrick Loetscher
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.collectors;
import org.strongswan.android.logic.imc.attributes.Attribute;
import org.strongswan.android.logic.imc.attributes.ProductInformationAttribute;
public class ProductInformationCollector implements Collector
{
@Override
public Attribute getMeasurement()
{ /* this is currently hardcoded in the attribute */
return new ProductInformationAttribute();
}
}
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.collectors;
public enum Protocol
{
TCP((byte)6, "tcp", "tcp6"),
UDP((byte)17, "udp", "udp6");
private final byte mValue;
private String[] mNames;
private Protocol(byte value, String... names)
{
mValue = value;
mNames = names;
}
/**
* Get the numeric value of the protocol.
* @return numeric value
*/
public byte getValue()
{
return mValue;
}
/**
* Get the protocol from the given protocol name, if found.
* @param name protocol name (e.g. "udp" or "tcp")
* @return enum entry or null
*/
public static Protocol fromName(String name)
{
for (Protocol protocol : Protocol.values())
{
for (String keyword : protocol.mNames)
{
if (keyword.equalsIgnoreCase(name))
{
return protocol;
}
}
}
return null;
}
}
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Copyright (C) 2012 Christoph Buehler
* Copyright (C) 2012 Patrick Loetscher
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.collectors;
import java.util.Locale;
import org.strongswan.android.logic.imc.attributes.Attribute;
import org.strongswan.android.logic.imc.attributes.SettingsAttribute;
import android.content.ContentResolver;
import android.content.Context;
public class SettingsCollector implements Collector
{
private final ContentResolver mContentResolver;
private final String[] mSettings;
public SettingsCollector(Context context, String[] args)
{
mContentResolver = context.getContentResolver();
mSettings = args;
}
@Override
public Attribute getMeasurement()
{
if (mSettings == null || mSettings.length == 0)
{
return null;
}
SettingsAttribute attribute = new SettingsAttribute();
for (String name : mSettings)
{
String value = android.provider.Settings.Secure.getString(mContentResolver, name.toLowerCase(Locale.US));
if (value == null)
{
value = android.provider.Settings.System.getString(mContentResolver, name.toLowerCase(Locale.US));
}
if (value != null)
{
attribute.addSetting(name, value);
}
}
return attribute;
}
}
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Copyright (C) 2012 Christoph Buehler
* Copyright (C) 2012 Patrick Loetscher
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic.imc.collectors;
import org.strongswan.android.logic.imc.attributes.Attribute;
import org.strongswan.android.logic.imc.attributes.StringVersionAttribute;
public class StringVersionCollector implements Collector
{
@Override
public Attribute getMeasurement()
{
StringVersionAttribute attribute = new StringVersionAttribute();
attribute.setProductVersionNumber(android.os.Build.VERSION.RELEASE);
attribute.setInternalBuildNumber(android.os.Build.DISPLAY);
return attribute;
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.security;
import java.security.Provider;
public class LocalCertificateKeyStoreProvider extends Provider
{
private static final long serialVersionUID = 3515038332469843219L;
public LocalCertificateKeyStoreProvider()
{
super("LocalCertificateKeyStoreProvider", 1.0, "KeyStore provider for local certificates");
put("KeyStore.LocalCertificateStore", LocalCertificateKeyStoreSpi.class.getName());
}
}
@@ -0,0 +1,139 @@
/*
* Copyright (C) 2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.security;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Key;
import java.security.KeyStoreException;
import java.security.KeyStoreSpi;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
public class LocalCertificateKeyStoreSpi extends KeyStoreSpi
{
private final LocalCertificateStore mStore = new LocalCertificateStore();
@Override
public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException
{
return null;
}
@Override
public Certificate[] engineGetCertificateChain(String alias)
{
return null;
}
@Override
public Certificate engineGetCertificate(String alias)
{
return mStore.getCertificate(alias);
}
@Override
public Date engineGetCreationDate(String alias)
{
return mStore.getCreationDate(alias);
}
@Override
public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) throws KeyStoreException
{
throw new UnsupportedOperationException();
}
@Override
public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) throws KeyStoreException
{
throw new UnsupportedOperationException();
}
@Override
public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException
{
/* we ignore the given alias as the store calculates it on its own,
* duplicates are replaced */
if (!mStore.addCertificate(cert))
{
throw new KeyStoreException();
}
}
@Override
public void engineDeleteEntry(String alias) throws KeyStoreException
{
mStore.deleteCertificate(alias);
}
@Override
public Enumeration<String> engineAliases()
{
return Collections.enumeration(mStore.aliases());
}
@Override
public boolean engineContainsAlias(String alias)
{
return mStore.containsAlias(alias);
}
@Override
public int engineSize()
{
return mStore.aliases().size();
}
@Override
public boolean engineIsKeyEntry(String alias)
{
return false;
}
@Override
public boolean engineIsCertificateEntry(String alias)
{
return engineContainsAlias(alias);
}
@Override
public String engineGetCertificateAlias(Certificate cert)
{
return mStore.getCertificateAlias(cert);
}
@Override
public void engineStore(OutputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException
{
throw new UnsupportedOperationException();
}
@Override
public void engineLoad(InputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException
{
if (stream != null)
{
throw new UnsupportedOperationException();
}
}
}
@@ -0,0 +1,230 @@
/*
* Copyright (C) 2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.security;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Date;
import java.util.regex.Pattern;
import org.strongswan.android.logic.StrongSwanApplication;
import org.strongswan.android.utils.Utils;
import android.content.Context;
public class LocalCertificateStore
{
private static final String FILE_PREFIX = "certificate-";
private static final String ALIAS_PREFIX = "local:";
private static final Pattern ALIAS_PATTERN = Pattern.compile("^" + ALIAS_PREFIX + "[0-9a-f]{40}$");
/**
* Add the given certificate to the store
* @param cert the certificate to add
* @return true if successful
*/
public boolean addCertificate(Certificate cert)
{
if (!(cert instanceof X509Certificate))
{ /* only accept X.509 certificates */
return false;
}
String keyid = getKeyId(cert);
if (keyid == null)
{
return false;
}
FileOutputStream out;
try
{
/* we replace any existing file with the same alias */
out = StrongSwanApplication.getContext().openFileOutput(FILE_PREFIX + keyid, Context.MODE_PRIVATE);
try
{
out.write(cert.getEncoded());
return true;
}
catch (CertificateEncodingException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
try
{
out.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
return false;
}
/**
* Delete the certificate with the given alias
* @param alias a certificate's alias
*/
public void deleteCertificate(String alias)
{
if (ALIAS_PATTERN.matcher(alias).matches())
{
alias = alias.substring(ALIAS_PREFIX.length());
StrongSwanApplication.getContext().deleteFile(FILE_PREFIX + alias);
}
}
/**
* Retrieve the certificate with the given alias
* @param alias a certificate's alias
* @return certificate object or null
*/
public X509Certificate getCertificate(String alias)
{
if (!ALIAS_PATTERN.matcher(alias).matches())
{
return null;
}
alias = alias.substring(ALIAS_PREFIX.length());
try
{
FileInputStream in = StrongSwanApplication.getContext().openFileInput(FILE_PREFIX + alias);
try
{
CertificateFactory factory = CertificateFactory.getInstance("X.509");
X509Certificate certificate = (X509Certificate)factory.generateCertificate(in);
return certificate;
}
catch (CertificateException e)
{
e.printStackTrace();
}
finally
{
try
{
in.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
return null;
}
/**
* Returns the creation date of the certificate with the given alias
* @param alias certificate alias
* @return creation date or null if not found
*/
public Date getCreationDate(String alias)
{
if (!ALIAS_PATTERN.matcher(alias).matches())
{
return null;
}
alias = alias.substring(ALIAS_PREFIX.length());
File file = StrongSwanApplication.getContext().getFileStreamPath(FILE_PREFIX + alias);
return file.exists() ? new Date(file.lastModified()) : null;
}
/**
* Returns a list of all known certificate aliases
* @return list of aliases
*/
public ArrayList<String> aliases()
{
ArrayList<String> list = new ArrayList<String>();
for (String file : StrongSwanApplication.getContext().fileList())
{
if (file.startsWith(FILE_PREFIX))
{
list.add(ALIAS_PREFIX + file.substring(FILE_PREFIX.length()));
}
}
return list;
}
/**
* Check if the store contains a certificate with the given alias
* @param alias certificate alias
* @return true if the store contains the certificate
*/
public boolean containsAlias(String alias)
{
return getCreationDate(alias) != null;
}
/**
* Returns a certificate alias based on a SHA-1 hash of the public key.
*
* @param cert certificate to get an alias for
* @return hex encoded alias, or null if failed
*/
public String getCertificateAlias(Certificate cert)
{
String keyid = getKeyId(cert);
return keyid != null ? ALIAS_PREFIX + keyid : null;
}
/**
* Calculates the SHA-1 hash of the public key of the given certificate.
* @param cert certificate to get the key ID from
* @return hex encoded SHA-1 hash of the public key or null if failed
*/
private String getKeyId(Certificate cert)
{
MessageDigest md;
try
{
md = java.security.MessageDigest.getInstance("SHA1");
byte[] hash = md.digest(cert.getPublicKey().getEncoded());
return Utils.bytesToHex(hash);
}
catch (NoSuchAlgorithmException e)
{
e.printStackTrace();
}
return null;
}
}
@@ -0,0 +1,133 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.security;
import java.security.cert.X509Certificate;
import android.net.http.SslCertificate;
public class TrustedCertificateEntry implements Comparable<TrustedCertificateEntry>
{
private final X509Certificate mCert;
private final String mAlias;
private String mSubjectPrimary;
private String mSubjectSecondary = "";
private String mString;
/**
* Create an entry for certificate lists.
*
* @param alias alias of the certificate (as used in the KeyStore)
* @param cert certificate associated with that alias
*/
public TrustedCertificateEntry(String alias, X509Certificate cert)
{
mCert = cert;
mAlias = alias;
SslCertificate ssl = new SslCertificate(mCert);
String o = ssl.getIssuedTo().getOName();
String ou = ssl.getIssuedTo().getUName();
String cn = ssl.getIssuedTo().getCName();
if (!o.isEmpty())
{
mSubjectPrimary = o;
if (!cn.isEmpty())
{
mSubjectSecondary = cn;
}
else if (!ou.isEmpty())
{
mSubjectSecondary = ou;
}
}
else if (!cn.isEmpty())
{
mSubjectPrimary = cn;
}
else
{
mSubjectPrimary = ssl.getIssuedTo().getDName();
}
}
/**
* The main subject of this certificate (O, CN or the complete DN, whatever
* is found first).
*
* @return the main subject
*/
public String getSubjectPrimary()
{
return mSubjectPrimary;
}
/**
* Get the secondary subject of this certificate (either CN or OU if primary
* subject is O, empty otherwise)
*
* @return the secondary subject
*/
public String getSubjectSecondary()
{
return mSubjectSecondary;
}
/**
* The alias associated with this certificate.
*
* @return KeyStore alias of this certificate
*/
public String getAlias()
{
return mAlias;
}
/**
* The certificate.
*
* @return certificate
*/
public X509Certificate getCertificate()
{
return mCert;
}
@Override
public String toString()
{ /* combination of both subject lines, used for filtering lists */
if (mString == null)
{
mString = mSubjectPrimary;
if (!mSubjectSecondary.isEmpty())
{
mString += ", " + mSubjectSecondary;
}
}
return mString;
}
@Override
public int compareTo(TrustedCertificateEntry another)
{
int diff = mSubjectPrimary.compareToIgnoreCase(another.mSubjectPrimary);
if (diff == 0)
{
diff = mSubjectSecondary.compareToIgnoreCase(another.mSubjectSecondary);
}
return diff;
}
}
@@ -0,0 +1,80 @@
/*
* Copyright (C) 2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import org.strongswan.android.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.os.Bundle;
/**
* Class that displays a confirmation dialog to delete a selected local
* certificate.
*/
public class CertificateDeleteConfirmationDialog extends DialogFragment
{
public static final String ALIAS = "alias";
OnCertificateDeleteListener mListener;
/**
* Interface that can be implemented by parent activities to get the
* alias of the certificate to delete, if the user confirms the deletion.
*/
public interface OnCertificateDeleteListener
{
public void onDelete(String alias);
}
@Override
public void onAttach(Activity activity)
{
super.onAttach(activity);
if (activity instanceof OnCertificateDeleteListener)
{
mListener = (OnCertificateDeleteListener)activity;
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
return new AlertDialog.Builder(getActivity())
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.delete_certificate_question)
.setMessage(R.string.delete_certificate)
.setPositiveButton(R.string.delete_profile, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int whichButton)
{
if (mListener != null)
{
mListener.onDelete(getArguments().getString(ALIAS));
}
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which)
{
dismiss();
}
}).create();
}
}
@@ -0,0 +1,207 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import java.util.ArrayList;
import org.strongswan.android.R;
import org.strongswan.android.logic.VpnStateService;
import org.strongswan.android.logic.VpnStateService.VpnStateListener;
import org.strongswan.android.logic.imc.ImcState;
import org.strongswan.android.logic.imc.RemediationInstruction;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
public class ImcStateFragment extends Fragment implements VpnStateListener
{
private TextView mStateView;
private TextView mAction;
private LinearLayout mButton;
private VpnStateService mService;
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name)
{
mService = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service)
{
mService = ((VpnStateService.LocalBinder)service).getService();
mService.registerListener(ImcStateFragment.this);
updateView();
}
};
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
/* bind to the service only seems to work from the ApplicationContext */
Context context = getActivity().getApplicationContext();
context.bindService(new Intent(context, VpnStateService.class),
mServiceConnection, Service.BIND_AUTO_CREATE);
/* hide it initially */
getFragmentManager().beginTransaction().hide(this).commit();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.imc_state_fragment, container, false);
mButton = (LinearLayout)view.findViewById(R.id.imc_state_button);
mButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v)
{
Intent intent;
if (mService != null && !mService.getRemediationInstructions().isEmpty())
{
intent = new Intent(getActivity(), RemediationInstructionsActivity.class);
intent.putParcelableArrayListExtra(RemediationInstructionsFragment.EXTRA_REMEDIATION_INSTRUCTIONS,
new ArrayList<RemediationInstruction>(mService.getRemediationInstructions()));
}
else
{
intent = new Intent(getActivity(), LogActivity.class);
}
startActivity(intent);
}
});
final GestureDetector gestures = new GestureDetector(getActivity(), new GestureDetector.SimpleOnGestureListener() {
/* a better value would be getScaledTouchExplorationTapSlop() but that is hidden */
private final int mMinDistance = ViewConfiguration.get(getActivity()).getScaledTouchSlop() * 4;
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
{
if (Math.abs(e1.getX() - e2.getX()) >= mMinDistance)
{ /* only if the user swiped a minimum horizontal distance */
if (mService != null)
{
mService.setImcState(ImcState.UNKNOWN);
}
return true;
}
return false;
}
});
mButton.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event)
{
return gestures.onTouchEvent(event);
}
});
mStateView = (TextView)view.findViewById(R.id.imc_state);
mAction = (TextView)view.findViewById(R.id.action);
return view;
}
@Override
public void onStart()
{
super.onStart();
if (mService != null)
{
mService.registerListener(this);
updateView();
}
}
@Override
public void onStop()
{
super.onStop();
if (mService != null)
{
mService.unregisterListener(this);
}
}
@Override
public void onDestroy()
{
super.onDestroy();
if (mService != null)
{
getActivity().getApplicationContext().unbindService(mServiceConnection);
}
}
@Override
public void stateChanged()
{
updateView();
}
public void updateView()
{
FragmentManager fm = getFragmentManager();
if (fm == null)
{
return;
}
FragmentTransaction ft = fm.beginTransaction();
switch (mService.getImcState())
{
case UNKNOWN:
case ALLOW:
ft.hide(this);
break;
case ISOLATE:
mStateView.setText(R.string.imc_state_isolate);
mStateView.setTextColor(getResources().getColor(R.color.warning_text));
ft.show(this);
break;
case BLOCK:
mStateView.setText(R.string.imc_state_block);
mStateView.setTextColor(getResources().getColor(R.color.error_text));
ft.show(this);
break;
}
ft.commit();
mAction.setText(mService.getRemediationInstructions().isEmpty() ? R.string.show_log
: R.string.show_remediation_instructions);
}
}
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import java.io.File;
import org.strongswan.android.R;
import org.strongswan.android.data.LogContentProvider;
import org.strongswan.android.logic.CharonVpnService;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
public class LogActivity extends Activity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.log_activity);
getActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
getMenuInflater().inflate(R.menu.log, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
finish();
return true;
case R.id.menu_send_log:
File logfile = new File(getFilesDir(), CharonVpnService.LOG_FILE);
if (!logfile.exists() || logfile.length() == 0)
{
Toast.makeText(this, getString(R.string.empty_log), Toast.LENGTH_SHORT).show();
return true;
}
String version = "";
try
{
version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
}
catch (NameNotFoundException e)
{
e.printStackTrace();
}
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_EMAIL, new String[] { MainActivity.CONTACT_EMAIL });
intent.putExtra(Intent.EXTRA_SUBJECT, String.format(getString(R.string.log_mail_subject), version));
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_STREAM, LogContentProvider.createContentUri());
startActivity(Intent.createChooser(intent, getString(R.string.send_log)));
return true;
}
return super.onOptionsItemSelected(item);
}
}
@@ -0,0 +1,227 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.StringReader;
import org.strongswan.android.R;
import org.strongswan.android.logic.CharonVpnService;
import android.app.Fragment;
import android.os.Bundle;
import android.os.FileObserver;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class LogFragment extends Fragment implements Runnable
{
private String mLogFilePath;
private Handler mLogHandler;
private TextView mLogView;
private LogScrollView mScrollView;
private BufferedReader mReader;
private Thread mThread;
private volatile boolean mRunning;
private FileObserver mDirectoryObserver;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
mLogFilePath = getActivity().getFilesDir() + File.separator + CharonVpnService.LOG_FILE;
/* use a handler to update the log view */
mLogHandler = new Handler();
mDirectoryObserver = new LogDirectoryObserver(getActivity().getFilesDir().getAbsolutePath());
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.log_fragment, null);
mLogView = (TextView)view.findViewById(R.id.log_view);
mScrollView = (LogScrollView)view.findViewById(R.id.scroll_view);
return view;
}
@Override
public void onStart()
{
super.onStart();
startLogReader();
mDirectoryObserver.startWatching();
}
@Override
public void onStop()
{
super.onStop();
mDirectoryObserver.stopWatching();
stopLogReader();
}
/**
* Start reading from the log file
*/
private void startLogReader()
{
try
{
mReader = new BufferedReader(new FileReader(mLogFilePath));
}
catch (FileNotFoundException e)
{
mReader = new BufferedReader(new StringReader(""));
}
mLogView.setText("");
mRunning = true;
mThread = new Thread(this);
mThread.start();
}
/**
* Stop reading from the log file
*/
private void stopLogReader()
{
try
{
mRunning = false;
mThread.interrupt();
mThread.join();
}
catch (InterruptedException e)
{
}
}
/**
* Write the given log line to the TextView. We strip the prefix off to save
* some space (it is not that helpful for regular users anyway).
*
* @param line log line to log
*/
public void logLine(final String line)
{
mLogHandler.post(new Runnable() {
@Override
public void run()
{
/* strip off prefix (month=3, day=2, time=8, thread=2, spaces=3) */
mLogView.append((line.length() > 18 ? line.substring(18) : line) + '\n');
/* calling autoScroll() directly does not work, probably because content
* is not yet updated, so we post this to be done later */
mScrollView.post(new Runnable() {
@Override
public void run()
{
mScrollView.autoScroll();
}
});
}
});
}
@Override
public void run()
{
while (mRunning)
{
try
{ /* this works as long as the file is not truncated */
String line = mReader.readLine();
if (line == null)
{ /* wait until there is more to log */
Thread.sleep(1000);
}
else
{
logLine(line);
}
}
catch (Exception e)
{
break;
}
}
}
/**
* FileObserver that checks for changes regarding the log file. Since charon
* truncates it (for which there is no explicit event) we check for any modification
* to the file, keep track of the file size and reopen it if it got smaller.
*/
private class LogDirectoryObserver extends FileObserver
{
private final File mFile;
private long mSize;
public LogDirectoryObserver(String path)
{
super(path, FileObserver.CREATE | FileObserver.MODIFY | FileObserver.DELETE);
mFile = new File(mLogFilePath);
mSize = mFile.length();
}
@Override
public void onEvent(int event, String path)
{
if (path == null || !path.equals(CharonVpnService.LOG_FILE))
{
return;
}
switch (event)
{ /* even though we only subscribed for these we check them,
* as strange events are sometimes received */
case FileObserver.CREATE:
case FileObserver.DELETE:
restartLogReader();
break;
case FileObserver.MODIFY:
/* if the size got smaller reopen the log file, as it was probably truncated */
long size = mFile.length();
if (size < mSize)
{
restartLogReader();
}
mSize = size;
break;
}
}
private void restartLogReader()
{
/* we are called from a separate thread, so we use the handler */
mLogHandler.post(new Runnable() {
@Override
public void run()
{
stopLogReader();
startLogReader();
}
});
}
}
}
@@ -0,0 +1,78 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ScrollView;
public class LogScrollView extends ScrollView
{
private boolean mAutoScroll = true;
public LogScrollView(Context context)
{
super(context);
}
public LogScrollView(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public LogScrollView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
}
@Override
public boolean onTouchEvent(MotionEvent ev)
{
/* disable auto-scrolling when the user starts scrolling around */
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN)
{
mAutoScroll = false;
}
return super.onTouchEvent(ev);
}
/**
* Call this to move newly added content into view by scrolling to the bottom.
* Nothing happens if auto-scrolling is disabled.
*/
public void autoScroll()
{
if (mAutoScroll)
{
fullScroll(View.FOCUS_DOWN);
}
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt)
{
super.onScrollChanged(l, t, oldl, oldt);
/* if the user scrolls to the bottom we enable auto-scrolling again */
if (t == getChildAt(getChildCount() - 1).getHeight() - getHeight())
{
mAutoScroll = true;
}
}
}
@@ -0,0 +1,440 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import org.strongswan.android.R;
import org.strongswan.android.data.VpnProfile;
import org.strongswan.android.data.VpnProfileDataSource;
import org.strongswan.android.data.VpnType.VpnTypeFeature;
import org.strongswan.android.logic.CharonVpnService;
import org.strongswan.android.logic.TrustedCertificateManager;
import org.strongswan.android.logic.VpnStateService;
import org.strongswan.android.logic.VpnStateService.State;
import org.strongswan.android.ui.VpnProfileListFragment.OnVpnProfileSelectedListener;
import android.app.ActionBar;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.app.Service;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.VpnService;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.IBinder;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.EditText;
import android.widget.Toast;
public class MainActivity extends Activity implements OnVpnProfileSelectedListener
{
public static final String CONTACT_EMAIL = "[email protected]";
public static final String START_PROFILE = "org.strongswan.android.action.START_PROFILE";
public static final String EXTRA_VPN_PROFILE_ID = "org.strongswan.android.VPN_PROFILE_ID";
/** Use "bring your own device" (BYOD) features */
public static final boolean USE_BYOD = true;
private static final int PREPARE_VPN_SERVICE = 0;
private static final String PROFILE_NAME = "org.strongswan.android.MainActivity.PROFILE_NAME";
private static final String PROFILE_REQUIRES_PASSWORD = "org.strongswan.android.MainActivity.REQUIRES_PASSWORD";
private static final String PROFILE_RECONNECT = "org.strongswan.android.MainActivity.RECONNECT";
private static final String DIALOG_TAG = "Dialog";
private Bundle mProfileInfo;
private VpnStateService mService;
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name)
{
mService = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service)
{
mService = ((VpnStateService.LocalBinder)service).getService();
if (START_PROFILE.equals(getIntent().getAction()))
{
startVpnProfile(getIntent());
}
}
};
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.main);
this.bindService(new Intent(this, VpnStateService.class),
mServiceConnection, Service.BIND_AUTO_CREATE);
ActionBar bar = getActionBar();
bar.setDisplayShowTitleEnabled(false);
/* load CA certificates in a background task */
new LoadCertificatesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@Override
protected void onDestroy()
{
super.onDestroy();
if (mService != null)
{
this.unbindService(mServiceConnection);
}
}
/**
* Due to launchMode=singleTop this is called if the Activity already exists
*/
@Override
protected void onNewIntent(Intent intent)
{
super.onNewIntent(intent);
if (START_PROFILE.equals(intent.getAction()))
{
startVpnProfile(intent);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case R.id.menu_manage_certs:
Intent certIntent = new Intent(this, TrustedCertificatesActivity.class);
startActivity(certIntent);
return true;
case R.id.menu_show_log:
Intent logIntent = new Intent(this, LogActivity.class);
startActivity(logIntent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* Prepare the VpnService. If this succeeds the current VPN profile is
* started.
* @param profileInfo a bundle containing the information about the profile to be started
*/
protected void prepareVpnService(Bundle profileInfo)
{
Intent intent;
try
{
intent = VpnService.prepare(this);
}
catch (IllegalStateException ex)
{
/* this happens if the always-on VPN feature (Android 4.2+) is activated */
VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported_during_lockdown);
return;
}
/* store profile info until the user grants us permission */
mProfileInfo = profileInfo;
if (intent != null)
{
try
{
startActivityForResult(intent, PREPARE_VPN_SERVICE);
}
catch (ActivityNotFoundException ex)
{
/* it seems some devices, even though they come with Android 4,
* don't have the VPN components built into the system image.
* com.android.vpndialogs/com.android.vpndialogs.ConfirmDialog
* will not be found then */
VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported);
}
}
else
{ /* user already granted permission to use VpnService */
onActivityResult(PREPARE_VPN_SERVICE, RESULT_OK, null);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (requestCode)
{
case PREPARE_VPN_SERVICE:
if (resultCode == RESULT_OK && mProfileInfo != null)
{
Intent intent = new Intent(this, CharonVpnService.class);
intent.putExtras(mProfileInfo);
this.startService(intent);
}
break;
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
public void onVpnProfileSelected(VpnProfile profile)
{
Bundle profileInfo = new Bundle();
profileInfo.putLong(VpnProfileDataSource.KEY_ID, profile.getId());
profileInfo.putString(VpnProfileDataSource.KEY_USERNAME, profile.getUsername());
profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, profile.getPassword());
profileInfo.putBoolean(PROFILE_REQUIRES_PASSWORD, profile.getVpnType().has(VpnTypeFeature.USER_PASS));
profileInfo.putString(PROFILE_NAME, profile.getName());
removeFragmentByTag(DIALOG_TAG);
if (mService != null && mService.getState() == State.CONNECTED)
{
profileInfo.putBoolean(PROFILE_RECONNECT, mService.getProfile().getId() == profile.getId());
ConfirmationDialog dialog = new ConfirmationDialog();
dialog.setArguments(profileInfo);
dialog.show(this.getFragmentManager(), DIALOG_TAG);
return;
}
startVpnProfile(profileInfo);
}
/**
* Start the given VPN profile asking the user for a password if required.
* @param profileInfo data about the profile
*/
private void startVpnProfile(Bundle profileInfo)
{
if (profileInfo.getBoolean(PROFILE_REQUIRES_PASSWORD) &&
profileInfo.getString(VpnProfileDataSource.KEY_PASSWORD) == null)
{
LoginDialog login = new LoginDialog();
login.setArguments(profileInfo);
login.show(getFragmentManager(), DIALOG_TAG);
return;
}
prepareVpnService(profileInfo);
}
/**
* Start the VPN profile referred to by the given intent. Displays an error
* if the profile doesn't exist.
* @param intent Intent that caused us to start this
*/
private void startVpnProfile(Intent intent)
{
long profileId = intent.getLongExtra(EXTRA_VPN_PROFILE_ID, 0);
if (profileId <= 0)
{ /* invalid invocation */
return;
}
VpnProfileDataSource dataSource = new VpnProfileDataSource(this);
dataSource.open();
VpnProfile profile = dataSource.getVpnProfile(profileId);
dataSource.close();
if (profile != null)
{
onVpnProfileSelected(profile);
}
else
{
Toast.makeText(this, R.string.profile_not_found, Toast.LENGTH_LONG).show();
}
}
/**
* Class that loads the cached CA certificates.
*/
private class LoadCertificatesTask extends AsyncTask<Void, Void, TrustedCertificateManager>
{
@Override
protected void onPreExecute()
{
setProgressBarIndeterminateVisibility(true);
}
@Override
protected TrustedCertificateManager doInBackground(Void... params)
{
return TrustedCertificateManager.getInstance().load();
}
@Override
protected void onPostExecute(TrustedCertificateManager result)
{
setProgressBarIndeterminateVisibility(false);
}
}
/**
* Dismiss dialog if shown
*/
public void removeFragmentByTag(String tag)
{
FragmentManager fm = getFragmentManager();
Fragment login = fm.findFragmentByTag(tag);
if (login != null)
{
FragmentTransaction ft = fm.beginTransaction();
ft.remove(login);
ft.commit();
}
}
/**
* Class that displays a confirmation dialog if a VPN profile is already connected
* and then initiates the selected VPN profile if the user confirms the dialog.
*/
public static class ConfirmationDialog extends DialogFragment
{
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
final Bundle profileInfo = getArguments();
int icon = android.R.drawable.ic_dialog_alert;
int title = R.string.connect_profile_question;
int message = R.string.replaces_active_connection;
int button = R.string.connect;
if (profileInfo.getBoolean(PROFILE_RECONNECT))
{
icon = android.R.drawable.ic_dialog_info;
title = R.string.vpn_connected;
message = R.string.vpn_profile_connected;
button = R.string.reconnect;
}
return new AlertDialog.Builder(getActivity())
.setIcon(icon)
.setTitle(String.format(getString(title), profileInfo.getString(PROFILE_NAME)))
.setMessage(message)
.setPositiveButton(button, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int whichButton)
{
MainActivity activity = (MainActivity)getActivity();
activity.startVpnProfile(profileInfo);
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which)
{
dismiss();
}
}).create();
}
}
/**
* Class that displays a login dialog and initiates the selected VPN
* profile if the user confirms the dialog.
*/
public static class LoginDialog extends DialogFragment
{
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
final Bundle profileInfo = getArguments();
LayoutInflater inflater = getActivity().getLayoutInflater();
View view = inflater.inflate(R.layout.login_dialog, null);
EditText username = (EditText)view.findViewById(R.id.username);
username.setText(profileInfo.getString(VpnProfileDataSource.KEY_USERNAME));
final EditText password = (EditText)view.findViewById(R.id.password);
Builder adb = new AlertDialog.Builder(getActivity());
adb.setView(view);
adb.setTitle(getString(R.string.login_title));
adb.setPositiveButton(R.string.login_confirm, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int whichButton)
{
MainActivity activity = (MainActivity)getActivity();
profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, password.getText().toString().trim());
activity.prepareVpnService(profileInfo);
}
});
adb.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which)
{
dismiss();
}
});
return adb.create();
}
}
/**
* Class representing an error message which is displayed if VpnService is
* not supported on the current device.
*/
public static class VpnNotSupportedError extends DialogFragment
{
static final String ERROR_MESSAGE_ID = "org.strongswan.android.VpnNotSupportedError.MessageId";
public static void showWithMessage(Activity activity, int messageId)
{
Bundle bundle = new Bundle();
bundle.putInt(ERROR_MESSAGE_ID, messageId);
VpnNotSupportedError dialog = new VpnNotSupportedError();
dialog.setArguments(bundle);
dialog.show(activity.getFragmentManager(), DIALOG_TAG);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
final Bundle arguments = getArguments();
final int messageId = arguments.getInt(ERROR_MESSAGE_ID);
return new AlertDialog.Builder(getActivity())
.setTitle(R.string.vpn_not_supported_title)
.setMessage(messageId)
.setCancelable(false)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id)
{
dialog.dismiss();
}
}).create();
}
}
}
@@ -0,0 +1,109 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import org.strongswan.android.R;
import org.strongswan.android.logic.imc.RemediationInstruction;
import android.app.ListFragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
public class RemediationInstructionFragment extends ListFragment
{
public static final String ARG_REMEDIATION_INSTRUCTION = "instruction";
private RemediationInstruction mInstruction = null;
private TextView mTitle;
private TextView mDescription;
private TextView mHeader;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
return inflater.inflate(R.layout.remediation_instruction, container, false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null)
{
mInstruction = savedInstanceState.getParcelable(ARG_REMEDIATION_INSTRUCTION);
}
/* show dividers only between list items */
getListView().setHeaderDividersEnabled(false);
getListView().setFooterDividersEnabled(false);
/* don't show loader while adapter is not set */
setListShown(true);
mTitle = (TextView)getView().findViewById(R.id.title);
mDescription = (TextView)getView().findViewById(R.id.description);
mHeader = (TextView)getView().findViewById(R.id.list_header);
}
@Override
public void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putParcelable(ARG_REMEDIATION_INSTRUCTION, mInstruction);
}
@Override
public void onStart()
{
super.onStart();
Bundle args = getArguments();
if (args != null)
{
mInstruction = args.getParcelable(ARG_REMEDIATION_INSTRUCTION);
}
updateView(mInstruction);
}
public void updateView(RemediationInstruction instruction)
{
mInstruction = instruction;
if (mInstruction != null)
{
mTitle.setText(mInstruction.getTitle());
mDescription.setText(mInstruction.getDescription());
if (mInstruction.getHeader() != null)
{
mHeader.setText(mInstruction.getHeader());
setListAdapter(new ArrayAdapter<String>(getActivity(),
android.R.layout.simple_list_item_1, mInstruction.getItems()));
}
else
{
mHeader.setText("");
setListAdapter(null);
}
}
else
{
mTitle.setText("");
mDescription.setText("");
mHeader.setText("");
setListAdapter(null);
}
}
}
@@ -0,0 +1,94 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import java.util.ArrayList;
import org.strongswan.android.R;
import org.strongswan.android.logic.imc.RemediationInstruction;
import org.strongswan.android.ui.RemediationInstructionsFragment.OnRemediationInstructionSelectedListener;
import android.app.Activity;
import android.os.Bundle;
import android.view.MenuItem;
public class RemediationInstructionsActivity extends Activity implements OnRemediationInstructionSelectedListener
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.remediation_instructions);
getActionBar().setDisplayHomeAsUpEnabled(true);
if (savedInstanceState != null)
{ /* only update if we're not restoring */
return;
}
RemediationInstructionsFragment frag = (RemediationInstructionsFragment)getFragmentManager().findFragmentById(R.id.remediation_instructions_fragment);
if (frag != null)
{ /* two-pane layout, update fragment */
Bundle extras = getIntent().getExtras();
ArrayList<RemediationInstruction> list = extras.getParcelableArrayList(RemediationInstructionsFragment.EXTRA_REMEDIATION_INSTRUCTIONS);
frag.updateView(list);
}
else
{ /* one-pane layout, create fragment */
frag = new RemediationInstructionsFragment();
frag.setArguments(getIntent().getExtras());
getFragmentManager().beginTransaction().add(R.id.fragment_container, frag).commit();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
/* one-pane layout, pop possible fragment from stack, finish otherwise */
if (!getFragmentManager().popBackStackImmediate())
{
finish();
}
getActionBar().setTitle(getTitle());
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onRemediationInstructionSelected(RemediationInstruction instruction)
{
RemediationInstructionFragment frag = (RemediationInstructionFragment)getFragmentManager().findFragmentById(R.id.remediation_instruction_fragment);
if (frag != null)
{ /* two-pane layout, update directly */
frag.updateView(instruction);
}
else
{ /* one-pane layout, replace fragment */
frag = new RemediationInstructionFragment();
Bundle args = new Bundle();
args.putParcelable(RemediationInstructionFragment.ARG_REMEDIATION_INSTRUCTION, instruction);
frag.setArguments(args);
getFragmentManager().beginTransaction().replace(R.id.fragment_container, frag).addToBackStack(null).commit();
getActionBar().setTitle(instruction.getTitle());
}
}
}
@@ -0,0 +1,122 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import java.util.ArrayList;
import org.strongswan.android.R;
import org.strongswan.android.logic.imc.RemediationInstruction;
import org.strongswan.android.ui.adapter.RemediationInstructionAdapter;
import android.app.Activity;
import android.app.ListFragment;
import android.os.Bundle;
import android.view.View;
import android.widget.ListView;
public class RemediationInstructionsFragment extends ListFragment
{
public static final String EXTRA_REMEDIATION_INSTRUCTIONS = "instructions";
private static final String KEY_POSITION = "position";
private ArrayList<RemediationInstruction> mInstructions = null;
private OnRemediationInstructionSelectedListener mListener;
private RemediationInstructionAdapter mAdapter;
private int mCurrentPosition = -1;
/**
* The activity containing this fragment should implement this interface
*/
public interface OnRemediationInstructionSelectedListener
{
public void onRemediationInstructionSelected(RemediationInstruction instruction);
}
@Override
public void onActivityCreated(Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null)
{
mInstructions = savedInstanceState.getParcelableArrayList(EXTRA_REMEDIATION_INSTRUCTIONS);
mCurrentPosition = savedInstanceState.getInt(KEY_POSITION);
}
}
@Override
public void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putParcelableArrayList(RemediationInstructionsFragment.EXTRA_REMEDIATION_INSTRUCTIONS, mInstructions);
outState.putInt(KEY_POSITION, mCurrentPosition);
}
@Override
public void onAttach(Activity activity)
{
super.onAttach(activity);
if (activity instanceof OnRemediationInstructionSelectedListener)
{
mListener = (OnRemediationInstructionSelectedListener)activity;
}
}
@Override
public void onStart()
{
super.onStart();
boolean two_pane = getFragmentManager().findFragmentById(R.id.remediation_instruction_fragment) != null;
if (two_pane)
{ /* two-pane layout, make list items selectable */
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
}
Bundle args = getArguments();
if (mInstructions == null && args != null)
{
mInstructions = args.getParcelableArrayList(EXTRA_REMEDIATION_INSTRUCTIONS);
}
updateView(mInstructions);
if (two_pane && mCurrentPosition == -1 && mInstructions.size() > 0)
{ /* two-pane layout, select first instruction */
mCurrentPosition = 0;
mListener.onRemediationInstructionSelected(mInstructions.get(0));
}
getListView().setItemChecked(mCurrentPosition, true);
}
@Override
public void onListItemClick(ListView l, View v, int position, long id)
{
mCurrentPosition = position;
mListener.onRemediationInstructionSelected(mInstructions.get(position));
getListView().setItemChecked(position, true);
}
public void updateView(ArrayList<RemediationInstruction> instructions)
{
if (mAdapter == null)
{
mAdapter = new RemediationInstructionAdapter(getActivity());
setListAdapter(mAdapter);
}
mInstructions = instructions;
mAdapter.setData(mInstructions);
}
}
@@ -0,0 +1,223 @@
/*
* Copyright (C) 2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import org.strongswan.android.R;
import org.strongswan.android.data.VpnProfileDataSource;
import org.strongswan.android.logic.TrustedCertificateManager;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.FragmentTransaction;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.widget.Toast;
public class TrustedCertificateImportActivity extends Activity
{
private static final int OPEN_DOCUMENT = 0;
private static final String DIALOG_TAG = "Dialog";
/* same as those listed in the manifest */
private static final String[] ACCEPTED_MIME_TYPES = {
"application/x-x509-ca-cert",
"application/x-x509-server-cert",
"application/x-pem-file",
"application/pkix-cert"
};
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
if (savedInstanceState != null)
{ /* do nothing when we are restoring */
return;
}
Intent intent = getIntent();
String action = intent.getAction();
if (Intent.ACTION_VIEW.equals(action))
{
importCertificate(intent.getData());
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
{
Intent openIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
openIntent.setType("*/*");
openIntent.putExtra(Intent.EXTRA_MIME_TYPES, ACCEPTED_MIME_TYPES);
startActivityForResult(openIntent, OPEN_DOCUMENT);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (requestCode)
{
case OPEN_DOCUMENT:
if (resultCode == Activity.RESULT_OK && data != null)
{
importCertificate(data.getData());
return;
}
finish();
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
/**
* Import the file pointed to by the given URI as a certificate.
* @param uri
*/
private void importCertificate(Uri uri)
{
X509Certificate certificate = parseCertificate(uri);
if (certificate == null)
{
Toast.makeText(this, R.string.cert_import_failed, Toast.LENGTH_LONG).show();
finish();
return;
}
/* Ask the user whether to import the certificate. This is particularly
* necessary because the import activity can be triggered by any app on
* the system. Also, if our app is the only one that is registered to
* open certificate files by MIME type the user would have no idea really
* where the file was imported just by reading the Toast we display. */
ConfirmImportDialog dialog = new ConfirmImportDialog();
Bundle args = new Bundle();
args.putSerializable(VpnProfileDataSource.KEY_CERTIFICATE, certificate);
dialog.setArguments(args);
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.add(dialog, DIALOG_TAG);
ft.commit();
}
/**
* Load the file from the given URI and try to parse it as X.509 certificate.
* @param uri
* @return certificate or null
*/
private X509Certificate parseCertificate(Uri uri)
{
X509Certificate certificate = null;
try
{
CertificateFactory factory = CertificateFactory.getInstance("X.509");
InputStream in = getContentResolver().openInputStream(uri);
certificate = (X509Certificate)factory.generateCertificate(in);
/* we don't check whether it's actually a CA certificate or not */
}
catch (CertificateException e)
{
e.printStackTrace();
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
return certificate;
}
/**
* Try to store the given certificate in the KeyStore.
* @param certificate
* @return whether it was successfully stored
*/
private boolean storeCertificate(X509Certificate certificate)
{
try
{
KeyStore store = KeyStore.getInstance("LocalCertificateStore");
store.load(null, null);
store.setCertificateEntry(null, certificate);
TrustedCertificateManager.getInstance().reset();
return true;
}
catch (Exception e)
{
e.printStackTrace();
return false;
}
}
/**
* Class that displays a confirmation dialog when a certificate should get
* imported. If the user confirms the import we try to store it.
*/
public static class ConfirmImportDialog extends DialogFragment
{
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
final X509Certificate certificate;
certificate = (X509Certificate)getArguments().getSerializable(VpnProfileDataSource.KEY_CERTIFICATE);
return new AlertDialog.Builder(getActivity())
.setIcon(R.drawable.ic_launcher)
.setTitle(R.string.import_certificate)
.setMessage(certificate.getSubjectDN().toString())
.setPositiveButton(R.string.import_certificate, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int whichButton)
{
TrustedCertificateImportActivity activity = (TrustedCertificateImportActivity)getActivity();
if (activity.storeCertificate(certificate))
{
Toast.makeText(getActivity(), R.string.cert_imported_successfully, Toast.LENGTH_LONG).show();
getActivity().setResult(Activity.RESULT_OK);
}
else
{
Toast.makeText(getActivity(), R.string.cert_import_failed, Toast.LENGTH_LONG).show();
}
getActivity().finish();
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which)
{
getActivity().finish();
}
}).create();
}
@Override
public void onCancel(DialogInterface dialog)
{
getActivity().finish();
}
}
}
@@ -0,0 +1,233 @@
/*
* Copyright (C) 2012-2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
import java.util.Map.Entry;
import org.strongswan.android.R;
import org.strongswan.android.logic.TrustedCertificateManager;
import org.strongswan.android.logic.TrustedCertificateManager.TrustedCertificateSource;
import org.strongswan.android.security.TrustedCertificateEntry;
import org.strongswan.android.ui.adapter.TrustedCertificateAdapter;
import android.app.Activity;
import android.app.ListFragment;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.AsyncTaskLoader;
import android.content.Context;
import android.content.Loader;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ListView;
import android.widget.SearchView;
import android.widget.SearchView.OnQueryTextListener;
public class TrustedCertificateListFragment extends ListFragment implements LoaderCallbacks<List<TrustedCertificateEntry>>, OnQueryTextListener
{
public static final String EXTRA_CERTIFICATE_SOURCE = "certificate_source";
private OnTrustedCertificateSelectedListener mListener;
private TrustedCertificateAdapter mAdapter;
private TrustedCertificateSource mSource = TrustedCertificateSource.SYSTEM;
/**
* The activity containing this fragment should implement this interface
*/
public interface OnTrustedCertificateSelectedListener {
public void onTrustedCertificateSelected(TrustedCertificateEntry selected);
}
@Override
public void onActivityCreated(Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(true);
setEmptyText(getString(R.string.no_certificates));
mAdapter = new TrustedCertificateAdapter(getActivity());
setListAdapter(mAdapter);
setListShown(false);
Bundle arguments = getArguments();
if (arguments != null)
{
mSource = (TrustedCertificateSource)arguments.getSerializable(EXTRA_CERTIFICATE_SOURCE);
}
getLoaderManager().initLoader(0, null, this);
}
@Override
public void onDestroy()
{
super.onDestroy();
}
@Override
public void onAttach(Activity activity)
{
super.onAttach(activity);
if (activity instanceof OnTrustedCertificateSelectedListener)
{
mListener = (OnTrustedCertificateSelectedListener)activity;
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
{
MenuItem item = menu.add(R.string.search);
item.setIcon(android.R.drawable.ic_menu_search);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
SearchView sv = new SearchView(getActivity());
sv.setOnQueryTextListener(this);
item.setActionView(sv);
}
@Override
public boolean onQueryTextSubmit(String query)
{ /* already handled when the text changes */
return true;
}
@Override
public boolean onQueryTextChange(String newText)
{
String search = TextUtils.isEmpty(newText) ? null : newText;
mAdapter.getFilter().filter(search);
return true;
}
/**
* Reset the loader of this list fragment
*/
public void reset()
{
if (isResumed())
{
setListShown(false);
}
getLoaderManager().restartLoader(0, null, this);
}
@Override
public Loader<List<TrustedCertificateEntry>> onCreateLoader(int id, Bundle args)
{ /* we don't need the id as we have only one loader */
return new CertificateListLoader(getActivity(), mSource);
}
@Override
public void onLoadFinished(Loader<List<TrustedCertificateEntry>> loader, List<TrustedCertificateEntry> data)
{
mAdapter.setData(data);
if (isResumed())
{
setListShown(true);
}
else
{
setListShownNoAnimation(true);
}
}
@Override
public void onLoaderReset(Loader<List<TrustedCertificateEntry>> loader)
{
mAdapter.setData(null);
}
@Override
public void onListItemClick(ListView l, View v, int position, long id)
{
if (mListener != null)
{
mListener.onTrustedCertificateSelected(mAdapter.getItem(position));
}
}
public static class CertificateListLoader extends AsyncTaskLoader<List<TrustedCertificateEntry>>
{
private List<TrustedCertificateEntry> mData;
private final TrustedCertificateSource mSource;
public CertificateListLoader(Context context, TrustedCertificateSource source)
{
super(context);
mSource = source;
}
@Override
public List<TrustedCertificateEntry> loadInBackground()
{
TrustedCertificateManager certman = TrustedCertificateManager.getInstance().load();
Hashtable<String,X509Certificate> certificates = certman.getCACertificates(mSource);
List<TrustedCertificateEntry> selected;
selected = new ArrayList<TrustedCertificateEntry>();
for (Entry<String, X509Certificate> entry : certificates.entrySet())
{
selected.add(new TrustedCertificateEntry(entry.getKey(), entry.getValue()));
}
Collections.sort(selected);
return selected;
}
@Override
protected void onStartLoading()
{
if (mData != null)
{ /* if we have data ready, deliver it directly */
deliverResult(mData);
return;
}
forceLoad();
}
@Override
public void deliverResult(List<TrustedCertificateEntry> data)
{
if (isReset())
{
return;
}
mData = data;
if (isStarted())
{ /* if it is started we deliver the data directly,
* otherwise this is handled in onStartLoading */
super.deliverResult(data);
}
}
@Override
protected void onReset()
{
mData = null;
}
}
}
@@ -0,0 +1,255 @@
/*
* Copyright (C) 2012-2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import java.security.KeyStore;
import org.strongswan.android.R;
import org.strongswan.android.data.VpnProfileDataSource;
import org.strongswan.android.logic.TrustedCertificateManager;
import org.strongswan.android.logic.TrustedCertificateManager.TrustedCertificateSource;
import org.strongswan.android.security.TrustedCertificateEntry;
import org.strongswan.android.ui.CertificateDeleteConfirmationDialog.OnCertificateDeleteListener;
import android.app.ActionBar;
import android.app.ActionBar.Tab;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
public class TrustedCertificatesActivity extends Activity implements TrustedCertificateListFragment.OnTrustedCertificateSelectedListener, OnCertificateDeleteListener
{
public static final String SELECT_CERTIFICATE = "org.strongswan.android.action.SELECT_CERTIFICATE";
private static final String DIALOG_TAG = "Dialog";
private static final int IMPORT_CERTIFICATE = 0;
private boolean mSelect;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.trusted_certificates_activity);
ActionBar actionBar = getActionBar();
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
TrustedCertificatesTabListener listener;
listener = new TrustedCertificatesTabListener(this, "system", TrustedCertificateSource.SYSTEM);
actionBar.addTab(actionBar
.newTab()
.setText(R.string.system_tab)
.setTag(listener)
.setTabListener(listener));
listener = new TrustedCertificatesTabListener(this, "user", TrustedCertificateSource.USER);
actionBar.addTab(actionBar
.newTab()
.setText(R.string.user_tab)
.setTag(listener)
.setTabListener(listener));
listener = new TrustedCertificatesTabListener(this, "local", TrustedCertificateSource.LOCAL);
actionBar.addTab(actionBar
.newTab()
.setText(R.string.local_tab)
.setTag(listener)
.setTabListener(listener));
if (savedInstanceState != null)
{
actionBar.setSelectedNavigationItem(savedInstanceState.getInt("tab", 0));
}
mSelect = SELECT_CERTIFICATE.equals(getIntent().getAction());
}
@Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putInt("tab", getActionBar().getSelectedNavigationIndex());
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
getMenuInflater().inflate(R.menu.certificates, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu)
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
{
menu.removeItem(R.id.menu_import_certificate);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
finish();
return true;
case R.id.menu_reload_certs:
reloadCertificates();
return true;
case R.id.menu_import_certificate:
Intent intent = new Intent(this, TrustedCertificateImportActivity.class);
startActivityForResult(intent, IMPORT_CERTIFICATE);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (requestCode)
{
case IMPORT_CERTIFICATE:
if (resultCode == Activity.RESULT_OK)
{
reloadCertificates();
}
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onTrustedCertificateSelected(TrustedCertificateEntry selected)
{
if (mSelect)
{
/* the user selected a certificate, return to calling activity */
Intent intent = new Intent();
intent.putExtra(VpnProfileDataSource.KEY_CERTIFICATE, selected.getAlias());
setResult(Activity.RESULT_OK, intent);
finish();
}
else
{
TrustedCertificatesTabListener listener;
listener = (TrustedCertificatesTabListener)getActionBar().getSelectedTab().getTag();
if (listener.mTag == "local")
{
Bundle args = new Bundle();
args.putString(CertificateDeleteConfirmationDialog.ALIAS, selected.getAlias());
CertificateDeleteConfirmationDialog dialog = new CertificateDeleteConfirmationDialog();
dialog.setArguments(args);
dialog.show(this.getFragmentManager(), DIALOG_TAG);
}
}
}
@Override
public void onDelete(String alias)
{
try
{
KeyStore store = KeyStore.getInstance("LocalCertificateStore");
store.load(null, null);
store.deleteEntry(alias);
reloadCertificates();
}
catch (Exception e)
{
e.printStackTrace();
}
}
private void reloadCertificates()
{
TrustedCertificateManager.getInstance().reset();
for (int i = 0; i < getActionBar().getTabCount(); i++)
{
Tab tab = getActionBar().getTabAt(i);
TrustedCertificatesTabListener listener = (TrustedCertificatesTabListener)tab.getTag();
listener.reset();
}
}
public static class TrustedCertificatesTabListener implements ActionBar.TabListener
{
private final String mTag;
private final TrustedCertificateSource mSource;
private Fragment mFragment;
public TrustedCertificatesTabListener(Activity activity, String tag, TrustedCertificateSource source)
{
mTag = tag;
mSource = source;
/* check to see if we already have a fragment for this tab, probably
* from a previously saved state. if so, deactivate it, because the
* initial state is that no tab is shown */
mFragment = activity.getFragmentManager().findFragmentByTag(mTag);
if (mFragment != null && !mFragment.isDetached())
{
FragmentTransaction ft = activity.getFragmentManager().beginTransaction();
ft.detach(mFragment);
ft.commit();
}
}
@Override
public void onTabSelected(Tab tab, FragmentTransaction ft)
{
if (mFragment == null)
{
mFragment = new TrustedCertificateListFragment();
Bundle args = new Bundle();
args.putSerializable(TrustedCertificateListFragment.EXTRA_CERTIFICATE_SOURCE, mSource);
mFragment.setArguments(args);
ft.add(android.R.id.content, mFragment, mTag);
}
else
{
ft.attach(mFragment);
}
}
@Override
public void onTabUnselected(Tab tab, FragmentTransaction ft)
{
if (mFragment != null)
{
ft.detach(mFragment);
}
}
@Override
public void onTabReselected(Tab tab, FragmentTransaction ft)
{
/* nothing to be done */
}
public void reset()
{
if (mFragment != null)
{
((TrustedCertificateListFragment)mFragment).reset();
}
}
}
}
@@ -0,0 +1,653 @@
/*
* Copyright (C) 2012-2014 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import java.security.cert.X509Certificate;
import org.strongswan.android.R;
import org.strongswan.android.data.VpnProfile;
import org.strongswan.android.data.VpnProfileDataSource;
import org.strongswan.android.data.VpnType;
import org.strongswan.android.data.VpnType.VpnTypeFeature;
import org.strongswan.android.logic.TrustedCertificateManager;
import org.strongswan.android.security.TrustedCertificateEntry;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
import android.security.KeyChainException;
import android.text.Html;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.RelativeLayout;
import android.widget.Spinner;
import android.widget.TextView;
public class VpnProfileDetailActivity extends Activity
{
private static final int SELECT_TRUSTED_CERTIFICATE = 0;
private static final int MTU_MIN = 1280;
private static final int MTU_MAX = 1500;
private VpnProfileDataSource mDataSource;
private Long mId;
private TrustedCertificateEntry mCertEntry;
private String mUserCertLoading;
private TrustedCertificateEntry mUserCertEntry;
private VpnType mVpnType = VpnType.IKEV2_EAP;
private VpnProfile mProfile;
private EditText mName;
private EditText mGateway;
private Spinner mSelectVpnType;
private ViewGroup mUsernamePassword;
private EditText mUsername;
private EditText mPassword;
private ViewGroup mUserCertificate;
private RelativeLayout mSelectUserCert;
private CheckBox mCheckAuto;
private RelativeLayout mSelectCert;
private RelativeLayout mTncNotice;
private CheckBox mShowAdvanced;
private ViewGroup mAdvancedSettings;
private EditText mMTU;
private EditText mPort;
private CheckBox mBlockIPv4;
private CheckBox mBlockIPv6;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
/* the title is set when we load the profile, if any */
getActionBar().setDisplayHomeAsUpEnabled(true);
mDataSource = new VpnProfileDataSource(this);
mDataSource.open();
setContentView(R.layout.profile_detail_view);
mName = (EditText)findViewById(R.id.name);
mGateway = (EditText)findViewById(R.id.gateway);
mSelectVpnType = (Spinner)findViewById(R.id.vpn_type);
mTncNotice = (RelativeLayout)findViewById(R.id.tnc_notice);
mUsernamePassword = (ViewGroup)findViewById(R.id.username_password_group);
mUsername = (EditText)findViewById(R.id.username);
mPassword = (EditText)findViewById(R.id.password);
mUserCertificate = (ViewGroup)findViewById(R.id.user_certificate_group);
mSelectUserCert = (RelativeLayout)findViewById(R.id.select_user_certificate);
mCheckAuto = (CheckBox)findViewById(R.id.ca_auto);
mSelectCert = (RelativeLayout)findViewById(R.id.select_certificate);
mShowAdvanced = (CheckBox)findViewById(R.id.show_advanced);
mAdvancedSettings = (ViewGroup)findViewById(R.id.advanced_settings);
mMTU = (EditText)findViewById(R.id.mtu);
mPort = (EditText)findViewById(R.id.port);
mBlockIPv4 = (CheckBox)findViewById(R.id.split_tunneling_v4);
mBlockIPv6 = (CheckBox)findViewById(R.id.split_tunneling_v6);
mSelectVpnType.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
{
mVpnType = VpnType.values()[position];
updateCredentialView();
}
@Override
public void onNothingSelected(AdapterView<?> parent)
{ /* should not happen */
mVpnType = VpnType.IKEV2_EAP;
updateCredentialView();
}
});
((TextView)mTncNotice.findViewById(android.R.id.text1)).setText(R.string.tnc_notice_title);
((TextView)mTncNotice.findViewById(android.R.id.text2)).setText(R.string.tnc_notice_subtitle);
mTncNotice.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v)
{
new TncNoticeDialog().show(VpnProfileDetailActivity.this.getFragmentManager(), "TncNotice");
}
});
mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener());
mCheckAuto.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
{
updateCertificateSelector();
}
});
mSelectCert.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v)
{
Intent intent = new Intent(VpnProfileDetailActivity.this, TrustedCertificatesActivity.class);
intent.setAction(TrustedCertificatesActivity.SELECT_CERTIFICATE);
startActivityForResult(intent, SELECT_TRUSTED_CERTIFICATE);
}
});
mShowAdvanced.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
{
updateAdvancedSettings();
}
});
mId = savedInstanceState == null ? null : savedInstanceState.getLong(VpnProfileDataSource.KEY_ID);
if (mId == null)
{
Bundle extras = getIntent().getExtras();
mId = extras == null ? null : extras.getLong(VpnProfileDataSource.KEY_ID);
}
loadProfileData(savedInstanceState);
updateCredentialView();
updateCertificateSelector();
updateAdvancedSettings();
}
@Override
protected void onDestroy()
{
super.onDestroy();
mDataSource.close();
}
@Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
if (mId != null)
{
outState.putLong(VpnProfileDataSource.KEY_ID, mId);
}
if (mUserCertEntry != null)
{
outState.putString(VpnProfileDataSource.KEY_USER_CERTIFICATE, mUserCertEntry.getAlias());
}
if (mCertEntry != null)
{
outState.putString(VpnProfileDataSource.KEY_CERTIFICATE, mCertEntry.getAlias());
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.profile_edit, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
case R.id.menu_cancel:
finish();
return true;
case R.id.menu_accept:
saveProfile();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (requestCode)
{
case SELECT_TRUSTED_CERTIFICATE:
if (resultCode == RESULT_OK)
{
String alias = data.getStringExtra(VpnProfileDataSource.KEY_CERTIFICATE);
X509Certificate certificate = TrustedCertificateManager.getInstance().getCACertificateFromAlias(alias);
mCertEntry = certificate == null ? null : new TrustedCertificateEntry(alias, certificate);
updateCertificateSelector();
}
break;
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
/**
* Update the UI to enter credentials depending on the type of VPN currently selected
*/
private void updateCredentialView()
{
mUsernamePassword.setVisibility(mVpnType.has(VpnTypeFeature.USER_PASS) ? View.VISIBLE : View.GONE);
mUserCertificate.setVisibility(mVpnType.has(VpnTypeFeature.CERTIFICATE) ? View.VISIBLE : View.GONE);
mTncNotice.setVisibility(mVpnType.has(VpnTypeFeature.BYOD) ? View.VISIBLE : View.GONE);
if (mVpnType.has(VpnTypeFeature.CERTIFICATE))
{
if (mUserCertLoading != null)
{
((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertLoading);
((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.loading);
}
else if (mUserCertEntry != null)
{ /* clear any errors and set the new data */
((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError(null);
((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertEntry.getAlias());
((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(mUserCertEntry.getCertificate().getSubjectDN().toString());
}
else
{
((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(R.string.profile_user_select_certificate_label);
((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.profile_user_select_certificate);
}
}
}
/**
* Show an alert in case the previously selected certificate is not found anymore
* or the user did not select a certificate in the spinner.
*/
private void showCertificateAlert()
{
AlertDialog.Builder adb = new AlertDialog.Builder(VpnProfileDetailActivity.this);
adb.setTitle(R.string.alert_text_nocertfound_title);
adb.setMessage(R.string.alert_text_nocertfound);
adb.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id)
{
dialog.cancel();
}
});
adb.show();
}
/**
* Update the CA certificate selection UI depending on whether the
* certificate should be automatically selected or not.
*/
private void updateCertificateSelector()
{
if (!mCheckAuto.isChecked())
{
mSelectCert.setEnabled(true);
mSelectCert.setVisibility(View.VISIBLE);
if (mCertEntry != null)
{
((TextView)mSelectCert.findViewById(android.R.id.text1)).setText(mCertEntry.getSubjectPrimary());
((TextView)mSelectCert.findViewById(android.R.id.text2)).setText(mCertEntry.getSubjectSecondary());
}
else
{
((TextView)mSelectCert.findViewById(android.R.id.text1)).setText(R.string.profile_ca_select_certificate_label);
((TextView)mSelectCert.findViewById(android.R.id.text2)).setText(R.string.profile_ca_select_certificate);
}
}
else
{
mSelectCert.setEnabled(false);
mSelectCert.setVisibility(View.GONE);
}
}
/**
* Update the advanced settings UI depending on whether any advanced
* settings have already been made.
*/
private void updateAdvancedSettings()
{
boolean show = mShowAdvanced.isChecked();
if (!show && mProfile != null)
{
Integer st = mProfile.getSplitTunneling();
show = mProfile.getMTU() != null || mProfile.getPort() != null || (st != null && st != 0);
}
mShowAdvanced.setVisibility(!show ? View.VISIBLE : View.GONE);
mAdvancedSettings.setVisibility(show ? View.VISIBLE : View.GONE);
}
/**
* Save or update the profile depending on whether we actually have a
* profile object or not (this was created in updateProfileData)
*/
private void saveProfile()
{
if (verifyInput())
{
if (mProfile != null)
{
updateProfileData();
mDataSource.updateVpnProfile(mProfile);
}
else
{
mProfile = new VpnProfile();
updateProfileData();
mDataSource.insertProfile(mProfile);
}
setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId()));
finish();
}
}
/**
* Verify the user input and display error messages.
* @return true if the input is valid
*/
private boolean verifyInput()
{
boolean valid = true;
if (mGateway.getText().toString().trim().isEmpty())
{
mGateway.setError(getString(R.string.alert_text_no_input_gateway));
valid = false;
}
if (mVpnType.has(VpnTypeFeature.USER_PASS))
{
if (mUsername.getText().toString().trim().isEmpty())
{
mUsername.setError(getString(R.string.alert_text_no_input_username));
valid = false;
}
}
if (mVpnType.has(VpnTypeFeature.CERTIFICATE) && mUserCertEntry == null)
{ /* let's show an error icon */
((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
valid = false;
}
if (!mCheckAuto.isChecked() && mCertEntry == null)
{
showCertificateAlert();
valid = false;
}
Integer mtu = getInteger(mMTU);
if (mtu != null && (mtu < MTU_MIN || mtu > MTU_MAX))
{
mMTU.setError(String.format(getString(R.string.alert_text_out_of_range), MTU_MIN, MTU_MAX));
valid = false;
}
Integer port = getInteger(mPort);
if (port != null && (port < 1 || port > 65535))
{
mPort.setError(String.format(getString(R.string.alert_text_out_of_range), 1, 65535));
valid = false;
}
return valid;
}
/**
* Update the profile object with the data entered by the user
*/
private void updateProfileData()
{
/* the name is optional, we default to the gateway if none is given */
String name = mName.getText().toString().trim();
String gateway = mGateway.getText().toString().trim();
mProfile.setName(name.isEmpty() ? gateway : name);
mProfile.setGateway(gateway);
mProfile.setVpnType(mVpnType);
if (mVpnType.has(VpnTypeFeature.USER_PASS))
{
mProfile.setUsername(mUsername.getText().toString().trim());
String password = mPassword.getText().toString().trim();
password = password.isEmpty() ? null : password;
mProfile.setPassword(password);
}
if (mVpnType.has(VpnTypeFeature.CERTIFICATE))
{
mProfile.setUserCertificateAlias(mUserCertEntry.getAlias());
}
String certAlias = mCheckAuto.isChecked() ? null : mCertEntry.getAlias();
mProfile.setCertificateAlias(certAlias);
mProfile.setMTU(getInteger(mMTU));
mProfile.setPort(getInteger(mPort));
int st = 0;
st |= mBlockIPv4.isChecked() ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4 : 0;
st |= mBlockIPv6.isChecked() ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6 : 0;
mProfile.setSplitTunneling(st == 0 ? null : st);
}
/**
* Load an existing profile if we got an ID
*
* @param savedInstanceState previously saved state
*/
private void loadProfileData(Bundle savedInstanceState)
{
String useralias = null, alias = null;
getActionBar().setTitle(R.string.add_profile);
if (mId != null && mId != 0)
{
mProfile = mDataSource.getVpnProfile(mId);
if (mProfile != null)
{
mName.setText(mProfile.getName());
mGateway.setText(mProfile.getGateway());
mVpnType = mProfile.getVpnType();
mUsername.setText(mProfile.getUsername());
mPassword.setText(mProfile.getPassword());
mMTU.setText(mProfile.getMTU() != null ? mProfile.getMTU().toString() : null);
mPort.setText(mProfile.getPort() != null ? mProfile.getPort().toString() : null);
mBlockIPv4.setChecked(mProfile.getSplitTunneling() != null ? (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) != 0 : false);
mBlockIPv6.setChecked(mProfile.getSplitTunneling() != null ? (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6) != 0 : false);
useralias = mProfile.getUserCertificateAlias();
alias = mProfile.getCertificateAlias();
getActionBar().setTitle(mProfile.getName());
}
else
{
Log.e(VpnProfileDetailActivity.class.getSimpleName(),
"VPN profile with id " + mId + " not found");
finish();
}
}
mSelectVpnType.setSelection(mVpnType.ordinal());
/* check if the user selected a user certificate previously */
useralias = savedInstanceState == null ? useralias: savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE);
if (useralias != null)
{
UserCertificateLoader loader = new UserCertificateLoader(this, useralias);
mUserCertLoading = useralias;
loader.execute();
}
/* check if the user selected a CA certificate previously */
alias = savedInstanceState == null ? alias : savedInstanceState.getString(VpnProfileDataSource.KEY_CERTIFICATE);
mCheckAuto.setChecked(alias == null);
if (alias != null)
{
X509Certificate certificate = TrustedCertificateManager.getInstance().getCACertificateFromAlias(alias);
if (certificate != null)
{
mCertEntry = new TrustedCertificateEntry(alias, certificate);
}
else
{ /* previously selected certificate is not here anymore */
showCertificateAlert();
mCertEntry = null;
}
}
}
/**
* Get the integer value in the given text box or null if empty
*
* @param view text box (numeric entry assumed)
*/
private Integer getInteger(EditText view)
{
String value = view.getText().toString().trim();
return value.isEmpty() ? null : Integer.valueOf(value);
}
private class SelectUserCertOnClickListener implements OnClickListener, KeyChainAliasCallback
{
@Override
public void onClick(View v)
{
String useralias = mUserCertEntry != null ? mUserCertEntry.getAlias() : null;
KeyChain.choosePrivateKeyAlias(VpnProfileDetailActivity.this, this, new String[] { "RSA" }, null, null, -1, useralias);
}
@Override
public void alias(final String alias)
{
if (alias != null)
{ /* otherwise the dialog was canceled, the request denied */
try
{
final X509Certificate[] chain = KeyChain.getCertificateChain(VpnProfileDetailActivity.this, alias);
/* alias() is not called from our main thread */
runOnUiThread(new Runnable() {
@Override
public void run()
{
if (chain != null && chain.length > 0)
{
mUserCertEntry = new TrustedCertificateEntry(alias, chain[0]);
}
updateCredentialView();
}
});
}
catch (KeyChainException e)
{
e.printStackTrace();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
/**
* Load the selected user certificate asynchronously. This cannot be done
* from the main thread as getCertificateChain() calls back to our main
* thread to bind to the KeyChain service resulting in a deadlock.
*/
private class UserCertificateLoader extends AsyncTask<Void, Void, X509Certificate>
{
private final Context mContext;
private final String mAlias;
public UserCertificateLoader(Context context, String alias)
{
mContext = context;
mAlias = alias;
}
@Override
protected X509Certificate doInBackground(Void... params)
{
X509Certificate[] chain = null;
try
{
chain = KeyChain.getCertificateChain(mContext, mAlias);
}
catch (KeyChainException e)
{
e.printStackTrace();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
if (chain != null && chain.length > 0)
{
return chain[0];
}
return null;
}
@Override
protected void onPostExecute(X509Certificate result)
{
if (result != null)
{
mUserCertEntry = new TrustedCertificateEntry(mAlias, result);
}
else
{ /* previously selected certificate is not here anymore */
((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
mUserCertEntry = null;
}
mUserCertLoading = null;
updateCredentialView();
}
}
/**
* Dialog with notification message if EAP-TNC is used.
*/
public static class TncNoticeDialog extends DialogFragment
{
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
return new AlertDialog.Builder(getActivity())
.setTitle(R.string.tnc_notice_title)
.setMessage(Html.fromHtml(getString(R.string.tnc_notice_details)))
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id)
{
dialog.dismiss();
}
}).create();
}
}
}
@@ -0,0 +1,289 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import org.strongswan.android.R;
import org.strongswan.android.data.VpnProfile;
import org.strongswan.android.data.VpnProfileDataSource;
import org.strongswan.android.ui.adapter.VpnProfileAdapter;
import android.app.Activity;
import android.app.Fragment;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.Toast;
public class VpnProfileListFragment extends Fragment
{
private static final int ADD_REQUEST = 1;
private static final int EDIT_REQUEST = 2;
private List<VpnProfile> mVpnProfiles;
private VpnProfileDataSource mDataSource;
private VpnProfileAdapter mListAdapter;
private ListView mListView;
private OnVpnProfileSelectedListener mListener;
private boolean mReadOnly;
/**
* The activity containing this fragment should implement this interface
*/
public interface OnVpnProfileSelectedListener {
public void onVpnProfileSelected(VpnProfile profile);
}
@Override
public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState)
{
super.onInflate(activity, attrs, savedInstanceState);
TypedArray a = activity.obtainStyledAttributes(attrs, R.styleable.Fragment);
mReadOnly = a.getBoolean(R.styleable.Fragment_read_only, false);
a.recycle();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.profile_list_fragment, null);
mListView = (ListView)view.findViewById(R.id.profile_list);
mListView.setAdapter(mListAdapter);
mListView.setEmptyView(view.findViewById(R.id.profile_list_empty));
mListView.setOnItemClickListener(mVpnProfileClicked);
if (!mReadOnly)
{
mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
mListView.setMultiChoiceModeListener(mVpnProfileSelected);
}
return view;
}
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if (args != null)
{
mReadOnly = args.getBoolean("read_only", mReadOnly);
}
if (!mReadOnly)
{
setHasOptionsMenu(true);
}
Context context = getActivity().getApplicationContext();
mDataSource = new VpnProfileDataSource(this.getActivity());
mDataSource.open();
/* cached list of profiles used as backend for the ListView */
mVpnProfiles = mDataSource.getAllVpnProfiles();
mListAdapter = new VpnProfileAdapter(context, R.layout.profile_list_item, mVpnProfiles);
}
@Override
public void onDestroy()
{
super.onDestroy();
mDataSource.close();
}
@Override
public void onAttach(Activity activity)
{
super.onAttach(activity);
if (activity instanceof OnVpnProfileSelectedListener)
{
mListener = (OnVpnProfileSelectedListener)activity;
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
{
inflater.inflate(R.menu.profile_list, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case R.id.add_profile:
Intent connectionIntent = new Intent(getActivity(),
VpnProfileDetailActivity.class);
startActivityForResult(connectionIntent, ADD_REQUEST);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (requestCode)
{
case ADD_REQUEST:
case EDIT_REQUEST:
if (resultCode != Activity.RESULT_OK)
{
return;
}
long id = data.getLongExtra(VpnProfileDataSource.KEY_ID, 0);
VpnProfile profile = mDataSource.getVpnProfile(id);
if (profile != null)
{ /* in case this was an edit, we remove it first */
mVpnProfiles.remove(profile);
mVpnProfiles.add(profile);
mListAdapter.notifyDataSetChanged();
}
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
private final OnItemClickListener mVpnProfileClicked = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> a, View v, int position, long id)
{
if (mListener != null)
{
mListener.onVpnProfileSelected((VpnProfile)a.getItemAtPosition(position));
}
}
};
private final MultiChoiceModeListener mVpnProfileSelected = new MultiChoiceModeListener() {
private HashSet<Integer> mSelected;
private MenuItem mEditProfile;
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu)
{
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode)
{
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu)
{
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.profile_list_context, menu);
mEditProfile = menu.findItem(R.id.edit_profile);
mSelected = new HashSet<Integer>();
mode.setTitle(R.string.select_profiles);
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item)
{
switch (item.getItemId())
{
case R.id.edit_profile:
{
int position = mSelected.iterator().next();
VpnProfile profile = (VpnProfile)mListView.getItemAtPosition(position);
Intent connectionIntent = new Intent(getActivity(), VpnProfileDetailActivity.class);
connectionIntent.putExtra(VpnProfileDataSource.KEY_ID, profile.getId());
startActivityForResult(connectionIntent, EDIT_REQUEST);
break;
}
case R.id.delete_profile:
{
ArrayList<VpnProfile> profiles = new ArrayList<VpnProfile>();
for (int position : mSelected)
{
profiles.add((VpnProfile)mListView.getItemAtPosition(position));
}
for (VpnProfile profile : profiles)
{
mDataSource.deleteVpnProfile(profile);
mVpnProfiles.remove(profile);
}
mListAdapter.notifyDataSetChanged();
Toast.makeText(VpnProfileListFragment.this.getActivity(),
R.string.profiles_deleted, Toast.LENGTH_SHORT).show();
break;
}
default:
return false;
}
mode.finish();
return true;
}
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position,
long id, boolean checked)
{
if (checked)
{
mSelected.add(position);
}
else
{
mSelected.remove(position);
}
final int checkedCount = mSelected.size();
mEditProfile.setEnabled(checkedCount == 1);
switch (checkedCount)
{
case 0:
mode.setSubtitle(R.string.no_profile_selected);
break;
case 1:
mode.setSubtitle(R.string.one_profile_selected);
break;
default:
mode.setSubtitle(String.format(getString(R.string.x_profiles_selected), checkedCount));
break;
}
}
};
}
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import org.strongswan.android.R;
import org.strongswan.android.data.VpnProfile;
import org.strongswan.android.ui.VpnProfileListFragment.OnVpnProfileSelectedListener;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
public class VpnProfileSelectActivity extends Activity implements OnVpnProfileSelectedListener
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.vpn_profile_select);
/* we should probably return a result also if the user clicks the back
* button before selecting a profile */
setResult(RESULT_CANCELED);
}
@Override
public void onVpnProfileSelected(VpnProfile profile)
{
Intent shortcut = new Intent(MainActivity.START_PROFILE);
shortcut.putExtra(MainActivity.EXTRA_VPN_PROFILE_ID, profile.getId());
Intent intent = new Intent();
intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcut);
intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, profile.getName());
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(this, R.drawable.ic_launcher));
setResult(RESULT_OK, intent);
finish();
}
}
@@ -0,0 +1,414 @@
/*
* Copyright (C) 2012-2013 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import java.util.ArrayList;
import java.util.List;
import org.strongswan.android.R;
import org.strongswan.android.data.VpnProfile;
import org.strongswan.android.logic.VpnStateService;
import org.strongswan.android.logic.VpnStateService.ErrorState;
import org.strongswan.android.logic.VpnStateService.State;
import org.strongswan.android.logic.VpnStateService.VpnStateListener;
import org.strongswan.android.logic.imc.ImcState;
import org.strongswan.android.logic.imc.RemediationInstruction;
import android.app.AlertDialog;
import android.app.Fragment;
import android.app.ProgressDialog;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
public class VpnStateFragment extends Fragment implements VpnStateListener
{
private static final String KEY_ERROR_CONNECTION_ID = "error_connection_id";
private static final String KEY_DISMISSED_CONNECTION_ID = "dismissed_connection_id";
private TextView mProfileNameView;
private TextView mProfileView;
private TextView mStateView;
private int stateBaseColor;
private Button mActionButton;
private ProgressDialog mConnectDialog;
private ProgressDialog mDisconnectDialog;
private ProgressDialog mProgressDialog;
private AlertDialog mErrorDialog;
private long mErrorConnectionID;
private long mDismissedConnectionID;
private VpnStateService mService;
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name)
{
mService = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service)
{
mService = ((VpnStateService.LocalBinder)service).getService();
mService.registerListener(VpnStateFragment.this);
updateView();
}
};
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
/* bind to the service only seems to work from the ApplicationContext */
Context context = getActivity().getApplicationContext();
context.bindService(new Intent(context, VpnStateService.class),
mServiceConnection, Service.BIND_AUTO_CREATE);
mErrorConnectionID = 0;
mDismissedConnectionID = 0;
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_ERROR_CONNECTION_ID))
{
mErrorConnectionID = (Long)savedInstanceState.getSerializable(KEY_ERROR_CONNECTION_ID);
mDismissedConnectionID = (Long)savedInstanceState.getSerializable(KEY_DISMISSED_CONNECTION_ID);
}
}
@Override
public void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putSerializable(KEY_ERROR_CONNECTION_ID, mErrorConnectionID);
outState.putSerializable(KEY_DISMISSED_CONNECTION_ID, mDismissedConnectionID);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.vpn_state_fragment, null);
mActionButton = (Button)view.findViewById(R.id.action);
mActionButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v)
{
if (mService != null)
{
mService.disconnect();
}
}
});
enableActionButton(false);
mStateView = (TextView)view.findViewById(R.id.vpn_state);
stateBaseColor = mStateView.getCurrentTextColor();
mProfileView = (TextView)view.findViewById(R.id.vpn_profile_label);
mProfileNameView = (TextView)view.findViewById(R.id.vpn_profile_name);
return view;
}
@Override
public void onStart()
{
super.onStart();
if (mService != null)
{
mService.registerListener(this);
updateView();
}
}
@Override
public void onStop()
{
super.onStop();
if (mService != null)
{
mService.unregisterListener(this);
}
hideErrorDialog();
hideProgressDialog();
}
@Override
public void onDestroy()
{
super.onDestroy();
if (mService != null)
{
getActivity().getApplicationContext().unbindService(mServiceConnection);
}
}
@Override
public void stateChanged()
{
updateView();
}
public void updateView()
{
long connectionID = mService.getConnectionID();
VpnProfile profile = mService.getProfile();
State state = mService.getState();
ErrorState error = mService.getErrorState();
ImcState imcState = mService.getImcState();
String name = "", gateway = "";
if (profile != null)
{
name = profile.getName();
gateway = profile.getGateway();
}
if (reportError(connectionID, name, error, imcState))
{
return;
}
enableActionButton(false);
mProfileNameView.setText(name);
switch (state)
{
case DISABLED:
showProfile(false);
hideProgressDialog();
mStateView.setText(R.string.state_disabled);
mStateView.setTextColor(stateBaseColor);
break;
case CONNECTING:
showProfile(true);
showConnectDialog(name, gateway);
mStateView.setText(R.string.state_connecting);
mStateView.setTextColor(stateBaseColor);
break;
case CONNECTED:
showProfile(true);
hideProgressDialog();
enableActionButton(true);
mStateView.setText(R.string.state_connected);
mStateView.setTextColor(getResources().getColor(R.color.success_text));
break;
case DISCONNECTING:
showProfile(true);
showDisconnectDialog(name);
mStateView.setText(R.string.state_disconnecting);
mStateView.setTextColor(stateBaseColor);
break;
}
}
private boolean reportError(long connectionID, String name, ErrorState error, ImcState imcState)
{
if (connectionID > mDismissedConnectionID)
{ /* report error if it hasn't been dismissed yet */
mErrorConnectionID = connectionID;
}
else
{ /* ignore all other errors */
error = ErrorState.NO_ERROR;
}
if (error == ErrorState.NO_ERROR)
{
hideErrorDialog();
return false;
}
else if (mErrorDialog != null)
{ /* we already show the dialog */
return true;
}
hideProgressDialog();
mProfileNameView.setText(name);
showProfile(true);
enableActionButton(false);
mStateView.setText(R.string.state_error);
mStateView.setTextColor(getResources().getColor(R.color.error_text));
switch (error)
{
case AUTH_FAILED:
if (imcState == ImcState.BLOCK)
{
showErrorDialog(R.string.error_assessment_failed);
}
else
{
showErrorDialog(R.string.error_auth_failed);
}
break;
case PEER_AUTH_FAILED:
showErrorDialog(R.string.error_peer_auth_failed);
break;
case LOOKUP_FAILED:
showErrorDialog(R.string.error_lookup_failed);
break;
case UNREACHABLE:
showErrorDialog(R.string.error_unreachable);
break;
default:
showErrorDialog(R.string.error_generic);
break;
}
return true;
}
private void showProfile(boolean show)
{
mProfileView.setVisibility(show ? View.VISIBLE : View.GONE);
mProfileNameView.setVisibility(show ? View.VISIBLE : View.GONE);
}
private void enableActionButton(boolean enable)
{
mActionButton.setEnabled(enable);
mActionButton.setVisibility(enable ? View.VISIBLE : View.GONE);
}
private void hideProgressDialog()
{
if (mProgressDialog != null)
{
mProgressDialog.dismiss();
mProgressDialog = null;
mDisconnectDialog = mConnectDialog = null;
}
}
private void hideErrorDialog()
{
if (mErrorDialog != null)
{
mErrorDialog.dismiss();
mErrorDialog = null;
}
}
private void clearError()
{
if (mService != null)
{
mService.disconnect();
}
mDismissedConnectionID = mErrorConnectionID;
updateView();
}
private void showConnectDialog(String profile, String gateway)
{
if (mConnectDialog != null)
{ /* already showing the dialog */
return;
}
hideProgressDialog();
mConnectDialog = new ProgressDialog(getActivity());
mConnectDialog.setTitle(String.format(getString(R.string.connecting_title), profile));
mConnectDialog.setMessage(String.format(getString(R.string.connecting_message), gateway));
mConnectDialog.setIndeterminate(true);
mConnectDialog.setCancelable(false);
mConnectDialog.setButton(DialogInterface.BUTTON_NEGATIVE,
getString(android.R.string.cancel),
new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
if (mService != null)
{
mService.disconnect();
}
}
});
mConnectDialog.show();
mProgressDialog = mConnectDialog;
}
private void showDisconnectDialog(String profile)
{
if (mDisconnectDialog != null)
{ /* already showing the dialog */
return;
}
hideProgressDialog();
mDisconnectDialog = new ProgressDialog(getActivity());
mDisconnectDialog.setMessage(getString(R.string.state_disconnecting));
mDisconnectDialog.setIndeterminate(true);
mDisconnectDialog.setCancelable(false);
mDisconnectDialog.show();
mProgressDialog = mDisconnectDialog;
}
private void showErrorDialog(int textid)
{
final List<RemediationInstruction> instructions = mService.getRemediationInstructions();
final boolean show_instructions = mService.getImcState() == ImcState.BLOCK && !instructions.isEmpty();
int text = show_instructions ? R.string.show_remediation_instructions : R.string.show_log;
mErrorDialog = new AlertDialog.Builder(getActivity())
.setMessage(getString(R.string.error_introduction) + " " + getString(textid))
.setCancelable(false)
.setNeutralButton(text, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which)
{
clearError();
dialog.dismiss();
Intent intent;
if (show_instructions)
{
intent = new Intent(getActivity(), RemediationInstructionsActivity.class);
intent.putParcelableArrayListExtra(RemediationInstructionsFragment.EXTRA_REMEDIATION_INSTRUCTIONS,
new ArrayList<RemediationInstruction>(instructions));
}
else
{
intent = new Intent(getActivity(), LogActivity.class);
}
startActivity(intent);
}
})
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id)
{
clearError();
dialog.dismiss();
}
}).create();
mErrorDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog)
{
mErrorDialog = null;
}
});
mErrorDialog.show();
}
}
@@ -0,0 +1,71 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui.adapter;
import java.util.List;
import org.strongswan.android.R;
import org.strongswan.android.logic.imc.RemediationInstruction;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
public class RemediationInstructionAdapter extends ArrayAdapter<RemediationInstruction>
{
public RemediationInstructionAdapter(Context context)
{
super(context, 0);
}
/**
* Set new data for this adapter.
*
* @param data the new data (null to clear)
*/
public void setData(List<RemediationInstruction> data)
{
clear();
if (data != null)
{
addAll(data);
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
View view;
if (convertView != null)
{
view = convertView;
}
else
{
LayoutInflater inflater = LayoutInflater.from(getContext());
view = inflater.inflate(R.layout.remediation_instruction_item, parent, false);
}
RemediationInstruction item = getItem(position);
TextView text = (TextView)view.findViewById(android.R.id.text1);
text.setText(item.getTitle());
text = (TextView)view.findViewById(android.R.id.text2);
text.setText(item.getDescription());
return view;
}
}
@@ -0,0 +1,71 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui.adapter;
import java.util.List;
import org.strongswan.android.R;
import org.strongswan.android.security.TrustedCertificateEntry;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
public class TrustedCertificateAdapter extends ArrayAdapter<TrustedCertificateEntry>
{
public TrustedCertificateAdapter(Context context)
{
super(context, R.layout.trusted_certificates_item);
}
/**
* Set new data for this adapter.
*
* @param data the new data (null to clear)
*/
public void setData(List<TrustedCertificateEntry> data)
{
clear();
if (data != null)
{
addAll(data);
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
View view;
if (convertView != null)
{
view = convertView;
}
else
{
LayoutInflater inflater = LayoutInflater.from(getContext());
view = inflater.inflate(R.layout.trusted_certificates_item, parent, false);
}
TrustedCertificateEntry item = getItem(position);
TextView text = (TextView)view.findViewById(R.id.subject_primary);
text.setText(item.getSubjectPrimary());
text = (TextView)view.findViewById(R.id.subject_secondary);
text.setText(item.getSubjectSecondary());
return view;
}
}
@@ -0,0 +1,107 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui.adapter;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.strongswan.android.R;
import org.strongswan.android.data.VpnProfile;
import org.strongswan.android.data.VpnType.VpnTypeFeature;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
public class VpnProfileAdapter extends ArrayAdapter<VpnProfile>
{
private final int resource;
private final List<VpnProfile> items;
public VpnProfileAdapter(Context context, int resource,
List<VpnProfile> items)
{
super(context, resource, items);
this.resource = resource;
this.items = items;
sortItems();
}
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
View vpnProfileView;
if (convertView != null)
{
vpnProfileView = convertView;
}
else
{
LayoutInflater inflater = LayoutInflater.from(getContext());
vpnProfileView = inflater.inflate(resource, null);
}
VpnProfile profile = getItem(position);
TextView tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_name);
tv.setText(profile.getName());
tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_gateway);
tv.setText(getContext().getString(R.string.profile_gateway_label) + " " + profile.getGateway());
tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_username);
if (profile.getVpnType().has(VpnTypeFeature.USER_PASS))
{ /* if the view is reused we make sure it is visible */
tv.setVisibility(View.VISIBLE);
tv.setText(getContext().getString(R.string.profile_username_label) + " " + profile.getUsername());
}
else
{
tv.setVisibility(View.GONE);
}
tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_certificate);
if (profile.getVpnType().has(VpnTypeFeature.CERTIFICATE))
{
tv.setText(getContext().getString(R.string.profile_user_certificate_label) + " " + profile.getUserCertificateAlias());
tv.setVisibility(View.VISIBLE);
}
else
{
tv.setVisibility(View.GONE);
}
return vpnProfileView;
}
@Override
public void notifyDataSetChanged()
{
sortItems();
super.notifyDataSetChanged();
}
private void sortItems()
{
Collections.sort(this.items, new Comparator<VpnProfile>() {
@Override
public int compare(VpnProfile lhs, VpnProfile rhs)
{
return lhs.getName().compareToIgnoreCase(rhs.getName());
}
});
}
}
@@ -0,0 +1,185 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.utils;
import java.nio.ByteBuffer;
/**
* Very similar to ByteBuffer (although with a stripped interface) but it
* automatically resizes the underlying buffer.
*/
public class BufferedByteWriter
{
/**
* The underlying byte buffer
*/
private byte[] mBuffer;
/**
* ByteBuffer used as wrapper around the buffer to easily convert values
*/
private ByteBuffer mWriter;
/**
* Create a writer with a default initial capacity
*/
public BufferedByteWriter()
{
this(0);
}
/**
* Create a writer with the given initial capacity (helps avoid expensive
* resizing if known).
* @param capacity initial capacity
*/
public BufferedByteWriter(int capacity)
{
capacity = capacity > 4 ? capacity : 32;
mBuffer = new byte[capacity];
mWriter = ByteBuffer.wrap(mBuffer);
}
/**
* Ensure that there is enough space available to write the requested
* number of bytes. If necessary the internal buffer is resized.
* @param required required number of bytes
*/
private void ensureCapacity(int required)
{
if (mWriter.remaining() >= required)
{
return;
}
byte[] buffer = new byte[(mBuffer.length + required) * 2];
System.arraycopy(mBuffer, 0, buffer, 0, mWriter.position());
mBuffer = buffer;
ByteBuffer writer = ByteBuffer.wrap(buffer);
writer.position(mWriter.position());
mWriter = writer;
}
/**
* Write the given byte array to the buffer
* @param value
* @return the writer
*/
public BufferedByteWriter put(byte[] value)
{
ensureCapacity(value.length);
mWriter.put(value);
return this;
}
/**
* Write the given byte to the buffer
* @param value
* @return the writer
*/
public BufferedByteWriter put(byte value)
{
ensureCapacity(1);
mWriter.put(value);
return this;
}
/**
* Write the 8-bit length of the given data followed by the data itself
* @param value
* @return the writer
*/
public BufferedByteWriter putLen8(byte[] value)
{
ensureCapacity(1 + value.length);
mWriter.put((byte)value.length);
mWriter.put(value);
return this;
}
/**
* Write the 16-bit length of the given data followed by the data itself
* @param value
* @return the writer
*/
public BufferedByteWriter putLen16(byte[] value)
{
ensureCapacity(2 + value.length);
mWriter.putShort((short)value.length);
mWriter.put(value);
return this;
}
/**
* Write the given short value (16-bit) in big-endian order to the buffer
* @param value
* @return the writer
*/
public BufferedByteWriter put16(short value)
{
ensureCapacity(2);
mWriter.putShort(value);
return this;
}
/**
* Write 24-bit of the given value in big-endian order to the buffer
* @param value
* @return the writer
*/
public BufferedByteWriter put24(int value)
{
ensureCapacity(3);
mWriter.put((byte)(value >> 16));
mWriter.putShort((short)value);
return this;
}
/**
* Write the given int value (32-bit) in big-endian order to the buffer
* @param value
* @return the writer
*/
public BufferedByteWriter put32(int value)
{
ensureCapacity(4);
mWriter.putInt(value);
return this;
}
/**
* Write the given long value (64-bit) in big-endian order to the buffer
* @param value
* @return the writer
*/
public BufferedByteWriter put64(long value)
{
ensureCapacity(8);
mWriter.putLong(value);
return this;
}
/**
* Convert the internal buffer to a new byte array.
* @return byte array
*/
public byte[] toByteArray()
{
int length = mWriter.position();
byte[] bytes = new byte[length];
System.arraycopy(mBuffer, 0, bytes, 0, length);
return bytes;
}
}
@@ -0,0 +1,160 @@
/*
* Copyright (C) 2015 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.utils;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map.Entry;
import java.util.regex.Pattern;
/**
* Simple generator for data/files that may be parsed by libstrongswan's
* settings_t class.
*/
public class SettingsWriter
{
/**
* Top-level section
*/
private final SettingsSection mTop = new SettingsSection();
/**
* Set a string value
* @param key
* @param value
* @return the writer
*/
public SettingsWriter setValue(String key, String value)
{
Pattern pattern = Pattern.compile("[^#{}=\"\\n\\t ]+");
if (key == null || !pattern.matcher(key).matches())
{
return this;
}
String[] keys = key.split("\\.");
SettingsSection section = mTop;
section = findOrCreateSection(Arrays.copyOfRange(keys, 0, keys.length-1));
section.Settings.put(keys[keys.length-1], value);
return this;
}
/**
* Set an integer value
* @param key
* @param value
* @return the writer
*/
public SettingsWriter setValue(String key, Integer value)
{
return setValue(key, value == null ? null : value.toString());
}
/**
* Set a boolean value
* @param key
* @param value
* @return the writer
*/
public SettingsWriter setValue(String key, Boolean value)
{
return setValue(key, value == null ? null : value ? "1" : "0");
}
/**
* Serializes the settings to a string in the format understood by
* libstrongswan's settings_t parser.
* @return serialized settings
*/
public String serialize()
{
StringBuilder builder = new StringBuilder();
serializeSection(mTop, builder);
return builder.toString();
}
/**
* Serialize the settings in a section and recursively serialize sub-sections
* @param section
* @param builder
*/
private void serializeSection(SettingsSection section, StringBuilder builder)
{
for (Entry<String, String> setting : section.Settings.entrySet())
{
builder.append(setting.getKey()).append('=');
if (setting.getValue() != null)
{
builder.append("\"").append(escapeValue(setting.getValue())).append("\"");
}
builder.append('\n');
}
for (Entry<String, SettingsSection> subsection : section.Sections.entrySet())
{
builder.append(subsection.getKey()).append(" {\n");
serializeSection(subsection.getValue(), builder);
builder.append("}\n");
}
}
/**
* Escape value so it may be wrapped in "
* @param value
* @return
*/
private String escapeValue(String value)
{
return value.replace("\"", "\\\"");
}
/**
* Find or create the nested sections with the given names
* @param sections list of section names
* @return final section
*/
private SettingsSection findOrCreateSection(String[] sections)
{
SettingsSection section = mTop;
for (String name : sections)
{
SettingsSection subsection = section.Sections.get(name);
if (subsection == null)
{
subsection = new SettingsSection();
section.Sections.put(name, subsection);
}
section = subsection;
}
return section;
}
/**
* A section containing sub-sections and settings.
*/
private class SettingsSection
{
/**
* Assigned key/value pairs
*/
LinkedHashMap<String,String> Settings = new LinkedHashMap<String, String>();
/**
* Assigned sub-sections
*/
LinkedHashMap<String,SettingsSection> Sections = new LinkedHashMap<String, SettingsWriter.SettingsSection>();
}
}
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.utils;
public class Utils
{
static final char[] HEXDIGITS = "0123456789abcdef".toCharArray();
/**
* Converts the given byte array to a hexadecimal string encoding.
*
* @param bytes byte array to convert
* @return hex string
*/
public static String bytesToHex(byte[] bytes)
{
char[] hex = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; i++)
{
int value = bytes[i];
hex[i*2] = HEXDIGITS[(value & 0xf0) >> 4];
hex[i*2+1] = HEXDIGITS[ value & 0x0f];
}
return new String(hex);
}
}
@@ -0,0 +1,2 @@
openssl
strongswan
@@ -0,0 +1,84 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# use "bring your own device" (BYOD) features (also see USE_BYOD in
# MainActivity.java)
strongswan_USE_BYOD := true
strongswan_CHARON_PLUGINS := android-log openssl fips-prf random nonce pubkey \
pkcs1 pkcs8 pem xcbc hmac socket-default \
eap-identity eap-mschapv2 eap-md5 eap-gtc eap-tls
ifneq ($(strongswan_USE_BYOD),)
strongswan_BYOD_PLUGINS := eap-ttls eap-tnc tnc-imc tnc-tnccs tnccs-20
endif
strongswan_PLUGINS := $(strongswan_CHARON_PLUGINS) \
$(strongswan_BYOD_PLUGINS)
include $(LOCAL_PATH)/strongswan/Android.common.mk
# includes
strongswan_PATH := $(LOCAL_PATH)/strongswan
openssl_PATH := $(LOCAL_PATH)/openssl/include
# CFLAGS (partially from a configure run using droid-gcc)
strongswan_CFLAGS := \
-Wall \
-Wextra \
-Wno-format \
-Wno-pointer-sign \
-Wno-pointer-arith \
-Wno-sign-compare \
-Wno-strict-aliasing \
-Wno-unused-parameter \
-DHAVE___BOOL \
-DHAVE_STDBOOL_H \
-DHAVE_ALLOCA_H \
-DHAVE_ALLOCA \
-DHAVE_CLOCK_GETTIME \
-DHAVE_DLADDR \
-DHAVE_PTHREAD_COND_TIMEDWAIT_MONOTONIC \
-DHAVE_PRCTL \
-DHAVE_LINUX_UDP_H \
-DHAVE_STRUCT_SADB_X_POLICY_SADB_X_POLICY_PRIORITY \
-DHAVE_IPSEC_MODE_BEET \
-DHAVE_IPSEC_DIR_FWD \
-DHAVE_IN6ADDR_ANY \
-DHAVE_NETINET_IP6_H \
-DOPENSSL_NO_ENGINE \
-DCONFIG_H_INCLUDED \
-DCAPABILITIES \
-DCAPABILITIES_NATIVE \
-DMONOLITHIC \
-DUSE_IKEV1 \
-DUSE_IKEV2 \
-DUSE_BUILTIN_PRINTF \
-DDEBUG \
-DCHARON_UDP_PORT=0 \
-DCHARON_NATT_PORT=0 \
-DVERSION=\"$(strongswan_VERSION)\" \
-DDEV_RANDOM=\"/dev/random\" \
-DDEV_URANDOM=\"/dev/urandom\"
ifneq ($(strongswan_USE_BYOD),)
strongswan_CFLAGS += -DUSE_BYOD
endif
strongswan_BUILD := \
openssl \
libandroidbridge \
strongswan/src/libipsec \
strongswan/src/libcharon \
strongswan/src/libhydra \
strongswan/src/libstrongswan
ifneq ($(strongswan_USE_BYOD),)
strongswan_BUILD += \
strongswan/src/libtnccs \
strongswan/src/libtncif \
strongswan/src/libimcv
endif
include $(addprefix $(LOCAL_PATH)/,$(addsuffix /Android.mk, \
$(strongswan_BUILD)))
@@ -0,0 +1,3 @@
# select the ABI(s) to build for (see CPU-ARCH-ABIS.html in the NDK docs).
APP_ABI := armeabi x86 mips
APP_PLATFORM := android-19
@@ -0,0 +1,63 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# copy-n-paste from Makefile.am
LOCAL_SRC_FILES := \
android_jni.c \
backend/android_attr.c \
backend/android_creds.c \
backend/android_dns_proxy.c \
backend/android_private_key.c \
backend/android_service.c \
charonservice.c \
kernel/android_ipsec.c \
kernel/android_net.c \
kernel/network_manager.c \
vpnservice_builder.c
ifneq ($(strongswan_USE_BYOD),)
LOCAL_SRC_FILES += \
byod/imc_android_state.c \
byod/imc_android.c
endif
# build libandroidbridge -------------------------------------------------------
LOCAL_C_INCLUDES += \
$(strongswan_PATH)/src/libipsec \
$(strongswan_PATH)/src/libhydra \
$(strongswan_PATH)/src/libcharon \
$(strongswan_PATH)/src/libstrongswan
ifneq ($(strongswan_USE_BYOD),)
LOCAL_C_INCLUDES += \
$(strongswan_PATH)/src/libimcv \
$(strongswan_PATH)/src/libtncif \
$(strongswan_PATH)/src/libtnccs \
$(strongswan_PATH)/src/libtls
endif
LOCAL_CFLAGS := $(strongswan_CFLAGS) \
-DPLUGINS='"$(strongswan_CHARON_PLUGINS)"'
ifneq ($(strongswan_USE_BYOD),)
LOCAL_CFLAGS += -DPLUGINS_BYOD='"$(strongswan_BYOD_PLUGINS)"'
endif
LOCAL_MODULE := libandroidbridge
LOCAL_MODULE_TAGS := optional
LOCAL_ARM_MODE := arm
LOCAL_PRELINK_MODULE := false
LOCAL_LDLIBS := -llog
LOCAL_SHARED_LIBRARIES := libstrongswan libhydra libipsec libcharon
ifneq ($(strongswan_USE_BYOD),)
LOCAL_SHARED_LIBRARIES += libimcv libtncif libtnccs
endif
include $(BUILD_SHARED_LIBRARY)
@@ -0,0 +1,151 @@
/*
* Copyright (C) 2012-2015 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include <dlfcn.h>
#include "android_jni.h"
#include <library.h>
#include <threading/thread_value.h>
/**
* JVM
*/
static JavaVM *android_jvm;
static struct {
char name[32];
void *handle;
} libs[] = {
{ "libstrongswan.so", NULL },
#ifdef USE_BYOD
{ "libtncif.so", NULL },
{ "libtnccs.so", NULL },
{ "libimcv.so", NULL },
#endif
{ "libhydra.so", NULL },
{ "libcharon.so", NULL },
{ "libipsec.so", NULL },
};
jclass *android_charonvpnservice_class;
jclass *android_charonvpnservice_builder_class;
android_sdk_version_t android_sdk_version;
/**
* Thread-local variable. Only used because of the destructor
*/
static thread_value_t *androidjni_threadlocal;
/**
* Thread-local destructor to ensure that a native thread is detached
* from the JVM even if androidjni_detach_thread() is not called.
*/
static void attached_thread_cleanup(void *arg)
{
(*android_jvm)->DetachCurrentThread(android_jvm);
}
/*
* Described in header
*/
void androidjni_attach_thread(JNIEnv **env)
{
if ((*android_jvm)->GetEnv(android_jvm, (void**)env,
JNI_VERSION_1_6) == JNI_OK)
{ /* already attached or even a Java thread */
return;
}
(*android_jvm)->AttachCurrentThread(android_jvm, env, NULL);
/* use a thread-local value with a destructor that automatically detaches
* the thread from the JVM before it terminates, if not done manually */
androidjni_threadlocal->set(androidjni_threadlocal, (void*)*env);
}
/*
* Described in header
*/
void androidjni_detach_thread()
{
if (androidjni_threadlocal->get(androidjni_threadlocal))
{ /* only do this if we actually attached this thread */
androidjni_threadlocal->set(androidjni_threadlocal, NULL);
(*android_jvm)->DetachCurrentThread(android_jvm);
}
}
/**
* Called when this library is loaded by the JVM
*/
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
JNIEnv *env;
jclass jversion;
jfieldID jsdk_int;
int i;
android_jvm = vm;
if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK)
{
return -1;
}
for (i = 0; i < countof(libs); i++)
{
libs[i].handle = dlopen(libs[i].name, RTLD_GLOBAL);
if (!libs[i].handle)
{
return -1;
}
}
androidjni_threadlocal = thread_value_create(attached_thread_cleanup);
android_charonvpnservice_class =
(*env)->NewGlobalRef(env, (*env)->FindClass(env,
JNI_PACKAGE_STRING "/CharonVpnService"));
android_charonvpnservice_builder_class =
(*env)->NewGlobalRef(env, (*env)->FindClass(env,
JNI_PACKAGE_STRING "/CharonVpnService$BuilderAdapter"));
jversion = (*env)->FindClass(env, "android/os/Build$VERSION");
jsdk_int = (*env)->GetStaticFieldID(env, jversion, "SDK_INT", "I");
android_sdk_version = (*env)->GetStaticIntField(env, jversion, jsdk_int);
return JNI_VERSION_1_6;
}
/**
* Called when this library is unloaded by the JVM (which never happens on
* Android)
*/
void JNI_OnUnload(JavaVM *vm, void *reserved)
{
int i;
androidjni_threadlocal->destroy(androidjni_threadlocal);
for (i = countof(libs) - 1; i >= 0; i--)
{
if (libs[i].handle)
{
dlclose(libs[i].handle);
}
}
}
@@ -0,0 +1,159 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
/**
* @defgroup android_jni android_jni
* @{ @ingroup libandroidbridge
*/
#ifndef ANDROID_JNI_H_
#define ANDROID_JNI_H_
#include <jni.h>
#include <library.h>
#define JNI_PACKAGE org_strongswan_android_logic
#define JNI_PACKAGE_STRING "org/strongswan/android/logic"
#define JNI_METHOD_PP(pack, klass, name, ret, ...) \
ret Java_##pack##_##klass##_##name(JNIEnv *env, jobject this, ##__VA_ARGS__)
#define JNI_METHOD_P(pack, klass, name, ret, ...) \
JNI_METHOD_PP(pack, klass, name, ret, ##__VA_ARGS__)
#define JNI_METHOD(klass, name, ret, ...) \
JNI_METHOD_P(JNI_PACKAGE, klass, name, ret, ##__VA_ARGS__)
/**
* Java classes
* Initialized in JNI_OnLoad()
*/
extern jclass *android_charonvpnservice_class;
extern jclass *android_charonvpnservice_builder_class;
/**
* Currently known (supported) SDK versions
*
* see android.os.Build.VERSION_CODES for definitions
*/
typedef enum {
ANDROID_ICE_CREAM_SANDWICH = 14,
ANDROID_ICE_CREAM_SANDWICH_MR1 = 15,
ANDROID_JELLY_BEAN = 16,
ANDROID_JELLY_BEAN_MR1 = 17,
ANDROID_JELLY_BEAN_MR2 = 18,
} android_sdk_version_t;
/**
* The current SDK version of the Android framework
*
* see android.os.Build.VERSION.SDK_INT
*/
extern android_sdk_version_t android_sdk_version;
/**
* Attach the current thread to the JVM
*
* As local JNI references are not freed until the thread detaches
* androidjni_detach_thread() should be called as soon as possible.
* If it is not called a thread-local destructor ensures that the
* thread is at least detached as soon as it terminates.
*
* @param env JNIEnv
*/
void androidjni_attach_thread(JNIEnv **env);
/**
* Detach the current thread from the JVM
*
* Call this as soon as possible to ensure that local JNI references are freed.
*/
void androidjni_detach_thread();
/**
* Handle exceptions thrown by a JNI call
*
* @param env JNIEnv
* @return TRUE if an exception was thrown
*/
static inline bool androidjni_exception_occurred(JNIEnv *env)
{
if ((*env)->ExceptionOccurred(env))
{ /* clear any exception, otherwise the VM is terminated */
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
return TRUE;
}
return FALSE;
}
/**
* Convert a Java string to a C string. Memory is allocated.
*
* @param env JNIEnv
* @param jstr Java string
* @return native C string (allocated)
*/
static inline char *androidjni_convert_jstring(JNIEnv *env, jstring jstr)
{
char *str = NULL;
jsize bytes, chars;
if (jstr)
{
chars = (*env)->GetStringLength(env, jstr);
bytes = (*env)->GetStringUTFLength(env, jstr);
str = malloc(bytes + 1);
(*env)->GetStringUTFRegion(env, jstr, 0, chars, str);
str[bytes] = '\0';
}
return str;
}
/**
* Converts the given Java byte array to a chunk
*
* @param env JNIEnv
* @param jbytearray Java byte array
* @return allocated chunk
*/
static inline chunk_t chunk_from_byte_array(JNIEnv *env, jbyteArray jbytearray)
{
chunk_t chunk;
chunk = chunk_alloc((*env)->GetArrayLength(env, jbytearray));
(*env)->GetByteArrayRegion(env, jbytearray, 0, chunk.len, chunk.ptr);
return chunk;
}
/**
* Converts the given chunk to a Java byte array
*
* @param env JNIEnv
* @param chunk native chunk
* @return allocated Java byte array
*/
static inline jbyteArray byte_array_from_chunk(JNIEnv *env, chunk_t chunk)
{
jbyteArray jbytearray;
jbytearray = (*env)->NewByteArray(env, chunk.len);
(*env)->SetByteArrayRegion(env, jbytearray, 0, chunk.len, chunk.ptr);
return jbytearray;
}
#endif /** ANDROID_JNI_H_ @}*/
@@ -0,0 +1,131 @@
/*
* Copyright (C) 2012-2013 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "android_attr.h"
#include "../charonservice.h"
#include <hydra.h>
#include <utils/debug.h>
#include <library.h>
typedef struct private_android_attr_t private_android_attr_t;
/**
* Private data of an android_attr_t object.
*/
struct private_android_attr_t {
/**
* Public interface.
*/
android_attr_t public;
};
METHOD(attribute_handler_t, handle, bool,
private_android_attr_t *this, ike_sa_t *ike_sa,
configuration_attribute_type_t type, chunk_t data)
{
vpnservice_builder_t *builder;
host_t *dns;
switch (type)
{
case INTERNAL_IP4_DNS:
dns = host_create_from_chunk(AF_INET, data, 0);
break;
case INTERNAL_IP6_DNS:
dns = host_create_from_chunk(AF_INET6, data, 0);
break;
default:
return FALSE;
}
if (!dns || dns->is_anyaddr(dns))
{
DESTROY_IF(dns);
return FALSE;
}
builder = charonservice->get_vpnservice_builder(charonservice);
builder->add_dns(builder, dns);
dns->destroy(dns);
return TRUE;
}
METHOD(attribute_handler_t, release, void,
private_android_attr_t *this, ike_sa_t *ike_sa,
configuration_attribute_type_t type, chunk_t data)
{
/* DNS servers cannot be removed from an existing TUN device */
}
METHOD(enumerator_t, enumerate_dns6, bool,
enumerator_t *this, configuration_attribute_type_t *type, chunk_t *data)
{
*type = INTERNAL_IP6_DNS;
*data = chunk_empty;
this->enumerate = (void*)return_false;
return TRUE;
}
METHOD(enumerator_t, enumerate_dns4, bool,
enumerator_t *this, configuration_attribute_type_t *type, chunk_t *data)
{
*type = INTERNAL_IP4_DNS;
*data = chunk_empty;
this->enumerate = (void*)_enumerate_dns6;
return TRUE;
}
METHOD(attribute_handler_t, create_attribute_enumerator, enumerator_t*,
private_android_attr_t *this, ike_sa_t *ike_sa, linked_list_t *vips)
{
enumerator_t *enumerator;
INIT(enumerator,
.enumerate = (void*)_enumerate_dns4,
.destroy = (void*)free,
);
return enumerator;
}
METHOD(android_attr_t, destroy, void,
private_android_attr_t *this)
{
free(this);
}
/**
* Described in header
*/
android_attr_t *android_attr_create()
{
private_android_attr_t *this;
INIT(this,
.public = {
.handler = {
.handle = _handle,
.release = _release,
.create_attribute_enumerator = _create_attribute_enumerator,
},
.destroy = _destroy,
},
);
return &this->public;
}
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
/**
* @defgroup android_attr android_attr
* @{ @ingroup android_backend
*/
#ifndef ANDROID_ATTR_H_
#define ANDROID_ATTR_H_
#include <library.h>
#include <attributes/attribute_handler.h>
typedef struct android_attr_t android_attr_t;
/**
* Handler for DNS configuration
*/
struct android_attr_t {
/**
* implements the attribute_handler_t interface
*/
attribute_handler_t handler;
/**
* Destroy a android_attr_t
*/
void (*destroy)(android_attr_t *this);
};
/**
* Create a android_attr_t instance.
*/
android_attr_t *android_attr_create(void);
#endif /** ANDROID_ATTR_H_ @}*/
@@ -0,0 +1,256 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "android_creds.h"
#include "../charonservice.h"
#include <daemon.h>
#include <library.h>
#include <credentials/sets/mem_cred.h>
#include <threading/rwlock.h>
typedef struct private_android_creds_t private_android_creds_t;
/**
* Private data of an android_creds_t object
*/
struct private_android_creds_t {
/**
* Public interface
*/
android_creds_t public;
/**
* Credential set storing trusted certificates and user credentials
*/
mem_cred_t *creds;
/**
* read/write lock to make sure certificates are only loaded once
*/
rwlock_t *lock;
/**
* TRUE if certificates have been loaded via JNI
*/
bool loaded;
};
/**
* Free allocated DER encoding
*/
static void free_encoding(chunk_t *chunk)
{
chunk_free(chunk);
free(chunk);
}
/**
* Load trusted certificates via charonservice (JNI).
*/
static void load_trusted_certificates(private_android_creds_t *this)
{
linked_list_t *certs;
certificate_t *cert;
chunk_t *current;
certs = charonservice->get_trusted_certificates(charonservice);
if (certs)
{
while (certs->remove_first(certs, (void**)&current) == SUCCESS)
{
cert = lib->creds->create(lib->creds, CRED_CERTIFICATE, CERT_X509,
BUILD_BLOB_ASN1_DER, *current, BUILD_END);
if (cert)
{
DBG2(DBG_CFG, "loaded CA certificate '%Y'",
cert->get_subject(cert));
this->creds->add_cert(this->creds, TRUE, cert);
}
free_encoding(current);
}
certs->destroy(certs);
}
}
METHOD(credential_set_t, create_cert_enumerator, enumerator_t*,
private_android_creds_t *this, certificate_type_t cert, key_type_t key,
identification_t *id, bool trusted)
{
enumerator_t *enumerator;
if (cert != CERT_ANY && cert != CERT_X509)
{
return NULL;
}
this->lock->read_lock(this->lock);
if (!this->loaded)
{
this->lock->unlock(this->lock);
this->lock->write_lock(this->lock);
/* check again after acquiring the write lock */
if (!this->loaded)
{
load_trusted_certificates(this);
this->loaded = TRUE;
}
this->lock->unlock(this->lock);
this->lock->read_lock(this->lock);
}
enumerator = this->creds->set.create_cert_enumerator(&this->creds->set,
cert, key, id, trusted);
return enumerator_create_cleaner(enumerator, (void*)this->lock->unlock,
this->lock);
}
METHOD(android_creds_t, add_username_password, void,
private_android_creds_t *this, char *username, char *password)
{
shared_key_t *shared_key;
identification_t *id;
chunk_t secret;
secret = chunk_create(password, strlen(password));
shared_key = shared_key_create(SHARED_EAP, chunk_clone(secret));
id = identification_create_from_string(username);
this->creds->add_shared(this->creds, shared_key, id, NULL);
}
METHOD(credential_set_t, create_shared_enumerator, enumerator_t*,
private_android_creds_t *this, shared_key_type_t type,
identification_t *me, identification_t *other)
{
return this->creds->set.create_shared_enumerator(&this->creds->set,
type, me, other);
}
METHOD(android_creds_t, load_user_certificate, certificate_t*,
private_android_creds_t *this)
{
linked_list_t *encodings;
certificate_t *cert = NULL, *ca_cert;
chunk_t *current;
encodings = charonservice->get_user_certificate(charonservice);
if (!encodings)
{
return NULL;
}
while (encodings->remove_first(encodings, (void**)&current) == SUCCESS)
{
if (!cert)
{ /* the first element is the user certificate */
cert = lib->creds->create(lib->creds, CRED_CERTIFICATE, CERT_X509,
BUILD_BLOB_ASN1_DER, *current, BUILD_END);
if (!cert)
{
DBG1(DBG_CFG, "failed to load user certificate");
free_encoding(current);
break;
}
DBG1(DBG_CFG, "loaded user certificate '%Y' and private key",
cert->get_subject(cert));
cert = this->creds->add_cert_ref(this->creds, TRUE, cert);
free_encoding(current);
continue;
}
/* the rest are CA certificates, we ignore failures */
ca_cert = lib->creds->create(lib->creds, CRED_CERTIFICATE, CERT_X509,
BUILD_BLOB_ASN1_DER, *current, BUILD_END);
if (ca_cert)
{
DBG1(DBG_CFG, "loaded CA certificate '%Y'",
ca_cert->get_subject(ca_cert));
this->creds->add_cert(this->creds, TRUE, ca_cert);
}
free_encoding(current);
}
encodings->destroy_function(encodings, (void*)free_encoding);
if (cert)
{
private_key_t *key;
key = charonservice->get_user_key(charonservice,
cert->get_public_key(cert));
if (key)
{
this->creds->add_key(this->creds, key);
}
else
{
DBG1(DBG_CFG, "failed to load private key");
cert->destroy(cert);
cert = NULL;
}
}
return cert;
}
METHOD(credential_set_t, create_private_enumerator, enumerator_t*,
private_android_creds_t *this, key_type_t type, identification_t *id)
{
return this->creds->set.create_private_enumerator(&this->creds->set,
type, id);
}
METHOD(android_creds_t, clear, void,
private_android_creds_t *this)
{
this->lock->write_lock(this->lock);
this->creds->clear(this->creds);
this->loaded = FALSE;
this->lock->unlock(this->lock);
}
METHOD(android_creds_t, destroy, void,
private_android_creds_t *this)
{
clear(this);
this->creds->destroy(this->creds);
this->lock->destroy(this->lock);
free(this);
}
/**
* Described in header.
*/
android_creds_t *android_creds_create()
{
private_android_creds_t *this;
INIT(this,
.public = {
.set = {
.create_cert_enumerator = _create_cert_enumerator,
.create_shared_enumerator = _create_shared_enumerator,
.create_private_enumerator = _create_private_enumerator,
.create_cdp_enumerator = (void*)return_null,
.cache_cert = (void*)nop,
},
.add_username_password = _add_username_password,
.load_user_certificate = _load_user_certificate,
.clear = _clear,
.destroy = _destroy,
},
.creds = mem_cred_create(),
.lock = rwlock_create(RWLOCK_TYPE_DEFAULT),
);
return &this->public;
}
@@ -0,0 +1,74 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
/**
* @defgroup android_creds android_creds
* @{ @ingroup android_backend
*/
#ifndef ANDROID_CREDS_H_
#define ANDROID_CREDS_H_
#include <library.h>
#include <credentials/credential_set.h>
typedef struct android_creds_t android_creds_t;
/**
* Android credential set that provides CA certificates via JNI and supplied
* user credentials.
*/
struct android_creds_t {
/**
* Implements credential_set_t
*/
credential_set_t set;
/**
* Add user name and password for EAP authentication
*
* @param username user name
* @param password password
*/
void (*add_username_password)(android_creds_t *this, char *username,
char *password);
/**
* Load the user certificate and private key
*
* @return loaded client certificate, NULL on failure
*/
certificate_t *(*load_user_certificate)(android_creds_t *this);
/**
* Clear the cached certificates and stored credentials.
*/
void (*clear)(android_creds_t *this);
/**
* Destroy a android_creds instance.
*/
void (*destroy)(android_creds_t *this);
};
/**
* Create an android_creds instance.
*/
android_creds_t *android_creds_create();
#endif /** ANDROID_CREDS_H_ @}*/
@@ -0,0 +1,419 @@
/*
* Copyright (C) 2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/udp.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include "android_dns_proxy.h"
#include <hydra.h>
#include <threading/rwlock.h>
#include <collections/hashtable.h>
#include <processing/jobs/callback_job.h>
/**
* Timeout in seconds for sockets (i.e. not used for x seconds -> delete)
*/
#define SOCKET_TIMEOUT 30
typedef struct private_android_dns_proxy_t private_android_dns_proxy_t;
struct private_android_dns_proxy_t {
/**
* Public interface
*/
android_dns_proxy_t public;
/**
* Mapping from source address to sockets
*/
hashtable_t *sockets;
/**
* Registered callback
*/
dns_proxy_response_cb_t cb;
/**
* Data passed to callback
*/
void *data;
/**
* Lock used to synchronize access to the private members
*/
rwlock_t *lock;
/**
* Hostnames to filter queries by
*/
hashtable_t *hostnames;
};
/**
* Data for proxy sockets
*/
typedef struct {
private_android_dns_proxy_t *proxy;
time_t last_use;
host_t *src;
int fd;
} proxy_socket_t;
/**
* Destroy a socket
*/
static void socket_destroy(proxy_socket_t *this)
{
this->src->destroy(this->src);
if (this->fd != -1)
{
close(this->fd);
}
free(this);
}
/**
* Hash a proxy socket by src address
*/
static u_int socket_hash(host_t *src)
{
u_int16_t port = src->get_port(src);
return chunk_hash_inc(src->get_address(src),
chunk_hash(chunk_from_thing(port)));
}
/**
* Compare proxy sockets by src address
*/
static bool socket_equals(host_t *a, host_t *b)
{
return a->equals(a, b);
}
/**
* Opens a UDP socket for the given address family
*/
static int open_socket(int family)
{
int skt;
skt = socket(family, SOCK_DGRAM, IPPROTO_UDP);
if (skt < 0)
{
DBG1(DBG_NET, "could not open proxy socket: %s", strerror(errno));
return -1;
}
if (!hydra->kernel_interface->bypass_socket(hydra->kernel_interface,
skt, family))
{
DBG1(DBG_NET, "installing bypass policy for proxy socket failed");
close(skt);
return -1;
}
return skt;
}
/**
* Create a proxy socket for the given source
*/
static proxy_socket_t *create_socket(private_android_dns_proxy_t *this,
host_t *src)
{
proxy_socket_t *skt;
INIT(skt,
.proxy = this,
.src = src->clone(src),
.fd = open_socket(src->get_family(src)),
);
if (skt->fd == -1)
{
socket_destroy(skt);
return NULL;
}
return skt;
}
CALLBACK(handle_response, bool,
proxy_socket_t *this, int fd, watcher_event_t event)
{
struct sockaddr_storage addr;
socklen_t addr_len = sizeof(addr);
char buf[4096];
ssize_t len;
host_t *src;
len = recvfrom(fd, buf, sizeof(buf), MSG_DONTWAIT, (struct sockaddr*)&addr,
&addr_len);
if (len > 0)
{
ip_packet_t *packet;
src = host_create_from_sockaddr((sockaddr_t*)&addr);
if (!src)
{
DBG1(DBG_NET, "failed to parse source address");
return TRUE;
}
packet = ip_packet_create_udp_from_data(src, this->src,
chunk_create(buf, len));
if (!packet)
{
DBG1(DBG_NET, "failed to parse DNS response");
return TRUE;
}
this->proxy->lock->read_lock(this->proxy->lock);
this->last_use = time_monotonic(NULL);
if (this->proxy->cb)
{
this->proxy->cb(this->proxy->data, packet);
}
else
{
packet->destroy(packet);
}
this->proxy->lock->unlock(this->proxy->lock);
}
else if (errno != EWOULDBLOCK)
{
DBG1(DBG_NET, "receiving DNS response failed: %s", strerror(errno));
}
return TRUE;
}
CALLBACK(handle_timeout, job_requeue_t,
proxy_socket_t *this)
{
time_t now, diff;
now = time_monotonic(NULL);
this->proxy->lock->write_lock(this->proxy->lock);
diff = now - this->last_use;
if (diff >= SOCKET_TIMEOUT)
{
this->proxy->sockets->remove(this->proxy->sockets, this->src);
lib->watcher->remove(lib->watcher, this->fd);
this->proxy->lock->unlock(this->proxy->lock);
socket_destroy(this);
return JOB_REQUEUE_NONE;
}
this->proxy->lock->unlock(this->proxy->lock);
return JOB_RESCHEDULE(SOCKET_TIMEOUT - diff);
}
/**
* DNS header and masks to access flags
*/
typedef struct __attribute__((packed)) {
u_int16_t id;
u_int16_t flags;
#define DNS_QR_MASK 0x8000
#define DNS_OPCODE_MASK 0x7800
u_int16_t qdcount;
u_int16_t ancount;
u_int16_t nscount;
u_int16_t arcount;
} dns_header_t;
/**
* Extract the hostname in the question section data points to.
* Hostnames can be at most 255 characters (including dots separating labels),
* each label must be between 1 and 63 characters.
* The format is [len][label][len][label], followed by a null byte to indicate
* the null label of the root.
*/
static char *extract_hostname(chunk_t data)
{
char *hostname, *pos, *end;
u_int8_t label;
if (!data.len || data.len > 255)
{
return NULL;
}
label = *data.ptr;
data = chunk_skip(data, 1);
hostname = strndup(data.ptr, data.len);
/* replace all label lengths with dots */
pos = hostname + label;
end = hostname + strlen(hostname);
while (pos < end)
{
label = *pos;
*pos++ = '.';
pos += label;
}
return hostname;
}
/**
* Check if the DNS query is for one of the allowed hostnames
*/
static bool check_hostname(private_android_dns_proxy_t *this, chunk_t data)
{
dns_header_t *dns;
char *hostname;
bool success = FALSE;
this->lock->read_lock(this->lock);
if (!this->hostnames->get_count(this->hostnames))
{
this->lock->unlock(this->lock);
return TRUE;
}
if (data.len < sizeof(dns_header_t))
{
this->lock->unlock(this->lock);
return FALSE;
}
dns = (dns_header_t*)data.ptr;
if ((ntohs(dns->flags) & DNS_QR_MASK) == 0 &&
(ntohs(dns->flags) & DNS_OPCODE_MASK) == 0 &&
dns->qdcount)
{
data = chunk_skip(data, sizeof(dns_header_t));
hostname = extract_hostname(data);
if (hostname && this->hostnames->get(this->hostnames, hostname))
{
success = TRUE;
}
free(hostname);
}
this->lock->unlock(this->lock);
return success;
}
METHOD(android_dns_proxy_t, handle, bool,
private_android_dns_proxy_t *this, ip_packet_t *packet)
{
proxy_socket_t *skt;
host_t *dst, *src;
chunk_t data;
if (packet->get_next_header(packet) != IPPROTO_UDP)
{
return FALSE;
}
dst = packet->get_destination(packet);
if (dst->get_port(dst) != 53)
{ /* no DNS packet */
return FALSE;
}
data = packet->get_payload(packet);
/* remove UDP header */
data = chunk_skip(data, 8);
if (!check_hostname(this, data))
{
return FALSE;
}
src = packet->get_source(packet);
this->lock->write_lock(this->lock);
skt = this->sockets->get(this->sockets, src);
if (!skt)
{
skt = create_socket(this, src);
if (!skt)
{
this->lock->unlock(this->lock);
return FALSE;
}
this->sockets->put(this->sockets, skt->src, skt);
lib->watcher->add(lib->watcher, skt->fd, WATCHER_READ, handle_response,
skt);
lib->scheduler->schedule_job(lib->scheduler,
(job_t*)callback_job_create(handle_timeout, skt,
NULL, (callback_job_cancel_t)return_false), SOCKET_TIMEOUT);
}
skt->last_use = time_monotonic(NULL);
if (sendto(skt->fd, data.ptr, data.len, 0, dst->get_sockaddr(dst),
*dst->get_sockaddr_len(dst)) != data.len)
{
DBG1(DBG_NET, "sending DNS request failed: %s", strerror(errno));
}
this->lock->unlock(this->lock);
return TRUE;
}
METHOD(android_dns_proxy_t, register_cb, void,
private_android_dns_proxy_t *this, dns_proxy_response_cb_t cb, void *data)
{
this->lock->write_lock(this->lock);
this->cb = cb;
this->data = data;
this->lock->unlock(this->lock);
}
METHOD(android_dns_proxy_t, unregister_cb, void,
private_android_dns_proxy_t *this, dns_proxy_response_cb_t cb)
{
this->lock->write_lock(this->lock);
if (this->cb == cb)
{
this->cb = NULL;
}
this->lock->unlock(this->lock);
}
METHOD(android_dns_proxy_t, add_hostname, void,
private_android_dns_proxy_t *this, char *hostname)
{
char *existing;
hostname = strdup(hostname);
this->lock->write_lock(this->lock);
existing = this->hostnames->put(this->hostnames, hostname, hostname);
this->lock->unlock(this->lock);
free(existing);
}
METHOD(android_dns_proxy_t, destroy, void,
private_android_dns_proxy_t *this)
{
this->hostnames->destroy_function(this->hostnames, (void*)free);
this->sockets->destroy_function(this->sockets, (void*)socket_destroy);
this->lock->destroy(this->lock);
free(this);
}
/**
* Described in header.
*/
android_dns_proxy_t *android_dns_proxy_create()
{
private_android_dns_proxy_t *this;
INIT(this,
.public = {
.handle = _handle,
.register_cb = _register_cb,
.unregister_cb = _unregister_cb,
.add_hostname = _add_hostname,
.destroy = _destroy,
},
.sockets = hashtable_create((hashtable_hash_t)socket_hash,
(hashtable_equals_t)socket_equals, 4),
.hostnames = hashtable_create(hashtable_hash_str,
hashtable_equals_str, 4),
.lock = rwlock_create(RWLOCK_TYPE_DEFAULT),
);
return &this->public;
}
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
/**
* @defgroup android_dns_proxy android_dns_proxy
* @{ @ingroup android_backend
*/
#ifndef ANDROID_DNS_PROXY_H_
#define ANDROID_DNS_PROXY_H_
#include <ip_packet.h>
typedef struct android_dns_proxy_t android_dns_proxy_t;
/**
* Callback called to deliver a DNS response packet.
*
* @param data data supplied during registration of the callback
* @param packet DNS response packet (has to be destroyed)
*/
typedef void (*dns_proxy_response_cb_t)(void *data, ip_packet_t *packet);
/**
* DNS proxy class
*/
struct android_dns_proxy_t {
/**
* Handle an outbound DNS packet (if the packet is one)
*
* @param packet packet to handle
* @return TRUE if handled, FALSE otherwise (no DNS)
*/
bool (*handle)(android_dns_proxy_t *this, ip_packet_t *packet);
/**
* Register the callback used to deliver DNS response packets.
*
* @param cb the callback function
* @param data optional data provided to callback
*/
void (*register_cb)(android_dns_proxy_t *this, dns_proxy_response_cb_t cb,
void *data);
/**
* Unregister the callback used to deliver DNS response packets.
*
* @param cb the callback function
* @param data optional data provided to callback
*/
void (*unregister_cb)(android_dns_proxy_t *this, dns_proxy_response_cb_t cb);
/**
* Add a hostname for which queries are proxied. If at least one hostname
* is configured DNS queries for others will not be handled.
*
* @param hostname hostname to add (gets cloned)
*/
void (*add_hostname)(android_dns_proxy_t *this, char *hostname);
/**
* Destroy an instance.
*/
void (*destroy)(android_dns_proxy_t *this);
};
/**
* Create an instance.
*/
android_dns_proxy_t *android_dns_proxy_create();
#endif /** ANDROID_DNS_PROXY_H_ @}*/
@@ -0,0 +1,360 @@
/*
* Copyright (C) 2012-2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "android_private_key.h"
#include "../android_jni.h"
#include <utils/debug.h>
#include <asn1/asn1.h>
typedef struct private_private_key_t private_private_key_t;
/**
* Private data of a android_private_key_t object.
*/
struct private_private_key_t {
/**
* Public interface
*/
private_key_t public;
/**
* reference to the Java PrivateKey object
*/
jobject key;
/**
* Java class used to build signatures
*/
jclass signature_class;
/**
* public key that belongs to this private key
*/
public_key_t *pubkey;
/**
* reference count
*/
refcount_t ref;
};
METHOD(private_key_t, sign, bool,
private_private_key_t *this, signature_scheme_t scheme,
chunk_t data, chunk_t *signature)
{
JNIEnv *env;
jmethodID method_id;
const char *method = NULL;
jstring jmethod;
jobject jsignature;
jbyteArray jdata, jsigarray;
switch (this->pubkey->get_type(this->pubkey))
{
case KEY_RSA:
switch (scheme)
{
case SIGN_RSA_EMSA_PKCS1_NULL:
method = "NONEwithRSA";
break;
case SIGN_RSA_EMSA_PKCS1_MD5:
method = "MD5withRSA";
break;
case SIGN_RSA_EMSA_PKCS1_SHA1:
method = "SHA1withRSA";
break;
case SIGN_RSA_EMSA_PKCS1_SHA224:
method = "SHA224withRSA";
break;
case SIGN_RSA_EMSA_PKCS1_SHA256:
method = "SHA256withRSA";
break;
case SIGN_RSA_EMSA_PKCS1_SHA384:
method = "SHA384withRSA";
break;
case SIGN_RSA_EMSA_PKCS1_SHA512:
method = "SHA512withRSA";
break;
default:
break;
}
break;
case KEY_ECDSA:
switch (scheme)
{
case SIGN_ECDSA_WITH_SHA1_DER:
method = "SHA1withECDSA";
break;
case SIGN_ECDSA_WITH_SHA256_DER:
case SIGN_ECDSA_256:
method = "SHA256withECDSA";
break;
case SIGN_ECDSA_WITH_SHA384_DER:
case SIGN_ECDSA_384:
method = "SHA384withECDSA";
break;
case SIGN_ECDSA_WITH_SHA512_DER:
case SIGN_ECDSA_521:
method = "SHA512withECDSA";
break;
default:
break;
}
break;
default:
break;
}
if (!method)
{
DBG1(DBG_LIB, "signature scheme %N not supported via JNI",
signature_scheme_names, scheme);
return FALSE;
}
androidjni_attach_thread(&env);
/* we use java.security.Signature to create the signature without requiring
* access to the actual private key */
method_id = (*env)->GetStaticMethodID(env, this->signature_class,
"getInstance", "(Ljava/lang/String;)Ljava/security/Signature;");
if (!method_id)
{
goto failed;
}
jmethod = (*env)->NewStringUTF(env, method);
if (!jmethod)
{
goto failed;
}
jsignature = (*env)->CallStaticObjectMethod(env, this->signature_class,
method_id, jmethod);
if (!jsignature)
{
goto failed;
}
method_id = (*env)->GetMethodID(env, this->signature_class, "initSign",
"(Ljava/security/PrivateKey;)V");
if (!method_id)
{
goto failed;
}
(*env)->CallVoidMethod(env, jsignature, method_id, this->key);
if (androidjni_exception_occurred(env))
{
goto failed;
}
method_id = (*env)->GetMethodID(env, this->signature_class, "update",
"([B)V");
if (!method_id)
{
goto failed;
}
jdata = byte_array_from_chunk(env, data);
(*env)->CallVoidMethod(env, jsignature, method_id, jdata);
if (androidjni_exception_occurred(env))
{
goto failed;
}
method_id = (*env)->GetMethodID(env, this->signature_class, "sign",
"()[B");
if (!method_id)
{
goto failed;
}
jsigarray = (*env)->CallObjectMethod(env, jsignature, method_id);
if (!jsigarray)
{
goto failed;
}
if (this->pubkey->get_type(this->pubkey) == KEY_ECDSA)
{
chunk_t encoded, parse, r, s;
size_t len = 0;
switch (scheme)
{
case SIGN_ECDSA_256:
len = 32;
break;
case SIGN_ECDSA_384:
len = 48;
break;
case SIGN_ECDSA_521:
len = 66;
break;
default:
break;
}
if (len)
{
/* we get an ASN.1 encoded sequence of integers r and s */
parse = encoded = chunk_from_byte_array(env, jsigarray);
if (asn1_unwrap(&parse, &parse) != ASN1_SEQUENCE ||
asn1_unwrap(&parse, &r) != ASN1_INTEGER ||
asn1_unwrap(&parse, &s) != ASN1_INTEGER)
{
chunk_free(&encoded);
goto failed;
}
r = chunk_skip_zero(r);
s = chunk_skip_zero(s);
if (r.len > len || s.len > len)
{
chunk_free(&encoded);
goto failed;
}
/* concatenate r and s (forced to the defined length) */
*signature = chunk_alloc(2*len);
memset(signature->ptr, 0, signature->len);
memcpy(signature->ptr + (len - r.len), r.ptr, r.len);
memcpy(signature->ptr + len + (len - s.len), s.ptr, s.len);
chunk_free(&encoded);
}
else
{
*signature = chunk_from_byte_array(env, jsigarray);
}
}
else
{
*signature = chunk_from_byte_array(env, jsigarray);
}
androidjni_detach_thread();
return TRUE;
failed:
DBG1(DBG_LIB, "failed to build %N signature via JNI",
signature_scheme_names, scheme);
androidjni_exception_occurred(env);
androidjni_detach_thread();
return FALSE;
}
METHOD(private_key_t, get_type, key_type_t,
private_private_key_t *this)
{
return this->pubkey->get_type(this->pubkey);
}
METHOD(private_key_t, decrypt, bool,
private_private_key_t *this, encryption_scheme_t scheme,
chunk_t crypto, chunk_t *plain)
{
DBG1(DBG_LIB, "private key decryption is currently not supported via JNI");
return FALSE;
}
METHOD(private_key_t, get_keysize, int,
private_private_key_t *this)
{
return this->pubkey->get_keysize(this->pubkey);
}
METHOD(private_key_t, get_public_key, public_key_t*,
private_private_key_t *this)
{
return this->pubkey->get_ref(this->pubkey);
}
METHOD(private_key_t, get_encoding, bool,
private_private_key_t *this, cred_encoding_type_t type,
chunk_t *encoding)
{
return FALSE;
}
METHOD(private_key_t, get_fingerprint, bool,
private_private_key_t *this, cred_encoding_type_t type, chunk_t *fp)
{
return this->pubkey->get_fingerprint(this->pubkey, type, fp);
}
METHOD(private_key_t, get_ref, private_key_t*,
private_private_key_t *this)
{
ref_get(&this->ref);
return &this->public;
}
METHOD(private_key_t, destroy, void,
private_private_key_t *this)
{
if (ref_put(&this->ref))
{
JNIEnv *env;
androidjni_attach_thread(&env);
if (android_sdk_version == ANDROID_JELLY_BEAN)
{ /* there is a bug in JB that causes a SIGSEGV if the key object is
* garbage collected so we intentionally leak the reference to it */
DBG1(DBG_LIB, "intentionally leaking private key reference due to "
"a bug in the framework");
}
else
{
(*env)->DeleteGlobalRef(env, this->key);
}
(*env)->DeleteGlobalRef(env, this->signature_class);
androidjni_detach_thread();
this->pubkey->destroy(this->pubkey);
free(this);
}
}
/*
* See header
*/
private_key_t *android_private_key_create(jobject key, public_key_t *pubkey)
{
JNIEnv *env;
private_private_key_t *this;
INIT(this,
.public = {
.get_type = _get_type,
.sign = _sign,
.decrypt = _decrypt,
.get_keysize = _get_keysize,
.get_public_key = _get_public_key,
.belongs_to = private_key_belongs_to,
.equals = private_key_equals,
.get_fingerprint = _get_fingerprint,
.has_fingerprint = private_key_has_fingerprint,
.get_encoding = _get_encoding,
.get_ref = _get_ref,
.destroy = _destroy,
},
.ref = 1,
.pubkey = pubkey,
);
if (!pubkey)
{
free(this);
return NULL;
}
/* in ICS we could simply call getEncoded and use the PKCS#8/DER encoded
* private key, since JB that's not possible as there is no direct access
* to private keys anymore (as these could now be hardware backed) */
androidjni_attach_thread(&env);
this->key = (*env)->NewGlobalRef(env, key);
this->signature_class = (*env)->NewGlobalRef(env, (*env)->FindClass(env,
"java/security/Signature"));
androidjni_detach_thread();
return &this->public;
}
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
/**
* @defgroup android_private_key android_private_key
* @{ @ingroup android_backend
*/
#ifndef ANDROID_PRIVATE_KEY_H_
#define ANDROID_PRIVATE_KEY_H_
#include <jni.h>
#include <credentials/keys/private_key.h>
/**
* Create a JNI backed key, stored in the Android KeyChain
*
* @param key PrivateKey instance
* @param pubkey public key as extracted from the certificate (gets adopted)
* @return private_key_t instance
*/
private_key_t *android_private_key_create(jobject key, public_key_t *pubkey);
#endif /** ANDROID_PRIVATE_KEY_H_ @}*/
@@ -0,0 +1,848 @@
/*
* Copyright (C) 2010-2015 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include <errno.h>
#include <unistd.h>
#include "android_service.h"
#include "android_dns_proxy.h"
#include "../charonservice.h"
#include "../vpnservice_builder.h"
#include <daemon.h>
#include <library.h>
#include <ipsec.h>
#include <processing/jobs/callback_job.h>
#include <threading/rwlock.h>
#include <threading/thread.h>
typedef struct private_android_service_t private_android_service_t;
/**
* private data of Android service
*/
struct private_android_service_t {
/**
* public interface
*/
android_service_t public;
/**
* credential set
*/
android_creds_t *creds;
/**
* current IKE_SA
*/
ike_sa_t *ike_sa;
/**
* configuration setttings
*/
settings_t *settings;
/**
* lock to safely access the TUN device fd
*/
rwlock_t *lock;
/**
* TUN device file descriptor
*/
int tunfd;
/**
* MTU of TUN device
*/
int mtu;
/**
* DNS proxy
*/
android_dns_proxy_t *dns_proxy;
/**
* Whether to use the DNS proxy or not
*/
bool use_dns_proxy;
};
/**
* Outbound callback
*/
static void send_esp(void *data, esp_packet_t *packet)
{
charon->sender->send_no_marker(charon->sender, (packet_t*)packet);
}
/**
* Inbound callback
*/
static void deliver_plain(private_android_service_t *this,
ip_packet_t *packet)
{
chunk_t encoding;
ssize_t len;
encoding = packet->get_encoding(packet);
this->lock->read_lock(this->lock);
if (this->tunfd < 0)
{ /* the TUN device is already closed */
this->lock->unlock(this->lock);
packet->destroy(packet);
return;
}
len = write(this->tunfd, encoding.ptr, encoding.len);
this->lock->unlock(this->lock);
if (len < 0 || len != encoding.len)
{
DBG1(DBG_DMN, "failed to write packet to TUN device: %s",
strerror(errno));
}
packet->destroy(packet);
}
/**
* Receiver callback
*/
static void receiver_esp_cb(void *data, packet_t *packet)
{
esp_packet_t *esp_packet;
esp_packet = esp_packet_create_from_packet(packet);
ipsec->processor->queue_inbound(ipsec->processor, esp_packet);
}
/**
* Job handling outbound plaintext packets
*/
static job_requeue_t handle_plain(private_android_service_t *this)
{
ip_packet_t *packet;
chunk_t raw;
fd_set set;
ssize_t len;
int tunfd;
bool old, dns_proxy;
timeval_t tv = {
/* check every second if tunfd is still valid */
.tv_sec = 1,
};
FD_ZERO(&set);
this->lock->read_lock(this->lock);
if (this->tunfd < 0)
{ /* the TUN device is already closed */
this->lock->unlock(this->lock);
return JOB_REQUEUE_NONE;
}
tunfd = this->tunfd;
FD_SET(tunfd, &set);
/* cache this while we have the lock */
dns_proxy = this->use_dns_proxy;
this->lock->unlock(this->lock);
old = thread_cancelability(TRUE);
len = select(tunfd + 1, &set, NULL, NULL, &tv);
thread_cancelability(old);
if (len < 0)
{
if (errno == EBADF)
{ /* the TUN device got closed just before calling select(), retry */
return JOB_REQUEUE_FAIR;
}
DBG1(DBG_DMN, "select on TUN device failed: %s", strerror(errno));
return JOB_REQUEUE_NONE;
}
else if (len == 0)
{ /* timeout, check again right away */
return JOB_REQUEUE_DIRECT;
}
raw = chunk_alloc(this->mtu);
len = read(tunfd, raw.ptr, raw.len);
if (len < 0)
{
DBG1(DBG_DMN, "reading from TUN device failed: %s", strerror(errno));
chunk_free(&raw);
return JOB_REQUEUE_FAIR;
}
raw.len = len;
packet = ip_packet_create(raw);
if (packet)
{
if (!dns_proxy || !this->dns_proxy->handle(this->dns_proxy, packet))
{
ipsec->processor->queue_outbound(ipsec->processor, packet);
}
}
else
{
DBG1(DBG_DMN, "invalid IP packet read from TUN device");
}
return JOB_REQUEUE_DIRECT;
}
/**
* Add a route to the TUN device builder
*/
static bool add_route(vpnservice_builder_t *builder, host_t *net,
u_int8_t prefix)
{
/* if route is 0.0.0.0/0, split it into two routes 0.0.0.0/1 and
* 128.0.0.0/1 because otherwise it would conflict with the current default
* route. likewise for IPv6 with ::/0. */
if (net->is_anyaddr(net) && prefix == 0)
{
bool success;
success = add_route(builder, net, 1);
if (net->get_family(net) == AF_INET)
{
net = host_create_from_string("128.0.0.0", 0);
}
else
{
net = host_create_from_string("8000::", 0);
}
success = success && add_route(builder, net, 1);
net->destroy(net);
return success;
}
return builder->add_route(builder, net, prefix);
}
/**
* Generate and set routes from installed IPsec policies
*/
static bool add_routes(vpnservice_builder_t *builder, child_sa_t *child_sa)
{
traffic_selector_t *src_ts, *dst_ts;
enumerator_t *enumerator;
bool success = TRUE;
enumerator = child_sa->create_policy_enumerator(child_sa);
while (success && enumerator->enumerate(enumerator, &src_ts, &dst_ts))
{
host_t *net;
u_int8_t prefix;
dst_ts->to_subnet(dst_ts, &net, &prefix);
success = add_route(builder, net, prefix);
net->destroy(net);
}
enumerator->destroy(enumerator);
return success;
}
/**
* Setup a new TUN device for the supplied SAs, also queues a job that
* reads packets from this device.
* Additional information such as DNS servers are gathered in appropriate
* listeners asynchronously. To be sure every required bit of information is
* available this should be called after the CHILD_SA has been established.
*/
static bool setup_tun_device(private_android_service_t *this,
ike_sa_t *ike_sa, child_sa_t *child_sa)
{
vpnservice_builder_t *builder;
enumerator_t *enumerator;
bool vip_found = FALSE, already_registered = FALSE;
host_t *vip;
int tunfd;
DBG1(DBG_DMN, "setting up TUN device for CHILD_SA %s{%u}",
child_sa->get_name(child_sa), child_sa->get_unique_id(child_sa));
builder = charonservice->get_vpnservice_builder(charonservice);
enumerator = ike_sa->create_virtual_ip_enumerator(ike_sa, TRUE);
while (enumerator->enumerate(enumerator, &vip))
{
if (!vip->is_anyaddr(vip))
{
if (!builder->add_address(builder, vip))
{
break;
}
vip_found = TRUE;
}
}
enumerator->destroy(enumerator);
if (!vip_found)
{
DBG1(DBG_DMN, "setting up TUN device failed, no virtual IP found");
return FALSE;
}
if (!add_routes(builder, child_sa) ||
!builder->set_mtu(builder, this->mtu))
{
return FALSE;
}
tunfd = builder->establish(builder);
if (tunfd == -1)
{
return FALSE;
}
this->lock->write_lock(this->lock);
if (this->tunfd > 0)
{ /* close previously opened TUN device */
close(this->tunfd);
already_registered = true;
}
this->tunfd = tunfd;
this->lock->unlock(this->lock);
DBG1(DBG_DMN, "successfully created TUN device");
if (!already_registered)
{
charon->receiver->add_esp_cb(charon->receiver,
(receiver_esp_cb_t)receiver_esp_cb, NULL);
ipsec->processor->register_inbound(ipsec->processor,
(ipsec_inbound_cb_t)deliver_plain, this);
ipsec->processor->register_outbound(ipsec->processor,
(ipsec_outbound_cb_t)send_esp, NULL);
this->dns_proxy->register_cb(this->dns_proxy,
(dns_proxy_response_cb_t)deliver_plain, this);
lib->processor->queue_job(lib->processor,
(job_t*)callback_job_create((callback_job_cb_t)handle_plain, this,
NULL, (callback_job_cancel_t)return_false));
}
return TRUE;
}
/**
* Setup a new TUN device based on the existing one, but without DNS server.
*/
static bool setup_tun_device_without_dns(private_android_service_t *this)
{
vpnservice_builder_t *builder;
int tunfd;
DBG1(DBG_DMN, "setting up TUN device without DNS");
builder = charonservice->get_vpnservice_builder(charonservice);
tunfd = builder->establish_no_dns(builder);
if (tunfd == -1)
{
return FALSE;
}
this->lock->write_lock(this->lock);
if (this->tunfd > 0)
{ /* close previously opened TUN device, this should always be the case */
close(this->tunfd);
}
this->tunfd = tunfd;
this->lock->unlock(this->lock);
DBG1(DBG_DMN, "successfully created TUN device without DNS");
return TRUE;
}
/**
* Close the current tun device
*/
static void close_tun_device(private_android_service_t *this)
{
int tunfd;
this->lock->write_lock(this->lock);
if (this->tunfd < 0)
{ /* already closed (or never created) */
this->lock->unlock(this->lock);
return;
}
tunfd = this->tunfd;
this->tunfd = -1;
this->lock->unlock(this->lock);
this->dns_proxy->unregister_cb(this->dns_proxy,
(dns_proxy_response_cb_t)deliver_plain);
ipsec->processor->unregister_outbound(ipsec->processor,
(ipsec_outbound_cb_t)send_esp);
ipsec->processor->unregister_inbound(ipsec->processor,
(ipsec_inbound_cb_t)deliver_plain);
charon->receiver->del_esp_cb(charon->receiver,
(receiver_esp_cb_t)receiver_esp_cb);
close(tunfd);
}
/**
* Terminate the IKE_SA with the given unique ID
*/
CALLBACK(terminate, job_requeue_t,
u_int32_t *id)
{
charon->controller->terminate_ike(charon->controller, *id,
controller_cb_empty, NULL, 0);
return JOB_REQUEUE_NONE;
}
/**
* Reestablish the IKE_SA with the given unique ID
*/
CALLBACK(reestablish, job_requeue_t,
u_int32_t *id)
{
ike_sa_t *ike_sa;
ike_sa = charon->ike_sa_manager->checkout_by_id(charon->ike_sa_manager, *id);
if (ike_sa)
{
if (ike_sa->reauth(ike_sa) == DESTROY_ME)
{
charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager,
ike_sa);
}
else
{
charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);
}
}
return JOB_REQUEUE_NONE;
}
METHOD(listener_t, child_updown, bool,
private_android_service_t *this, ike_sa_t *ike_sa, child_sa_t *child_sa,
bool up)
{
if (this->ike_sa == ike_sa)
{
if (up)
{
/* disable the hooks registered to catch initiation failures */
this->public.listener.ike_updown = NULL;
/* CHILD_SA is up so we can disable the DNS proxy we enabled to
* reestablish the SA */
this->lock->write_lock(this->lock);
this->use_dns_proxy = FALSE;
this->lock->unlock(this->lock);
if (!setup_tun_device(this, ike_sa, child_sa))
{
DBG1(DBG_DMN, "failed to setup TUN device");
charonservice->update_status(charonservice,
CHARONSERVICE_GENERIC_ERROR);
return FALSE;
}
charonservice->update_status(charonservice,
CHARONSERVICE_CHILD_STATE_UP);
}
else
{
charonservice->update_status(charonservice,
CHARONSERVICE_CHILD_STATE_DOWN);
}
}
return TRUE;
}
METHOD(listener_t, ike_updown, bool,
private_android_service_t *this, ike_sa_t *ike_sa, bool up)
{
/* this callback is only registered during initiation, so if the IKE_SA
* goes down we assume some kind of authentication error, more specific
* errors are catched in the alert() handler */
if (this->ike_sa == ike_sa && !up)
{
charonservice->update_status(charonservice,
CHARONSERVICE_AUTH_ERROR);
return FALSE;
}
return TRUE;
}
METHOD(listener_t, alert, bool,
private_android_service_t *this, ike_sa_t *ike_sa, alert_t alert,
va_list args)
{
if (this->ike_sa == ike_sa)
{
switch (alert)
{
case ALERT_PEER_ADDR_FAILED:
charonservice->update_status(charonservice,
CHARONSERVICE_LOOKUP_ERROR);
break;
case ALERT_PEER_AUTH_FAILED:
charonservice->update_status(charonservice,
CHARONSERVICE_PEER_AUTH_ERROR);
break;
case ALERT_KEEP_ON_CHILD_SA_FAILURE:
{
u_int32_t *id = malloc_thing(u_int32_t);
/* because close_ike_on_child_failure is set this is only
* triggered when CHILD_SA rekeying failed. reestablish it in
* the hope that the initial setup works again. */
*id = ike_sa->get_unique_id(ike_sa);
lib->processor->queue_job(lib->processor,
(job_t*)callback_job_create_with_prio(
(callback_job_cb_t)reestablish, id, free,
(callback_job_cancel_t)return_false, JOB_PRIO_HIGH));
break;
}
case ALERT_PEER_INIT_UNREACHABLE:
this->lock->read_lock(this->lock);
if (this->tunfd < 0)
{
u_int32_t *id = malloc_thing(u_int32_t);
/* always fail if we are not able to initiate the IKE_SA
* initially */
charonservice->update_status(charonservice,
CHARONSERVICE_UNREACHABLE_ERROR);
/* terminate the IKE_SA so no further keying tries are
* attempted */
*id = ike_sa->get_unique_id(ike_sa);
lib->processor->queue_job(lib->processor,
(job_t*)callback_job_create_with_prio(
(callback_job_cb_t)terminate, id, free,
(callback_job_cancel_t)return_false, JOB_PRIO_HIGH));
}
else
{
peer_cfg_t *peer_cfg;
u_int32_t tries, try;
/* when reestablishing and if keyingtries is not %forever
* the IKE_SA is destroyed after the set number of tries,
* so notify the GUI */
peer_cfg = ike_sa->get_peer_cfg(ike_sa);
tries = peer_cfg->get_keyingtries(peer_cfg);
try = va_arg(args, u_int32_t);
if (tries != 0 && try == tries-1)
{
charonservice->update_status(charonservice,
CHARONSERVICE_UNREACHABLE_ERROR);
}
}
this->lock->unlock(this->lock);
break;
default:
break;
}
}
return TRUE;
}
METHOD(listener_t, ike_rekey, bool,
private_android_service_t *this, ike_sa_t *old, ike_sa_t *new)
{
if (this->ike_sa == old)
{
this->ike_sa = new;
}
return TRUE;
}
METHOD(listener_t, ike_reestablish_pre, bool,
private_android_service_t *this, ike_sa_t *old, ike_sa_t *new)
{
if (this->ike_sa == old)
{
/* enable DNS proxy so hosts are properly resolved while the TUN device
* is still active */
this->lock->write_lock(this->lock);
this->use_dns_proxy = TRUE;
this->lock->unlock(this->lock);
/* if DNS servers are installed that are only reachable through the VPN
* the DNS proxy doesn't help, so uninstall DNS servers */
if (!setup_tun_device_without_dns(this))
{
DBG1(DBG_DMN, "failed to setup TUN device without DNS");
charonservice->update_status(charonservice,
CHARONSERVICE_GENERIC_ERROR);
}
}
return TRUE;
}
METHOD(listener_t, ike_reestablish_post, bool,
private_android_service_t *this, ike_sa_t *old, ike_sa_t *new,
bool initiated)
{
if (this->ike_sa == old && initiated)
{
this->ike_sa = new;
/* re-register hook to detect initiation failures */
this->public.listener.ike_updown = _ike_updown;
/* if the IKE_SA got deleted by the responder we get the child_down()
* event on the old IKE_SA after this hook has been called, so they
* get ignored and thus we trigger the event here */
charonservice->update_status(charonservice,
CHARONSERVICE_CHILD_STATE_DOWN);
}
return TRUE;
}
static void add_auth_cfg_pw(private_android_service_t *this,
peer_cfg_t *peer_cfg, bool byod)
{
identification_t *user;
auth_cfg_t *auth;
char *username, *password;
auth = auth_cfg_create();
auth->add(auth, AUTH_RULE_AUTH_CLASS, AUTH_CLASS_EAP);
if (byod)
{ /* use EAP-TTLS if BYOD is enabled */
auth->add(auth, AUTH_RULE_EAP_TYPE, EAP_TTLS);
}
username = this->settings->get_str(this->settings, "connection.username",
NULL);
password = this->settings->get_str(this->settings, "connection.password",
NULL);
user = identification_create_from_string(username);
auth->add(auth, AUTH_RULE_IDENTITY, user);
this->creds->add_username_password(this->creds, username, password);
peer_cfg->add_auth_cfg(peer_cfg, auth, TRUE);
}
static bool add_auth_cfg_cert(private_android_service_t *this,
peer_cfg_t *peer_cfg)
{
certificate_t *cert;
identification_t *id;
auth_cfg_t *auth;
char *type;
cert = this->creds->load_user_certificate(this->creds);
if (!cert)
{
return FALSE;
}
type = this->settings->get_str(this->settings, "connection.type", NULL);
auth = auth_cfg_create();
if (strpfx("ikev2-eap-tls", type))
{
auth->add(auth, AUTH_RULE_AUTH_CLASS, AUTH_CLASS_EAP);
auth->add(auth, AUTH_RULE_EAP_TYPE, EAP_TLS);
id = identification_create_from_string("%any");
auth->add(auth, AUTH_RULE_AAA_IDENTITY, id);
}
else
{
auth->add(auth, AUTH_RULE_AUTH_CLASS, AUTH_CLASS_PUBKEY);
}
auth->add(auth, AUTH_RULE_SUBJECT_CERT, cert);
id = cert->get_subject(cert);
auth->add(auth, AUTH_RULE_IDENTITY, id->clone(id));
peer_cfg->add_auth_cfg(peer_cfg, auth, TRUE);
return TRUE;
}
static job_requeue_t initiate(private_android_service_t *this)
{
identification_t *gateway;
ike_cfg_t *ike_cfg;
peer_cfg_t *peer_cfg;
child_cfg_t *child_cfg;
traffic_selector_t *ts;
ike_sa_t *ike_sa;
auth_cfg_t *auth;
lifetime_cfg_t lifetime = {
.time = {
.life = 3600, /* 1h */
.rekey = 3000, /* 50min */
.jitter = 300 /* 5min */
}
};
char *type, *server;
int port;
server = this->settings->get_str(this->settings, "connection.server", NULL);
port = this->settings->get_int(this->settings, "connection.port",
IKEV2_UDP_PORT);
ike_cfg = ike_cfg_create(IKEV2, TRUE, TRUE, "0.0.0.0",
charon->socket->get_port(charon->socket, FALSE),
server, port, FRAGMENTATION_YES, 0);
ike_cfg->add_proposal(ike_cfg, proposal_create_default(PROTO_IKE));
ike_cfg->add_proposal(ike_cfg, proposal_create_default_aead(PROTO_IKE));
peer_cfg = peer_cfg_create("android", ike_cfg, CERT_SEND_IF_ASKED,
UNIQUE_REPLACE, 0, /* keyingtries */
36000, 0, /* rekey 10h, reauth none */
600, 600, /* jitter, over 10min */
TRUE, FALSE, TRUE, /* mobike, aggressive, pull */
0, 0, /* DPD delay, timeout */
FALSE, NULL, NULL); /* mediation */
peer_cfg->add_virtual_ip(peer_cfg, host_create_any(AF_INET));
peer_cfg->add_virtual_ip(peer_cfg, host_create_any(AF_INET6));
type = this->settings->get_str(this->settings, "connection.type", NULL);
/* local auth config */
if (streq("ikev2-cert", type) ||
streq("ikev2-cert-eap", type) ||
streq("ikev2-eap-tls", type))
{
if (!add_auth_cfg_cert(this, peer_cfg))
{
peer_cfg->destroy(peer_cfg);
charonservice->update_status(charonservice,
CHARONSERVICE_GENERIC_ERROR);
return JOB_REQUEUE_NONE;
}
}
if (streq("ikev2-eap", type) ||
streq("ikev2-cert-eap", type) ||
streq("ikev2-byod-eap", type))
{
add_auth_cfg_pw(this, peer_cfg, strpfx(type, "ikev2-byod"));
}
/* remote auth config */
auth = auth_cfg_create();
gateway = identification_create_from_string(server);
auth->add(auth, AUTH_RULE_IDENTITY, gateway);
auth->add(auth, AUTH_RULE_IDENTITY_LOOSE, TRUE);
auth->add(auth, AUTH_RULE_AUTH_CLASS, AUTH_CLASS_PUBKEY);
peer_cfg->add_auth_cfg(peer_cfg, auth, FALSE);
child_cfg = child_cfg_create("android", &lifetime, NULL, TRUE, MODE_TUNNEL,
ACTION_NONE, ACTION_RESTART, ACTION_RESTART,
FALSE, 0, 0, NULL, NULL, 0);
/* create ESP proposals with and without DH groups, let responder decide
* if PFS is used */
child_cfg->add_proposal(child_cfg, proposal_create_from_string(PROTO_ESP,
"aes128gcm16-aes256gcm16-ecp256"));
child_cfg->add_proposal(child_cfg, proposal_create_from_string(PROTO_ESP,
"aes128-sha256-ecp256-modp3072"));
child_cfg->add_proposal(child_cfg, proposal_create_from_string(PROTO_ESP,
"aes256-sha384-ecp521-modp8192"));
child_cfg->add_proposal(child_cfg, proposal_create_from_string(PROTO_ESP,
"aes128-aes192-aes256-sha1-sha256-sha384-sha512-"
"ecp256-ecp384-ecp521-"
"modp2048-modp3072-modp4096-modp1024"));
child_cfg->add_proposal(child_cfg, proposal_create_from_string(PROTO_ESP,
"aes128gcm16-aes256gcm16"));
child_cfg->add_proposal(child_cfg, proposal_create_from_string(PROTO_ESP,
"aes128-sha256"));
child_cfg->add_proposal(child_cfg, proposal_create_from_string(PROTO_ESP,
"aes256-sha384"));
child_cfg->add_proposal(child_cfg, proposal_create_from_string(PROTO_ESP,
"aes128-aes192-aes256-sha1-sha256-sha384-sha512"));
ts = traffic_selector_create_from_cidr("0.0.0.0/0", 0, 0, 65535);
child_cfg->add_traffic_selector(child_cfg, TRUE, ts);
ts = traffic_selector_create_from_cidr("0.0.0.0/0", 0, 0, 65535);
child_cfg->add_traffic_selector(child_cfg, FALSE, ts);
ts = traffic_selector_create_from_cidr("::/0", 0, 0, 65535);
child_cfg->add_traffic_selector(child_cfg, TRUE, ts);
ts = traffic_selector_create_from_cidr("::/0", 0, 0, 65535);
child_cfg->add_traffic_selector(child_cfg, FALSE, ts);
peer_cfg->add_child_cfg(peer_cfg, child_cfg);
/* get us an IKE_SA */
ike_sa = charon->ike_sa_manager->checkout_by_config(charon->ike_sa_manager,
peer_cfg);
if (!ike_sa)
{
peer_cfg->destroy(peer_cfg);
charonservice->update_status(charonservice,
CHARONSERVICE_GENERIC_ERROR);
return JOB_REQUEUE_NONE;
}
if (!ike_sa->get_peer_cfg(ike_sa))
{
ike_sa->set_peer_cfg(ike_sa, peer_cfg);
}
peer_cfg->destroy(peer_cfg);
/* store the IKE_SA so we can track its progress */
this->ike_sa = ike_sa;
/* get an additional reference because initiate consumes one */
child_cfg->get_ref(child_cfg);
if (ike_sa->initiate(ike_sa, child_cfg, 0, NULL, NULL) != SUCCESS)
{
DBG1(DBG_CFG, "failed to initiate tunnel");
charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager,
ike_sa);
return JOB_REQUEUE_NONE;
}
charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);
return JOB_REQUEUE_NONE;
}
METHOD(android_service_t, destroy, void,
private_android_service_t *this)
{
charon->bus->remove_listener(charon->bus, &this->public.listener);
/* make sure the tun device is actually closed */
close_tun_device(this);
this->dns_proxy->destroy(this->dns_proxy);
this->lock->destroy(this->lock);
this->settings->destroy(this->settings);
free(this);
}
/**
* See header
*/
android_service_t *android_service_create(android_creds_t *creds,
settings_t *settings)
{
private_android_service_t *this;
INIT(this,
.public = {
.listener = {
.ike_rekey = _ike_rekey,
.ike_reestablish_pre = _ike_reestablish_pre,
.ike_reestablish_post = _ike_reestablish_post,
.ike_updown = _ike_updown,
.child_updown = _child_updown,
.alert = _alert,
},
.destroy = _destroy,
},
.lock = rwlock_create(RWLOCK_TYPE_DEFAULT),
.dns_proxy = android_dns_proxy_create(),
.settings = settings,
.creds = creds,
.tunfd = -1,
.mtu = settings->get_int(settings, "global.mtu", ANDROID_DEFAULT_MTU),
);
/* only allow queries for the VPN gateway */
this->dns_proxy->add_hostname(this->dns_proxy,
this->settings->get_str(this->settings, "connection.server", NULL));
charon->bus->add_listener(charon->bus, &this->public.listener);
lib->processor->queue_job(lib->processor,
(job_t*)callback_job_create((callback_job_cb_t)initiate, this,
NULL, NULL));
return &this->public;
}
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2010-2015 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
/**
* @defgroup android_service android_service
* @{ @ingroup android_backend
*/
#ifndef ANDROID_SERVICE_H_
#define ANDROID_SERVICE_H_
#include "android_creds.h"
#include <library.h>
#include <bus/listeners/listener.h>
typedef struct android_service_t android_service_t;
/**
* Service that sets up an IKE_SA/CHILD_SA and handles events
*/
struct android_service_t {
/**
* Implements listener_t.
*/
listener_t listener;
/**
* Destroy a android_service_t.
*/
void (*destroy)(android_service_t *this);
};
/**
* Create an Android service instance. Queues a job that starts initiation of a
* new IKE SA.
*
* @param creds Android specific credential set
* @param settings configuration settings (gets adopted)
*/
android_service_t *android_service_create(android_creds_t *creds,
settings_t *settings);
#endif /** ANDROID_SERVICE_H_ @}*/
@@ -0,0 +1,722 @@
/*
* Copyright (C) 2012-2013 Tobias Brunner
* Copyright (C) 2012 Christoph Buehler
* Copyright (C) 2012 Patrick Loetscher
* Copyright (C) 2011-2012 Andreas Steffen
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "imc_android_state.h"
#include "../android_jni.h"
#include "../charonservice.h"
#include <tnc/tnc.h>
#include <imcv.h>
#include <imc/imc_agent.h>
#include <imc/imc_msg.h>
#include <pa_tnc/pa_tnc_msg.h>
#include <ietf/ietf_attr.h>
#include <ietf/ietf_attr_attr_request.h>
#include <ietf/ietf_attr_installed_packages.h>
#include <ietf/ietf_attr_pa_tnc_error.h>
#include <ietf/ietf_attr_product_info.h>
#include <ietf/ietf_attr_remediation_instr.h>
#include <ietf/ietf_attr_string_version.h>
#include <ita/ita_attr.h>
#include <ita/ita_attr_get_settings.h>
#include <tcg/pts/tcg_pts_attr_file_meas.h>
#include <tcg/pts/tcg_pts_attr_meas_algo.h>
#include <tcg/pts/tcg_pts_attr_proto_caps.h>
#include <tcg/pts/tcg_pts_attr_req_file_meas.h>
#include <os_info/os_info.h>
#include <tncif_pa_subtypes.h>
#include <pen/pen.h>
#include <utils/debug.h>
#include <stdio.h>
/* IMC definitions */
static const char imc_name[] = "Android";
static pen_type_t msg_types[] = {
{ PEN_IETF, PA_SUBTYPE_IETF_OPERATING_SYSTEM },
{ PEN_IETF, PA_SUBTYPE_IETF_VPN },
{ PEN_TCG, PA_SUBTYPE_TCG_PTS },
};
static imc_agent_t *imc_android;
/**
* AndroidImc object accessed via JNI
*/
static jobject android_imc;
/**
* AndroidImc class object
*/
static jclass android_imc_cls;
/**
* see section 3.8.1 of TCG TNC IF-IMC Specification 1.3
*/
static TNC_Result tnc_imc_initialize(TNC_IMCID imc_id,
TNC_Version min_version,
TNC_Version max_version,
TNC_Version *actual_version)
{
if (imc_android)
{
DBG1(DBG_IMC, "IMC \"%s\" has already been initialized", imc_name);
return TNC_RESULT_ALREADY_INITIALIZED;
}
imc_android = imc_agent_create(imc_name, msg_types, countof(msg_types),
imc_id, actual_version);
if (!imc_android)
{
return TNC_RESULT_FATAL;
}
if (min_version > TNC_IFIMC_VERSION_1 || max_version < TNC_IFIMC_VERSION_1)
{
DBG1(DBG_IMC, "no common IF-IMC version");
return TNC_RESULT_NO_COMMON_VERSION;
}
return TNC_RESULT_SUCCESS;
}
/**
* Update the state in the GUI.
*/
static void update_imc_state(TNC_ConnectionState state)
{
android_imc_state_t imc_state = ANDROID_IMC_STATE_UNKNOWN;
switch (state)
{ /* map connection states to the values used by the GUI */
case TNC_CONNECTION_STATE_ACCESS_ALLOWED:
imc_state = ANDROID_IMC_STATE_ALLOW;
break;
case TNC_CONNECTION_STATE_ACCESS_ISOLATED:
imc_state = ANDROID_IMC_STATE_ISOLATE;
break;
case TNC_CONNECTION_STATE_ACCESS_NONE:
imc_state = ANDROID_IMC_STATE_BLOCK;
break;
}
charonservice->update_imc_state(charonservice, imc_state);
}
/**
* see section 3.8.2 of TCG TNC IF-IMC Specification 1.3
*/
static TNC_Result tnc_imc_notifyconnectionchange(TNC_IMCID imc_id,
TNC_ConnectionID connection_id,
TNC_ConnectionState new_state)
{
imc_state_t *state;
if (!imc_android)
{
DBG1(DBG_IMC, "IMC \"%s\" has not been initialized", imc_name);
return TNC_RESULT_NOT_INITIALIZED;
}
switch (new_state)
{
case TNC_CONNECTION_STATE_CREATE:
state = imc_android_state_create(connection_id);
return imc_android->create_state(imc_android, state);
case TNC_CONNECTION_STATE_HANDSHAKE:
if (imc_android->change_state(imc_android, connection_id, new_state,
&state) != TNC_RESULT_SUCCESS)
{
return TNC_RESULT_FATAL;
}
state->set_result(state, imc_id,
TNC_IMV_EVALUATION_RESULT_DONT_KNOW);
return TNC_RESULT_SUCCESS;
case TNC_CONNECTION_STATE_DELETE:
return imc_android->delete_state(imc_android, connection_id);
case TNC_CONNECTION_STATE_ACCESS_ALLOWED:
case TNC_CONNECTION_STATE_ACCESS_ISOLATED:
case TNC_CONNECTION_STATE_ACCESS_NONE:
update_imc_state(new_state);
/* fall-through */
default:
return imc_android->change_state(imc_android, connection_id,
new_state, NULL);
}
}
/**
* Convert the native C strings in the enumerator to a Java String array.
* The given enumerator gets destroyed.
*/
static jobjectArray string_array_create(JNIEnv *env, enumerator_t *enumerator)
{
linked_list_t *list;
jobjectArray jarray;
jstring jstring;
char *native;
jclass cls;
int i = 0;
cls = (*env)->FindClass(env, "java/lang/String");
list = linked_list_create_from_enumerator(enumerator);
jarray = (*env)->NewObjectArray(env, list->get_count(list), cls, NULL);
if (!jarray)
{
goto failed;
}
enumerator = list->create_enumerator(list);
while (enumerator->enumerate(enumerator, (void**)&native))
{
jstring = (*env)->NewStringUTF(env, native);
if (!jstring)
{
enumerator->destroy(enumerator);
goto failed;
}
(*env)->SetObjectArrayElement(env, jarray, i++, jstring);
}
enumerator->destroy(enumerator);
list->destroy(list);
return jarray;
failed:
androidjni_exception_occurred(env);
list->destroy(list);
return NULL;
}
/**
* Get a measurement for the given attribute type from the Android IMC.
* NULL is returned if no measurement is available or an error occurred.
*
* The optional args is an enumerator over char* (gets destroyed).
*/
static pa_tnc_attr_t *get_measurement(pen_type_t attr_type, enumerator_t *args)
{
JNIEnv *env;
pa_tnc_attr_t *attr;
jmethodID method_id;
jbyteArray jmeasurement;
jobjectArray jargs = NULL;
chunk_t data;
androidjni_attach_thread(&env);
if (args)
{
jargs = string_array_create(env, args);
if (!jargs)
{
goto failed;
}
method_id = (*env)->GetMethodID(env, android_imc_cls, "getMeasurement",
"(II[Ljava/lang/String;)[B");
}
else
{
method_id = (*env)->GetMethodID(env, android_imc_cls, "getMeasurement",
"(II)[B");
}
if (!method_id)
{
goto failed;
}
jmeasurement = (*env)->CallObjectMethod(env, android_imc, method_id,
attr_type.vendor_id, attr_type.type,
jargs);
if (!jmeasurement || androidjni_exception_occurred(env))
{
goto failed;
}
data = chunk_create((*env)->GetByteArrayElements(env, jmeasurement, NULL),
(*env)->GetArrayLength(env, jmeasurement));
if (!data.ptr)
{
goto failed;
}
attr = imcv_pa_tnc_attributes->construct(imcv_pa_tnc_attributes,
attr_type.vendor_id, attr_type.type, data);
(*env)->ReleaseByteArrayElements(env, jmeasurement, data.ptr, JNI_ABORT);
androidjni_detach_thread();
return attr;
failed:
androidjni_exception_occurred(env);
androidjni_detach_thread();
return NULL;
}
/**
* Add the measurement for the requested attribute type with optional
* arguments (enumerator over char*, gets destroyed).
*/
static void add_measurement(pen_type_t attr_type, imc_msg_t *msg,
enumerator_t *args)
{
pa_tnc_attr_t *attr;
enum_name_t *pa_attr_names;
attr = get_measurement(attr_type, args);
if (attr)
{
msg->add_attribute(msg, attr);
return;
}
pa_attr_names = imcv_pa_tnc_attributes->get_names(imcv_pa_tnc_attributes,
attr_type.vendor_id);
if (pa_attr_names)
{
DBG1(DBG_IMC, "no measurement available for PA-TNC attribute type "
"'%N/%N' 0x%06x/0x%08x", pen_names, attr_type.vendor_id,
pa_attr_names, attr_type.type, attr_type.vendor_id, attr_type.type);
}
else
{
DBG1(DBG_IMC, "no measurement available for PA-TNC attribute type '%N' "
"0x%06x/0x%08x", pen_names, attr_type.vendor_id,
attr_type.vendor_id, attr_type.type);
}
}
/**
* Handle an IETF attribute
*/
static void handle_ietf_attribute(pen_type_t attr_type, pa_tnc_attr_t *attr,
imc_msg_t *out_msg)
{
if (attr_type.type == IETF_ATTR_ATTRIBUTE_REQUEST)
{
ietf_attr_attr_request_t *attr_cast;
pen_type_t *entry;
enumerator_t *enumerator;
attr_cast = (ietf_attr_attr_request_t*)attr;
enumerator = attr_cast->create_enumerator(attr_cast);
while (enumerator->enumerate(enumerator, &entry))
{
add_measurement(*entry, out_msg, NULL);
}
enumerator->destroy(enumerator);
}
else if (attr_type.type == IETF_ATTR_REMEDIATION_INSTRUCTIONS)
{
ietf_attr_remediation_instr_t *attr_cast;
pen_type_t param;
chunk_t str;
char *instr;
attr_cast = (ietf_attr_remediation_instr_t*)attr;
param = attr_cast->get_parameters_type(attr_cast);
if (pen_type_is(param, PEN_IETF, IETF_REMEDIATION_PARAMETERS_STRING))
{
str = attr_cast->get_string(attr_cast, NULL);
instr = strndup(str.ptr, str.len);
charonservice->add_remediation_instr(charonservice, instr);
free (instr);
}
}
}
/**
* Handle an ITA attribute
*/
static void handle_ita_attribute(pen_type_t attr_type, pa_tnc_attr_t *attr,
imc_msg_t *out_msg)
{
if (attr_type.type == ITA_ATTR_GET_SETTINGS)
{
ita_attr_get_settings_t *attr_cast;
attr_cast = (ita_attr_get_settings_t*)attr;
add_measurement((pen_type_t){ PEN_ITA, ITA_ATTR_SETTINGS },
out_msg, attr_cast->create_enumerator(attr_cast));
}
}
/**
* Handle a TCG attribute
*/
static void handle_tcg_attribute(imc_android_state_t *state,
pen_type_t attr_type, pa_tnc_attr_t *attr,
imc_msg_t *out_msg)
{
pts_t *pts;
pts = state->get_pts(state);
switch (attr_type.type)
{
case TCG_PTS_REQ_PROTO_CAPS:
{
tcg_pts_attr_proto_caps_t *attr_cast;
pts_proto_caps_flag_t caps;
attr_cast = (tcg_pts_attr_proto_caps_t*)attr;
caps = attr_cast->get_flags(attr_cast) & pts->get_proto_caps(pts);
pts->set_proto_caps(pts, caps);
attr = tcg_pts_attr_proto_caps_create(caps, FALSE);
out_msg->add_attribute(out_msg, attr);
break;
}
case TCG_PTS_MEAS_ALGO:
{
tcg_pts_attr_meas_algo_t *attr_cast;
pts_meas_algorithms_t supported, algo;
if (!pts_meas_algo_probe(&supported))
{
attr = pts_hash_alg_error_create(PTS_MEAS_ALGO_NONE);
out_msg->add_attribute(out_msg, attr);
break;
}
attr_cast = (tcg_pts_attr_meas_algo_t*)attr;
algo = pts_meas_algo_select(supported,
attr_cast->get_algorithms(attr_cast));
if (algo == PTS_MEAS_ALGO_NONE)
{
attr = pts_hash_alg_error_create(supported);
out_msg->add_attribute(out_msg, attr);
break;
}
pts->set_meas_algorithm(pts, algo);
attr = tcg_pts_attr_meas_algo_create(algo, TRUE);
out_msg->add_attribute(out_msg, attr);
break;
}
case TCG_PTS_REQ_FILE_MEAS:
{
tcg_pts_attr_req_file_meas_t *attr_cast;
pts_file_meas_t *measurements;
pts_error_code_t pts_error;
u_int32_t delim;
u_int16_t req_id;
bool is_dir;
char *path;
attr_cast = (tcg_pts_attr_req_file_meas_t*)attr;
path = attr_cast->get_pathname(attr_cast);
if (!pts->is_path_valid(pts, path, &pts_error))
{ /* silently ignore internal errors */
break;
}
else if (pts_error)
{
attr = ietf_attr_pa_tnc_error_create(pen_type_create(PEN_TCG,
pts_error), attr->get_value(attr));
out_msg->add_attribute(out_msg, attr);
break;
}
delim = attr_cast->get_delimiter(attr_cast);
if (delim != SOLIDUS_UTF && delim != REVERSE_SOLIDUS_UTF)
{
attr = ietf_attr_pa_tnc_error_create(pen_type_create(PEN_TCG,
TCG_PTS_INVALID_DELIMITER), attr->get_value(attr));
out_msg->add_attribute(out_msg, attr);
break;
}
req_id = attr_cast->get_request_id(attr_cast);
is_dir = attr_cast->get_directory_flag(attr_cast);
DBG1(DBG_IMC, "measurement request %d for %s '%s'", req_id,
is_dir ? "directory" : "file", path);
measurements = pts_file_meas_create_from_path(req_id, path, is_dir,
TRUE, pts->get_meas_algorithm(pts));
if (!measurements)
{
attr = ietf_attr_pa_tnc_error_create(pen_type_create(PEN_TCG,
TCG_PTS_FILE_NOT_FOUND), attr->get_value(attr));
out_msg->add_attribute(out_msg, attr);
break;
}
attr = tcg_pts_attr_file_meas_create(measurements);
attr->set_noskip_flag(attr, TRUE);
out_msg->add_attribute(out_msg, attr);
break;
}
default:
DBG1(DBG_IMC, "received unsupported TCG attribute '%N'",
tcg_attr_names, attr_type.type);
break;
}
}
/**
* see section 3.8.3 of TCG TNC IF-IMC Specification 1.3
*/
static TNC_Result tnc_imc_beginhandshake(TNC_IMCID imc_id,
TNC_ConnectionID connection_id)
{
imc_state_t *state;
imc_msg_t *out_msg;
TNC_Result result = TNC_RESULT_SUCCESS;
if (!imc_android)
{
DBG1(DBG_IMC, "IMC \"%s\" has not been initialized", imc_name);
return TNC_RESULT_NOT_INITIALIZED;
}
if (!imc_android->get_state(imc_android, connection_id, &state))
{
return TNC_RESULT_FATAL;
}
if (lib->settings->get_bool(lib->settings,
"android.imc.send_os_info", TRUE))
{
out_msg = imc_msg_create(imc_android, state, connection_id, imc_id,
TNC_IMVID_ANY, msg_types[0]);
add_measurement((pen_type_t){ PEN_IETF, IETF_ATTR_PRODUCT_INFORMATION },
out_msg, NULL);
add_measurement((pen_type_t){ PEN_IETF, IETF_ATTR_STRING_VERSION },
out_msg, NULL);
add_measurement((pen_type_t){ PEN_ITA, ITA_ATTR_DEVICE_ID },
out_msg, NULL);
/* send PA-TNC message with the excl flag not set */
result = out_msg->send(out_msg, FALSE);
out_msg->destroy(out_msg);
}
return result;
}
static TNC_Result receive_message(imc_android_state_t *state, imc_msg_t *in_msg)
{
imc_msg_t *out_msg;
enumerator_t *enumerator;
pa_tnc_attr_t *attr;
pen_type_t attr_type;
TNC_Result result;
bool fatal_error = FALSE;
out_msg = imc_msg_create_as_reply(in_msg);
/* parse received PA-TNC message and handle local and remote errors */
result = in_msg->receive(in_msg, out_msg, &fatal_error);
if (result != TNC_RESULT_SUCCESS)
{
out_msg->destroy(out_msg);
return result;
}
/* analyze PA-TNC attributes */
enumerator = in_msg->create_attribute_enumerator(in_msg);
while (enumerator->enumerate(enumerator, &attr))
{
attr_type = attr->get_type(attr);
switch (attr_type.vendor_id)
{
case PEN_IETF:
handle_ietf_attribute(attr_type, attr, out_msg);
continue;
case PEN_ITA:
handle_ita_attribute(attr_type, attr, out_msg);
continue;
case PEN_TCG:
handle_tcg_attribute(state, attr_type, attr, out_msg);
continue;
default:
continue;
}
}
enumerator->destroy(enumerator);
if (fatal_error)
{
result = TNC_RESULT_FATAL;
}
else
{
result = out_msg->send(out_msg, TRUE);
}
out_msg->destroy(out_msg);
return result;
}
/**
* see section 3.8.4 of TCG TNC IF-IMC Specification 1.3
*/
static TNC_Result tnc_imc_receivemessage(TNC_IMCID imc_id,
TNC_ConnectionID connection_id,
TNC_BufferReference msg,
TNC_UInt32 msg_len,
TNC_MessageType msg_type)
{
imc_state_t *state;
imc_msg_t *in_msg;
TNC_Result result;
if (!imc_android)
{
DBG1(DBG_IMC, "IMC \"%s\" has not been initialized", imc_name);
return TNC_RESULT_NOT_INITIALIZED;
}
if (!imc_android->get_state(imc_android, connection_id, &state))
{
return TNC_RESULT_FATAL;
}
in_msg = imc_msg_create_from_data(imc_android, state, connection_id,
msg_type, chunk_create(msg, msg_len));
result = receive_message((imc_android_state_t*)state, in_msg);
in_msg->destroy(in_msg);
return result;
}
/**
* see section 3.8.6 of TCG TNC IF-IMV Specification 1.3
*/
static TNC_Result tnc_imc_receivemessagelong(TNC_IMCID imc_id,
TNC_ConnectionID connection_id,
TNC_UInt32 msg_flags,
TNC_BufferReference msg,
TNC_UInt32 msg_len,
TNC_VendorID msg_vid,
TNC_MessageSubtype msg_subtype,
TNC_UInt32 src_imv_id,
TNC_UInt32 dst_imc_id)
{
imc_state_t *state;
imc_msg_t *in_msg;
TNC_Result result;
if (!imc_android)
{
DBG1(DBG_IMC, "IMC \"%s\" has not been initialized", imc_name);
return TNC_RESULT_NOT_INITIALIZED;
}
if (!imc_android->get_state(imc_android, connection_id, &state))
{
return TNC_RESULT_FATAL;
}
in_msg = imc_msg_create_from_long_data(imc_android, state, connection_id,
src_imv_id, dst_imc_id,msg_vid, msg_subtype,
chunk_create(msg, msg_len));
result = receive_message((imc_android_state_t*)state, in_msg);
in_msg->destroy(in_msg);
return result;
}
/**
* see section 3.8.7 of TCG TNC IF-IMC Specification 1.3
*/
static TNC_Result tnc_imc_batchending(TNC_IMCID imc_id,
TNC_ConnectionID connection_id)
{
if (!imc_android)
{
DBG1(DBG_IMC, "IMC \"%s\" has not been initialized", imc_name);
return TNC_RESULT_NOT_INITIALIZED;
}
return TNC_RESULT_SUCCESS;
}
/**
* see section 3.8.8 of TCG TNC IF-IMC Specification 1.3
*/
static TNC_Result tnc_imc_terminate(TNC_IMCID imc_id)
{
if (!imc_android)
{
DBG1(DBG_IMC, "IMC \"%s\" has not been initialized", imc_name);
return TNC_RESULT_NOT_INITIALIZED;
}
imc_android->destroy(imc_android);
imc_android = NULL;
return TNC_RESULT_SUCCESS;
}
/**
* see section 4.2.8.1 of TCG TNC IF-IMC Specification 1.3
*/
static TNC_Result tnc_imc_providebindfunction(TNC_IMCID imc_id,
TNC_TNCC_BindFunctionPointer bind_function)
{
if (!imc_android)
{
DBG1(DBG_IMC, "IMC \"%s\" has not been initialized", imc_name);
return TNC_RESULT_NOT_INITIALIZED;
}
return imc_android->bind_functions(imc_android, bind_function);
}
/*
* Described in header
*/
bool imc_android_register(plugin_t *plugin, plugin_feature_t *feature,
bool reg, void *data)
{
JNIEnv *env;
jmethodID method_id;
jobject obj, context = (jobject)data;
jclass cls;
bool success = TRUE;
androidjni_attach_thread(&env);
if (reg)
{
cls = (*env)->FindClass(env, JNI_PACKAGE_STRING "/imc/AndroidImc");
if (!cls)
{
goto failed;
}
android_imc_cls = (*env)->NewGlobalRef(env, cls);
method_id = (*env)->GetMethodID(env, cls, "<init>",
"(Landroid/content/Context;)V");
if (!method_id)
{
goto failed;
}
obj = (*env)->NewObject(env, cls, method_id, context);
if (!obj)
{
goto failed;
}
android_imc = (*env)->NewGlobalRef(env, obj);
androidjni_detach_thread();
if (tnc->imcs->load_from_functions(tnc->imcs, "Android",
tnc_imc_initialize, tnc_imc_notifyconnectionchange,
tnc_imc_beginhandshake, tnc_imc_receivemessage,
tnc_imc_receivemessagelong, tnc_imc_batchending,
tnc_imc_terminate, tnc_imc_providebindfunction))
{
return TRUE;
}
failed:
DBG1(DBG_IMC, "initialization of Android IMC failed");
androidjni_exception_occurred(env);
success = FALSE;
}
if (android_imc)
{
(*env)->DeleteGlobalRef(env, android_imc);
android_imc = NULL;
}
if (android_imc_cls)
{
(*env)->DeleteGlobalRef(env, android_imc_cls);
android_imc_cls = NULL;
}
androidjni_detach_thread();
return success;
}
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2013 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
/**
* @defgroup android_imc android_imc
* @{ @ingroup android_byod
*/
#ifndef ANDROID_IMC_H_
#define ANDROID_IMC_H_
/**
* Callback for the Android IMC plugin
*/
bool imc_android_register(plugin_t *plugin, plugin_feature_t *feature,
bool reg, void *data);
#endif /** ANDROID_IMC_H_ @}*/
@@ -0,0 +1,187 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Copyright (C) 2012 Andreas Steffen
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "imc_android_state.h"
#include <tncif_names.h>
#include <utils/debug.h>
typedef struct private_imc_android_state_t private_imc_android_state_t;
/**
* Private data of an imc_state_t object.
*/
struct private_imc_android_state_t {
/**
* Public interface
*/
imc_android_state_t public;
/**
* TNCCS connection ID
*/
TNC_ConnectionID connection_id;
/**
* TNCCS connection state
*/
TNC_ConnectionState state;
/**
* Assessment/Evaluation Result
*/
TNC_IMV_Evaluation_Result result;
/**
* Does the TNCCS connection support long message types?
*/
bool has_long;
/**
* Does the TNCCS connection support exclusive delivery?
*/
bool has_excl;
/**
* Maximum PA-TNC message size for this TNCCS connection
*/
u_int32_t max_msg_len;
/**
* PA-TNC attribute segmentation contracts associated with TNCCS connection
*/
seg_contract_manager_t *contracts;
/**
* TCG Platform Trust Service (PTS)
*/
pts_t *pts;
};
METHOD(imc_state_t, get_connection_id, TNC_ConnectionID,
private_imc_android_state_t *this)
{
return this->connection_id;
}
METHOD(imc_state_t, has_long, bool,
private_imc_android_state_t *this)
{
return this->has_long;
}
METHOD(imc_state_t, has_excl, bool,
private_imc_android_state_t *this)
{
return this->has_excl;
}
METHOD(imc_state_t, set_flags, void,
private_imc_android_state_t *this, bool has_long, bool has_excl)
{
this->has_long = has_long;
this->has_excl = has_excl;
}
METHOD(imc_state_t, set_max_msg_len, void,
private_imc_android_state_t *this, u_int32_t max_msg_len)
{
this->max_msg_len = max_msg_len;
}
METHOD(imc_state_t, get_max_msg_len, u_int32_t,
private_imc_android_state_t *this)
{
return this->max_msg_len;
}
METHOD(imc_state_t, get_contracts, seg_contract_manager_t*,
private_imc_android_state_t *this)
{
return this->contracts;
}
METHOD(imc_state_t, change_state, void,
private_imc_android_state_t *this, TNC_ConnectionState new_state)
{
this->state = new_state;
}
METHOD(imc_state_t, set_result, void,
private_imc_android_state_t *this, TNC_IMCID id, TNC_IMV_Evaluation_Result result)
{
this->result = result;
}
METHOD(imc_state_t, get_result, bool,
private_imc_android_state_t *this, TNC_IMCID id, TNC_IMV_Evaluation_Result *result)
{
if (result)
{
*result = this->result;
}
return this->result != TNC_IMV_EVALUATION_RESULT_DONT_KNOW;
}
METHOD(imc_state_t, destroy, void,
private_imc_android_state_t *this)
{
this->contracts->destroy(this->contracts);
this->pts->destroy(this->pts);
free(this);
}
METHOD(imc_android_state_t, get_pts, pts_t*,
private_imc_android_state_t *this)
{
return this->pts;
}
/**
* Described in header.
*/
imc_state_t *imc_android_state_create(TNC_ConnectionID connection_id)
{
private_imc_android_state_t *this;
INIT(this,
.public = {
.interface = {
.get_connection_id = _get_connection_id,
.has_long = _has_long,
.has_excl = _has_excl,
.set_flags = _set_flags,
.set_max_msg_len = _set_max_msg_len,
.get_max_msg_len = _get_max_msg_len,
.get_contracts = _get_contracts,
.change_state = _change_state,
.set_result = _set_result,
.get_result = _get_result,
.destroy = _destroy,
},
.get_pts = _get_pts,
},
.state = TNC_CONNECTION_STATE_CREATE,
.result = TNC_IMV_EVALUATION_RESULT_DONT_KNOW,
.connection_id = connection_id,
.contracts = seg_contract_manager_create(),
.pts = pts_create(TRUE),
);
return &this->public.interface;
}
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
/**
* @defgroup imc_android_state imc_android_state
* @{ @ingroup android_byod
*/
#ifndef IMC_ANDROID_STATE_H_
#define IMC_ANDROID_STATE_H_
#include <imc/imc_state.h>
#include <pts/pts.h>
typedef struct imc_android_state_t imc_android_state_t;
/**
* Internal state of an imc_android_t connection instance
*/
struct imc_android_state_t {
/**
* imc_state_t interface
*/
imc_state_t interface;
/**
* Get TCG Platform Trust Service (PTS) object
*/
pts_t *(*get_pts)(imc_android_state_t *this);
};
/**
* Create an imc_android_state_t instance
*
* @param id connection ID
*/
imc_state_t* imc_android_state_create(TNC_ConnectionID id);
#endif /** IMC_ANDROID_STATE_H_ @}*/
@@ -0,0 +1,706 @@
/*
* Copyright (C) 2012-2015 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include <signal.h>
#include <string.h>
#include <sys/utsname.h>
#include <android/log.h>
#include <errno.h>
#include "charonservice.h"
#include "android_jni.h"
#include "backend/android_attr.h"
#include "backend/android_creds.h"
#include "backend/android_private_key.h"
#include "backend/android_service.h"
#include "kernel/android_ipsec.h"
#include "kernel/android_net.h"
#ifdef USE_BYOD
#include "byod/imc_android.h"
#endif
#include <daemon.h>
#include <hydra.h>
#include <ipsec.h>
#include <library.h>
#include <threading/thread.h>
#define ANDROID_DEBUG_LEVEL 1
#define ANDROID_RETRASNMIT_TRIES 3
#define ANDROID_RETRANSMIT_TIMEOUT 2.0
#define ANDROID_RETRANSMIT_BASE 1.4
typedef struct private_charonservice_t private_charonservice_t;
/**
* private data of charonservice
*/
struct private_charonservice_t {
/**
* public interface
*/
charonservice_t public;
/**
* android_attr instance
*/
android_attr_t *attr;
/**
* android_creds instance
*/
android_creds_t *creds;
/**
* android_service instance
*/
android_service_t *service;
/**
* VpnService builder (accessed via JNI)
*/
vpnservice_builder_t *builder;
/**
* NetworkManager instance (accessed via JNI)
*/
network_manager_t *network_manager;
/**
* CharonVpnService reference
*/
jobject vpn_service;
/**
* Sockets that were bypassed and we keep track for
*/
linked_list_t *sockets;
};
/**
* Single instance of charonservice_t.
*/
charonservice_t *charonservice;
/**
* hook in library for debugging messages
*/
extern void (*dbg)(debug_t group, level_t level, char *fmt, ...);
/**
* Logging hook for library logs, using android specific logging
*/
static void dbg_android(debug_t group, level_t level, char *fmt, ...)
{
va_list args;
if (level <= ANDROID_DEBUG_LEVEL)
{
char sgroup[16], buffer[8192];
char *current = buffer, *next;
snprintf(sgroup, sizeof(sgroup), "%N", debug_names, group);
va_start(args, fmt);
vsnprintf(buffer, sizeof(buffer), fmt, args);
va_end(args);
while (current)
{ /* log each line separately */
next = strchr(current, '\n');
if (next)
{
*(next++) = '\0';
}
__android_log_print(ANDROID_LOG_INFO, "charon", "00[%s] %s\n",
sgroup, current);
current = next;
}
}
}
METHOD(charonservice_t, update_status, bool,
private_charonservice_t *this, android_vpn_state_t code)
{
JNIEnv *env;
jmethodID method_id;
bool success = FALSE;
androidjni_attach_thread(&env);
method_id = (*env)->GetMethodID(env, android_charonvpnservice_class,
"updateStatus", "(I)V");
if (!method_id)
{
goto failed;
}
(*env)->CallVoidMethod(env, this->vpn_service, method_id, (jint)code);
success = !androidjni_exception_occurred(env);
failed:
androidjni_exception_occurred(env);
androidjni_detach_thread();
return success;
}
METHOD(charonservice_t, update_imc_state, bool,
private_charonservice_t *this, android_imc_state_t state)
{
JNIEnv *env;
jmethodID method_id;
bool success = FALSE;
androidjni_attach_thread(&env);
method_id = (*env)->GetMethodID(env, android_charonvpnservice_class,
"updateImcState", "(I)V");
if (!method_id)
{
goto failed;
}
(*env)->CallVoidMethod(env, this->vpn_service, method_id, (jint)state);
success = !androidjni_exception_occurred(env);
failed:
androidjni_exception_occurred(env);
androidjni_detach_thread();
return success;
}
METHOD(charonservice_t, add_remediation_instr, bool,
private_charonservice_t *this, char *instr)
{
JNIEnv *env;
jmethodID method_id;
jstring jinstr;
bool success = FALSE;
androidjni_attach_thread(&env);
method_id = (*env)->GetMethodID(env, android_charonvpnservice_class,
"addRemediationInstruction",
"(Ljava/lang/String;)V");
if (!method_id)
{
goto failed;
}
jinstr = (*env)->NewStringUTF(env, instr);
if (!jinstr)
{
goto failed;
}
(*env)->CallVoidMethod(env, this->vpn_service, method_id, jinstr);
success = !androidjni_exception_occurred(env);
failed:
androidjni_exception_occurred(env);
androidjni_detach_thread();
return success;
}
/**
* Bypass a single socket
*/
static bool bypass_single_socket(intptr_t fd, private_charonservice_t *this)
{
JNIEnv *env;
jmethodID method_id;
androidjni_attach_thread(&env);
method_id = (*env)->GetMethodID(env, android_charonvpnservice_class,
"protect", "(I)Z");
if (!method_id)
{
goto failed;
}
if (!(*env)->CallBooleanMethod(env, this->vpn_service, method_id, fd))
{
DBG2(DBG_KNL, "VpnService.protect() failed");
goto failed;
}
androidjni_detach_thread();
return TRUE;
failed:
androidjni_exception_occurred(env);
androidjni_detach_thread();
return FALSE;
}
METHOD(charonservice_t, bypass_socket, bool,
private_charonservice_t *this, int fd, int family)
{
if (fd >= 0)
{
this->sockets->insert_last(this->sockets, (void*)(intptr_t)fd);
return bypass_single_socket((intptr_t)fd, this);
}
this->sockets->invoke_function(this->sockets, (void*)bypass_single_socket,
this);
return TRUE;
}
/**
* Converts the given Java array of byte arrays (byte[][]) to a linked list
* of chunk_t objects.
*/
static linked_list_t *convert_array_of_byte_arrays(JNIEnv *env,
jobjectArray jarray)
{
linked_list_t *list;
jsize i;
list = linked_list_create();
for (i = 0; i < (*env)->GetArrayLength(env, jarray); ++i)
{
chunk_t *chunk;
jbyteArray jbytearray;
chunk = malloc_thing(chunk_t);
list->insert_last(list, chunk);
jbytearray = (*env)->GetObjectArrayElement(env, jarray, i);
*chunk = chunk_alloc((*env)->GetArrayLength(env, jbytearray));
(*env)->GetByteArrayRegion(env, jbytearray, 0, chunk->len, chunk->ptr);
(*env)->DeleteLocalRef(env, jbytearray);
}
return list;
}
METHOD(charonservice_t, get_trusted_certificates, linked_list_t*,
private_charonservice_t *this)
{
JNIEnv *env;
jmethodID method_id;
jobjectArray jcerts;
linked_list_t *list;
androidjni_attach_thread(&env);
method_id = (*env)->GetMethodID(env,
android_charonvpnservice_class,
"getTrustedCertificates", "()[[B");
if (!method_id)
{
goto failed;
}
jcerts = (*env)->CallObjectMethod(env, this->vpn_service, method_id);
if (!jcerts || androidjni_exception_occurred(env))
{
goto failed;
}
list = convert_array_of_byte_arrays(env, jcerts);
androidjni_detach_thread();
return list;
failed:
androidjni_exception_occurred(env);
androidjni_detach_thread();
return NULL;
}
METHOD(charonservice_t, get_user_certificate, linked_list_t*,
private_charonservice_t *this)
{
JNIEnv *env;
jmethodID method_id;
jobjectArray jencodings;
linked_list_t *list;
androidjni_attach_thread(&env);
method_id = (*env)->GetMethodID(env,
android_charonvpnservice_class,
"getUserCertificate", "()[[B");
if (!method_id)
{
goto failed;
}
jencodings = (*env)->CallObjectMethod(env, this->vpn_service, method_id);
if (!jencodings || androidjni_exception_occurred(env))
{
goto failed;
}
list = convert_array_of_byte_arrays(env, jencodings);
androidjni_detach_thread();
return list;
failed:
androidjni_exception_occurred(env);
androidjni_detach_thread();
return NULL;
}
METHOD(charonservice_t, get_user_key, private_key_t*,
private_charonservice_t *this, public_key_t *pubkey)
{
JNIEnv *env;
jmethodID method_id;
private_key_t *key;
jobject jkey;
androidjni_attach_thread(&env);
method_id = (*env)->GetMethodID(env,
android_charonvpnservice_class,
"getUserKey", "()Ljava/security/PrivateKey;");
if (!method_id)
{
goto failed;
}
jkey = (*env)->CallObjectMethod(env, this->vpn_service, method_id);
if (!jkey || androidjni_exception_occurred(env))
{
goto failed;
}
key = android_private_key_create(jkey, pubkey);
androidjni_detach_thread();
return key;
failed:
DESTROY_IF(pubkey);
androidjni_exception_occurred(env);
androidjni_detach_thread();
return NULL;
}
METHOD(charonservice_t, get_vpnservice_builder, vpnservice_builder_t*,
private_charonservice_t *this)
{
return this->builder;
}
METHOD(charonservice_t, get_network_manager, network_manager_t*,
private_charonservice_t *this)
{
return this->network_manager;
}
/**
* Initiate a new connection
*
* @param settings configuration settings (gets owned)
*/
static void initiate(settings_t *settings)
{
private_charonservice_t *this = (private_charonservice_t*)charonservice;
lib->settings->set_str(lib->settings,
"charon.plugins.tnc-imc.preferred_language",
settings->get_str(settings, "global.language", "en"));
/* this is actually the size of the complete IKE/IP packet, so if the MTU
* for the TUN devices has to be reduced to pass traffic the IKE packets
* will be a bit smaller than necessary as there is no IPsec overhead like
* for the tunneled traffic (but compensating that seems like overkill) */
lib->settings->set_int(lib->settings,
"charon.fragment_size",
settings->get_int(settings, "global.mtu",
ANDROID_DEFAULT_MTU));
this->creds->clear(this->creds);
DESTROY_IF(this->service);
this->service = android_service_create(this->creds, settings);
}
/**
* Initialize/deinitialize Android backend
*/
static bool charonservice_register(plugin_t *plugin, plugin_feature_t *feature,
bool reg, void *data)
{
private_charonservice_t *this = (private_charonservice_t*)charonservice;
if (reg)
{
lib->credmgr->add_set(lib->credmgr, &this->creds->set);
charon->attributes->add_handler(charon->attributes,
&this->attr->handler);
}
else
{
lib->credmgr->remove_set(lib->credmgr, &this->creds->set);
charon->attributes->remove_handler(charon->attributes,
&this->attr->handler);
if (this->service)
{
this->service->destroy(this->service);
this->service = NULL;
}
}
return TRUE;
}
/**
* Set strongswan.conf options
*/
static void set_options(char *logfile)
{
lib->settings->set_int(lib->settings,
"charon.plugins.android_log.loglevel", ANDROID_DEBUG_LEVEL);
/* setup file logger */
lib->settings->set_str(lib->settings,
"charon.filelog.%s.time_format", "%b %e %T", logfile);
lib->settings->set_bool(lib->settings,
"charon.filelog.%s.append", FALSE, logfile);
lib->settings->set_bool(lib->settings,
"charon.filelog.%s.flush_line", TRUE, logfile);
lib->settings->set_int(lib->settings,
"charon.filelog.%s.default", ANDROID_DEBUG_LEVEL, logfile);
lib->settings->set_int(lib->settings,
"charon.retransmit_tries", ANDROID_RETRASNMIT_TRIES);
lib->settings->set_double(lib->settings,
"charon.retransmit_timeout", ANDROID_RETRANSMIT_TIMEOUT);
lib->settings->set_double(lib->settings,
"charon.retransmit_base", ANDROID_RETRANSMIT_BASE);
lib->settings->set_bool(lib->settings,
"charon.initiator_only", TRUE);
lib->settings->set_bool(lib->settings,
"charon.close_ike_on_child_failure", TRUE);
/* setting the source address breaks the VpnService.protect() function which
* uses SO_BINDTODEVICE internally. the addresses provided to the kernel as
* auxiliary data have precedence over this option causing a routing loop if
* the gateway is contained in the VPN routes. alternatively, providing an
* explicit device (in addition or instead of the source address) in the
* auxiliary data would also work, but we currently don't have that
* information */
lib->settings->set_bool(lib->settings,
"charon.plugins.socket-default.set_source", FALSE);
/* the Linux kernel does currently not support UDP encaspulation for IPv6
* so lets disable IPv6 for now to avoid issues with dual-stack gateways */
lib->settings->set_bool(lib->settings,
"charon.plugins.socket-default.use_ipv6", FALSE);
#ifdef USE_BYOD
lib->settings->set_str(lib->settings,
"charon.plugins.eap-tnc.protocol", "tnccs-2.0");
lib->settings->set_int(lib->settings,
"charon.plugins.eap-ttls.max_message_count", 0);
lib->settings->set_bool(lib->settings,
"android.imc.send_os_info", TRUE);
lib->settings->set_str(lib->settings,
"libtnccs.tnc_config", "");
#endif
}
/**
* Initialize the charonservice object
*/
static void charonservice_init(JNIEnv *env, jobject service, jobject builder,
jboolean byod)
{
private_charonservice_t *this;
static plugin_feature_t features[] = {
PLUGIN_CALLBACK(kernel_ipsec_register, kernel_android_ipsec_create),
PLUGIN_PROVIDE(CUSTOM, "kernel-ipsec"),
PLUGIN_CALLBACK(kernel_net_register, kernel_android_net_create),
PLUGIN_PROVIDE(CUSTOM, "kernel-net"),
PLUGIN_CALLBACK(charonservice_register, NULL),
PLUGIN_PROVIDE(CUSTOM, "android-backend"),
PLUGIN_DEPENDS(CUSTOM, "libcharon"),
};
INIT(this,
.public = {
.update_status = _update_status,
.update_imc_state = _update_imc_state,
.add_remediation_instr = _add_remediation_instr,
.bypass_socket = _bypass_socket,
.get_trusted_certificates = _get_trusted_certificates,
.get_user_certificate = _get_user_certificate,
.get_user_key = _get_user_key,
.get_vpnservice_builder = _get_vpnservice_builder,
.get_network_manager = _get_network_manager,
},
.attr = android_attr_create(),
.creds = android_creds_create(),
.builder = vpnservice_builder_create(builder),
.network_manager = network_manager_create(service),
.sockets = linked_list_create(),
.vpn_service = (*env)->NewGlobalRef(env, service),
);
charonservice = &this->public;
lib->plugins->add_static_features(lib->plugins, "androidbridge", features,
countof(features), TRUE, NULL, NULL);
#ifdef USE_BYOD
if (byod)
{
plugin_feature_t byod_features[] = {
PLUGIN_CALLBACK(imc_android_register, this->vpn_service),
PLUGIN_PROVIDE(CUSTOM, "android-imc"),
PLUGIN_DEPENDS(CUSTOM, "android-backend"),
PLUGIN_DEPENDS(CUSTOM, "imc-manager"),
};
lib->plugins->add_static_features(lib->plugins, "android-byod",
byod_features, countof(byod_features), TRUE, NULL, NULL);
}
#endif
}
/**
* Deinitialize the charonservice object
*/
static void charonservice_deinit(JNIEnv *env)
{
private_charonservice_t *this = (private_charonservice_t*)charonservice;
this->network_manager->destroy(this->network_manager);
this->sockets->destroy(this->sockets);
this->builder->destroy(this->builder);
this->creds->destroy(this->creds);
this->attr->destroy(this->attr);
(*env)->DeleteGlobalRef(env, this->vpn_service);
free(this);
charonservice = NULL;
}
/**
* Handle SIGSEGV/SIGILL signals raised by threads
*/
static void segv_handler(int signal)
{
dbg_android(DBG_DMN, 1, "thread %u received %d", thread_current_id(),
signal);
exit(1);
}
/**
* Initialize charon and the libraries via JNI
*/
JNI_METHOD(CharonVpnService, initializeCharon, jboolean,
jobject builder, jstring jlogfile, jboolean byod)
{
struct sigaction action;
struct utsname utsname;
char *logfile, *plugins;
/* logging for library during initialization, as we have no bus yet */
dbg = dbg_android;
/* initialize library */
if (!library_init(NULL, "charon"))
{
library_deinit();
return FALSE;
}
/* set options before initializing other libraries that might read them */
logfile = androidjni_convert_jstring(env, jlogfile);
set_options(logfile);
free(logfile);
if (!libhydra_init())
{
libhydra_deinit();
library_deinit();
return FALSE;
}
if (!libipsec_init())
{
libipsec_deinit();
libhydra_deinit();
library_deinit();
return FALSE;
}
if (!libcharon_init())
{
libcharon_deinit();
libipsec_deinit();
libhydra_deinit();
library_deinit();
return FALSE;
}
charon->load_loggers(charon, NULL, FALSE);
charonservice_init(env, this, builder, byod);
if (uname(&utsname) != 0)
{
memset(&utsname, 0, sizeof(utsname));
}
DBG1(DBG_DMN, "Starting IKE charon daemon (strongSwan "VERSION", %s %s, %s)",
utsname.sysname, utsname.release, utsname.machine);
#ifdef PLUGINS_BYOD
if (byod)
{
plugins = PLUGINS " " PLUGINS_BYOD;
}
else
#endif
{
plugins = PLUGINS;
}
if (!charon->initialize(charon, plugins))
{
libcharon_deinit();
charonservice_deinit(env);
libipsec_deinit();
libhydra_deinit();
library_deinit();
return FALSE;
}
lib->plugins->status(lib->plugins, LEVEL_CTRL);
/* add handler for SEGV and ILL etc. */
action.sa_handler = segv_handler;
action.sa_flags = 0;
sigemptyset(&action.sa_mask);
sigaction(SIGSEGV, &action, NULL);
sigaction(SIGILL, &action, NULL);
sigaction(SIGBUS, &action, NULL);
action.sa_handler = SIG_IGN;
sigaction(SIGPIPE, &action, NULL);
/* start daemon (i.e. the threads in the thread-pool) */
charon->start(charon);
return TRUE;
}
/**
* Deinitialize charon and all libraries
*/
JNI_METHOD(CharonVpnService, deinitializeCharon, void)
{
/* deinitialize charon before we destroy our own objects */
libcharon_deinit();
charonservice_deinit(env);
libipsec_deinit();
libhydra_deinit();
library_deinit();
}
/**
* Initiate SA
*/
JNI_METHOD(CharonVpnService, initiate, void,
jstring jconfig)
{
settings_t *settings;
char *config;
config = androidjni_convert_jstring(env, jconfig);
settings = settings_create_string(config);
initiate(settings);
}
@@ -0,0 +1,167 @@
/*
* Copyright (C) 2012-2013 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
/**
* @defgroup libandroidbridge libandroidbridge
*
* @defgroup android_backend backend
* @ingroup libandroidbridge
*
* @defgroup android_byod byod
* @ingroup libandroidbridge
*
* @defgroup android_kernel kernel
* @ingroup libandroidbridge
*
* @defgroup charonservice charonservice
* @{ @ingroup libandroidbridge
*/
#ifndef CHARONSERVICE_H_
#define CHARONSERVICE_H_
#include "vpnservice_builder.h"
#include "kernel/network_manager.h"
#include <library.h>
#include <collections/linked_list.h>
typedef enum android_vpn_state_t android_vpn_state_t;
typedef enum android_imc_state_t android_imc_state_t;
typedef struct charonservice_t charonservice_t;
/**
* Default value for the MTU of TUN device and the size of IKE fragments
*/
#define ANDROID_DEFAULT_MTU 1400
/**
* VPN status codes. As defined in CharonVpnService.java
*/
enum android_vpn_state_t {
CHARONSERVICE_CHILD_STATE_UP = 1,
CHARONSERVICE_CHILD_STATE_DOWN,
CHARONSERVICE_AUTH_ERROR,
CHARONSERVICE_PEER_AUTH_ERROR,
CHARONSERVICE_LOOKUP_ERROR,
CHARONSERVICE_UNREACHABLE_ERROR,
CHARONSERVICE_GENERIC_ERROR,
};
/**
* Final IMC state as defined in ImcState.java
*/
enum android_imc_state_t {
ANDROID_IMC_STATE_UNKNOWN = 0,
ANDROID_IMC_STATE_ALLOW = 1,
ANDROID_IMC_STATE_BLOCK = 2,
ANDROID_IMC_STATE_ISOLATE = 3,
};
/**
* Public interface of charonservice.
*
* Used to communicate with CharonVpnService via JNI
*/
struct charonservice_t {
/**
* Update the status in the Java domain (UI)
*
* @param code status code
* @return TRUE on success
*/
bool (*update_status)(charonservice_t *this, android_vpn_state_t code);
/**
* Update final IMC state in the Java domain (UI)
*
* @param state IMC state
* @return TRUE on success
*/
bool (*update_imc_state)(charonservice_t *this, android_imc_state_t state);
/**
* Add a remediation instruction via JNI
*
* @param instr remediation instruction
* @return TRUE on success
*/
bool (*add_remediation_instr)(charonservice_t *this, char *instr);
/**
* Install a bypass policy for the given socket using the protect() Method
* of the Android VpnService interface.
*
* Use -1 as fd to re-bypass previously bypassed sockets.
*
* @param fd socket file descriptor
* @param family socket protocol family
* @return TRUE if operation successful
*/
bool (*bypass_socket)(charonservice_t *this, int fd, int family);
/**
* Get a list of trusted certificates via JNI
*
* @return list of DER encoded certificates (as chunk_t*),
* NULL on failure
*/
linked_list_t *(*get_trusted_certificates)(charonservice_t *this);
/**
* Get the configured user certificate chain via JNI
*
* The first item in the returned list is the user certificate followed
* by any remaining elements of the certificate chain.
*
* @return list of DER encoded certificates (as chunk_t*),
* NULL on failure
*/
linked_list_t *(*get_user_certificate)(charonservice_t *this);
/**
* Get the configured private key via JNI
*
* @param pubkey the public key as extracted from the certificate
* @return PrivateKey object, NULL on failure
*/
private_key_t *(*get_user_key)(charonservice_t *this, public_key_t *pubkey);
/**
* Get the current vpnservice_builder_t object
*
* @return VpnService.Builder instance
*/
vpnservice_builder_t *(*get_vpnservice_builder)(charonservice_t *this);
/**
* Get the current network_manager_t object
*
* @return NetworkManager instance
*/
network_manager_t *(*get_network_manager)(charonservice_t *this);
};
/**
* The single instance of charonservice_t.
*
* Set between JNI calls to initializeCharon() and deinitializeCharon().
*/
extern charonservice_t *charonservice;
#endif /** CHARONSERVICE_H_ @}*/
@@ -0,0 +1,198 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. *
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "android_ipsec.h"
#include "../charonservice.h"
#include <utils/debug.h>
#include <library.h>
#include <hydra.h>
#include <ipsec.h>
typedef struct private_kernel_android_ipsec_t private_kernel_android_ipsec_t;
struct private_kernel_android_ipsec_t {
/**
* Public kernel interface
*/
kernel_android_ipsec_t public;
/**
* Listener for lifetime expire events
*/
ipsec_event_listener_t ipsec_listener;
};
/**
* Callback registrered with libipsec.
*/
static void expire(u_int8_t protocol, u_int32_t spi, host_t *dst, bool hard)
{
hydra->kernel_interface->expire(hydra->kernel_interface, protocol,
spi, dst, hard);
}
METHOD(kernel_ipsec_t, get_spi, status_t,
private_kernel_android_ipsec_t *this, host_t *src, host_t *dst,
u_int8_t protocol, u_int32_t *spi)
{
return ipsec->sas->get_spi(ipsec->sas, src, dst, protocol, spi);
}
METHOD(kernel_ipsec_t, get_cpi, status_t,
private_kernel_android_ipsec_t *this, host_t *src, host_t *dst,
u_int16_t *cpi)
{
return NOT_SUPPORTED;
}
METHOD(kernel_ipsec_t, add_sa, status_t,
private_kernel_android_ipsec_t *this, host_t *src, host_t *dst,
u_int32_t spi, u_int8_t protocol, u_int32_t reqid, mark_t mark,
u_int32_t tfc, lifetime_cfg_t *lifetime, u_int16_t enc_alg, chunk_t enc_key,
u_int16_t int_alg, chunk_t int_key, ipsec_mode_t mode,
u_int16_t ipcomp, u_int16_t cpi, u_int32_t replay_window,
bool initiator, bool encap, bool esn, bool inbound, bool update,
linked_list_t *src_ts, linked_list_t *dst_ts)
{
return ipsec->sas->add_sa(ipsec->sas, src, dst, spi, protocol, reqid, mark,
tfc, lifetime, enc_alg, enc_key, int_alg, int_key,
mode, ipcomp, cpi, initiator, encap, esn,
inbound, update);
}
METHOD(kernel_ipsec_t, update_sa, status_t,
private_kernel_android_ipsec_t *this, u_int32_t spi, u_int8_t protocol,
u_int16_t cpi, host_t *src, host_t *dst, host_t *new_src, host_t *new_dst,
bool encap, bool new_encap, mark_t mark)
{
return ipsec->sas->update_sa(ipsec->sas, spi, protocol, cpi, src, dst,
new_src, new_dst, encap, new_encap, mark);
}
METHOD(kernel_ipsec_t, query_sa, status_t,
private_kernel_android_ipsec_t *this, host_t *src, host_t *dst,
u_int32_t spi, u_int8_t protocol, mark_t mark,
u_int64_t *bytes, u_int64_t *packets, time_t *time)
{
return ipsec->sas->query_sa(ipsec->sas, src, dst, spi, protocol, mark,
bytes, packets, time);
}
METHOD(kernel_ipsec_t, del_sa, status_t,
private_kernel_android_ipsec_t *this, host_t *src, host_t *dst,
u_int32_t spi, u_int8_t protocol, u_int16_t cpi, mark_t mark)
{
return ipsec->sas->del_sa(ipsec->sas, src, dst, spi, protocol, cpi, mark);
}
METHOD(kernel_ipsec_t, flush_sas, status_t,
private_kernel_android_ipsec_t *this)
{
return ipsec->sas->flush_sas(ipsec->sas);
}
METHOD(kernel_ipsec_t, add_policy, status_t,
private_kernel_android_ipsec_t *this, host_t *src, host_t *dst,
traffic_selector_t *src_ts, traffic_selector_t *dst_ts,
policy_dir_t direction, policy_type_t type, ipsec_sa_cfg_t *sa, mark_t mark,
policy_priority_t priority)
{
return ipsec->policies->add_policy(ipsec->policies, src, dst, src_ts,
dst_ts, direction, type, sa, mark,
priority);
}
METHOD(kernel_ipsec_t, query_policy, status_t,
private_kernel_android_ipsec_t *this, traffic_selector_t *src_ts,
traffic_selector_t *dst_ts, policy_dir_t direction, mark_t mark,
time_t *use_time)
{
return NOT_SUPPORTED;
}
METHOD(kernel_ipsec_t, del_policy, status_t,
private_kernel_android_ipsec_t *this, host_t *src, host_t *dst,
traffic_selector_t *src_ts, traffic_selector_t *dst_ts,
policy_dir_t direction, policy_type_t type, ipsec_sa_cfg_t *sa,
mark_t mark, policy_priority_t priority)
{
return ipsec->policies->del_policy(ipsec->policies, src_ts, dst_ts,
direction, sa->reqid, mark, priority);
}
METHOD(kernel_ipsec_t, flush_policies, status_t,
private_kernel_android_ipsec_t *this)
{
ipsec->policies->flush_policies(ipsec->policies);
return SUCCESS;
}
METHOD(kernel_ipsec_t, bypass_socket, bool,
private_kernel_android_ipsec_t *this, int fd, int family)
{
return charonservice->bypass_socket(charonservice, fd, family);
}
METHOD(kernel_ipsec_t, enable_udp_decap, bool,
private_kernel_android_ipsec_t *this, int fd, int family, u_int16_t port)
{
return NOT_SUPPORTED;
}
METHOD(kernel_ipsec_t, destroy, void,
private_kernel_android_ipsec_t *this)
{
ipsec->events->unregister_listener(ipsec->events, &this->ipsec_listener);
free(this);
}
/*
* Described in header.
*/
kernel_android_ipsec_t *kernel_android_ipsec_create()
{
private_kernel_android_ipsec_t *this;
INIT(this,
.public = {
.interface = {
.get_spi = _get_spi,
.get_cpi = _get_cpi,
.add_sa = _add_sa,
.update_sa = _update_sa,
.query_sa = _query_sa,
.del_sa = _del_sa,
.flush_sas = _flush_sas,
.add_policy = _add_policy,
.query_policy = _query_policy,
.del_policy = _del_policy,
.flush_policies = _flush_policies,
.bypass_socket = _bypass_socket,
.enable_udp_decap = _enable_udp_decap,
.destroy = _destroy,
},
},
.ipsec_listener = {
.expire = expire,
},
);
ipsec->events->register_listener(ipsec->events, &this->ipsec_listener);
return &this->public;
}
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
/**
* @defgroup kernel_android_ipsec kernel_android_ipsec
* @{ @ingroup android_kernel
*/
#ifndef KERNEL_ANDROID_IPSEC_H_
#define KERNEL_ANDROID_IPSEC_H_
#include <library.h>
#include <kernel/kernel_ipsec.h>
typedef struct kernel_android_ipsec_t kernel_android_ipsec_t;
/**
* Implementation of the ipsec interface using libipsec on Android
*/
struct kernel_android_ipsec_t {
/**
* Implements kernel_ipsec_t interface
*/
kernel_ipsec_t interface;
};
/**
* Create a android ipsec interface instance.
*
* @return kernel_android_ipsec_t instance
*/
kernel_android_ipsec_t *kernel_android_ipsec_create();
#endif /** KERNEL_ANDROID_IPSEC_H_ @}*/
@@ -0,0 +1,314 @@
/*
* Copyright (C) 2012-2015 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. *
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <errno.h>
#include "android_net.h"
#include "../android_jni.h"
#include "../charonservice.h"
#include <hydra.h>
#include <processing/jobs/callback_job.h>
#include <threading/mutex.h>
/** delay before firing roam events (ms) */
#define ROAM_DELAY 100
#define ROAM_DELAY_RECHECK 1000
typedef struct private_android_net_t private_android_net_t;
struct private_android_net_t {
/**
* Public kernel interface
*/
kernel_net_t public;
/**
* Reference to NetworkManager object
*/
network_manager_t *network_manager;
/**
* Earliest time of the next roam event
*/
timeval_t next_roam;
/**
* Mutex to check and update roam event time, and other private members
*/
mutex_t *mutex;
/**
* List of virtual IPs
*/
linked_list_t *vips;
/**
* Socket used to determine source address
*/
int socket_v4;
/**
* Whether the device is currently connected
*/
bool connected;
};
/**
* callback function that raises the delayed roam event
*/
static job_requeue_t roam_event()
{
/* this will fail if no connection is up */
charonservice->bypass_socket(charonservice, -1, 0);
hydra->kernel_interface->roam(hydra->kernel_interface, TRUE);
return JOB_REQUEUE_NONE;
}
/**
* Listen for connectivity change events and queue a roam event
*/
static void connectivity_cb(private_android_net_t *this,
bool disconnected)
{
timeval_t now;
job_t *job;
time_monotonic(&now);
this->mutex->lock(this->mutex);
this->connected = !disconnected;
if (!timercmp(&now, &this->next_roam, >))
{
this->mutex->unlock(this->mutex);
return;
}
timeval_add_ms(&now, ROAM_DELAY);
this->next_roam = now;
this->mutex->unlock(this->mutex);
job = (job_t*)callback_job_create((callback_job_cb_t)roam_event, NULL,
NULL, NULL);
lib->scheduler->schedule_job_ms(lib->scheduler, job, ROAM_DELAY);
}
METHOD(kernel_net_t, get_source_addr, host_t*,
private_android_net_t *this, host_t *dest, host_t *src)
{
union {
struct sockaddr sockaddr;
struct sockaddr_in sin;
struct sockaddr_in6 sin6;
} addr;
socklen_t addrlen;
timeval_t now;
job_t *job;
addrlen = *dest->get_sockaddr_len(dest);
addr.sockaddr.sa_family = AF_UNSPEC;
if (connect(this->socket_v4, &addr.sockaddr, addrlen) < 0)
{
DBG1(DBG_KNL, "failed to disconnect socket: %s", strerror(errno));
return NULL;
}
if (android_sdk_version <= ANDROID_JELLY_BEAN_MR2)
{ /* this seems to help avoiding the VIP, unless there is no connectivity
* at all */
charonservice->bypass_socket(charonservice, -1, 0);
}
if (connect(this->socket_v4, dest->get_sockaddr(dest), addrlen) < 0)
{
/* don't report an error if we are not connected (ENETUNREACH) */
if (errno != ENETUNREACH)
{
DBG1(DBG_KNL, "failed to connect socket: %s", strerror(errno));
}
else
{
time_monotonic(&now);
this->mutex->lock(this->mutex);
if (this->connected && timercmp(&now, &this->next_roam, >))
{ /* we were not able to find a source address but reportedly are
* connected, trigger a recheck in case an IP address appears
* delayed but the callback is not triggered again */
timeval_add_ms(&now, ROAM_DELAY_RECHECK);
this->next_roam = now;
this->mutex->unlock(this->mutex);
job = (job_t*)callback_job_create((callback_job_cb_t)roam_event,
NULL, NULL, NULL);
lib->scheduler->schedule_job_ms(lib->scheduler, job,
ROAM_DELAY_RECHECK);
}
else
{
this->mutex->unlock(this->mutex);
}
}
return NULL;
}
if (getsockname(this->socket_v4, &addr.sockaddr, &addrlen) < 0)
{
DBG1(DBG_KNL, "failed to determine src address: %s", strerror(errno));
return NULL;
}
return host_create_from_sockaddr((sockaddr_t*)&addr);
}
METHOD(kernel_net_t, get_source_addr_old, host_t*,
private_android_net_t *this, host_t *dest, host_t *src)
{
host_t *host;
/* on older Android versions we might get the virtual IP back because
* the protect() implementation there and connect() don't properly work
* together, on newer releases (using fwmarks) that's not a problem */
host = get_source_addr(this, dest, src);
if (host)
{
this->mutex->lock(this->mutex);
if (this->vips->find_first(this->vips, (void*)host->ip_equals,
NULL, host) == SUCCESS)
{
host->destroy(host);
host = NULL;
}
this->mutex->unlock(this->mutex);
}
return host;
}
METHOD(kernel_net_t, get_nexthop, host_t*,
private_android_net_t *this, host_t *dest, int prefix, host_t *src)
{
return NULL;
}
METHOD(kernel_net_t, get_interface, bool,
private_android_net_t *this, host_t *host, char **name)
{
if (name)
{ /* the actual name does not matter in our case */
*name = strdup("strongswan");
}
return TRUE;
}
METHOD(kernel_net_t, create_address_enumerator, enumerator_t*,
private_android_net_t *this, kernel_address_type_t which)
{
/* return virtual IPs if requested, nothing otherwise */
if (which & ADDR_TYPE_VIRTUAL)
{
this->mutex->lock(this->mutex);
return enumerator_create_cleaner(
this->vips->create_enumerator(this->vips),
(void*)this->mutex->unlock, this->mutex);
}
return enumerator_create_empty();
}
METHOD(kernel_net_t, add_ip, status_t,
private_android_net_t *this, host_t *virtual_ip, int prefix, char *iface)
{
this->mutex->lock(this->mutex);
this->vips->insert_last(this->vips, virtual_ip->clone(virtual_ip));
this->mutex->unlock(this->mutex);
return SUCCESS;
}
METHOD(kernel_net_t, del_ip, status_t,
private_android_net_t *this, host_t *virtual_ip, int prefix, bool wait)
{
host_t *vip;
this->mutex->lock(this->mutex);
if (this->vips->find_first(this->vips, (void*)virtual_ip->ip_equals,
(void**)&vip, virtual_ip) == SUCCESS)
{
this->vips->remove(this->vips, vip, NULL);
vip->destroy(vip);
}
this->mutex->unlock(this->mutex);
return SUCCESS;
}
METHOD(kernel_net_t, add_route, status_t,
private_android_net_t *this, chunk_t dst_net, u_int8_t prefixlen,
host_t *gateway, host_t *src_ip, char *if_name)
{
return NOT_SUPPORTED;
}
METHOD(kernel_net_t, del_route, status_t,
private_android_net_t *this, chunk_t dst_net, u_int8_t prefixlen,
host_t *gateway, host_t *src_ip, char *if_name)
{
return NOT_SUPPORTED;
}
METHOD(kernel_net_t, destroy, void,
private_android_net_t *this)
{
this->network_manager->remove_connectivity_cb(this->network_manager,
(void*)connectivity_cb);
this->mutex->destroy(this->mutex);
this->vips->destroy(this->vips);
close(this->socket_v4);
free(this);
}
kernel_net_t *kernel_android_net_create()
{
private_android_net_t *this;
INIT(this,
.public = {
.get_source_addr = _get_source_addr,
.get_nexthop = _get_nexthop,
.get_interface = _get_interface,
.create_address_enumerator = _create_address_enumerator,
.add_ip = _add_ip,
.del_ip = _del_ip,
.add_route = _add_route,
.del_route = _del_route,
.destroy = _destroy,
},
.mutex = mutex_create(MUTEX_TYPE_DEFAULT),
.vips = linked_list_create(),
.network_manager = charonservice->get_network_manager(charonservice),
);
timerclear(&this->next_roam);
if (android_sdk_version <= ANDROID_JELLY_BEAN_MR2)
{
this->public.get_source_addr = _get_source_addr_old;
}
this->socket_v4 = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (this->socket_v4 < 0)
{
DBG1(DBG_KNL, "failed to create socket to lookup src addresses: %s",
strerror(errno));
}
charonservice->bypass_socket(charonservice, this->socket_v4, AF_INET);
this->mutex->lock(this->mutex);
this->network_manager->add_connectivity_cb(
this->network_manager, (void*)connectivity_cb, this);
this->connected = this->network_manager->is_connected(this->network_manager);
this->mutex->unlock(this->mutex);
return &this->public;
}
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2012-2015 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
/**
* @defgroup android_net android_net
* @{ @ingroup android_kernel
*/
#ifndef ANDROID_NET_H_
#define ANDROID_NET_H_
#include <library.h>
#include <kernel/kernel_net.h>
/**
* Create an Android-specific kernel_net_t instance.
*
* @return kernel_net_t instance
*/
kernel_net_t *kernel_android_net_create();
#endif /** ANDROID_NET_H_ @}*/
@@ -0,0 +1,233 @@
/*
* Copyright (C) 2012-2015 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. *
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "network_manager.h"
#include "../android_jni.h"
#include "../charonservice.h"
#include <utils/debug.h>
#include <threading/mutex.h>
typedef struct private_network_manager_t private_network_manager_t;
struct private_network_manager_t {
/**
* Public interface
*/
network_manager_t public;
/**
* Reference to NetworkManager object
*/
jobject obj;
/**
* Java class for NetworkManager
*/
jclass cls;
/**
* Registered callback
*/
struct {
connectivity_cb_t cb;
void *data;
} connectivity_cb;
/**
* Mutex to access callback
*/
mutex_t *mutex;
};
JNI_METHOD(NetworkManager, networkChanged, void,
bool disconnected)
{
private_network_manager_t *nm;
nm = (private_network_manager_t*)charonservice->get_network_manager(
charonservice);
nm->mutex->lock(nm->mutex);
if (nm->connectivity_cb.cb)
{
nm->connectivity_cb.cb(nm->connectivity_cb.data, disconnected);
}
nm->mutex->unlock(nm->mutex);
}
METHOD(network_manager_t, add_connectivity_cb, void,
private_network_manager_t *this, connectivity_cb_t cb, void *data)
{
this->mutex->lock(this->mutex);
if (!this->connectivity_cb.cb)
{
JNIEnv *env;
jmethodID method_id;
androidjni_attach_thread(&env);
method_id = (*env)->GetMethodID(env, this->cls, "Register", "()V");
if (!method_id)
{
androidjni_exception_occurred(env);
}
else
{
(*env)->CallVoidMethod(env, this->obj, method_id);
if (!androidjni_exception_occurred(env))
{
this->connectivity_cb.cb = cb;
this->connectivity_cb.data = data;
}
}
androidjni_detach_thread();
}
this->mutex->unlock(this->mutex);
}
/**
* Unregister the NetworkManager via JNI.
*
* this->mutex has to be locked
*/
static void unregister_network_manager(private_network_manager_t *this)
{
JNIEnv *env;
jmethodID method_id;
androidjni_attach_thread(&env);
method_id = (*env)->GetMethodID(env, this->cls, "Unregister", "()V");
if (!method_id)
{
androidjni_exception_occurred(env);
}
else
{
(*env)->CallVoidMethod(env, this->obj, method_id);
androidjni_exception_occurred(env);
}
androidjni_detach_thread();
}
METHOD(network_manager_t, remove_connectivity_cb, void,
private_network_manager_t *this, connectivity_cb_t cb)
{
this->mutex->lock(this->mutex);
if (this->connectivity_cb.cb == cb)
{
this->connectivity_cb.cb = NULL;
unregister_network_manager(this);
}
this->mutex->unlock(this->mutex);
}
METHOD(network_manager_t, is_connected, bool,
private_network_manager_t *this)
{
JNIEnv *env;
jmethodID method_id;
bool connected = FALSE;
androidjni_attach_thread(&env);
method_id = (*env)->GetMethodID(env, this->cls, "isConnected", "()Z");
if (!method_id)
{
androidjni_exception_occurred(env);
}
else
{
connected = (*env)->CallBooleanMethod(env, this->obj, method_id);
connected = !androidjni_exception_occurred(env) && connected;
}
androidjni_detach_thread();
return connected;
}
METHOD(network_manager_t, destroy, void,
private_network_manager_t *this)
{
JNIEnv *env;
this->mutex->lock(this->mutex);
if (this->connectivity_cb.cb)
{
this->connectivity_cb.cb = NULL;
unregister_network_manager(this);
}
this->mutex->unlock(this->mutex);
androidjni_attach_thread(&env);
if (this->obj)
{
(*env)->DeleteGlobalRef(env, this->obj);
}
if (this->cls)
{
(*env)->DeleteGlobalRef(env, this->cls);
}
androidjni_detach_thread();
this->mutex->destroy(this->mutex);
free(this);
}
/*
* Described in header.
*/
network_manager_t *network_manager_create(jobject context)
{
private_network_manager_t *this;
JNIEnv *env;
jmethodID method_id;
jobject obj;
jclass cls;
INIT(this,
.public = {
.add_connectivity_cb = _add_connectivity_cb,
.remove_connectivity_cb = _remove_connectivity_cb,
.is_connected = _is_connected,
.destroy = _destroy,
},
.mutex = mutex_create(MUTEX_TYPE_DEFAULT),
);
androidjni_attach_thread(&env);
cls = (*env)->FindClass(env, JNI_PACKAGE_STRING "/NetworkManager");
if (!cls)
{
goto failed;
}
this->cls = (*env)->NewGlobalRef(env, cls);
method_id = (*env)->GetMethodID(env, cls, "<init>",
"(Landroid/content/Context;)V");
if (!method_id)
{
goto failed;
}
obj = (*env)->NewObject(env, cls, method_id, context);
if (!obj)
{
goto failed;
}
this->obj = (*env)->NewGlobalRef(env, obj);
androidjni_detach_thread();
return &this->public;
failed:
DBG1(DBG_KNL, "failed to build NetworkManager object");
androidjni_exception_occurred(env);
androidjni_detach_thread();
destroy(this);
return NULL;
};
@@ -0,0 +1,89 @@
/*
* Copyright (C) 2012-2015 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
/**
* @defgroup network_manager network_manager
* @{ @ingroup android_kernel
*/
#ifndef NETWORK_MANAGER_H_
#define NETWORK_MANAGER_H_
#include <jni.h>
#include <library.h>
#include <networking/host.h>
typedef struct network_manager_t network_manager_t;
/**
* Callback called if connectivity changes somehow.
*
* Implementation should be quick as the call is made by the Java apps main
* thread.
*
* @param data data supplied during registration
* @param disconnected TRUE if currently disconnected
*/
typedef void (*connectivity_cb_t)(void *data, bool disconnected);
/**
* NetworkManager, used to listen for network changes.
*
* Communicates with NetworkManager via JNI
*/
struct network_manager_t {
/**
* Register a callback that is called if connectivity changes
*
* @note Only the first registered callback is currently used
*
* @param cb callback to register
* @param data data provided to callback
*/
void (*add_connectivity_cb)(network_manager_t *this, connectivity_cb_t cb,
void *data);
/**
* Unregister a previously registered callback for connectivity changes
*
* @param cb previously registered callback
*/
void (*remove_connectivity_cb)(network_manager_t *this,
connectivity_cb_t cb);
/**
* Check whether we currently have connectivity
*
* @return TRUE if currently connected
*/
bool (*is_connected)(network_manager_t *this);
/**
* Destroy a network_manager_t instance
*/
void (*destroy)(network_manager_t *this);
};
/**
* Create a network_manager_t instance
*
* @param context Context object
* @return network_manager_t instance
*/
network_manager_t *network_manager_create(jobject context);
#endif /** NETWORK_MANAGER_H_ @}*/
@@ -0,0 +1,279 @@
/*
* Copyright (C) 2012-2014 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "vpnservice_builder.h"
#include "android_jni.h"
#include <utils/debug.h>
#include <library.h>
typedef struct private_vpnservice_builder_t private_vpnservice_builder_t;
/**
* private data of vpnservice_builder
*/
struct private_vpnservice_builder_t {
/**
* public interface
*/
vpnservice_builder_t public;
/**
* Java object
*/
jobject builder;
};
METHOD(vpnservice_builder_t, add_address, bool,
private_vpnservice_builder_t *this, host_t *addr)
{
JNIEnv *env;
jmethodID method_id;
jstring str;
char buf[INET6_ADDRSTRLEN];
int prefix;
androidjni_attach_thread(&env);
DBG2(DBG_LIB, "builder: adding interface address %H", addr);
prefix = addr->get_family(addr) == AF_INET ? 32 : 128;
if (snprintf(buf, sizeof(buf), "%H", addr) >= sizeof(buf))
{
goto failed;
}
method_id = (*env)->GetMethodID(env, android_charonvpnservice_builder_class,
"addAddress", "(Ljava/lang/String;I)Z");
if (!method_id)
{
goto failed;
}
str = (*env)->NewStringUTF(env, buf);
if (!str)
{
goto failed;
}
if (!(*env)->CallBooleanMethod(env, this->builder, method_id, str, prefix))
{
goto failed;
}
androidjni_detach_thread();
return TRUE;
failed:
DBG1(DBG_LIB, "builder: failed to add address");
androidjni_exception_occurred(env);
androidjni_detach_thread();
return FALSE;
}
METHOD(vpnservice_builder_t, set_mtu, bool,
private_vpnservice_builder_t *this, int mtu)
{
JNIEnv *env;
jmethodID method_id;
androidjni_attach_thread(&env);
DBG2(DBG_LIB, "builder: setting MTU to %d", mtu);
method_id = (*env)->GetMethodID(env, android_charonvpnservice_builder_class,
"setMtu", "(I)Z");
if (!method_id)
{
goto failed;
}
if (!(*env)->CallBooleanMethod(env, this->builder, method_id, mtu))
{
goto failed;
}
androidjni_detach_thread();
return TRUE;
failed:
DBG1(DBG_LIB, "builder: failed to set MTU");
androidjni_exception_occurred(env);
androidjni_detach_thread();
return FALSE;
}
METHOD(vpnservice_builder_t, add_route, bool,
private_vpnservice_builder_t *this, host_t *net, int prefix)
{
JNIEnv *env;
jmethodID method_id;
jstring str;
char buf[INET6_ADDRSTRLEN];
androidjni_attach_thread(&env);
DBG2(DBG_LIB, "builder: adding route %+H/%d", net, prefix);
if (snprintf(buf, sizeof(buf), "%+H", net) >= sizeof(buf))
{
goto failed;
}
method_id = (*env)->GetMethodID(env, android_charonvpnservice_builder_class,
"addRoute", "(Ljava/lang/String;I)Z");
if (!method_id)
{
goto failed;
}
str = (*env)->NewStringUTF(env, buf);
if (!str)
{
goto failed;
}
if (!(*env)->CallBooleanMethod(env, this->builder, method_id, str, prefix))
{
goto failed;
}
androidjni_detach_thread();
return TRUE;
failed:
DBG1(DBG_LIB, "builder: failed to add route");
androidjni_exception_occurred(env);
androidjni_detach_thread();
return FALSE;
}
METHOD(vpnservice_builder_t, add_dns, bool,
private_vpnservice_builder_t *this, host_t *dns)
{
JNIEnv *env;
jmethodID method_id;
jstring str;
char buf[INET6_ADDRSTRLEN];
androidjni_attach_thread(&env);
DBG2(DBG_LIB, "builder: adding DNS server %H", dns);
if (snprintf(buf, sizeof(buf), "%H", dns) >= sizeof(buf))
{
goto failed;
}
method_id = (*env)->GetMethodID(env, android_charonvpnservice_builder_class,
"addDnsServer", "(Ljava/lang/String;)Z");
if (!method_id)
{
goto failed;
}
str = (*env)->NewStringUTF(env, buf);
if (!str)
{
goto failed;
}
if (!(*env)->CallBooleanMethod(env, this->builder, method_id, str))
{
goto failed;
}
androidjni_detach_thread();
return TRUE;
failed:
DBG1(DBG_LIB, "builder: failed to add DNS server");
androidjni_exception_occurred(env);
androidjni_detach_thread();
return FALSE;
}
/**
* Establish or reestablish the TUN device
*/
static int establish_internal(private_vpnservice_builder_t *this, char *method)
{
JNIEnv *env;
jmethodID method_id;
int fd;
androidjni_attach_thread(&env);
DBG2(DBG_LIB, "builder: building TUN device");
method_id = (*env)->GetMethodID(env, android_charonvpnservice_builder_class,
method, "()I");
if (!method_id)
{
goto failed;
}
fd = (*env)->CallIntMethod(env, this->builder, method_id);
if (fd == -1)
{
goto failed;
}
androidjni_detach_thread();
return fd;
failed:
DBG1(DBG_LIB, "builder: failed to build TUN device");
androidjni_exception_occurred(env);
androidjni_detach_thread();
return -1;
}
METHOD(vpnservice_builder_t, establish, int,
private_vpnservice_builder_t *this)
{
return establish_internal(this, "establish");
}
METHOD(vpnservice_builder_t, establish_no_dns, int,
private_vpnservice_builder_t *this)
{
return establish_internal(this, "establishNoDns");
}
METHOD(vpnservice_builder_t, destroy, void,
private_vpnservice_builder_t *this)
{
JNIEnv *env;
androidjni_attach_thread(&env);
(*env)->DeleteGlobalRef(env, this->builder);
androidjni_detach_thread();
free(this);
}
vpnservice_builder_t *vpnservice_builder_create(jobject builder)
{
JNIEnv *env;
private_vpnservice_builder_t *this;
INIT(this,
.public = {
.add_address = _add_address,
.add_route = _add_route,
.add_dns = _add_dns,
.set_mtu = _set_mtu,
.establish = _establish,
.establish_no_dns = _establish_no_dns,
.destroy = _destroy,
},
);
androidjni_attach_thread(&env);
this->builder = (*env)->NewGlobalRef(env, builder);
androidjni_detach_thread();
return &this->public;
}
@@ -0,0 +1,102 @@
/*
* Copyright (C) 2012-2014 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
/**
* @defgroup vpnservice_builder vpnservice_builder
* @{ @ingroup libandroidbridge
*/
#ifndef VPNSERVICE_BUILDER_H_
#define VPNSERVICE_BUILDER_H_
#include <jni.h>
#include <library.h>
#include <networking/host.h>
typedef struct vpnservice_builder_t vpnservice_builder_t;
/**
* VpnService.Builder, used to build a TUN device.
*
* Communicates with CharonVpnService.BuilderAdapter via JNI
*/
struct vpnservice_builder_t {
/**
* Add an interface address
*
* @param addr the desired interface address
* @return TRUE on success
*/
bool (*add_address)(vpnservice_builder_t *this, host_t *addr);
/**
* Add a route
*
* @param net the network address
* @param prefix_length the prefix length
* @return TRUE on success
*/
bool (*add_route)(vpnservice_builder_t *this, host_t *net, int prefix);
/**
* Add a DNS server
*
* @param dns the address of the DNS server
* @return TRUE on success
*/
bool (*add_dns)(vpnservice_builder_t *this, host_t *dns);
/**
* Set the MTU for the TUN device
*
* @param mtu the MTU to set
* @return TRUE on success
*/
bool (*set_mtu)(vpnservice_builder_t *this, int mtu);
/**
* Build the TUN device
*
* @return the TUN file descriptor, -1 if failed
*/
int (*establish)(vpnservice_builder_t *this);
/**
* Build the TUN device without DNS related data
*
* @return the TUN file descriptor, -1 if failed
*/
int (*establish_no_dns)(vpnservice_builder_t *this);
/**
* Destroy a vpnservice_builder
*/
void (*destroy)(vpnservice_builder_t *this);
};
/**
* Create a vpnservice_builder instance
*
* @param builder CharonVpnService.BuilderAdapter object
* @return vpnservice_builder_t instance
*/
vpnservice_builder_t *vpnservice_builder_create(jobject builder);
#endif /** VPNSERVICE_BUILDER_H_ @}*/
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2013 Tobias Brunner
Hochschule fuer Technik Rapperswil
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid
android:color="@color/panel_separator" />
</shape>
</item>
<item android:left="2dp">
<shape>
<solid
android:color="@color/panel_background" />
</shape>
</item>
</layer-list>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012-2013 Tobias Brunner
Hochschule fuer Technik Rapperswil
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid
android:color="@color/panel_separator" />
</shape>
</item>
<item android:bottom="2dp">
<shape>
<solid
android:color="@color/panel_background" />
</shape>
</item>
</layer-list>
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2013 Tobias Brunner
Hochschule fuer Technik Rapperswil
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:baselineAligned="false" >
<fragment
class="org.strongswan.android.ui.RemediationInstructionsFragment"
android:id="@+id/remediation_instructions_fragment"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_width="0dp" />
<FrameLayout
android:layout_height="match_parent"
android:layout_weight="2"
android:layout_width="0dp"
android:background="@drawable/remediation_instruction_background_large"
android:padding="5dp" >
<fragment
class="org.strongswan.android.ui.RemediationInstructionFragment"
android:id="@+id/remediation_instruction_fragment"
android:layout_height="match_parent"
android:layout_width="match_parent" />
</FrameLayout>
</LinearLayout>
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2013 Tobias Brunner
Hochschule fuer Technik Rapperswil
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="2dp"
android:background="@drawable/state_background"
android:orientation="vertical" >
<LinearLayout
android:id="@+id/imc_state_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="vertical" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="10dp"
android:orientation="horizontal" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="5dp"
android:text="@string/imc_state_label"
android:textColor="?android:textColorPrimary"
android:textSize="20sp" />
<TextView
android:id="@+id/imc_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textColor="?android:textColorSecondary"
android:textSize="20sp" />
</LinearLayout>
<TextView
android:id="@+id/action"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:text="@string/show_remediation_instructions"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 Tobias Brunner
Hochschule fuer Technik Rapperswil
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<fragment
class="org.strongswan.android.ui.LogFragment"
android:id="@+id/log_frag"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 Tobias Brunner
Copyright (C) 2012 Giuliano Grassi
Copyright (C) 2012 Ralf Sager
Hochschule fuer Technik Rapperswil
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<org.strongswan.android.ui.LogScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:scrollbarFadeDuration="0"
android:scrollbarAlwaysDrawVerticalTrack="true" >
<TextView
android:id="@+id/log_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="9sp"
android:typeface="monospace" >
</TextView>
</org.strongswan.android.ui.LogScrollView>
</LinearLayout>
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2012 Tobias Brunner
Copyright (C) 2012 Giuliano Grassi
Copyright (C) 2012 Ralf Sager
Hochschule fuer Technik Rapperswil
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
-->
<LinearLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical"
android:padding="5dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="@string/profile_username_label"
android:textStyle="bold" />
<EditText
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:id="@+id/username"
android:enabled="false"
android:inputType="none" />
<TextView
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="@string/profile_password_label"
android:textStyle="bold" />
<EditText
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:id="@+id/password"
android:inputType="textPassword|textNoSuggestions"
android:singleLine="true" />
</LinearLayout>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012-2013 Tobias Brunner
Hochschule fuer Technik Rapperswil
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<fragment
class="org.strongswan.android.ui.VpnStateFragment"
android:id="@+id/vpn_state_frag"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<fragment
class="org.strongswan.android.ui.ImcStateFragment"
android:id="@+id/imc_state_frag"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<fragment
class="org.strongswan.android.ui.VpnProfileListFragment"
android:id="@+id/profile_list_frag"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
@@ -0,0 +1,203 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012-2015 Tobias Brunner
Copyright (C) 2012 Giuliano Grassi
Copyright (C) 2012 Ralf Sager
Hochschule fuer Technik Rapperswil
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/profile_name_label" />
<EditText
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="textNoSuggestions"
android:hint="@string/profile_name_hint" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/profile_gateway_label" />
<EditText
android:id="@+id/gateway"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="textNoSuggestions" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/profile_vpn_type_label" />
<Spinner
android:id="@+id/vpn_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:spinnerMode="dropdown"
android:entries="@array/vpn_types" />
<include
android:id="@+id/tnc_notice"
layout="@layout/two_line_button"
android:visibility="gone" />
<LinearLayout
android:id="@+id/username_password_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/profile_username_label" />
<EditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="textNoSuggestions" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/profile_password_label" />
<EditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="textPassword|textNoSuggestions"
android:hint="@string/profile_password_hint" />
</LinearLayout>
<LinearLayout
android:id="@+id/user_certificate_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/profile_user_certificate_label" />
<include
android:id="@+id/select_user_certificate"
layout="@layout/two_line_button" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/profile_ca_label" />
<CheckBox
android:id="@+id/ca_auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/profile_ca_auto_label" />
<include
android:id="@+id/select_certificate"
layout="@layout/two_line_button" />
<CheckBox
android:id="@+id/show_advanced"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/profile_show_advanced_label" />
<LinearLayout
android:id="@+id/advanced_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/profile_mtu_label" />
<EditText
android:id="@+id/mtu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="number|textNoSuggestions"
android:hint="@string/profile_use_default_hint" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/profile_port_label" />
<EditText
android:id="@+id/port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="number|textNoSuggestions"
android:hint="@string/profile_use_default_hint" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/profile_split_tunneling_label" />
<CheckBox
android:id="@+id/split_tunneling_v4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/profile_split_tunnelingv4_title" />
<CheckBox
android:id="@+id/split_tunneling_v6"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/profile_split_tunnelingv6_title" />
</LinearLayout>
</LinearLayout>
</ScrollView>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 Tobias Brunner
Hochschule fuer Technik Rapperswil
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:paddingLeft="5dp"
android:paddingRight="5dp" >
<ListView
android:id="@+id/profile_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:dividerHeight="1dp"
android:divider="?android:attr/listDivider"
android:scrollbarAlwaysDrawVerticalTrack="true" />
<TextView android:id="@+id/profile_list_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="15dp"
android:text="@string/no_profiles"/>
</FrameLayout>

Some files were not shown because too many files have changed in this diff Show More