/*
 * Copyright (c) 2012-2016 Arne Schwabe
 * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
 */

package de.blinkt.openvpn;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.preference.PreferenceManager;
import android.security.KeyChain;
import android.security.KeyChainException;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Base64;

import com.routeone.dc.vpn.DcUtils;

import org.spongycastle.util.io.pem.PemObject;
import org.spongycastle.util.io.pem.PemWriter;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.Vector;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import de.blinkt.openvpn.core.Connection;
import de.blinkt.openvpn.core.NativeUtils;
import de.blinkt.openvpn.core.OpenVPNService;
import de.blinkt.openvpn.core.VPNLaunchHelper;
import de.blinkt.openvpn.core.VpnStatus;
import de.blinkt.openvpn.core.X509Utils;

public class VpnProfile implements Serializable, Cloneable {
    // Note that this class cannot be moved to core where it belongs since
    // the profile loading depends on it being here
    // The Serializable documentation mentions that class name change are possible
    // but the how is unclear
    //
    transient public static final long MAX_EMBED_FILE_SIZE = 2048 * 1024; // 2048kB
    // Don't change this, not all parts of the program use this constant
    public static final String EXTRA_PROFILEUUID = "de.blinkt.openvpn.profileUUID";
    public static final String EXTRA_ROUTE1PROFILEUUID = "com.routeone.dc.vpn.profileUUID";
    public static final String INLINE_TAG = "[[INLINE]]";
    public static final String DISPLAYNAME_TAG = "[[NAME]]";

    private static final long serialVersionUID = 7085688938959334563L;
    public static final int MAXLOGLEVEL = 4;
    public static final int CURRENT_PROFILE_VERSION = 6;
    public static final int DEFAULT_MSSFIX_SIZE = 1280;
    public static String DEFAULT_DNS1 = "8.8.8.8";
    public static String DEFAULT_DNS2 = "8.8.4.4";

    public transient String mTransientPW = null;
    public transient String mTransientPCKS12PW = null;


    public static final int TYPE_CERTIFICATES = 0;
    public static final int TYPE_PKCS12 = 1;

    //public static final int TYPE_KEYSTORE = 2;
    //public static final int TYPE_USERPASS = 3;
    //public static final int TYPE_STATICKEYS = 4;
    //public static final int TYPE_USERPASS_CERTIFICATES = 5;
    //public static final int TYPE_USERPASS_PKCS12 = 6;
    //public static final int TYPE_USERPASS_KEYSTORE = 7;
    /*
     * This section is a derivative work from the original project.
     *
     * -----  Beginning of Section  -----
     *
     * Copyright (c) 2016 Route1 Inc.  All rights reserved.  <sales@route1.com>
     *
     * You may use, distribute and modify this code under the
     * terms of the GNU General Public License version 2
     * as published by the Free Software Foundation.

     * Contributor:  A. M. Jehad Sarkar  <amjehad.sarkar@route1.com>
     */
    public static final int TYPE_DERIVID_CERTIFICATES = 2; // Note: we (Route1) inserted the new type in the middle of list, so previously created OpenVPN profiles (.ovpn) become incompatible with this app.
    public static final int TYPE_KEYSTORE = 3; //2;
    public static final int TYPE_USERPASS = 4; //3;
    public static final int TYPE_STATICKEYS = 5; //4;
    public static final int TYPE_USERPASS_CERTIFICATES = 6; //5;
    public static final int TYPE_USERPASS_PKCS12 = 7; //6;
    public static final int TYPE_USERPASS_KEYSTORE = 8; //7;

    public static final int TYPEDC_UNDEFINED = 0;
    public static final int TYPEDC_TUNNEL = 1;
    public static final int TYPEDC_PIV_AUTHENTICATION = 2;
    public static final int TYPEDC_DIGITAL_SIGNATURE = 3;

    public static final  String TYPE_PIV_AUTH       = "WindowsLogon";
    public static final  String TYPE_DIGITAL_SIG    = "MailCertificate";
    public static final  String TYPE_TUNNEL = "Tunnel";
/*
 * -----  End of Section  -----
 * Copyright (c) 2016 Route1 Inc.  All rights reserved.  <sales@route1.com>
 */

    public static final int X509_VERIFY_TLSREMOTE = 0;
    public static final int X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING = 1;
    public static final int X509_VERIFY_TLSREMOTE_DN = 2;
    public static final int X509_VERIFY_TLSREMOTE_RDN = 3;
    public static final int X509_VERIFY_TLSREMOTE_RDN_PREFIX = 4;
    // variable named wrong and should haven beeen transient
    // but needs to keep wrong name to guarante loading of old
    // profiles
    public transient boolean profileDeleted = false;

    //public int mAuthenticationType = TYPE_KEYSTORE;
 /*
     * This section is a derivative work from the original project.
     *
     * -----  Beginning of Section  -----
     *
     * Copyright (c) 2016 Route1 Inc.  All rights reserved.  <sales@route1.com>
     *
     * You may use, distribute and modify this code under the
     * terms of the GNU General Public License version 2
     * as published by the Free Software Foundation.

     * Contributor:  A. M. Jehad Sarkar  <amjehad.sarkar@route1.com>
     */
    public int mAuthenticationType = TYPE_DERIVID_CERTIFICATES;
    public int mDCType = TYPEDC_UNDEFINED;
/*
 * -----  End of Section  -----
 * Copyright (c) 2016 Route1 Inc.  All rights reserved.  <sales@route1.com>
 */

    public String mName;
    public String mAlias;
    public String mClientCertFilename;
    public String mTLSAuthDirection = "";
    public String mTLSAuthFilename;
    public String mClientKeyFilename;
    public String mCaFilename;
    public boolean mUseLzo = true;
    public String mPKCS12Filename;
    public String mPKCS12Password;
    public boolean mUseTLSAuth = false;

