/**
* Copyright (c) 2013 LEAP Encryption Access Project and contributers
*
* 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 3 of the License, or
* (at your option) any later version.
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package se.leap.bitmaskclient;
import android.app.Activity;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.util.Log;
import de.blinkt.openvpn.LaunchVPN;
import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.activities.DisconnectVPN;
import de.blinkt.openvpn.core.ConfigParser;
import de.blinkt.openvpn.core.ConfigParser.ConfigParseError;
import de.blinkt.openvpn.core.ProfileManager;
import de.blinkt.openvpn.core.VpnStatus.ConnectionStatus;
import java.io.IOException;
import java.io.StringReader;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeMap;
import java.util.Vector;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import se.leap.bitmaskclient.Dashboard;
import se.leap.bitmaskclient.Provider;
import se.leap.bitmaskclient.R;
/**
* EIP is the abstract base class for interacting with and managing the Encrypted
* Internet Proxy connection. Connections are started, stopped, and queried through
* this IntentService.
* Contains logic for parsing eip-service.json from the provider, configuring and selecting
* gateways, and controlling {@link de.blinkt.openvpn.core.OpenVPNService} connections.
*
* @author Sean Leonard
* @author Parménides GV
*/
public final class EIP extends IntentService {
public final static String AUTHED_EIP = "authed eip";
public final static String ACTION_CHECK_CERT_VALIDITY = "se.leap.bitmaskclient.CHECK_CERT_VALIDITY";
public final static String ACTION_START_EIP = "se.leap.bitmaskclient.START_EIP";
public final static String ACTION_STOP_EIP = "se.leap.bitmaskclient.STOP_EIP";
public final static String ACTION_UPDATE_EIP_SERVICE = "se.leap.bitmaskclient.UPDATE_EIP_SERVICE";
public final static String ACTION_IS_EIP_RUNNING = "se.leap.bitmaskclient.IS_RUNNING";
public final static String ACTION_REBUILD_PROFILES = "se.leap.bitmaskclient.REBUILD_PROFILES";
public final static String EIP_NOTIFICATION = "EIP_NOTIFICATION";
public final static String STATUS = "eip status";
public final static String DATE_FROM_CERTIFICATE = "date from certificate";
public final static String ALLOWED_ANON = "allow_anonymous";
public final static String ALLOWED_REGISTERED = "allow_registration";
public final static String CERTIFICATE = "cert";
public final static String PRIVATE_KEY = "private_key";
public final static String KEY = "eip";
public final static String PARSED_SERIAL = "eip_parsed_serial";
public final static String SERVICE_API_PATH = "config/eip-service.json";
public final static String RECEIVER_TAG = "receiverTag";
public final static String REQUEST_TAG = "requestTag";
public final static String TAG = "se.leap.bitmaskclient.EIP";
public final static SimpleDateFormat certificate_date_format = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US);
private static Context context;
private static ResultReceiver mReceiver;
private static boolean mBound = false;
private static int parsedEipSerial;
private static JSONObject eipDefinition = null;
private static OVPNGateway activeGateway = null;
protected static ConnectionStatus lastConnectionStatusLevel;
protected static boolean mIsDisconnecting = false;
protected static boolean mIsStarting = false;
public EIP(){
super("LEAPEIP");
}
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
updateEIPService();
}
@Override
public void onDestroy() {
mBound = false;
super.onDestroy();
}
@Override
protected void onHandleIntent(Intent intent) {
String action = intent.getAction();
mReceiver = intent.getParcelableExtra(RECEIVER_TAG);
if ( action == ACTION_START_EIP )
startEIP();
else if ( action == ACTION_STOP_EIP )
stopEIP();
else if ( action == ACTION_IS_EIP_RUNNING )
isRunning();
else if ( action == ACTION_UPDATE_EIP_SERVICE )
updateEIPService();
else if ( action == ACTION_CHECK_CERT_VALIDITY )
checkCertValidity();
else if ( action == ACTION_REBUILD_PROFILES )
updateGateways();
}
/**
* Initiates an EIP connection by selecting a gateway and preparing and sending an
* Intent to {@link se.leap.openvpn.LaunchVPN}.
* It also sets up early routes.
*/
private void startEIP() {
earlyRoutes();
activeGateway = selectGateway();
if(activeGateway != null && activeGateway.mVpnProfile != null) {
mReceiver = EipServiceFragment.getReceiver();
launchActiveGateway();
}
}
/**
* Early routes are routes that block traffic until a new
* VpnService is started properly.
*/
private void earlyRoutes() {
Intent void_vpn_launcher = new Intent(context, VoidVpnLauncher.class);
void_vpn_launcher.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(void_vpn_launcher);
}
/**
* Choose a gateway to connect to based on timezone from system locale data
*
* @return The gateway to connect to
*/
private OVPNGateway selectGateway() {
String closest_location = closestGateway();
String chosen_host = chooseHost(closest_location);
return new OVPNGateway(chosen_host);
}
private String closestGateway() {
TreeMap> offsets = calculateOffsets();
return offsets.isEmpty() ? "" : offsets.firstEntry().getValue().iterator().next();
}
private TreeMap> calculateOffsets() {
TreeMap> offsets = new TreeMap>();
int localOffset = Calendar.getInstance().get(Calendar.ZONE_OFFSET) / 3600000;
JSONObject locations = availableLocations();
Iterator locations_names = locations.keys();
while(locations_names.hasNext()) {
try {
String location_name = locations_names.next();
JSONObject location = locations.getJSONObject(location_name);
int dist = timezoneDistance(localOffset, location.optInt("timezone"));
Set set = (offsets.get(dist) != null) ?
offsets.get(dist) : new HashSet();
set.add(location_name);
offsets.put(dist, set);
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return offsets;
}
private JSONObject availableLocations() {
JSONObject locations = null;
try {
if(eipDefinition == null) updateEIPService();
locations = eipDefinition.getJSONObject("locations");
} catch (JSONException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
return locations;
}
private int timezoneDistance(int local_timezone, int remote_timezone) {
// Distance along the numberline of Prime Meridian centric, assumes UTC-11 through UTC+12
int dist = Math.abs(local_timezone - remote_timezone);
// Farther than 12 timezones and it's shorter around the "back"
if (dist > 12)
dist = 12 - (dist -12); // Well i'll be. Absolute values make equations do funny things.
return dist;
}
private String chooseHost(String location) {
String chosen_host = "";
try {
JSONArray gateways = eipDefinition.getJSONArray("gateways");
for (int i = 0; i < gateways.length(); i++) {
JSONObject gw = gateways.getJSONObject(i);
if ( gw.getString("location").equalsIgnoreCase(location) || location.isEmpty()){
chosen_host = eipDefinition.getJSONObject("locations").getJSONObject(gw.getString("location")).getString("name");
break;
}
}
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return chosen_host;
}
private void launchActiveGateway() {
Intent intent = new Intent(this,LaunchVPN.class);
intent.setAction(Intent.ACTION_MAIN);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(LaunchVPN.EXTRA_KEY, activeGateway.mVpnProfile.getUUID().toString() );
intent.putExtra(LaunchVPN.EXTRA_NAME, activeGateway.mVpnProfile.getName() );
intent.putExtra(LaunchVPN.EXTRA_HIDELOG, true);
intent.putExtra(RECEIVER_TAG, mReceiver);
startActivity(intent);
}
/**
* Disconnects the EIP connection gracefully through the bound service or forcefully
* if there is no bound service. Sends a message to the requesting ResultReceiver.
*/
private void stopEIP() {
if(isConnected()) {
Intent disconnect_vpn = new Intent(this, DisconnectVPN.class);
disconnect_vpn.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(disconnect_vpn);
mIsDisconnecting = true;
lastConnectionStatusLevel = ConnectionStatus.UNKNOWN_LEVEL; // Wait for the decision of the user
Log.d(TAG, "mIsDisconnecting = true");
}
tellToReceiver(ACTION_STOP_EIP, Activity.RESULT_OK);
}
private void tellToReceiver(String action, int resultCode) {
if (mReceiver != null){
Bundle resultData = new Bundle();
resultData.putString(REQUEST_TAG, action);
mReceiver.send(resultCode, resultData);
}
}
/**
* Checks the last stored status notified by ics-openvpn
* Sends Activity.RESULT_CANCELED
to the ResultReceiver that made the
* request if it's not connected, Activity.RESULT_OK
otherwise.
*/
private void isRunning() {
int resultCode = Activity.RESULT_CANCELED;
boolean is_connected = isConnected();
resultCode = (is_connected) ? Activity.RESULT_OK : Activity.RESULT_CANCELED;
tellToReceiver(ACTION_IS_EIP_RUNNING, resultCode);
}
protected static boolean isConnected() {
return lastConnectionStatusLevel != null && lastConnectionStatusLevel.equals(ConnectionStatus.LEVEL_CONNECTED) && !mIsDisconnecting;
}
/**
* Loads eip-service.json from SharedPreferences and calls {@link updateGateways()}
* to parse gateway definitions.
* TODO Implement API call to refresh eip-service.json from the provider
*/
private void updateEIPService() {
try {
eipDefinition = new JSONObject(getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(KEY, ""));
parsedEipSerial = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getInt(PARSED_SERIAL, 0);
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(parsedEipSerial == 0) {
deleteAllVpnProfiles();
}
if (eipDefinition != null && eipDefinition.optInt("serial") > parsedEipSerial)
updateGateways();
}
private void deleteAllVpnProfiles() {
ProfileManager vpl = ProfileManager.getInstance(context);
VpnProfile[] profiles = (VpnProfile[]) vpl.getProfiles().toArray(new VpnProfile[vpl.getProfiles().size()]);
for (int current_profile = 0; current_profile < profiles.length; current_profile++){
vpl.removeProfile(context, profiles[current_profile]);
}
}
/**
* Walk the list of gateways defined in eip-service.json and parse them into
* OVPNGateway objects.
* TODO Store the OVPNGateways (as Serializable) in SharedPreferences
*/
private void updateGateways(){
JSONArray gatewaysDefined = null;
try {
gatewaysDefined = eipDefinition.getJSONArray("gateways");
for ( int i=0 ; i < gatewaysDefined.length(); i++ ){
JSONObject gw = null;
gw = gatewaysDefined.getJSONObject(i);
if ( gw.getJSONObject("capabilities").getJSONArray("transport").toString().contains("openvpn") )
new OVPNGateway(gw);
}
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putInt(PARSED_SERIAL, eipDefinition.optInt(Provider.API_RETURN_SERIAL)).commit();
}
private void checkCertValidity() {
String certificate_string = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(CERTIFICATE, "");
if(!certificate_string.isEmpty()) {
String date_from_certificate_string = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(DATE_FROM_CERTIFICATE, certificate_date_format.format(Calendar.getInstance().getTime()).toString());
X509Certificate certificate_x509 = ConfigHelper.parseX509CertificateFromString(certificate_string);
Calendar offset_date = Calendar.getInstance();
try {
Date date_from_certificate = certificate_date_format.parse(date_from_certificate_string);
long difference = Math.abs(date_from_certificate.getTime() - certificate_x509.getNotAfter().getTime())/2;
long current_date_millis = offset_date.getTimeInMillis();
offset_date.setTimeInMillis(current_date_millis + difference);
Log.d(TAG, "certificate not after = " + certificate_x509.getNotAfter());
} catch(ParseException e) {
e.printStackTrace();
}
Bundle result_data = new Bundle();
result_data.putString(REQUEST_TAG, ACTION_CHECK_CERT_VALIDITY);
try {
Log.d(TAG, "offset_date = " + offset_date.getTime().toString());
certificate_x509.checkValidity(offset_date.getTime());
mReceiver.send(Activity.RESULT_OK, result_data);
Log.d(TAG, "Valid certificate");
} catch(CertificateExpiredException e) {
mReceiver.send(Activity.RESULT_CANCELED, result_data);
Log.d(TAG, "Updating certificate");
} catch(CertificateNotYetValidException e) {
mReceiver.send(Activity.RESULT_CANCELED, result_data);
}
}
}
/**
* OVPNGateway provides objects defining gateways and their options and metadata.
* Each instance contains a VpnProfile for OpenVPN specific data and member
* variables describing capabilities and location
*
* @author Sean Leonard
*/
private class OVPNGateway {
private String TAG = "OVPNGateway";
private String mName;
private VpnProfile mVpnProfile;
private JSONObject mGateway;
private HashMap>> options = new HashMap>>();
/**
* Attempts to retrieve a VpnProfile by name and build an OVPNGateway around it.
* FIXME This needs to become a findGatewayByName() method
*
* @param name The hostname of the gateway to inflate
*/
private OVPNGateway(String name){
mName = name;
this.loadVpnProfile();
}
private void loadVpnProfile() {
ProfileManager vpl = ProfileManager.getInstance(context);
try {
if ( mName == null )
mVpnProfile = vpl.getProfiles().iterator().next();
else
mVpnProfile = vpl.getProfileByName(mName);
} catch (NoSuchElementException e) {
updateEIPService();
this.loadVpnProfile(); // FIXME catch infinite loops
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* Build a gateway object from a JSON OpenVPN gateway definition in eip-service.json
* and create a VpnProfile belonging to it.
*
* @param gateway The JSON OpenVPN gateway definition to parse
*/
protected OVPNGateway(JSONObject gateway){
mGateway = gateway;
// Currently deletes VpnProfile for host, if there already is one, and builds new
ProfileManager vpl = ProfileManager.getInstance(context);
Collection profiles = vpl.getProfiles();
for (Iterator it = profiles.iterator(); it.hasNext(); ){
VpnProfile p = it.next();
if ( p.mName.equalsIgnoreCase( mName ) ) {
it.remove();
vpl.removeProfile(context, p);
}
}
this.createVPNProfile();
vpl.addProfile(mVpnProfile);
vpl.saveProfile(context, mVpnProfile);
vpl.saveProfileList(context);
}
/**
* Create and attach the VpnProfile to our gateway object
*/
protected void createVPNProfile(){
try {
ConfigParser cp = new ConfigParser();
cp.parseConfig(new StringReader(configFromEipServiceDotJson()));
cp.parseConfig(new StringReader(caSecretFromSharedPreferences()));
cp.parseConfig(new StringReader(keySecretFromSharedPreferences()));
cp.parseConfig(new StringReader(certSecretFromSharedPreferences()));
cp.parseConfig(new StringReader("remote-cert-tls server"));
cp.parseConfig(new StringReader("persist-tun"));
VpnProfile vp = cp.convertProfile();
//vp.mAuthenticationType=VpnProfile.TYPE_STATICKEYS;
mVpnProfile = vp;
mVpnProfile.mName = mName = locationAsName();
Log.v(TAG,"Created VPNProfile");
} catch (ConfigParseError e) {
// FIXME We didn't get a VpnProfile! Error handling! and log level
Log.v(TAG,"Error creating VPNProfile");
e.printStackTrace();
} catch (IOException e) {
// FIXME We didn't get a VpnProfile! Error handling! and log level
Log.v(TAG,"Error creating VPNProfile");
e.printStackTrace();
}
}
/**
* Parses data from eip-service.json to a section of the openvpn config file
*/
private String configFromEipServiceDotJson() {
String parsed_configuration = "";
String location_key = "location";
String locations = "locations";
parsed_configuration += extractCommonOptionsFromEipServiceDotJson();
parsed_configuration += extractRemotesFromEipServiceDotJson();
return parsed_configuration;
}
private String extractCommonOptionsFromEipServiceDotJson() {
String common_options = "";
try {
String common_options_key = "openvpn_configuration";
JSONObject openvpn_configuration = eipDefinition.getJSONObject(common_options_key);
Iterator keys = openvpn_configuration.keys();
Vector> value = new Vector>();
while ( keys.hasNext() ){
String key = keys.next().toString();
common_options += key + " ";
for ( String word : openvpn_configuration.getString(key).split(" ") )
common_options += word + " ";
common_options += System.getProperty("line.separator");
}
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
common_options += "client" + System.getProperty("line.separator");
return common_options;
}
private String extractRemotesFromEipServiceDotJson() {
String remotes = "";
String remote = "ip_address";
String remote_openvpn_keyword = "remote";
String ports = "ports";
String protos = "protocols";
String capabilities = "capabilities";
String udp = "udp";
try {
JSONArray protocolsJSON = mGateway.getJSONObject(capabilities).getJSONArray(protos);
for ( int i=0; i";
secret_lines += System.getProperty("line.separator");
secret_lines += preferences.getString(Provider.CA_CERT, "");
secret_lines += System.getProperty("line.separator");
secret_lines += "";
return secret_lines;
}
private String keySecretFromSharedPreferences() {
String secret_lines = "";
SharedPreferences preferences = context.getSharedPreferences(Dashboard.SHARED_PREFERENCES, context.MODE_PRIVATE);
secret_lines += System.getProperty("line.separator");
secret_lines +="";
secret_lines += System.getProperty("line.separator");
secret_lines += preferences.getString(EIP.PRIVATE_KEY, "");
secret_lines += System.getProperty("line.separator");
secret_lines += "";
secret_lines += System.getProperty("line.separator");
return secret_lines;
}
private String certSecretFromSharedPreferences() {
String secret_lines = "";
SharedPreferences preferences = context.getSharedPreferences(Dashboard.SHARED_PREFERENCES, context.MODE_PRIVATE);
secret_lines += System.getProperty("line.separator");
secret_lines +="";
secret_lines += System.getProperty("line.separator");
secret_lines += preferences.getString(EIP.CERTIFICATE, "");
secret_lines += System.getProperty("line.separator");
secret_lines += "";
secret_lines += System.getProperty("line.separator");
return secret_lines;
}
public String locationAsName() {
try {
return eipDefinition.getJSONObject("locations").getJSONObject(mGateway.getString("location")).getString("name");
} catch (JSONException e) {
Log.v(TAG,"Couldn't read gateway name for profile creation! Returning original name = " + mName);
e.printStackTrace();
return (mName != null) ? mName : "";
}
}
}
}