/* * 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 static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PROFILE; import static se.leap.bitmaskclient.base.utils.ConfigHelper.stringEqual; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; 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.text.TextUtils; import android.util.Base64; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.gson.Gson; import com.google.gson.GsonBuilder; 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.Serializable; import java.io.StringWriter; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.Signature; import java.security.SignatureException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.HashSet; 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.ExtAuthHelper; import de.blinkt.openvpn.core.NativeUtils; import de.blinkt.openvpn.core.OpenVPNService; import de.blinkt.openvpn.core.OrbotHelper; import de.blinkt.openvpn.core.PasswordCache; import de.blinkt.openvpn.core.Preferences; import de.blinkt.openvpn.core.VPNLaunchHelper; import de.blinkt.openvpn.core.VpnStatus; import de.blinkt.openvpn.core.X509Utils; import de.blinkt.openvpn.core.connection.Connection; import de.blinkt.openvpn.core.connection.ConnectionAdapter; import se.leap.bitmaskclient.BuildConfig; import se.leap.bitmaskclient.R; 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 INLINE_TAG = "[[INLINE]]"; public static final String DISPLAYNAME_TAG = "[[NAME]]"; public static final int MAXLOGLEVEL = 4; public static final int CURRENT_PROFILE_VERSION = 7; public static final int DEFAULT_MSSFIX_SIZE = 1280; 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; public static final int TYPE_EXTERNAL_APP = 8; 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; public static final int AUTH_RETRY_NONE_FORGET = 0; public static final int AUTH_RETRY_NOINTERACT = 2; public static final boolean mIsOpenVPN22 = false; private static final long serialVersionUID = 7085688938959334563L; private static final int AUTH_RETRY_NONE_KEEP = 1; private static final int AUTH_RETRY_INTERACT = 3; public static String DEFAULT_DNS1 = "8.8.8.8"; public static String DEFAULT_DNS2 = "8.8.4.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; public String mName; public String mAlias; public String mClientCertFilename; public String mTLSAuthDirection = ""; public String mTLSAuthFilename; public String mClientKeyFilename; public String mCaFilename; public boolean mUseLzo = false; 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 String mDataCiphers = ""; public boolean mNobind = true; 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; public boolean mAllowLocalLAN; 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 mAllowedAppsVpn = new HashSet<>(); public boolean mAllowedAppsVpnAreDisallowed = true; public boolean mAllowAppVpnBypass = false; public String mCrlFilename; public String mProfileCreator; public String mExternalAuthenticator; public int mAuthRetry = AUTH_RETRY_NONE_FORGET; public int mTunMtu; public boolean mPushPeerInfo = false; public int mVersion = 0; // timestamp when the profile was last used public long mLastUsed; public String importedProfileHash; //TODO: cleanup here /* Options no longer used in new profiles */ public String mServerName = "openvpn.example.com"; public String mServerPort = "1194"; @Deprecated public boolean mUseUdp = true; public boolean mTemporaryProfile = false; private transient PrivateKey mPrivateKey; // Public attributes, since I got mad with getter/setter // set members to default values private UUID mUuid; private int mProfileVersion; public boolean mBlockUnusedAddressFamilies = true; public String mGatewayIp; private final int mTransportType; public VpnProfile(String name, Connection.TransportType transportType) { mUuid = UUID.randomUUID(); mName = name; mProfileVersion = CURRENT_PROFILE_VERSION; mConnections = new Connection[1]; mLastUsed = System.currentTimeMillis(); mTransportType = transportType.toInt(); } 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 static boolean doUseOpenVPN3(Context c) { SharedPreferences prefs = Preferences.getDefaultSharedPreferences(c); boolean useOpenVPN3 = prefs.getBoolean("ovpn3", false); if (!BuildConfig.openvpn3) useOpenVPN3 = false; return useOpenVPN3; } //! 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\n", cfgentry, dataWithOutHeader, cfgentry); } else { return String.format(Locale.ENGLISH, "%s %s\n", cfgentry, openVpnEscape(filedata)); } } 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; } @Override public boolean equals(Object obj) { if (obj instanceof VpnProfile) { VpnProfile vp = (VpnProfile) obj; return stringEqual(vp.mGatewayIp, mGatewayIp) && vp.mTransportType == mTransportType; } return false; } 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; mNobind = false; } public UUID getUUID() { return mUuid; } // Only used for the special case of managed profiles public void setUUID(UUID uuid) { mUuid = uuid; } public boolean usePluggableTransports() { Connection.TransportType type = Connection.TransportType.fromInt(mTransportType); return type != null && type.isPluggableTransport(); } public Connection.TransportType getTransportType() { return Connection.TransportType.fromInt(mTransportType); } public String getName() { if (TextUtils.isEmpty(mName)) return "No profile name"; return mName; } @Deprecated public void upgradeProfile() { if (mProfileVersion < 6) { if (TextUtils.isEmpty(mProfileCreator)) mUserEditable = true; } if (mProfileVersion < 7) { for (Connection c : mConnections) if (c.getProxyType() == null) c.setProxyType(Connection.ProxyType.NONE); } mProfileVersion = CURRENT_PROFILE_VERSION; } public String getConfigFile(Context context, boolean configForOvpn3) { File cacheDir = context.getCacheDir(); StringBuilder cfg = new StringBuilder(); if (!configForOvpn3) { // Enable management interface cfg.append("# Config for OpenVPN 2.x\n"); cfg.append("# Enables connection to GUI\n"); cfg.append("management "); cfg.append(cacheDir.getAbsolutePath()).append("/").append("mgmtsocket"); cfg.append(" unix\n"); cfg.append("management-client\n"); // Not needed, see updated man page in 2.3 //cfg += "management-signal\n"; cfg.append("management-query-passwords\n"); cfg.append("management-hold\n\n"); cfg.append(String.format("setenv IV_GUI_VER %s \n", openVpnEscape(getVersionEnvString(context)))); String versionString = getPlatformVersionEnvString(); cfg.append(String.format("setenv IV_PLAT_VER %s\n", openVpnEscape(versionString))); } else { cfg.append("# Config for OpenVPN 3 C++\n"); } if (!configForOvpn3) { cfg.append("machine-readable-output\n"); if (!mIsOpenVPN22) cfg.append("allow-recursive-routing\n"); // Users are confused by warnings that are misleading... cfg.append("ifconfig-nowarn\n"); } boolean useTLSClient = (mAuthenticationType != TYPE_STATICKEYS); if (useTLSClient && mUsePull) cfg.append("client\n"); else if (mUsePull) cfg.append("pull\n"); else if (useTLSClient) cfg.append("tls-client\n"); //cfg += "verb " + mVerb + "\n"; cfg.append("verb " + MAXLOGLEVEL + "\n"); if (mConnectRetryMax == null) { mConnectRetryMax = "-1"; } if (!mConnectRetryMax.equals("-1")) cfg.append("connect-retry-max ").append(mConnectRetryMax).append("\n"); if (TextUtils.isEmpty(mConnectRetry)) mConnectRetry = "2"; if (TextUtils.isEmpty(mConnectRetryMaxTime)) mConnectRetryMaxTime = "300"; if (!mIsOpenVPN22) cfg.append("connect-retry ").append(mConnectRetry).append(" ").append(mConnectRetryMaxTime).append("\n"); else if (mIsOpenVPN22 && !mUseUdp) cfg.append("connect-retry ").append(mConnectRetry).append("\n"); cfg.append("resolv-retry 60\n"); // We cannot use anything else than tun cfg.append("dev tun\n"); boolean canUsePlainRemotes = true; if (mConnections.length == 1) { cfg.append(mConnections[0].getConnectionBlock(configForOvpn3)); } else { for (Connection conn : mConnections) { canUsePlainRemotes = canUsePlainRemotes && conn.isOnlyRemote(); } if (mRemoteRandom) cfg.append("remote-random\n"); if (canUsePlainRemotes) { for (Connection conn : mConnections) { if (conn.isEnabled()) { cfg.append(conn.getConnectionBlock(configForOvpn3)); } } } } switch (mAuthenticationType) { case VpnProfile.TYPE_USERPASS_CERTIFICATES: cfg.append("auth-user-pass\n"); case VpnProfile.TYPE_CERTIFICATES: // Ca cfg.append(insertFileData("ca", mCaFilename)); // Client Cert + Key cfg.append(insertFileData("key", mClientKeyFilename)); cfg.append(insertFileData("cert", mClientCertFilename)); break; case VpnProfile.TYPE_USERPASS_PKCS12: cfg.append("auth-user-pass\n"); case VpnProfile.TYPE_PKCS12: cfg.append(insertFileData("pkcs12", mPKCS12Filename)); if (!TextUtils.isEmpty(mCaFilename)) { cfg.append(insertFileData("ca", mCaFilename)); } break; case VpnProfile.TYPE_USERPASS_KEYSTORE: cfg.append("auth-user-pass\n"); case VpnProfile.TYPE_KEYSTORE: case VpnProfile.TYPE_EXTERNAL_APP: if (!configForOvpn3) { String[] ks = getExternalCertificates(context); cfg.append("### From Keystore/ext auth app ####\n"); if (ks != null) { cfg.append("\n").append(ks[0]).append("\n\n"); if (!TextUtils.isEmpty(ks[1])) cfg.append("\n").append(ks[1]).append("\n\n"); cfg.append("\n").append(ks[2]).append("\n\n"); cfg.append("management-external-key nopadding\n"); } else { cfg.append(context.getString(R.string.keychain_access)).append("\n"); } } break; case VpnProfile.TYPE_USERPASS: cfg.append("auth-user-pass\n"); cfg.append(insertFileData("ca", mCaFilename)); if (configForOvpn3) { // OpenVPN 3 needs to be told that a client certificate is not required cfg.append("client-cert-not-required\n"); } } if (isUserPWAuth()) { if (mAuthRetry == AUTH_RETRY_NOINTERACT) cfg.append("auth-retry nointeract\n"); } if (!TextUtils.isEmpty(mCrlFilename)) cfg.append(insertFileData("crl-verify", mCrlFilename)); // compression does not work in conjunction with shapeshifter-dispatcher so far if (mUseLzo && !usePluggableTransports()) { cfg.append("comp-lzo\n"); } if (mUseTLSAuth) { boolean useTlsCrypt = mTLSAuthDirection.equals("tls-crypt"); boolean useTlsCrypt2 = mTLSAuthDirection.equals("tls-crypt-v2"); if (mAuthenticationType == TYPE_STATICKEYS) cfg.append(insertFileData("secret", mTLSAuthFilename)); else if (useTlsCrypt) cfg.append(insertFileData("tls-crypt", mTLSAuthFilename)); else if (useTlsCrypt2) cfg.append(insertFileData("tls-crypt-v2", mTLSAuthFilename)); else cfg.append(insertFileData("tls-auth", mTLSAuthFilename)); if (!TextUtils.isEmpty(mTLSAuthDirection) && !useTlsCrypt && !useTlsCrypt2) { cfg.append("key-direction "); cfg.append(mTLSAuthDirection); cfg.append("\n"); } } if (!mUsePull) { if (!TextUtils.isEmpty(mIPv4Address)) cfg.append("ifconfig ").append(cidrToIPAndNetmask(mIPv4Address)).append("\n"); if (!TextUtils.isEmpty(mIPv6Address)) { // Use our own ip as gateway since we ignore it anyway String fakegw = mIPv6Address.split("/", 2)[0]; cfg.append("ifconfig-ipv6 ").append(mIPv6Address).append(" ").append(fakegw).append("\n"); } } if (mUsePull && mRoutenopull) cfg.append("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.append("route-ipv6 ::/0\n"); else for (String route : getCustomRoutesv6(mCustomRoutesv6)) { routes += "route-ipv6 " + route + "\n"; } cfg.append(routes); if (mOverrideDNS || !mUsePull) { if (!TextUtils.isEmpty(mDNS1)) { cfg.append("dhcp-option DNS ").append(mDNS1).append("\n"); } if (!TextUtils.isEmpty(mDNS2)) { cfg.append("dhcp-option DNS ").append(mDNS2).append("\n"); } if (!TextUtils.isEmpty(mSearchDomain)) cfg.append("dhcp-option DOMAIN ").append(mSearchDomain).append("\n"); } if (mMssFix != 0) { if (mMssFix != 1450) { cfg.append(String.format(Locale.US, "mssfix %d\n", mMssFix)); } else cfg.append("mssfix\n"); } if (mTunMtu >= 48 && mTunMtu != 1500) { cfg.append(String.format(Locale.US, "tun-mtu %d\n", mTunMtu)); } if (mNobind) cfg.append("nobind\n"); // Authentication if (mAuthenticationType != TYPE_STATICKEYS) { if (mCheckRemoteCN) { if (mRemoteCN == null || mRemoteCN.equals("")) cfg.append("verify-x509-name ").append(openVpnEscape(mConnections[0].getServerName())).append(" name\n"); else switch (mX509AuthType) { // 2.2 style x509 checks case X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING: cfg.append("compat-names no-remapping\n"); case X509_VERIFY_TLSREMOTE: cfg.append("tls-remote ").append(openVpnEscape(mRemoteCN)).append("\n"); break; case X509_VERIFY_TLSREMOTE_RDN: cfg.append("verify-x509-name ").append(openVpnEscape(mRemoteCN)).append(" name\n"); break; case X509_VERIFY_TLSREMOTE_RDN_PREFIX: cfg.append("verify-x509-name ").append(openVpnEscape(mRemoteCN)).append(" name-prefix\n"); break; case X509_VERIFY_TLSREMOTE_DN: cfg.append("verify-x509-name ").append(openVpnEscape(mRemoteCN)).append("\n"); break; } if (!TextUtils.isEmpty(mx509UsernameField)) cfg.append("x509-username-field ").append(openVpnEscape(mx509UsernameField)).append("\n"); } if (mExpectTLSCert) cfg.append("remote-cert-tls server\n"); } if (!TextUtils.isEmpty(mDataCiphers)) { cfg.append("data-ciphers ").append(mDataCiphers).append("\n"); } if (!TextUtils.isEmpty(mCipher)) { cfg.append("cipher ").append(mCipher).append("\n"); } if (!TextUtils.isEmpty(mAuth)) { cfg.append("auth ").append(mAuth).append("\n"); } // Obscure Settings dialog if (mUseRandomHostname) cfg.append("#my favorite options :)\nremote-random-hostname\n"); if (mUseFloat) cfg.append("float\n"); if (mPersistTun) { cfg.append("persist-tun\n"); cfg.append("# persist-tun also enables pre resolving to avoid DNS resolve problem\n"); if (!mIsOpenVPN22) cfg.append("preresolve\n"); } if (mPushPeerInfo) cfg.append("push-peer-info\n"); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean usesystemproxy = prefs.getBoolean("usesystemproxy", true); if (usesystemproxy && !mIsOpenVPN22 && !configForOvpn3 && !usesExtraProxyOptions()) { cfg.append("# Use system proxy setting\n"); cfg.append("management-query-proxy\n"); } if (mUseCustomConfig) { cfg.append("# Custom configuration options\n"); cfg.append("# You are on your on own here :)\n"); cfg.append(mCustomConfigOptions); cfg.append("\n"); } if (!canUsePlainRemotes) { cfg.append("# 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.isEnabled()) { cfg.append("\n"); cfg.append(conn.getConnectionBlock(configForOvpn3)); cfg.append("\n"); } } } return cfg.toString(); } public String getPlatformVersionEnvString() { return 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); } 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); } @NonNull private Collection getCustomRoutes(String routes) { Vector 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 getCustomRoutesv6(String routes) { Vector 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; // } 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) { Intent intent = new Intent(context, OpenVPNService.class); intent.putExtra(PROVIDER_PROFILE, this); return intent; } 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) { new Thread(new Runnable() { @Override public void run() { getExternalCertificates(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) 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; } } public void pwDidFail(Context c) { } private X509Certificate[] getKeyStoreCertificates(Context context) throws KeyChainException, InterruptedException { PrivateKey privateKey = KeyChain.getPrivateKey(context, mAlias); mPrivateKey = privateKey; X509Certificate[] caChain = KeyChain.getCertificateChain(context, mAlias); return caChain; } private X509Certificate[] getExtAppCertificates(Context context) throws KeyChainException { if (mExternalAuthenticator == null || mAlias == null) throw new KeyChainException("Alias or external auth provider name not set"); return ExtAuthHelper.getCertificateChain(context, mExternalAuthenticator, mAlias); } public String[] getExternalCertificates(Context context) { return getExternalCertificates(context, 5); } synchronized String[] getExternalCertificates(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; X509Certificate caChain[]; if (mAuthenticationType == TYPE_EXTERNAL_APP) { caChain = getExtAppCertificates(context); } else { caChain = getKeyStoreCertificates(context); } 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); 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 getExternalCertificates(context, tries - 1); } } public int checkProfile(Context c) { return checkProfile(c, doUseOpenVPN3(c)); } //! Return an error if something is wrong public int checkProfile(Context context, boolean useOpenVPN3) { if (mAuthenticationType == TYPE_KEYSTORE || mAuthenticationType == TYPE_USERPASS_KEYSTORE || mAuthenticationType == TYPE_EXTERNAL_APP) { if (mAlias == null) return R.string.no_keystore_cert_selected; } else if (mAuthenticationType == TYPE_CERTIFICATES || mAuthenticationType == TYPE_USERPASS_CERTIFICATES) { if (TextUtils.isEmpty(mCaFilename)) return R.string.no_ca_cert_selected; } if (mCheckRemoteCN && mX509AuthType == X509_VERIFY_TLSREMOTE) return R.string.deprecated_tls_remote; if (!mUsePull || mAuthenticationType == TYPE_STATICKEYS) { if (mIPv4Address == null || cidrToIPAndNetmask(mIPv4Address) == null) return R.string.ipv4_format_error; } 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))) return R.string.missing_certificates; if ((mAuthenticationType == TYPE_CERTIFICATES || mAuthenticationType == TYPE_USERPASS_CERTIFICATES) && TextUtils.isEmpty(mCaFilename)) return R.string.missing_ca_certificate; boolean noRemoteEnabled = true; for (Connection c : mConnections) { if (c.isEnabled()) noRemoteEnabled = false; } if (noRemoteEnabled) return R.string.remote_no_server_selected; if (useOpenVPN3) { if (mAuthenticationType == TYPE_STATICKEYS) { return R.string.openvpn3_nostatickeys; } if (mAuthenticationType == TYPE_PKCS12 || mAuthenticationType == TYPE_USERPASS_PKCS12) { return R.string.openvpn3_pkcs12; } for (Connection conn : mConnections) { if (conn.getProxyType() == Connection.ProxyType.ORBOT || conn.getProxyType() == Connection.ProxyType.SOCKS5) return R.string.openvpn3_socksproxy; } } for (Connection c : mConnections) { if (c.getProxyType() == Connection.ProxyType.ORBOT) { if (usesExtraProxyOptions()) return R.string.error_orbot_and_proxy_options; if (!OrbotHelper.checkTorReceier(context)) return R.string.no_orbotfound; } } // Everything okay return R.string.no_error_found; } //! Openvpn asks for a "Private Key", this should be pkcs12 key // public String getPasswordPrivateKey() { String cachedPw = PasswordCache.getPKCS12orCertificatePassword(mUuid, true); if (cachedPw != null) { return cachedPw; } 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: 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: 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(String transientCertOrPkcs12PW, String mTransientAuthPW) { if ((mAuthenticationType == TYPE_PKCS12 || mAuthenticationType == TYPE_USERPASS_PKCS12) && (mPKCS12Password == null || mPKCS12Password.equals(""))) { if (transientCertOrPkcs12PW == null) return R.string.pkcs12_file_encryption_key; } if (mAuthenticationType == TYPE_CERTIFICATES || mAuthenticationType == TYPE_USERPASS_CERTIFICATES) { if (requireTLSKeyPassword() && TextUtils.isEmpty(mKeyPassword)) if (transientCertOrPkcs12PW == null) { return R.string.private_key_password; } } if (isUserPWAuth() && (TextUtils.isEmpty(mUsername) || (TextUtils.isEmpty(mPassword) && mTransientAuthPW == null))) { return R.string.password; } return 0; } public String getPasswordAuth() { String cachedPw = PasswordCache.getAuthPassword(mUuid, true); if (cachedPw != null) { return cachedPw; } else { return mPassword; } } // Used by the Array Adapter @Override public String toString() { return mName; } public String toJson() { Gson gson = new Gson(); try { return gson.toJson(this); } catch (Exception e) { e.printStackTrace(); } return null; } public static VpnProfile fromJson(String json) { try { GsonBuilder builder = new GsonBuilder(); builder.registerTypeAdapter(Connection.class, new ConnectionAdapter()); return builder.create().fromJson(json, VpnProfile.class); } catch (Exception e) { e.printStackTrace(); } return null; } public String getUUIDString() { return mUuid.toString().toLowerCase(Locale.ENGLISH); } public PrivateKey getKeystoreKey() { return mPrivateKey; } @Nullable public String getSignedData(Context c, String b64data, boolean pkcs1padding) { byte[] data = Base64.decode(b64data, Base64.DEFAULT); byte[] signed_bytes; if (mAuthenticationType == TYPE_EXTERNAL_APP) signed_bytes = getExtAppSignedData(c, data); else signed_bytes = getKeyChainSignedData(data, pkcs1padding); if (signed_bytes != null) return Base64.encodeToString(signed_bytes, Base64.NO_WRAP); else return null; } private byte[] getExtAppSignedData(Context c, byte[] data) { if (TextUtils.isEmpty(mExternalAuthenticator)) return null; try { return ExtAuthHelper.signData(c, mExternalAuthenticator, mAlias, data); } catch (KeyChainException | InterruptedException e) { VpnStatus.logError(R.string.error_extapp_sign, mExternalAuthenticator, e.getClass().toString(), e.getLocalizedMessage()); return null; } } private byte[] getKeyChainSignedData(byte[] data, boolean pkcs1padding) { PrivateKey privkey = getKeystoreKey(); try { @SuppressLint("GetInstance") String keyalgorithm = privkey.getAlgorithm(); byte[] signed_bytes; if (keyalgorithm.equals("EC")) { Signature signer = Signature.getInstance("NONEwithECDSA"); signer.initSign(privkey); signer.update(data); signed_bytes = signer.sign(); } else { /* ECB is perfectly fine in this special case, since we are using it for the public/private part in the TLS exchange */ Cipher signer; if (pkcs1padding) signer = Cipher.getInstance("RSA/ECB/PKCS1PADDING"); else signer = Cipher.getInstance("RSA/ECB/NoPadding"); signer.init(Cipher.ENCRYPT_MODE, privkey); signed_bytes = signer.doFinal(data); } return signed_bytes; } catch (NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException | SignatureException e) { VpnStatus.logError(R.string.error_rsa_sign, e.getClass().toString(), e.getLocalizedMessage()); return null; } } private boolean usesExtraProxyOptions() { if (mUseCustomConfig && mCustomConfigOptions != null && mCustomConfigOptions.contains("http-proxy-option ")) return true; for (Connection c : mConnections) if (c.usesExtraProxyOptions()) return true; return false; } class NoCertReturnedException extends Exception { public NoCertReturnedException(String msg) { super(msg); } } }