    public String mDNS1 = DEFAULT_DNS1;
    public String mDNS2 = DEFAULT_DNS2;
    public String mIPv4Address;
    public String mIPv6Address;
    public boolean mOverrideDNS = false;
    public String mSearchDomain = "blinkt.de";
    public boolean mUseDefaultRoute = true;
    public boolean mUsePull = true;
    public String mCustomRoutes;
    public boolean mCheckRemoteCN = true;
    public boolean mExpectTLSCert = false;
    public String mRemoteCN = "";
    public String mPassword = "";
    public String mUsername = "";
    public boolean mRoutenopull = false;
    public boolean mUseRandomHostname = false;
    public boolean mUseFloat = false;
    public boolean mUseCustomConfig = false;
    public String mCustomConfigOptions = "";
    public String mVerb = "1";  //ignored
    public String mCipher = "";
    public boolean mNobind = false;
    public boolean mUseDefaultRoutev6 = true;
    public String mCustomRoutesv6 = "";
    public String mKeyPassword = "";
    public boolean mPersistTun = false;
    public String mConnectRetryMax = "-1";
    public String mConnectRetry = "2";
    public String mConnectRetryMaxTime = "300";
    public boolean mUserEditable = true;
    public String mAuth = "";
    public int mX509AuthType = X509_VERIFY_TLSREMOTE_RDN;
    public String mx509UsernameField = null;

    private transient PrivateKey mPrivateKey;
    // Public attributes, since I got mad with getter/setter
    // set members to default values
    private UUID mUuid;
    public boolean mAllowLocalLAN;
    private int mProfileVersion;
    public String mExcludedRoutes;
    public String mExcludedRoutesv6;
    public int mMssFix = 0; // -1 is default,
    public Connection[] mConnections = new Connection[0];
    public boolean mRemoteRandom = false;
    public HashSet<String> mAllowedAppsVpn = new HashSet<>();
    public boolean mAllowedAppsVpnAreDisallowed = true;

    public String mCrlFilename;
    public String mProfileCreator;


    public boolean mPushPeerInfo = false;
    public static final boolean mIsOpenVPN22 = false;

    /* Options no longer used in new profiles */
    public String mServerName = "openvpn.blinkt.de";
    public String mServerPort = "1194";
    public boolean mUseUdp = true;

    /*
     * This section is a derivative work from the original project.
     *
     * -----  Beginning of Section  -----
     *
     * Copyright (c) 2016 Route1 Inc.  All rights reserved.  <sales@route1.com>
     *
     * You may use, distribute and modify this code under the
     * terms of the GNU General Public License version 2
     * as published by the Free Software Foundation.

     * Contributor:  A. M. Jehad Sarkar  <amjehad.sarkar@route1.com>
     */
    private ArrayList<X509Certificate> mCertificates;
    /*
     * -----  End of Section  -----
     * Copyright (c) 2016 Route1 Inc.  All rights reserved.  <sales@route1.com>
     */

    public VpnProfile(String name) {
        mUuid = UUID.randomUUID();
        mName = name;
        mProfileVersion = CURRENT_PROFILE_VERSION;

        mConnections = new Connection[1];
        mConnections[0] = new Connection();
    }

    public static String openVpnEscape(String unescaped) {
        if (unescaped == null)
            return null;
        String escapedString = unescaped.replace("\\", "\\\\");
        escapedString = escapedString.replace("\"", "\\\"");
        escapedString = escapedString.replace("\n", "\\n");

        if (escapedString.equals(unescaped) && !escapedString.contains(" ") &&
                !escapedString.contains("#") && !escapedString.contains(";")
                && !escapedString.equals(""))
            return unescaped;
        else
            return '"' + escapedString + '"';
    }

    public void clearDefaults() {
        mServerName = "unknown";
        mUsePull = false;
        mUseLzo = false;
        mUseDefaultRoute = false;
        mUseDefaultRoutev6 = false;
        mExpectTLSCert = false;
        mCheckRemoteCN = false;
        mPersistTun = false;
        mAllowLocalLAN = true;
        mPushPeerInfo = false;
        mMssFix = 0;
    }
    public void setUUID(UUID uuid)
    {
        mUuid = uuid;
    }
    public UUID getUUID() {
        return mUuid;

    }

    public String getName() {
        if (mName == null)
            return "No profile name";
        return mName;
    }

    public void upgradeProfile() {
        if (mProfileVersion < 2) {
            /* default to the behaviour the OS used */
            mAllowLocalLAN = Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT;
        }

        if (mProfileVersion < 4) {
            moveOptionsToConnection();
            mAllowedAppsVpnAreDisallowed = true;
        }
        if (mAllowedAppsVpn == null)
            mAllowedAppsVpn = new HashSet<>();
        if (mConnections == null)
            mConnections = new Connection[0];

        if (mProfileVersion < 6) {
            if (TextUtils.isEmpty(mProfileCreator))
                mUserEditable = true;
        }


        mProfileVersion = CURRENT_PROFILE_VERSION;

    }

    private void moveOptionsToConnection() {
        mConnections = new Connection[1];
        Connection conn = new Connection();

        conn.mServerName = mServerName;
        conn.mServerPort = mServerPort;
        conn.mUseUdp = mUseUdp;
        conn.mCustomConfiguration = "";

        mConnections[0] = conn;

    }

