package de.blinkt.openvpn; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; 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.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Collection; 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 org.spongycastle.util.io.pem.PemObject; import org.spongycastle.util.io.pem.PemWriter; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.os.Build; import android.preference.PreferenceManager; import android.security.KeyChain; import android.security.KeyChainException; import android.util.Base64; public class VpnProfile implements Serializable{ // Parcable /** * */ private static final long serialVersionUID = 7085688938959334563L; static final int TYPE_CERTIFICATES=0; static final int TYPE_PKCS12=1; 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 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; // 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]]"; private static final String OVPNCONFIGFILE = "android.conf"; protected transient String mTransientPW=null; protected transient String mTransientPCKS12PW=null; private transient PrivateKey mPrivateKey; protected boolean profileDleted=false; public static String DEFAULT_DNS1="131.234.137.23"; public static String DEFAULT_DNS2="131.234.137.24"; // Public attributes, since I got mad with getter/setter // set members to default values private UUID mUuid; 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=true; public String mServerPort= "1194" ; public boolean mUseUdp = true; public String mPKCS12Filename; public String mPKCS12Password; public boolean mUseTLSAuth = false; public String mServerName = "openvpn.blinkt.de" ; 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=false; public boolean mExpectTLSCert=true; 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"; public String mCipher=""; public boolean mNobind=false; public boolean mUseDefaultRoutev6=true; public String mCustomRoutesv6=""; public String mKeyPassword=""; public boolean mPersistTun = false; public String mConnectRetryMax="5"; public String mConnectRetry="5"; public boolean mUserEditable=true; public String mAuth=""; public int mX509AuthType=X509_VERIFY_TLSREMOTE_RDN; static final String MINIVPN = "miniopenvpn"; static private native byte[] rsasign(byte[] input,int pkey) throws InvalidKeyException; static { System.loadLibrary("opvpnutil"); } public void clearDefaults() { mServerName="unkown"; mUsePull=false; mUseLzo=false; mUseDefaultRoute=false; mUseDefaultRoutev6=false; mExpectTLSCert=false; mPersistTun = false; } 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("#")) return unescaped; else return '"' + escapedString + '"'; } public VpnProfile(String name) { mUuid = UUID.randomUUID(); mName = name; } public UUID getUUID() { return mUuid; } public String getName() { return mName; } public String getConfigFile(Context context, boolean configForOvpn3) { File cacheDir= context.getCacheDir(); String cfg=""; // Enable managment 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"; /* tmp-dir patched out :) cfg+="# /tmp does not exist on Android\n"; cfg+="tmp-dir "; cfg+=cacheDir.getAbsolutePath(); cfg+="\n\n"; */ cfg+="# Log window is better readable this way\n"; cfg+="suppress-timestamps\n"; boolean useTLSClient = (mAuthenticationType != TYPE_STATICKEYS); if(useTLSClient && mUsePull) cfg+="client\n"; else if (mUsePull) cfg+="pull\n"; else if(useTLSClient) cfg+="tls-client\n"; cfg+="verb " + mVerb + "\n"; if(mConnectRetryMax ==null) { mConnectRetryMax="5"; } if(!mConnectRetryMax.equals("-1")) cfg+="connect-retry-max " + mConnectRetryMax+ "\n"; if(mConnectRetry==null) mConnectRetry="5"; cfg+="connect-retry " + mConnectRetry + "\n"; cfg+="resolv-retry 60\n"; // We cannot use anything else than tun cfg+="dev tun\n"; // Server Address cfg+="remote "; cfg+=mServerName; cfg+=" "; cfg+=mServerPort; if(mUseUdp) cfg+=" udp\n"; else cfg+=" tcp-client\n"; switch(mAuthenticationType) { 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"; cfg+="\n" + ks[0] + "\n"; cfg+="\n" + ks[0] + "\n"; cfg+="management-external-key\n"; } break; case VpnProfile.TYPE_USERPASS: cfg+="auth-user-pass\n"; cfg+=insertFileData("ca",mCaFilename); } if(mUseLzo) { cfg+="comp-lzo\n"; } if(mUseTLSAuth) { if(mAuthenticationType==TYPE_STATICKEYS) cfg+=insertFileData("secret",mTLSAuthFilename); else cfg+=insertFileData("tls-auth",mTLSAuthFilename); if(nonNull(mTLSAuthDirection)) { cfg+= "key-direction "; cfg+= mTLSAuthDirection; cfg+="\n"; } } if(!mUsePull ) { if(nonNull(mIPv4Address)) cfg +="ifconfig " + cidrToIPAndNetmask(mIPv4Address) + "\n"; if(nonNull(mIPv6Address)) cfg +="ifconfig-ipv6 " + mIPv6Address + "\n"; } if(mUsePull && mRoutenopull) cfg += "route-nopull\n"; String routes = ""; int numroutes=0; if(mUseDefaultRoute) routes += "route 0.0.0.0 0.0.0.0\n"; else for(String route:getCustomRoutes()) { routes += "route " + route + "\n"; numroutes++; } if(mUseDefaultRoutev6) cfg += "route-ipv6 ::/0\n"; else for(String route:getCustomRoutesv6()) { routes += "route-ipv6 " + route + "\n"; numroutes++; } // Round number to next 100 if(numroutes> 90) { numroutes = ((numroutes / 100)+1) * 100; cfg+="# Alot of routes are set, increase max-routes\n"; cfg+="max-routes " + numroutes + "\n"; } cfg+=routes; if(mOverrideDNS || !mUsePull) { if(nonNull(mDNS1)) cfg+="dhcp-option DNS " + mDNS1 + "\n"; if(nonNull(mDNS2)) cfg+="dhcp-option DNS " + mDNS2 + "\n"; if(nonNull(mSearchDomain)) cfg+="dhcp-option DOMAIN " + mSearchDomain + "\n"; } if(mNobind) cfg+="nobind\n"; // Authentication if(mCheckRemoteCN) { if(mRemoteCN == null || mRemoteCN.equals("") ) cfg+="x509-verify-name " + 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+="x509-verify-name " + openVpnEscape(mRemoteCN) + " name\n"; break; case X509_VERIFY_TLSREMOTE_RDN_PREFIX: cfg+="x509-verify-name " + openVpnEscape(mRemoteCN) + " name-prefix\n"; break; case X509_VERIFY_TLSREMOTE_DN: cfg+="x509-verify-name " + openVpnEscape(mRemoteCN) + "\n"; break; } } if(mExpectTLSCert) cfg += "remote-cert-tls server\n"; if(nonNull(mCipher)){ cfg += "cipher " + mCipher + "\n"; } if(nonNull(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 sets persist-remote-ip to avoid DNS resolve problem\n"; cfg+= "persist-remote-ip\n"; } SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean usesystemproxy = prefs.getBoolean("usesystemproxy", true); if(usesystemproxy) { 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"; } return cfg; } //! Put inline data inline and other data as normal escaped filename private String insertFileData(String cfgentry, String filedata) { if(filedata==null) { // TODO: generate good error return String.format("%s %s\n",cfgentry,"missing"); }else if(filedata.startsWith(VpnProfile.INLINE_TAG)){ String datawoheader = filedata.substring(VpnProfile.INLINE_TAG.length()); return String.format(Locale.ENGLISH,"<%s>\n%s\n\n",cfgentry,datawoheader,cfgentry); } else { return String.format(Locale.ENGLISH,"%s %s\n",cfgentry,openVpnEscape(filedata)); } } private boolean nonNull(String val) { if(val == null || val.equals("")) return false; else return true; } private Collection getCustomRoutes() { Vector cidrRoutes=new Vector(); if(mCustomRoutes==null) { // No routes set, return empty vector return cidrRoutes; } for(String route:mCustomRoutes.split("[\n \t]")) { if(!route.equals("")) { String cidrroute = cidrToIPAndNetmask(route); if(cidrRoutes == null) return null; cidrRoutes.add(cidrroute); } } return cidrRoutes; } private Collection getCustomRoutesv6() { Vector cidrRoutes=new Vector(); if(mCustomRoutesv6==null) { // No routes set, return empty vector return cidrRoutes; } for(String route:mCustomRoutesv6.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; } private String[] buildOpenvpnArgv(File cacheDir) { Vector args = new Vector(); // Add fixed paramenters //args.add("/data/data/de.blinkt.openvpn/lib/openvpn"); args.add(cacheDir.getAbsolutePath() +"/" + VpnProfile.MINIVPN); args.add("--config"); args.add(cacheDir.getAbsolutePath() + "/" + OVPNCONFIGFILE); // Silences script security warning args.add("script-security"); args.add("0"); return (String[]) args.toArray(new String[args.size()]); } public Intent prepareIntent(Context context) { String prefix = context.getPackageName(); Intent intent = new Intent(context,OpenVpnService.class); if(mAuthenticationType == VpnProfile.TYPE_KEYSTORE || mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE) { if(getKeyStoreCertificates(context)==null) return null; } intent.putExtra(prefix + ".ARGV" , buildOpenvpnArgv(context.getCacheDir())); intent.putExtra(prefix + ".profileUUID", mUuid.toString()); ApplicationInfo info = context.getApplicationInfo(); intent.putExtra(prefix +".nativelib",info.nativeLibraryDir); try { FileWriter cfg = new FileWriter(context.getCacheDir().getAbsolutePath() + "/" + OVPNCONFIGFILE); cfg.write(getConfigFile(context,false)); cfg.flush(); cfg.close(); } catch (IOException e) { e.printStackTrace(); } return intent; } String[] getKeyStoreCertificates(Context context) { PrivateKey privateKey = null; X509Certificate[] cachain=null; try { privateKey = KeyChain.getPrivateKey(context,mAlias); mPrivateKey = privateKey; cachain = KeyChain.getCertificateChain(context, mAlias); if(cachain.length <= 1 && !nonNull(mCaFilename)) OpenVPN.logMessage(0, "", context.getString(R.string.keychain_nocacert)); for(X509Certificate cert:cachain) { OpenVPN.logInfo(R.string.cert_from_keystore,cert.getSubjectDN()); } if(nonNull(mCaFilename)) { try { Certificate cacert = getCacertFromFile(); X509Certificate[] newcachain = new X509Certificate[cachain.length+1]; for(int i=0;i= 1){ X509Certificate usercert = cachain[0]; PemWriter upw = new PemWriter(certout); upw.writeObject(new PemObject("CERTIFICATE", usercert.getEncoded())); upw.close(); } return new String[] {caout.toString(),certout.toString()}; } catch (InterruptedException e) { e.printStackTrace(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (CertificateException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (KeyChainException e) { OpenVPN.logMessage(0,"",context.getString(R.string.keychain_access)); if(Build.VERSION.SDK_INT==Build.VERSION_CODES.JELLY_BEAN){ if(!mAlias.matches("^[a-zA-Z0-9]$")) { OpenVPN.logError(R.string.jelly_keystore_alphanumeric_bug); } } } return null; } private Certificate getCacertFromFile() throws FileNotFoundException, CertificateException { CertificateFactory certFact = CertificateFactory.getInstance("X.509"); InputStream inStream; if(mCaFilename.startsWith(INLINE_TAG)) inStream = new ByteArrayInputStream(mCaFilename.replace(INLINE_TAG,"").getBytes()); else inStream = new FileInputStream(mCaFilename); return certFact.generateCertificate(inStream); } //! Return an error if somethign 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; } if(!mUsePull) { if(mIPv4Address == null || cidrToIPAndNetmask(mIPv4Address) == null) return R.string.ipv4_format_error; } if(isUserPWAuth() && !nonNull(mUsername)) { return R.string.error_empty_username; } if(!mUseDefaultRoute && getCustomRoutes()==null) return R.string.custom_route_format_error; // 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: return null; } } 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(!nonNull(mClientKeyFilename)) return false; String data = ""; if(mClientKeyFilename.startsWith(INLINE_TAG)) 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() { if((mAuthenticationType == TYPE_PKCS12 || mAuthenticationType == TYPE_USERPASS_PKCS12)&& (mPKCS12Password == null || mPKCS12Password.equals(""))) { if(mTransientPCKS12PW==null) return R.string.pkcs12_file_encryption_key; } if(mAuthenticationType == TYPE_CERTIFICATES || mAuthenticationType == TYPE_USERPASS_CERTIFICATES) { if(requireTLSKeyPassword() && !nonNull(mKeyPassword)) if(mTransientPCKS12PW==null) { return R.string.private_key_password; } } if(isUserPWAuth() && (mPassword.equals("") || mPassword == null)) { if(mTransientPW==null) 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(); Exception err =null; 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{ Cipher rsasinger = Cipher.getInstance("RSA/ECB/PKCS1PADDING"); rsasinger.init(Cipher.ENCRYPT_MODE, privkey); byte[] signed_bytes = rsasinger.doFinal(data); return Base64.encodeToString(signed_bytes, Base64.NO_WRAP); } catch (NoSuchAlgorithmException e){ err =e; } catch (InvalidKeyException e) { err =e; } catch (NoSuchPaddingException e) { err =e; } catch (IllegalBlockSizeException e) { err =e; } catch (BadPaddingException e) { err =e; } if(err !=null) { OpenVPN.logError(R.string.error_rsa_sign,err.getClass().toString(),err.getLocalizedMessage()); } return null; } private String processSignJellyBeans(PrivateKey privkey, byte[] data) { Exception err =null; try { Method[] allm = privkey.getClass().getSuperclass().getDeclaredMethods(); System.out.println(allm); 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); byte[] signed_bytes = rsasign(data, pkey); return Base64.encodeToString(signed_bytes, Base64.NO_WRAP); } catch (NoSuchMethodException e) { err=e; } catch (IllegalArgumentException e) { err=e; } catch (IllegalAccessException e) { err=e; } catch (InvocationTargetException e) { err=e; } catch (InvalidKeyException e) { err=e; } if(err !=null) { OpenVPN.logError(R.string.error_rsa_sign,err.getClass().toString(),err.getLocalizedMessage()); } return null; } }