    public String getConfigFile(Context context, boolean configForOvpn3) {

        File cacheDir = context.getCacheDir();
        String cfg = "";

        // Enable management interface
        cfg += "# Enables connection to GUI\n";
        cfg += "management ";

        cfg += cacheDir.getAbsolutePath() + "/" + "mgmtsocket";
        cfg += " unix\n";
        cfg += "management-client\n";
        // Not needed, see updated man page in 2.3
        //cfg += "management-signal\n";
        cfg += "management-query-passwords\n";
        cfg += "management-hold\n\n";

        if (!configForOvpn3) {
            cfg += String.format("setenv IV_GUI_VER %s \n", openVpnEscape(getVersionEnvString(context)));
            String versionString = String.format(Locale.US, "%d %s %s %s %s %s", Build.VERSION.SDK_INT, Build.VERSION.RELEASE,
                    NativeUtils.getNativeAPI(), Build.BRAND, Build.BOARD, Build.MODEL);
            cfg += String.format("setenv IV_PLAT_VER %s\n", openVpnEscape(versionString));
        }

        cfg += "machine-readable-output\n";

        // Users are confused by warnings that are misleading...
        cfg += "ifconfig-nowarn\n";


        boolean useTLSClient = (mAuthenticationType != TYPE_STATICKEYS); //Route1 Stuffs TODO: How about TYPE_DERIVID_CERTIFICATES?

        if (useTLSClient && mUsePull)
            cfg += "client\n";
        else if (mUsePull)
            cfg += "pull\n";
        else if (useTLSClient)
            cfg += "tls-client\n";


        //cfg += "verb " + mVerb + "\n";
        cfg += "verb " + MAXLOGLEVEL + "\n";

        if (mConnectRetryMax == null) {
            mConnectRetryMax = "-1";
        }

        if (!mConnectRetryMax.equals("-1"))
            cfg += "connect-retry-max " + mConnectRetryMax + "\n";

        if (TextUtils.isEmpty(mConnectRetry))
            mConnectRetry = "2";

        if (TextUtils.isEmpty(mConnectRetryMaxTime))
            mConnectRetryMaxTime="300";


        if (!mIsOpenVPN22)
            cfg += "connect-retry " + mConnectRetry + " " + mConnectRetryMaxTime + "\n";
        else if (mIsOpenVPN22 && mUseUdp)
            cfg += "connect-retry " + mConnectRetry + "\n";


        cfg += "resolv-retry 60\n";


        // We cannot use anything else than tun
        cfg += "dev tun\n";


        boolean canUsePlainRemotes = true;

        if (mConnections.length == 1) {
            cfg += mConnections[0].getConnectionBlock();
        } else {
            for (Connection conn : mConnections) {
                canUsePlainRemotes = canUsePlainRemotes && conn.isOnlyRemote();
            }

            if (mRemoteRandom)
                cfg += "remote-random\n";

            if (canUsePlainRemotes) {
                for (Connection conn : mConnections) {
                    if (conn.mEnabled) {
                        cfg += conn.getConnectionBlock();
                    }
                }
            }
        }


        switch (mAuthenticationType) {
/*
 * This section is a derivative work from the original project.
 *
 * -----  Beginning of Section  -----
 *
 * Copyright (c) 2016 Route1 Inc.  All rights reserved.  <sales@route1.com>
 *
 * You may use, distribute and modify this code under the
 * terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.

 * Contributor:  A. M. Jehad Sarkar  <amjehad.sarkar@route1.com>
 */
            case VpnProfile.TYPE_DERIVID_CERTIFICATES:
                if (!configForOvpn3) {
                    String[] ks = getDerivedCertificates(context);
                    cfg += "### From Keystore ####\n";
                    if (ks != null) {
                        cfg += "<ca>\n" + ks[0] + "\n</ca>\n";
                        if (ks[1] != null)
                            cfg += "<extra-certs>\n" + ks[1] + "\n</extra-certs>\n";
                        cfg += "<cert>\n" + ks[2] + "\n</cert>\n";
                        cfg += "management-external-key\n";
                    } else {
                        cfg += context.getString(R.string.keychain_access) + "\n";
                        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN)
                            if (!mAlias.matches("^[a-zA-Z0-9]$"))
                                cfg += context.getString(R.string.jelly_keystore_alphanumeric_bug) + "\n";
                    }
                }
                break;
/*
 * -----  End of Section  -----
 * Copyright (c) 2016 Route1 Inc.  All rights reserved.  <sales@route1.com>
 */


            case VpnProfile.TYPE_USERPASS_CERTIFICATES:
                cfg += "auth-user-pass\n";
            case VpnProfile.TYPE_CERTIFICATES:
                // Ca
                cfg += insertFileData("ca", mCaFilename);

                // Client Cert + Key
                cfg += insertFileData("key", mClientKeyFilename);
                cfg += insertFileData("cert", mClientCertFilename);

                break;
            case VpnProfile.TYPE_USERPASS_PKCS12:
                cfg += "auth-user-pass\n";
            case VpnProfile.TYPE_PKCS12:
                cfg += insertFileData("pkcs12", mPKCS12Filename);
                break;

            case VpnProfile.TYPE_USERPASS_KEYSTORE:
                cfg += "auth-user-pass\n";
            case VpnProfile.TYPE_KEYSTORE:
                if (!configForOvpn3) {
                    String[] ks = getKeyStoreCertificates(context);
                    cfg += "### From Keystore ####\n";
                    if (ks != null) {
                        cfg += "<ca>\n" + ks[0] + "\n</ca>\n";
                        if (ks[1] != null)
                            cfg += "<extra-certs>\n" + ks[1] + "\n</extra-certs>\n";
                        cfg += "<cert>\n" + ks[2] + "\n</cert>\n";
                        cfg += "management-external-key\n";
                    } else {
                        cfg += context.getString(R.string.keychain_access) + "\n";
                        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN)
                            if (!mAlias.matches("^[a-zA-Z0-9]$"))
                                cfg += context.getString(R.string.jelly_keystore_alphanumeric_bug) + "\n";
                    }
                }
                break;
            case VpnProfile.TYPE_USERPASS:
                cfg += "auth-user-pass\n";
/*
 * This section is a derivative work from the original project.
 *
 * -----  Beginning of Section  -----
 *
 * Copyright (c) 2016 Route1 Inc.  All rights reserved.  <sales@route1.com>
 *
 * You may use, distribute and modify this code under the
 * terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.

 * Contributor:  A. M. Jehad Sarkar  <amjehad.sarkar@route1.com>
 */
                if(mProfileCreator.equalsIgnoreCase("Route1")) {
                    if (!configForOvpn3) {
                        String[] ks = getDerivedCertificates(context);
                        cfg += "### From Keystore ####\n";
                        if (ks != null) {
                            cfg += "<ca>\n" + ks[0] + "\n</ca>\n";
                            if (ks[1] != null)
                                cfg += "<extra-certs>\n" + ks[1] + "\n</extra-certs>\n";
                            cfg += "<cert>\n" + ks[2] + "\n</cert>\n";
                            cfg += "management-external-key\n";
                        } else {
                            cfg += context.getString(R.string.keychain_access) + "\n";
                            if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN)
                                if (!mAlias.matches("^[a-zA-Z0-9]$"))
                                    cfg += context.getString(R.string.jelly_keystore_alphanumeric_bug) + "\n";
                        }
                    }
                }
                else
/*
 * -----  End of Section  -----
 * Copyright (c) 2016 Route1 Inc.  All rights reserved.  <sales@route1.com>
 */

                    cfg += insertFileData("ca", mCaFilename);
        }

        if (!TextUtils.isEmpty(mCrlFilename))
            cfg += insertFileData("crl-verify", mCrlFilename);

        if (mUseLzo) {
            cfg += "comp-lzo\n";
        }

        if (mUseTLSAuth) {
            if (mAuthenticationType == TYPE_STATICKEYS)
                cfg += insertFileData("secret", mTLSAuthFilename);
            else //Route1 Stuffs TODO: How about TYPE_DERIVID_CERTIFICATES?
            {
                cfg += insertFileData("tls-auth", mTLSAuthFilename);
            }

            if (!TextUtils.isEmpty(mTLSAuthDirection)) {
                cfg += "key-direction ";
                cfg += mTLSAuthDirection;
                cfg += "\n";
            }

        }

        if (!mUsePull) {
            if (!TextUtils.isEmpty(mIPv4Address))
                cfg += "ifconfig " + cidrToIPAndNetmask(mIPv4Address) + "\n";

            if (!TextUtils.isEmpty(mIPv6Address))
                cfg += "ifconfig-ipv6 " + mIPv6Address + "\n";
        }

        if (mUsePull && mRoutenopull)
            cfg += "route-nopull\n";

        String routes = "";

        if (mUseDefaultRoute)
            routes += "route 0.0.0.0 0.0.0.0 vpn_gateway\n";
        else {
            for (String route : getCustomRoutes(mCustomRoutes)) {
                routes += "route " + route + " vpn_gateway\n";
            }

            for (String route : getCustomRoutes(mExcludedRoutes)) {
                routes += "route " + route + " net_gateway\n";
            }
        }


        if (mUseDefaultRoutev6)
            cfg += "route-ipv6 ::/0\n";
        else
            for (String route : getCustomRoutesv6(mCustomRoutesv6)) {
                routes += "route-ipv6 " + route + "\n";
            }

        cfg += routes;

        if (mOverrideDNS || !mUsePull) {
            if (!TextUtils.isEmpty(mDNS1))
                cfg += "dhcp-option DNS " + mDNS1 + "\n";
            if (!TextUtils.isEmpty(mDNS2))
                cfg += "dhcp-option DNS " + mDNS2 + "\n";
            if (!TextUtils.isEmpty(mSearchDomain))
                cfg += "dhcp-option DOMAIN " + mSearchDomain + "\n";

        }

        if (mMssFix != 0) {
            if (mMssFix != 1450) {
                cfg += String.format(Locale.US, "mssfix %d\n", mMssFix);
            } else
                cfg += "mssfix\n";
        }

        if (mNobind)
            cfg += "nobind\n";


        // Authentication
        if (mAuthenticationType != TYPE_STATICKEYS) { // TODO: How about TYPE_DERIVID_CERTIFICATES?
            if (mCheckRemoteCN) {
                if (mRemoteCN == null || mRemoteCN.equals(""))
                    cfg += "verify-x509-name " + openVpnEscape(mConnections[0].mServerName) + " name\n";
                else
                    switch (mX509AuthType) {

                        // 2.2 style x509 checks
                        case X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING:
                            cfg += "compat-names no-remapping\n";
                        case X509_VERIFY_TLSREMOTE:
                            cfg += "tls-remote " + openVpnEscape(mRemoteCN) + "\n";
                            break;

                        case X509_VERIFY_TLSREMOTE_RDN:
                            cfg += "verify-x509-name " + openVpnEscape(mRemoteCN) + " name\n";
                            break;

                        case X509_VERIFY_TLSREMOTE_RDN_PREFIX:
                            cfg += "verify-x509-name " + openVpnEscape(mRemoteCN) + " name-prefix\n";
                            break;

                        case X509_VERIFY_TLSREMOTE_DN:
                            cfg += "verify-x509-name " + openVpnEscape(mRemoteCN) + "\n";
                            break;
                    }
                if (!TextUtils.isEmpty(mx509UsernameField))
                    cfg+= "x509-username-field " + openVpnEscape(mx509UsernameField) +"\n";
            }
            if (mExpectTLSCert)
                cfg += "remote-cert-tls server\n";
        }

        if (!TextUtils.isEmpty(mCipher)) {
            cfg += "cipher " + mCipher + "\n";
        }

        if (!TextUtils.isEmpty(mAuth)) {
            cfg += "auth " + mAuth + "\n";
        }

        // Obscure Settings dialog
        if (mUseRandomHostname)
            cfg += "#my favorite options :)\nremote-random-hostname\n";

        if (mUseFloat)
            cfg += "float\n";

        if (mPersistTun) {
            cfg += "persist-tun\n";
            cfg += "# persist-tun also enables pre resolving to avoid DNS resolve problem\n";
            cfg += "preresolve\n";
        }

        if (mPushPeerInfo)
            cfg += "push-peer-info\n";

        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        boolean usesystemproxy = prefs.getBoolean("usesystemproxy", true);
        if (usesystemproxy && !mIsOpenVPN22) {
            cfg += "# Use system proxy setting\n";
            cfg += "management-query-proxy\n";
        }


        if (mUseCustomConfig) {
            cfg += "# Custom configuration options\n";
            cfg += "# You are on your on own here :)\n";
            cfg += mCustomConfigOptions;
            cfg += "\n";

        }

        if (!canUsePlainRemotes) {
            cfg += "# Connection Options are at the end to allow global options (and global custom options) to influence connection blocks\n";
            for (Connection conn : mConnections) {
                if (conn.mEnabled) {
                    cfg += "<connection>\n";
                    cfg += conn.getConnectionBlock();
                    cfg += "</connection>\n";
                }
            }
        }
        return cfg;
    }

    public String getVersionEnvString(Context c) {
        String version = "unknown";
        try {
            PackageInfo packageinfo = c.getPackageManager().getPackageInfo(c.getPackageName(), 0);
            version = packageinfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            VpnStatus.logException(e);
        }
        return String.format(Locale.US, "%s %s", c.getPackageName(), version);

    }

    //! Put inline data inline and other data as normal escaped filename
    public static String insertFileData(String cfgentry, String filedata) {
        if (filedata == null) {
            return String.format("%s %s\n", cfgentry, "file missing in config profile");
        } else if (isEmbedded(filedata)) {
            String dataWithOutHeader = getEmbeddedContent(filedata);
            return String.format(Locale.ENGLISH, "<%s>\n%s\n</%s>\n", cfgentry, dataWithOutHeader, cfgentry);
        } else {
            return String.format(Locale.ENGLISH, "%s %s\n", cfgentry, openVpnEscape(filedata));
        }
    }

    @NonNull
    private Collection<String> getCustomRoutes(String routes) {
        Vector<String> cidrRoutes = new Vector<>();
        if (routes == null) {
            // No routes set, return empty vector
            return cidrRoutes;
        }
        for (String route : routes.split("[\n \t]")) {
            if (!route.equals("")) {
                String cidrroute = cidrToIPAndNetmask(route);
                if (cidrroute == null)
                    return cidrRoutes;

                cidrRoutes.add(cidrroute);
            }
        }

        return cidrRoutes;
    }

    private Collection<String> getCustomRoutesv6(String routes) {
        Vector<String> cidrRoutes = new Vector<>();
        if (routes == null) {
            // No routes set, return empty vector
            return cidrRoutes;
        }
        for (String route : routes.split("[\n \t]")) {
            if (!route.equals("")) {
                cidrRoutes.add(route);
            }
        }

        return cidrRoutes;
    }

    private String cidrToIPAndNetmask(String route) {
        String[] parts = route.split("/");

        // No /xx, assume /32 as netmask
        if (parts.length == 1)
            parts = (route + "/32").split("/");

        if (parts.length != 2)
            return null;
        int len;
        try {
            len = Integer.parseInt(parts[1]);
        } catch (NumberFormatException ne) {
            return null;
        }
        if (len < 0 || len > 32)
            return null;


        long nm = 0xffffffffL;
        nm = (nm << (32 - len)) & 0xffffffffL;

        String netmask = String.format(Locale.ENGLISH, "%d.%d.%d.%d", (nm & 0xff000000) >> 24, (nm & 0xff0000) >> 16, (nm & 0xff00) >> 8, nm & 0xff);
        return parts[0] + "  " + netmask;
    }


    public Intent prepareStartService(Context context) {
        Intent intent = getStartServiceIntent(context);

        // TODO: Handle this?!
//        if (mAuthenticationType == VpnProfile.TYPE_KEYSTORE || mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE) {
//            if (getKeyStoreCertificates(context) == null)
//                return null;
//        } // TODO: How about TYPE_DERIVID_CERTIFICATES?

        return intent;
    }

    public void writeConfigFile(Context context) throws IOException {
        FileWriter cfg = new FileWriter(VPNLaunchHelper.getConfigFilePath(context));
        cfg.write(getConfigFile(context, false));
        cfg.flush();
        cfg.close();

    }

    public Intent getStartServiceIntent(Context context) {
        String prefix = context.getPackageName();

        Intent intent = new Intent(context, OpenVPNService.class);
        intent.putExtra(prefix + ".profileUUID", mUuid.toString());

        return intent;
    }

    public String[] getKeyStoreCertificates(Context context) {
        return getKeyStoreCertificates(context, 5);
    }
/*
 * This section is a derivative work from the original project.
 *
 * -----  Beginning of Section  -----
 *
 * Copyright (c) 2016 Route1 Inc.  All rights reserved.  <sales@route1.com>
 *
 * You may use, distribute and modify this code under the
 * terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.

 * Contributor:  A. M. Jehad Sarkar  <amjehad.sarkar@route1.com>
 */

    public String[] getDerivedCertificates(Context context) {
        return getDerivedCertificates(context, 5);
    }
    public void setDerivedCertificates(ArrayList<X509Certificate> aCertificates) {
        this.mCertificates = aCertificates;
    }
    public PrivateKey getPrivateKey() {
        return mPrivateKey;
    }
    public void setPrivateKey(PrivateKey aPrivateKey) {
        mPrivateKey = aPrivateKey;
    }
/*
 * -----  End of Section  -----
 * Copyright (c) 2016 Route1 Inc.  All rights reserved.  <sales@route1.com>
 */

    public static String getDisplayName(String embeddedFile) {
        int start = DISPLAYNAME_TAG.length();
        int end = embeddedFile.indexOf(INLINE_TAG);
        return embeddedFile.substring(start, end);
    }

    public static String getEmbeddedContent(String data) {
        if (!data.contains(INLINE_TAG))
            return data;

        int start = data.indexOf(INLINE_TAG) + INLINE_TAG.length();
        return data.substring(start);
    }

    public static boolean isEmbedded(String data) {
        if (data == null)
            return false;
        if (data.startsWith(INLINE_TAG) || data.startsWith(DISPLAYNAME_TAG))
            return true;
        else
            return false;
    }

    public void checkForRestart(final Context context) {
        /* This method is called when OpenVPNService is restarted */

        if ((mAuthenticationType == VpnProfile.TYPE_KEYSTORE || mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE)
                && mPrivateKey == null) { // TODO: How about TYPE_DERIVID_CERTIFICATES?
            new Thread(new Runnable() {
                @Override
                public void run() {
                    getKeyStoreCertificates(context);

                }
            }).start();
        }
    }

    @Override
    protected VpnProfile clone() throws CloneNotSupportedException {
        VpnProfile copy = (VpnProfile) super.clone();
        copy.mUuid = UUID.randomUUID();
        copy.mConnections = new Connection[mConnections.length];
        int i = 0;
        for (Connection conn : mConnections) {
            copy.mConnections[i++] = conn.clone();
        }
        copy.mAllowedAppsVpn = (HashSet<String>) mAllowedAppsVpn.clone();
        return copy;
    }

    public VpnProfile copy(String name) {
        try {
            VpnProfile copy = clone();
            copy.mName = name;
            return copy;

        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }


    class NoCertReturnedException extends Exception {
        public NoCertReturnedException(String msg) {
            super(msg);
        }
    }

/*
 * This section is a derivative work from the original project.
 *
 * -----  Beginning of Section  -----
 *
 * Copyright (c) 2016 Route1 Inc.  All rights reserved.  <sales@route1.com>
 *
 * You may use, distribute and modify this code under the
 * terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.

 * Contributor:  A. M. Jehad Sarkar  <amjehad.sarkar@route1.com>
 */

    synchronized String[] getDerivedCertificates(Context context, int tries) {
        // Force application context- KeyChain methods will block long enough that by the time they
        // are finished and try to unbind, the original activity context might have been destroyed.
        context = context.getApplicationContext();
        try {
            String keystoreChain = null;
            if (mCertificates == null) {
                VpnStatus.logError(R.string.keychain_access);
                throw new NoCertReturnedException("No derived credential found");
            }
            if (mCertificates.isEmpty()) {
                VpnStatus.logError(R.string.keychain_access);
                throw new NoCertReturnedException("No derived credential found");
            }

            if (mCertificates.size() <= 1) {
                VpnStatus.logMessage(VpnStatus.LogLevel.ERROR, "", context.getString(R.string.DerivID_nocacert));
            } else {
                StringWriter ksStringWriter = new StringWriter();

                PemWriter pw = new PemWriter(ksStringWriter);
                for (int i = 1; i < mCertificates.size(); i++) {
                    X509Certificate cert = (X509Certificate)mCertificates.get(i);
                    pw.writeObject(new PemObject("CERTIFICATE", cert.getEncoded()));
                }
                pw.close();
                keystoreChain = ksStringWriter.toString();
            }
            String caout = null;

            if (!TextUtils.isEmpty(mCaFilename)) {
                try {

                    Certificate[] cacerts = X509Utils.readTrustFile(mCaFilename);//X509Utils.getCertificatesFromFile(mCaFilename);
                    StringWriter caoutWriter = new StringWriter();
                    PemWriter pw = new PemWriter(caoutWriter);

                    for (Certificate cert : cacerts)
                        pw.writeObject(new PemObject("CERTIFICATE", cert.getEncoded()));
                    pw.close();
                    caout = caoutWriter.toString();
                } catch (Exception e) {
                    VpnStatus.logError("Could not read CA certificate" + e.getLocalizedMessage());
                }
            }
            StringWriter certout = new StringWriter();
          if (mCertificates.size() >= 1) {
                X509Certificate usercert = (X509Certificate) mCertificates.get(0);
/*
 * This section is a derivative work from the original project.
 *
 * -----  Beginning of Section  -----
 *
 * Copyright (c) 2016 Route1 Inc.  All rights reserved.  <sales@route1.com>
 *
 * You may use, distribute and modify this code under the
 * terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.

 * Contributor:  A. M. Jehad Sarkar  <amjehad.sarkar@route1.com>
 */

              mUsername = "********";//Dummy for certificate based authentication
              mPassword = "********";
/*
 * -----  End of Section  -----
 * Copyright (c) 2016 Route1 Inc.  All rights reserved.  <sales@route1.com>
 */



              PemWriter upw = new PemWriter(certout);
                upw.writeObject(new PemObject("CERTIFICATE", usercert.getEncoded()));
                upw.close();
            }
            String user = certout.toString();
            String ca, extra;
            if (caout == null) {
                ca = keystoreChain;
                extra = null;
            } else {
                ca = caout;
                extra = keystoreChain;
            }

            return new String[]{ca, extra, user};
        } catch (IOException | NoCertReturnedException | IllegalArgumentException
                | CertificateException e) {
            e.printStackTrace();
            VpnStatus.logError(R.string.keyChainAccessError, e.getLocalizedMessage());

            VpnStatus.logError(R.string.keychain_access);
            if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) {
                if (!mAlias.matches("^[a-zA-Z0-9]$")) {
                    VpnStatus.logError(R.string.jelly_keystore_alphanumeric_bug);
                }
            }
            return null;

        } catch (AssertionError e) {
            if (tries == 0)
                return null;
            VpnStatus.logError(String.format("Failure getting Keystore Keys (%s), retrying", e.getLocalizedMessage()));
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e1) {
                VpnStatus.logException(e1);
            }
            return getKeyStoreCertificates(context, tries - 1);
        }
    }

    public String getSubjectAlternativeName(X509Certificate usercert) {
        String san = null;
        try {
            if (usercert != null) {
                Collection<List<?>> altNames = usercert.getSubjectAlternativeNames();
                if (altNames != null) {
                    for (List item : altNames) {
                        Integer type = (Integer) item.get(0);
                        if (type == 1) {
                            if (item.toArray()[1] instanceof String) {
                                san = (String) item.toArray()[1];
                                san = san.substring(0, san.indexOf("@"));
                                break;
                            }
                        } else {
                            VpnStatus.logDebug("SubjectAltName of invalid type found: " + usercert);
                        }
                    }
                }
            }
        }
        catch (CertificateParsingException e) {
            VpnStatus.logError("Error parsing SubjectAltName in certificate -- error:" + e.getLocalizedMessage());
        }
        return san;
    }
/*
 * -----  End of Section  -----
 * Copyright (c) 2016 Route1 Inc.  All rights reserved.  <sales@route1.com>
 */

    synchronized String[] getKeyStoreCertificates(Context context, int tries) {
        // Force application context- KeyChain methods will block long enough that by the time they
        // are finished and try to unbind, the original activity context might have been destroyed.
        context = context.getApplicationContext();

        try {
            PrivateKey privateKey = KeyChain.getPrivateKey(context, mAlias);
            mPrivateKey = privateKey;
            String keystoreChain = null;


            X509Certificate[] caChain = KeyChain.getCertificateChain(context, mAlias);
            if (caChain == null)
                throw new NoCertReturnedException("No certificate returned from Keystore");

            if (caChain.length <= 1 && TextUtils.isEmpty(mCaFilename)) {
                VpnStatus.logMessage(VpnStatus.LogLevel.ERROR, "", context.getString(R.string.keychain_nocacert));
            } else {
                StringWriter ksStringWriter = new StringWriter();

                PemWriter pw = new PemWriter(ksStringWriter);
                for (int i = 1; i < caChain.length; i++) {
                    X509Certificate cert = caChain[i];
                    pw.writeObject(new PemObject("CERTIFICATE", cert.getEncoded()));
                }
                pw.close();
                keystoreChain = ksStringWriter.toString();
            }


            String caout = null;
            if (!TextUtils.isEmpty(mCaFilename)) {
                try {
                    Certificate[] cacerts = X509Utils.getCertificatesFromFile(mCaFilename);
                    StringWriter caoutWriter = new StringWriter();
                    PemWriter pw = new PemWriter(caoutWriter);

                    for (Certificate cert : cacerts)
                        pw.writeObject(new PemObject("CERTIFICATE", cert.getEncoded()));
                    pw.close();
                    caout = caoutWriter.toString();

                } catch (Exception e) {
                    VpnStatus.logError("Could not read CA certificate" + e.getLocalizedMessage());
                }
            }


            StringWriter certout = new StringWriter();


            if (caChain.length >= 1) {
                X509Certificate usercert = caChain[0];
                PemWriter upw = new PemWriter(certout);
                upw.writeObject(new PemObject("CERTIFICATE", usercert.getEncoded()));
                upw.close();

            }
            String user = certout.toString();


            String ca, extra;
            if (caout == null) {
                ca = keystoreChain;
                extra = null;
            } else {
                ca = caout;
                extra = keystoreChain;
            }

            return new String[]{ca, extra, user};
        } catch (InterruptedException | IOException | KeyChainException | NoCertReturnedException | IllegalArgumentException
                | CertificateException e) {
            e.printStackTrace();
            VpnStatus.logError(R.string.keyChainAccessError, e.getLocalizedMessage());

            VpnStatus.logError(R.string.keychain_access);
            if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) {
                if (!mAlias.matches("^[a-zA-Z0-9]$")) {
                    VpnStatus.logError(R.string.jelly_keystore_alphanumeric_bug);
                }
            }
            return null;

        } catch (AssertionError e) {
            if (tries == 0)
                return null;
            VpnStatus.logError(String.format("Failure getting Keystore Keys (%s), retrying", e.getLocalizedMessage()));
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e1) {
                VpnStatus.logException(e1);
            }
            return getKeyStoreCertificates(context, tries - 1);
        }

    }

    //! Return an error if something is wrong
    public int checkProfile(Context context) {
        if (mAuthenticationType == TYPE_KEYSTORE || mAuthenticationType == TYPE_USERPASS_KEYSTORE) {
            if (mAlias == null)
                return R.string.no_keystore_cert_selected;
        } // TODO: How about TYPE_DERIVID_CERTIFICATES?

        if (!mUsePull || mAuthenticationType == TYPE_STATICKEYS) {
            if (mIPv4Address == null || cidrToIPAndNetmask(mIPv4Address) == null)
                return R.string.ipv4_format_error;
        } // TODO: How about TYPE_DERIVID_CERTIFICATES?
        if (!mUseDefaultRoute) {
            if (!TextUtils.isEmpty(mCustomRoutes) && getCustomRoutes(mCustomRoutes).size() == 0)
                return R.string.custom_route_format_error;

            if (!TextUtils.isEmpty(mExcludedRoutes) && getCustomRoutes(mExcludedRoutes).size() == 0)
                return R.string.custom_route_format_error;

        }

        if (mUseTLSAuth && TextUtils.isEmpty(mTLSAuthFilename))
            return R.string.missing_tlsauth;

        if ((mAuthenticationType == TYPE_USERPASS_CERTIFICATES || mAuthenticationType == TYPE_CERTIFICATES)
                && (TextUtils.isEmpty(mClientCertFilename) || TextUtils.isEmpty(mClientKeyFilename)))
                // TODO: How about TYPE_DERIVID_CERTIFICATES?
            return R.string.missing_certificates;

        if ((mAuthenticationType == TYPE_CERTIFICATES || mAuthenticationType == TYPE_USERPASS_CERTIFICATES)
                && TextUtils.isEmpty(mCaFilename))
                // TODO: How about TYPE_DERIVID_CERTIFICATES?
            return R.string.missing_ca_certificate;


        boolean noRemoteEnabled = true;
        for (Connection c : mConnections)
            if (c.mEnabled)
                noRemoteEnabled = false;

        if (noRemoteEnabled)
            return R.string.remote_no_server_selected;

        // Everything okay
        return R.string.no_error_found;

    }

    //! Openvpn asks for a "Private Key", this should be pkcs12 key
    //
    public String getPasswordPrivateKey() {
        if (mTransientPCKS12PW != null) {
            String pwcopy = mTransientPCKS12PW;
            mTransientPCKS12PW = null;
            return pwcopy;
        }
        switch (mAuthenticationType) {
            case TYPE_PKCS12:
            case TYPE_USERPASS_PKCS12:
                return mPKCS12Password;

            case TYPE_CERTIFICATES:
            case TYPE_USERPASS_CERTIFICATES:
                return mKeyPassword;

            case TYPE_USERPASS:
            case TYPE_STATICKEYS:
            default: // TODO: How about TYPE_DERIVID_CERTIFICATES?
                return null;
        }
    }

    public boolean isUserPWAuth() {
        switch (mAuthenticationType) {
            case TYPE_USERPASS:
            case TYPE_USERPASS_CERTIFICATES:
            case TYPE_USERPASS_KEYSTORE:
            case TYPE_USERPASS_PKCS12:
                return true;
            default: // TODO: How about TYPE_DERIVID_CERTIFICATES?
                return false;

        }
    }

    public boolean requireTLSKeyPassword() {
        if (TextUtils.isEmpty(mClientKeyFilename))
            return false;

        String data = "";
        if (isEmbedded(mClientKeyFilename))
            data = mClientKeyFilename;
        else {
            char[] buf = new char[2048];
            FileReader fr;
            try {
                fr = new FileReader(mClientKeyFilename);
                int len = fr.read(buf);
                while (len > 0) {
                    data += new String(buf, 0, len);
                    len = fr.read(buf);
                }
                fr.close();
            } catch (FileNotFoundException e) {
                return false;
            } catch (IOException e) {
                return false;
            }

        }

        if (data.contains("Proc-Type: 4,ENCRYPTED"))
            return true;
        else if (data.contains("-----BEGIN ENCRYPTED PRIVATE KEY-----"))
            return true;
        else
            return false;
    }

    public int needUserPWInput(boolean ignoreTransient) {
        if ((mAuthenticationType == TYPE_PKCS12 || mAuthenticationType == TYPE_USERPASS_PKCS12) &&
                // TODO: How about TYPE_DERIVID_CERTIFICATES?
                (mPKCS12Password == null || mPKCS12Password.equals(""))) {
            if (ignoreTransient || mTransientPCKS12PW == null)
                return R.string.pkcs12_file_encryption_key;
        }

        if (mAuthenticationType == TYPE_CERTIFICATES || mAuthenticationType == TYPE_USERPASS_CERTIFICATES) {
            if (requireTLSKeyPassword() && TextUtils.isEmpty(mKeyPassword))
                if (ignoreTransient || mTransientPCKS12PW == null) {
                    return R.string.private_key_password;
                }
        } // TODO: How about TYPE_DERIVID_CERTIFICATES?

        if (isUserPWAuth() &&
                (TextUtils.isEmpty(mUsername) ||
                        (TextUtils.isEmpty(mPassword) && (mTransientPW == null || ignoreTransient)))) {
            return R.string.password;
        }
        return 0;
    }

    public String getPasswordAuth() {
        if (mTransientPW != null) {
            String pwcopy = mTransientPW;
            mTransientPW = null;
            return pwcopy;
        } else {
            return mPassword;
        }
    }

    // Used by the Array Adapter
    @Override
    public String toString() {
        return mName;
    }

    public String getUUIDString() {
        return mUuid.toString();
    }

    public PrivateKey getKeystoreKey() {
        return mPrivateKey;
    }

    public String getSignedData(String b64data) {
        PrivateKey privkey = getKeystoreKey();

        byte[] data = Base64.decode(b64data, Base64.DEFAULT);

        // The Jelly Bean *evil* Hack
        // 4.2 implements the RSA/ECB/PKCS1PADDING in the OpenSSLprovider
        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) {
            return processSignJellyBeans(privkey, data);
        }


        try {

            /* ECB is perfectly fine in this special case, since we are using it for
               the public/private part in the TLS exchange
             */
            @SuppressLint("GetInstance")
            Cipher rsaSigner = Cipher.getInstance("RSA/ECB/PKCS1PADDING");

            rsaSigner.init(Cipher.ENCRYPT_MODE, privkey);

            byte[] signed_bytes = rsaSigner.doFinal(data);
            return Base64.encodeToString(signed_bytes, Base64.NO_WRAP);

        } catch (NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException
                | BadPaddingException | NoSuchPaddingException e) {
            VpnStatus.logError(R.string.error_rsa_sign, e.getClass().toString(), e.getLocalizedMessage());
            return null;
        }
    }

    private String processSignJellyBeans(PrivateKey privkey, byte[] data) {
        try {
            Method getKey = privkey.getClass().getSuperclass().getDeclaredMethod("getOpenSSLKey");
            getKey.setAccessible(true);

            // Real object type is OpenSSLKey
            Object opensslkey = getKey.invoke(privkey);

            getKey.setAccessible(false);

            Method getPkeyContext = opensslkey.getClass().getDeclaredMethod("getPkeyContext");

            // integer pointer to EVP_pkey
            getPkeyContext.setAccessible(true);
            int pkey = (Integer) getPkeyContext.invoke(opensslkey);
            getPkeyContext.setAccessible(false);

            // 112 with TLS 1.2 (172 back with 4.3), 36 with TLS 1.0
            byte[] signed_bytes = NativeUtils.rsasign(data, pkey);
            return Base64.encodeToString(signed_bytes, Base64.NO_WRAP);

        } catch (NoSuchMethodException | InvalidKeyException | InvocationTargetException | IllegalAccessException | IllegalArgumentException e) {
            VpnStatus.logError(R.string.error_rsa_sign, e.getClass().toString(), e.getLocalizedMessage());
            return null;
        }
    }


}




