I'm working on an app for which the (power)users have to set up their own server (i.e. nginx) to run the backend application. The corresponding domain needs to be configured in the app so it can connect. I've been testing primarily on my own phone (sony z3c) and started developing for 5.1. Later I received an update for 6.0 but still maintained a working 5.1 inside the emulator. Not too long ago, I started to work on an AVD with an image for 7.0 and to my suprise it won't connect to my server, telling me the ssl handshake failed. My nginx configuration is pretty strict, but it works for both 5.1 and 6.0, so .... ?!
Here is what I know:
I use v24 for support libs, i.e. my compileSdkVersion is 24.
I use Volley v1.0.0.
I've tried the TLSSocketFactory, but it doesn't change anything. This seems to be used most of the times to prevent SSL3 use for older SDK versions anyway.
I've tried increasing the timeout, but it doesn't change anything.
I've tried using HttpURLConnection directly, but it doesn't change anything apart from the stack trace (it's without the volley references, but identical otherwise).
Without the TLSSocketFactory the request are made through a bare request queue, instantiated with Volley.newRequestQueue(context).
This is what I see in android studio:
W/System.err: com.android.volley.NoConnectionError: javax.net.ssl.SSLHandshakeException: Connection closed by peer
W/System.err: at com.android.volley.toolbox.BasicNetwork.performRequest(BasicNetwork.java:151)
W/System.err: at com.android.volley.NetworkDispatcher.run(NetworkDispatcher.java:112)
W/System.err: Caused by: javax.net.ssl.SSLHandshakeException: Connection closed by peer
W/System.err: at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
W/System.err: at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:357)
W/System.err: at com.android.okhttp.Connection.connectTls(Connection.java:235)
W/System.err: at com.android.okhttp.Connection.connectSocket(Connection.java:199)
W/System.err: at com.android.okhttp.Connection.connect(Connection.java:172)
W/System.err: at com.android.okhttp.Connection.connectAndSetOwner(Connection.java:367)
W/System.err: at com.android.okhttp.OkHttpClient$1.connectAndSetOwner(OkHttpClient.java:130)
W/System.err: at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:329)
W/System.err: at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:246)
W/System.err: at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:457)
W/System.err: at com.android.okhttp.internal.huc.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:126)
W/System.err: at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getOutputStream(HttpURLConnectionImpl.java:257)
W/System.err: at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.getOutputStream(DelegatingHttpsURLConnection.java:218)
W/System.err: at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.getOutputStream(HttpsURLConnectionImpl.java)
W/System.err: at com.android.volley.toolbox.HurlStack.addBodyIfExists(HurlStack.java:264)
W/System.err: at com.android.volley.toolbox.HurlStack.setConnectionParametersForRequest(HurlStack.java:234)
W/System.err: at com.android.volley.toolbox.HurlStack.performRequest(HurlStack.java:107)
W/System.err: at com.android.volley.toolbox.BasicNetwork.performRequest(BasicNetwork.java:96)
W/System.err: ... 1 more
W/System.err: Suppressed: javax.net.ssl.SSLHandshakeException: Handshake failed
W/System.err: at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:429)
W/System.err: ... 17 more
W/System.err: Caused by: javax.net.ssl.SSLProtocolException: SSL handshake terminated: ssl=0x7ffef3748040: Failure in SSL library, usually a protocol error
W/System.err: error:10000410:SSL routines:OPENSSL_internal:SSLV3_ALERT_HANDSHAKE_FAILURE (external/boringssl/src/ssl/s3_pkt.c:610 0x7ffeda1d2240:0x00000001)
W/System.err: error:1000009a:SSL routines:OPENSSL_internal:HANDSHAKE_FAILURE_ON_CLIENT_HELLO (external/boringssl/src/ssl/s3_clnt.c:764 0x7ffee9d2b70a:0x00000000)
W/System.err: at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
W/System.err: at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:357)
W/System.err: ... 17 more
Since it says SSLV3_ALERT_HANDSHAKE_FAILURE I can only assume it for some reason tries to connect using SSLv3 and fails, but this doesn't make any sense to me whatsoever. It might be a cipher-issue, but how can I tell what it is trying to use ? I would rather not enable a ciphers on the server, make a connection attempt and repeat.
My nginx site uses a let's encrypt certificate and has the following configuration:
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/certs/lets-encrypt-x1-cross-signed.pem;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:!aNULL;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_ecdh_curve secp384r1;
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1.2;
To test these ciphers I've a script and it confirms these ciphers (run on a wheezy vps outside the server's network):
Testing ECDHE-RSA-AES256-GCM-SHA384...YES
Testing ECDHE-ECDSA-AES256-GCM-SHA384...NO (sslv3 alert handshake failure)
Testing ECDHE-RSA-AES256-SHA384...NO (sslv3 alert handshake failure)
Testing ECDHE-ECDSA-AES256-SHA384...NO (sslv3 alert handshake failure)
Testing ECDHE-RSA-AES256-SHA...NO (sslv3 alert handshake failure)
Testing ECDHE-ECDSA-AES256-SHA...NO (sslv3 alert handshake failure)
Testing SRP-DSS-AES-256-CBC-SHA...NO (sslv3 alert handshake failure)
Testing SRP-RSA-AES-256-CBC-SHA...NO (sslv3 alert handshake failure)
Testing DHE-DSS-AES256-GCM-SHA384...NO (sslv3 alert handshake failure)
Testing DHE-RSA-AES256-GCM-SHA384...NO (sslv3 alert handshake failure)
Testing DHE-RSA-AES256-SHA256...NO (sslv3 alert handshake failure)
Testing DHE-DSS-AES256-SHA256...NO (sslv3 alert handshake failure)
Testing DHE-RSA-AES256-SHA...NO (sslv3 alert handshake failure)
Testing DHE-DSS-AES256-SHA...NO (sslv3 alert handshake failure)
Testing DHE-RSA-CAMELLIA256-SHA...NO (sslv3 alert handshake failure)
Testing DHE-DSS-CAMELLIA256-SHA...NO (sslv3 alert handshake failure)
Testing AECDH-AES256-SHA...NO (sslv3 alert handshake failure)
Testing SRP-AES-256-CBC-SHA...NO (sslv3 alert handshake failure)
Testing ADH-AES256-GCM-SHA384...NO (sslv3 alert handshake failure)
Testing ADH-AES256-SHA256...NO (sslv3 alert handshake failure)
Testing ADH-AES256-SHA...NO (sslv3 alert handshake failure)
Testing ADH-CAMELLIA256-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-AES256-GCM-SHA384...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-AES256-GCM-SHA384...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-AES256-SHA384...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-AES256-SHA384...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-AES256-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-AES256-SHA...NO (sslv3 alert handshake failure)
Testing AES256-GCM-SHA384...NO (sslv3 alert handshake failure)
Testing AES256-SHA256...NO (sslv3 alert handshake failure)
Testing AES256-SHA...NO (sslv3 alert handshake failure)
Testing CAMELLIA256-SHA...NO (sslv3 alert handshake failure)
Testing PSK-AES256-CBC-SHA...NO (no ciphers available)
Testing ECDHE-RSA-DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing ECDHE-ECDSA-DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing SRP-DSS-3DES-EDE-CBC-SHA...NO (sslv3 alert handshake failure)
Testing SRP-RSA-3DES-EDE-CBC-SHA...NO (sslv3 alert handshake failure)
Testing EDH-RSA-DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing EDH-DSS-DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing AECDH-DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing SRP-3DES-EDE-CBC-SHA...NO (sslv3 alert handshake failure)
Testing ADH-DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing DES-CBC3-SHA...NO (sslv3 alert handshake failure)
Testing PSK-3DES-EDE-CBC-SHA...NO (no ciphers available)
Testing ECDHE-RSA-AES128-GCM-SHA256...YES
Testing ECDHE-ECDSA-AES128-GCM-SHA256...NO (sslv3 alert handshake failure)
Testing ECDHE-RSA-AES128-SHA256...NO (sslv3 alert handshake failure)
Testing ECDHE-ECDSA-AES128-SHA256...NO (sslv3 alert handshake failure)
Testing ECDHE-RSA-AES128-SHA...NO (sslv3 alert handshake failure)
Testing ECDHE-ECDSA-AES128-SHA...NO (sslv3 alert handshake failure)
Testing SRP-DSS-AES-128-CBC-SHA...NO (sslv3 alert handshake failure)
Testing SRP-RSA-AES-128-CBC-SHA...NO (sslv3 alert handshake failure)
Testing DHE-DSS-AES128-GCM-SHA256...NO (sslv3 alert handshake failure)
Testing DHE-RSA-AES128-GCM-SHA256...NO (sslv3 alert handshake failure)
Testing DHE-RSA-AES128-SHA256...NO (sslv3 alert handshake failure)
Testing DHE-DSS-AES128-SHA256...NO (sslv3 alert handshake failure)
Testing DHE-RSA-AES128-SHA...NO (sslv3 alert handshake failure)
Testing DHE-DSS-AES128-SHA...NO (sslv3 alert handshake failure)
Testing DHE-RSA-SEED-SHA...NO (sslv3 alert handshake failure)
Testing DHE-DSS-SEED-SHA...NO (sslv3 alert handshake failure)
Testing DHE-RSA-CAMELLIA128-SHA...NO (sslv3 alert handshake failure)
Testing DHE-DSS-CAMELLIA128-SHA...NO (sslv3 alert handshake failure)
Testing AECDH-AES128-SHA...NO (sslv3 alert handshake failure)
Testing SRP-AES-128-CBC-SHA...NO (sslv3 alert handshake failure)
Testing ADH-AES128-GCM-SHA256...NO (sslv3 alert handshake failure)
Testing ADH-AES128-SHA256...NO (sslv3 alert handshake failure)
Testing ADH-AES128-SHA...NO (sslv3 alert handshake failure)
Testing ADH-SEED-SHA...NO (sslv3 alert handshake failure)
Testing ADH-CAMELLIA128-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-AES128-GCM-SHA256...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-AES128-GCM-SHA256...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-AES128-SHA256...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-AES128-SHA256...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-AES128-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-AES128-SHA...NO (sslv3 alert handshake failure)
Testing AES128-GCM-SHA256...NO (sslv3 alert handshake failure)
Testing AES128-SHA256...NO (sslv3 alert handshake failure)
Testing AES128-SHA...NO (sslv3 alert handshake failure)
Testing SEED-SHA...NO (sslv3 alert handshake failure)
Testing CAMELLIA128-SHA...NO (sslv3 alert handshake failure)
Testing PSK-AES128-CBC-SHA...NO (no ciphers available)
Testing ECDHE-RSA-RC4-SHA...NO (sslv3 alert handshake failure)
Testing ECDHE-ECDSA-RC4-SHA...NO (sslv3 alert handshake failure)
Testing AECDH-RC4-SHA...NO (sslv3 alert handshake failure)
Testing ADH-RC4-MD5...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-RC4-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-RC4-SHA...NO (sslv3 alert handshake failure)
Testing RC4-SHA...NO (sslv3 alert handshake failure)
Testing RC4-MD5...NO (sslv3 alert handshake failure)
Testing PSK-RC4-SHA...NO (no ciphers available)
Testing EDH-RSA-DES-CBC-SHA...NO (sslv3 alert handshake failure)
Testing EDH-DSS-DES-CBC-SHA...NO (sslv3 alert handshake failure)
Testing ADH-DES-CBC-SHA...NO (sslv3 alert handshake failure)
Testing DES-CBC-SHA...NO (sslv3 alert handshake failure)
Testing EXP-EDH-RSA-DES-CBC-SHA...NO (sslv3 alert handshake failure)
Testing EXP-EDH-DSS-DES-CBC-SHA...NO (sslv3 alert handshake failure)
Testing EXP-ADH-DES-CBC-SHA...NO (sslv3 alert handshake failure)
Testing EXP-DES-CBC-SHA...NO (sslv3 alert handshake failure)
Testing EXP-RC2-CBC-MD5...NO (sslv3 alert handshake failure)
Testing EXP-ADH-RC4-MD5...NO (sslv3 alert handshake failure)
Testing EXP-RC4-MD5...NO (sslv3 alert handshake failure)
Testing ECDHE-RSA-NULL-SHA...NO (sslv3 alert handshake failure)
Testing ECDHE-ECDSA-NULL-SHA...NO (sslv3 alert handshake failure)
Testing AECDH-NULL-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-RSA-NULL-SHA...NO (sslv3 alert handshake failure)
Testing ECDH-ECDSA-NULL-SHA...NO (sslv3 alert handshake failure)
Testing NULL-SHA256...NO (sslv3 alert handshake failure)
Testing NULL-SHA...NO (sslv3 alert handshake failure)
Testing NULL-MD5...NO (sslv3 alert handshake failure
I can open the server-url in the emulator's browser and get a perfect json response so I know the system itself is capable.
So the question is, why can't I connect on Android 7 ?
Update:
I've looked at a captured packet using tcpdump and wireshark and the enabled ciphers are in the ClientHello, so that should not be a problem.
Cipher Suites (18 suites)
Cipher Suite: Unknown (0xcca9)
Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)
Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c)
Cipher Suite: Unknown (0xcca8)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)
Cipher Suite: TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (0x009e)
Cipher Suite: TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 (0x009f)
Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014)
Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0x0033)
Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0x0039)
Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c)
Cipher Suite: TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d)
Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)
Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035)
As you can see 0xc02f and 0xc030 match, but the next TLSv1.2 packet says: Alert (21), Handshake Failure (40).
Update 2:
These are the curves from Android 5.1 in the ClientHello:
Elliptic curves (25 curves)
Elliptic curve: sect571r1 (0x000e)
Elliptic curve: sect571k1 (0x000d)
Elliptic curve: secp521r1 (0x0019)
Elliptic curve: sect409k1 (0x000b)
Elliptic curve: sect409r1 (0x000c)
Elliptic curve: secp384r1 (0x0018)
Elliptic curve: sect283k1 (0x0009)
Elliptic curve: sect283r1 (0x000a)
Elliptic curve: secp256k1 (0x0016)
Elliptic curve: secp256r1 (0x0017)
Elliptic curve: sect239k1 (0x0008)
Elliptic curve: sect233k1 (0x0006)
Elliptic curve: sect233r1 (0x0007)
Elliptic curve: secp224k1 (0x0014)
Elliptic curve: secp224r1 (0x0015)
Elliptic curve: sect193r1 (0x0004)
Elliptic curve: sect193r2 (0x0005)
Elliptic curve: secp192k1 (0x0012)
Elliptic curve: secp192r1 (0x0013)
Elliptic curve: sect163k1 (0x0001)
Elliptic curve: sect163r1 (0x0002)
Elliptic curve: sect163r2 (0x0003)
Elliptic curve: secp160k1 (0x000f)
Elliptic curve: secp160r1 (0x0010)
Elliptic curve: secp160r2 (0x0011)
In the ServerHello secp384r1 (0x0018) is returned.
And this is from Android 7:
Elliptic curves (1 curve)
Elliptic curve: secp256r1 (0x0017)
Resulting in the Handshake Failure.
Changing the nginx configuration by removing secp384r1 or replacing it with the default (prime256v1) does get it to work. So I guess the question remains: am I able to add elliptic curves ?
The captured data is the same when using the emulator as when using an Android 7.0 device (General Mobile 4G).
Update 3:
Small update, but worth mentioning: I got it to work in the emulator using Android 7.1.1 (!). It shows the following data (again, grabbed using tcpdump and viewed using wireshark):
Elliptic curves (3 curves)
Elliptic curve: secp256r1 (0x0017)
Elliptic curve: secp384r1 (0x0018)
Elliptic curve: secp512r1 (0x0019)
It shows the same 18 Cipher Suites.
This is a known regression in Android 7.0, acknowledged by Google and fixed sometime before the release of Android 7.1.1. Here is the bug report: https://code.google.com/p/android/issues/detail?id=224438.
To be clear, the bug here is that 7.0 only supports ONE elliptic curve: prime256v1 aka secp256r1 aka NIST P-256, as Cornelis points out in the question. So if your users are facing this issue, these are the workarounds available to you (ignoring the fact that your users should ideally just upgrade to Android 7.1.1):
Configure your server to use the elliptic curve prime256v1. For example, in Nginx 1.10 you do this by setting ssl_ecdh_curve prime256v1;.
If that doesn't work, use older cipher suites that don't rely on elliptic-curve cryptography (e.g., DHE-RSA-AES256-GCM-SHA384) (make sure you understand what you're doing here in terms of data security)
NOTE: I am not an expert in elliptic-curve cryptography, make sure to do your own research on the security implications of my suggestions. Here are some other links I referred to while writing this answer:
https://community.letsencrypt.org/t/warning-android-7-0-clients-not-browsers-can-only-use-curve-prime256v1/23212
https://github.com/nodejs/node/issues/1495
I had the problem with a Self Signed Certificate and the issue was in the cipher which wasn't accepted by Android 7.0
I ran: openssl s_client -showcerts -connect <domain>:<port>
in the result I found:
Protocol : TLSv1
Cipher : DHE-RSA-AES256-SHA
I searched for the Android Equivalent of the Cipher and added it to my Retrofit Restadapter:
ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2)
.cipherSuites(
CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA)
.build();
clientBuilder.connectionSpecs(Collections.singletonList(spec));
From here every connection with the correct certificate pinning or a correct certificate but with the 'wrong' cipher according to Android 7.0 was accepted.
When looking back at this answer a year later I have to admit I'm still happy I posted it, on the other hand, if you are in the position to change the certificate to the correct Cypher suite please do this always over "downgrading" te accepted suites in your app. If you have to work with a self-signed certificate (for example for embedded) this is a working solution for you.
I have used this to fix "javax.net.ssl.SSLHandshakeException: Handshake failed" error and working fine for Android 7.0 and others version too.
put this in onCreate() method of application class.
fun initializeSSLContext(mContext: Context) {
try {
SSLContext.getInstance("TLSv1.2")
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
}
try {
ProviderInstaller.installIfNeeded(mContext.applicationContext)
} catch (e: GooglePlayServicesRepairableException) {
e.printStackTrace()
} catch (e: GooglePlayServicesNotAvailableException) {
e.printStackTrace()
}
}
Here you working solution for Volley:
before you create queue in singleton codes:
public class VolleyServiceSingleton {
private RequestQueue mRequestQueue;
private HurlStack mStack;
private VolleyServiceSingleton(){
SSLSocketFactoryExtended factory = null;
try {
factory = new SSLSocketFactoryExtended();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
final SSLSocketFactoryExtended finalFactory = factory;
mStack = new HurlStack() {
#Override
protected HttpURLConnection createConnection(URL url) throws IOException {
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) super.createConnection(url);
try {
httpsURLConnection.setSSLSocketFactory(finalFactory);
httpsURLConnection.setRequestProperty("charset", "utf-8");
} catch (Exception e) {
e.printStackTrace();
}
return httpsURLConnection;
}
};
mRequestQueue = Volley.newRequestQueue(YourApplication.getContext(), mStack, -1);
}
}
and here is SSLSocketFactoryExtended:
public class SSLSocketFactoryExtended extends SSLSocketFactory
{
private SSLContext mSSLContext;
private String[] mCiphers;
private String[] mProtocols;
public SSLSocketFactoryExtended() throws NoSuchAlgorithmException, KeyManagementException
{
initSSLSocketFactoryEx(null,null,null);
}
public String[] getDefaultCipherSuites()
{
return mCiphers;
}
public String[] getSupportedCipherSuites()
{
return mCiphers;
}
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException
{
SSLSocketFactory factory = mSSLContext.getSocketFactory();
SSLSocket ss = (SSLSocket)factory.createSocket(s, host, port, autoClose);
ss.setEnabledProtocols(mProtocols);
ss.setEnabledCipherSuites(mCiphers);
return ss;
}
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException
{
SSLSocketFactory factory = mSSLContext.getSocketFactory();
SSLSocket ss = (SSLSocket)factory.createSocket(address, port, localAddress, localPort);
ss.setEnabledProtocols(mProtocols);
ss.setEnabledCipherSuites(mCiphers);
return ss;
}
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException
{
SSLSocketFactory factory = mSSLContext.getSocketFactory();
SSLSocket ss = (SSLSocket)factory.createSocket(host, port, localHost, localPort);
ss.setEnabledProtocols(mProtocols);
ss.setEnabledCipherSuites(mCiphers);
return ss;
}
public Socket createSocket(InetAddress host, int port) throws IOException
{
SSLSocketFactory factory = mSSLContext.getSocketFactory();
SSLSocket ss = (SSLSocket)factory.createSocket(host, port);
ss.setEnabledProtocols(mProtocols);
ss.setEnabledCipherSuites(mCiphers);
return ss;
}
public Socket createSocket(String host, int port) throws IOException
{
SSLSocketFactory factory = mSSLContext.getSocketFactory();
SSLSocket ss = (SSLSocket)factory.createSocket(host, port);
ss.setEnabledProtocols(mProtocols);
ss.setEnabledCipherSuites(mCiphers);
return ss;
}
private void initSSLSocketFactoryEx(KeyManager[] km, TrustManager[] tm, SecureRandom random)
throws NoSuchAlgorithmException, KeyManagementException
{
mSSLContext = SSLContext.getInstance("TLS");
mSSLContext.init(km, tm, random);
mProtocols = GetProtocolList();
mCiphers = GetCipherList();
}
protected String[] GetProtocolList()
{
String[] protocols = { "TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"};
String[] availableProtocols = null;
SSLSocket socket = null;
try
{
SSLSocketFactory factory = mSSLContext.getSocketFactory();
socket = (SSLSocket)factory.createSocket();
availableProtocols = socket.getSupportedProtocols();
}
catch(Exception e)
{
return new String[]{ "TLSv1" };
}
finally
{
if(socket != null)
try {
socket.close();
} catch (IOException e) {
}
}
List<String> resultList = new ArrayList<String>();
for(int i = 0; i < protocols.length; i++)
{
int idx = Arrays.binarySearch(availableProtocols, protocols[i]);
if(idx >= 0)
resultList.add(protocols[i]);
}
return resultList.toArray(new String[0]);
}
protected String[] GetCipherList()
{
List<String> resultList = new ArrayList<String>();
SSLSocketFactory factory = mSSLContext.getSocketFactory();
for(String s : factory.getSupportedCipherSuites()){
Log.e("CipherSuite type = ",s);
resultList.add(s);
}
return resultList.toArray(new String[resultList.size()]);
}
}
in this codes I simple add all Ciphers that are supported by device, for me this works ), may be will help someone ) Cheers )
p.s. no need to add security network config parameter in manifest.
By default, secure connections (using protocols like TLS and HTTPS) from all apps trust the pre-installed system CAs, and apps targeting Android 6.0 (API level 23) and lower also trust the user-added CA store by default.
Which means that on Android Nougat (7.0), the game for CAs changed completely. If you have your key cert, you can add a network security configuration file (if you have your cert), as described here:
https://developer.android.com/training/articles/security-config.html
Or you can create your own TrustManager, as described here:
https://developer.android.com/training/articles/security-ssl.html#SelfSigned
Or you can enable cipher suites that your server demands but aren't enabled by default on Android N. For example, here are two ciphers that I needed to add in my own application talking to an old Windows CE server:
SSLSocket sslsock = (SSLSocket) createSocket();
List<String> cipherSuitesToEnable = new ArrayList<>();
cipherSuitesToEnable.add("SSL_RSA_WITH_RC4_128_SHA");
cipherSuitesToEnable.add("SSL_RSA_WITH_3DES_EDE_CBC_SHA");
sslsock.setEnabledCipherSuites(cipherSuitesToEnable.toArray(new String[cipherSuitesToEnable.size()]));
I spent 4 days around this problem and tried everything, the problem I had was with using LetsEncrypt (certbot etc.) to generate my certificates.
Once I switched to a different CA, Android 7.0 requests started coming through.
Same here. My Nginx server using sll_ecdh_curve prime384v1 setting. Unfortunately, backend guy not allowed me to configure the Nginx server following Vicky Chijwani's instructions due to Client's secirity policies. I was tried to use Valley and the latest version of the OkHttp library, but it did not help.
To bypass that bug, I had to use WebView to communicate with the API Service on Adroid 7.0 devices. Here is my Adapter class. I hope someone else will find it useful.
/**
* Connection to API service using WebView (for Android 7.0 devices)
*
* Created by fishbone on 09.08.17.
*/
#RequiresApi(api = Build.VERSION_CODES.N)
class WebViewHttpsConnection extends ApiConnection {
private static final long TIMEOUT = 30000;
private static final String POST_DATA_SCRIPT = "javascript:(function (){" +
"var xhr = new XMLHttpRequest();\n" +
"xhr.open(\"POST\", \"%1$s\", true);\n" +
"xhr.setRequestHeader(\"Content-type\", \"application/json\");\n" +
"xhr.onreadystatechange = function () {\n" +
" if (xhr.readyState === 4) {\n" +
" listener.onResult(xhr.status, xhr.responseText);\n" +
" }\n" +
"};\n" +
"xhr.send('%2$s');\n" +
"})();";
WebViewHttpsConnection(Context context) {
super(context);
}
/**
* Send data to API Service.
*
* #param url URL of API Service
* #param request JSON Object serialized into String
* #return API response
* #throws IOException errors
*/
#Override
public String sendData(final URL url, final String request) throws IOException {
// We should escape backslashes in JSON because JS unescape it back
final String javaScript = String.format(POST_DATA_SCRIPT, url.toString(),
request.replace("\\", "\\\\"));
final RequestResultListener listener = new RequestResultListener();
// We must use WebView only from 'main' Thread, therefore I using Handler with Application context
Handler handler = new Handler(getContext().getApplicationContext().getMainLooper());
handler.post(new Runnable() {
#SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface"}) // JavaScript is only for me and I'll use it only on Android 7.0 devices, so not scary
#Override
public void run() {
// WebView must be created, configured and called from the same Thread
final WebView webView = new WebView(getContext(), null);
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(listener, "listener");
webView.setWebViewClient(new WebViewClient() {
#Override
public void onPageFinished(WebView view, String url) {
// As soon as loaded any page from target domain, we call JS-script that will make POST request
webView.loadUrl(javaScript);
}
});
// I cant use postUrl() method because WebView doesn't allow to define 'Content-type' header, but my API service accepts only 'application/json' content type
// To complete CORS requirements we make any GET request to lets WebView navigate to the target domain, otherwise it will send 'null' as 'Origin' in headers
webView.loadUrl(url.toString());
}
});
// And further we waiting for response of API service
try {
if (!listener.latch.await(TIMEOUT, TimeUnit.MILLISECONDS)) {
throw new IOException("Timeout connection to server");
}
} catch (InterruptedException e) {
throw new IOException("Connection to server was interrupted");
}
if (listener.code != HttpURLConnection.HTTP_OK) {
throw new HttpRetryException("Server return error code " + listener.code,
listener.code);
}
if (TextUtils.isEmpty(listener.result)) {
throw new HttpRetryException("Service return empty response", listener.code);
}
return listener.result;
}
/**
* Callback interface for receiving API Service response from JavaScript inside WebView
*/
private static class RequestResultListener {
CountDownLatch latch = new CountDownLatch(1);
String result = null;
int code;
#JavascriptInterface
public void onResult(int code, String result) {
this.result = result;
this.code = code;
latch.countDown();
}
}
}
When using OkHttp you can use Conscrypt which supports this security provider out of the box.
Just install it with Security.insertProviderAt(Conscrypt.newProvider(), 1) and it works!
I ended up having to implement a similar solution to Nikolay, as none of the other solutions fixed the problem (I didn't have access to the API server, although the administrator did try to fix it without compromising his security).
I wrote my solution as a class that you should just be able to plugin to your app - as follows:
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.webkit.ValueCallback;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.google.gson.Gson;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class BackupAPIService
{
private static final String TAG = "BackupAPIService";
private static BackupAPIService sService = null;
// A context is required in order to run the webview.
private Context mContext;
// Headers for the request are stored here - they can be set, added to and removed from.
private HashMap<String, String> mHeaders = new HashMap<>();
// This sets the current location of the webview - it is probably best. although generally unnecessary to set this to the main page of the API.
// However not setting it at all will throw 'no access-control-allow-origin header is present' errors.
private String mOriginURL = "";
// Setting a base url to the domain of the API means that any requests don't need to include this beginning of the url,
// This must remain the same for all requests.
private String mBaseURL = "";
public static final int GET = 0, POST = 1;
// NOTE
// Please note that a singleton pattern has been used, assuming all calls are made to the same API.
// If you are using several APIs, you may wish to modify this service to give a separate BackupAPIService object for each.
// If you are using an unknown number of APIs, you may wish to make the constructor public and remove the static methods.
// The datatype returned by the API is assumed to be in JSON format - if you wish to change this, you will need to manually edit line 9 of the ajaxRequest String in the GenerateRequest method.
/**
* This returns the created BackupAPIService if it exists, and creates a new one otherwise. However the new one will need to be set up.
* #param context is the context in which to create the WebView. This can include the context from a service.
* #return the current instance of the BackupAPIService if it exists, otherwise it creates a new one.
*/
public static BackupAPIService getService(Context context)
{
if(sService == null)
{
sService = new BackupAPIService(context);
}
return sService;
}
/**
* This allows the headers to be set for this instance of the BackupAPIService.
* #param context is the context in which to create the WebView. This can include the context from a service.
* #param headers are the custom headers to be sent with this request. If these already exist in the service, they will be updated. Otherwise they will be added onto the list. Please note that String headers and values should be surrounded by single quotes, ie. 'header'. To reset the headers run ResetService.
* #return the current instance of the BackupAPIService if it exists, otherwise it creates a new one.
*/
public static BackupAPIService getService(Context context, HashMap<String, String> headers)
{
return getService(context).addHeaders(headers);
}
/**
* This allows the headers and the originURL to be set for this instance of the BackupAPIService.
* #param context is the context in which to create the WebView. This can include the context from a service.
* #param headers are the custom headers to be sent with this request. If these already exist in the service, they will be updated. Otherwise they will be added onto the list. Please note that String headers and values should be surrounded by single quotes, ie. 'header'. To reset the headers run ResetService.
* #param originURL is the URL from which the WebView will make its requests. This should be set, as the API will throw a 'no access-control-allow-origin header is present' error otherwise.
* #return the current instance of the BackupAPIService if it exists, otherwise it creates a new one.
*/
public static BackupAPIService getService(Context context, HashMap<String, String> headers, String originURL)
{
return getService(context).addHeaders(headers).addOriginURL(originURL);
}
/**
* This allows the headers and the originURL to be set for this instance of the BackupAPIService.
* #param context is the context in which to create the WebView. This can include the context from a service.
* #param headers are the custom headers to be sent with this request. If these already exist in the service, they will be updated. Otherwise they will be added onto the list. Please note that String headers and values should be surrounded by single quotes, ie. 'header'. To reset the headers run ResetService.
* #param originURL is the URL from which the WebView will make its requests. This should be set, as the API will throw a 'no access-control-allow-origin header is present' error otherwise.
* #param baseURL is the part of the API URL that never changes. This will allow you to only pass in the different URL endings as required, saving a lot of writing.
* #return the current instance of the BackupAPIService if it exists, otherwise it creates a new one.
*/
public static BackupAPIService getService(Context context, HashMap<String, String> headers, String originURL, String baseURL)
{
return getService(context).addHeaders(headers).addOriginURL(originURL).addBaseURL(baseURL);
}
/**
* This resets all of the parameters of the service to their defaults as desired.
* #param resetHeaders clears the headers if set to true.
* #param resetBaseURL clears the baseURL if set to true.
* #param resetOriginURL clears the originURL if set to true.
*/
public static void ResetService(boolean resetHeaders, boolean resetBaseURL, boolean resetOriginURL)
{
if(sService != null)
{
if(resetHeaders) sService.mHeaders = new HashMap<>();
if(resetBaseURL) sService.mBaseURL = "";
if(resetOriginURL) sService.mOriginURL = "";
}
}
/**
* This is the basic constructor for the API.
* #param context is the context in which to create the WebView. This can include the context from a service.
*/
private BackupAPIService(Context context)
{
this.mContext = context;
}
/**
* This allows the headers to be updated with the headers in the object that is sent. Existing headers in the list will be updated with their new values, and new headers will be added. Please note that String headers and values should be surrounded by single quotes, ie. 'header'. This can be done programmatically using the getCompatibleString method.
* #param headers are the headers being added.
* #return the current BackupAPIService object.
*/
public BackupAPIService addHeaders(HashMap<String, String> headers)
{
this.mHeaders.putAll(headers);
return this;
}
/**
* This allows a single header to be added if it does not exist, or updated if it does.
* #param key is the name of the header. Please note that String headers and values should be surrounded by single quotes, ie. 'header'.
* #param value is the value of the header as a String.
* #return the current BackupAPIService object.
*/
public BackupAPIService addHeader(String key, String value)
{
this.mHeaders.put(key, value);
return this;
}
/**
* This allows a single header to be removed if it exists in the service
* #param key is the name of the header
* #return the current BackupAPIService object
*/
public BackupAPIService removeHeader(String key)
{
this.mHeaders.remove(key);
return this;
}
/**
* This returns whether a variable with a given name exists in the Service
* #param key is the name of the header
* #return the current BackupAPIService object
*/
public boolean getHeaderExists(String key)
{
return mHeaders.containsKey(key);
}
/**
* This sets the originURL, which tells the API which website the request is coming from. If this is not set, the request is likely to be blocked.
* #param originURL is the URL stating which website the request is coming from: you may wish to set this to the main website address of your API.
* #return the current BackupAPIService object
*/
public BackupAPIService addOriginURL(String originURL)
{
mOriginURL = originURL;
return this;
}
/**
* This sets the baseURL. If there is a common URL beginning for all your API calls, you can set it here and the just send the rest of the address in your calls to the Post or Get methods.
* #param baseURL will be added onto the beginning of all of the API URL requests, allowing you to avoid having to write this each time, and send shorter requests.
* #return the current BackupAPIService object
*/
public BackupAPIService addBaseURL(String baseURL)
{
mBaseURL = baseURL;
return this;
}
/**
* This allows a post request to be sent, with the parameters as a hashmap. Please note that String parameters should take the format String param = "'{param}'", so that ajax recognises the String as a String - the getCompatibleString method can be used to automatically adds these single quotes to a given String.
* #param URL is the url of the request - this could just be the second half of the url, if the first half has been set as the baseURL.
* #param parameters is a hashmap of the parameters.
* #param ajaxHandler allows you to define your own custom response with the returned information.
*/
public void Post(String URL, HashMap<String, String> parameters, AjaxHandler ajaxHandler)
{
Launch(POST, URL, ajaxHandler, parameters);
}
/**
* This allows a get request to be sent.
* #param URL is the url of the request - this could just be the second half of the url, if the first half has been set as the baseURL.
* #param ajaxHandler allows you to define your own custom response with the returned information.
*/
public void Get(String URL, AjaxHandler ajaxHandler)
{
Launch(GET, URL, ajaxHandler);
}
/**
* This is used by the public Post and Get methods to launch a request.
* #param launchType is defined as either post or get by the POST and GET static constants.
* #param URL is the url of the request - this could just be the second half of the url, if the first half has been set as the baseURL.
* #param ajaxHandler allows you to define your own custom response with the returned information.
* #param parameters is an optional hashmap of the parameters for a post request.
*/
private void Launch(final int launchType, final String URL, final AjaxHandler ajaxHandler, final HashMap<String, String> ... parameters)
{
// This piece of code is required in order to allow the WebView to run from a service without throwing errors
Handler handler = new Handler(Looper.getMainLooper());
try
{
handler.post(
new Runnable()
{
#Override
public void run()
{
GenerateRequest(launchType, URL, ajaxHandler, parameters);
}
});
} catch (Exception e)
{
e.printStackTrace();
}
}
/**
* This method generates the actual request.
* #param launchType is defined as either post or get by the POST and GET static constants.
* #param URL is the url of the request - this could just be the second half of the url, if the first half has been set as the baseURL.
* #param ajaxHandler allows you to define your own custom response with the returned information.
* #param parameters is an optional hashmap of the parameters for a post request.
*/
private void GenerateRequest(int launchType, String URL, AjaxHandler ajaxHandler, HashMap<String, String> ... parameters)
{
String importAjax = "<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script>";
String customiseAjaxHeaders = "$.ajaxSetup({headers: { ";
for (Map.Entry<String, String> entry : mHeaders.entrySet())
{
customiseAjaxHeaders += entry.getKey() + ": " + entry.getValue() + ", ";
}
customiseAjaxHeaders = customiseAjaxHeaders.substring(0, customiseAjaxHeaders.length()-2) + "}});";
String postParameters = "";
if(parameters.length>0)
{
for (Map.Entry<String, String> entry : parameters[0].entrySet())
{
postParameters += entry.getKey() + ": " + entry.getValue() + ", ";
}
postParameters = postParameters.substring(0, postParameters.length()-2);
}
//String origin = "'app.cleopatra.im'";
String requestAddress = "'"+mBaseURL + URL + "'";
String requestType = "Get";
if(launchType == POST) requestType = "Post";
String ajaxRequest = customiseAjaxHeaders + " var saveData = " +
"$.ajax" +
"(" +
"{" +
"type: '" + requestType + "'," +
"url: " + requestAddress + ", " +
"data: " +
"{" + postParameters + "}," +
"dataType: 'json'," +
"success: function(data)" +
"{" +
"ajaxHandler.handleResults(JSON.stringify(data));" + // This runs the ajax handler created below when the handler successfully returns data
"}," +
"error:function(request, status)" +
"{" +
"ajaxHandler.handleFailure('Request Failed: ' + JSON.stringify(request) + ' due to: ' + JSON.stringify(status));" + // This runs the ajax handler created below when the handler unsuccessfully returns data
"}" +
"}" +
");";
CreateRequestThroughWebView(importAjax, ajaxRequest, ajaxHandler);
}
/**
* This is used to create the generated request through a webview object
* #param content is the html content of the webview - in this implementation it is currently just an import script for JQuery
* #param request is the Ajax request script to be run on the webview
* #param ajaxHandler allows you to define your own custom response with the returned information.
*/
private void CreateRequestThroughWebView(String content, final String request, AjaxHandler ajaxHandler)
{
Log.i(TAG, "Content: " + content + "\nRequest: " + request);
HashMap<String, String> headers = new HashMap<>();
// create the new webview - this can run invisibly
WebView webView = new WebView(mContext);
webView.getSettings().setAllowUniversalAccessFromFileURLs(true);
// This creates a webpage at the expected location, which can be accept AJAX commands
webView.loadDataWithBaseURL(mOriginURL, content, "text/html; charset=utf-8", "utf-8", mOriginURL);
// Allow JavaScript to run on the page
webView.getSettings().setJavaScriptEnabled(true);
// Add a JavaScript interface allowing completed AJAX requests to run Java methods
webView.addJavascriptInterface(ajaxHandler, "ajaxHandler");
// override onPageFinished method of WebViewClient to handle AJAX calls
webView.setWebViewClient(new WebViewClient()
{
#Override
public void onPageFinished(WebView view, String url)
{
super.onPageFinished(view, url);
// Run the JavaScript command once the page has loaded
view.evaluateJavascript(request, new ValueCallback<String>()
{
#Override
public void onReceiveValue(String s)
{
Log.i(TAG, "Request Completed: " + s);
}
});
}
});
}
/**
* This adds additional single quotes to enclose a string representing a header name, header value, variable name or variable value, so it is still recognised as a String when it is passed into Ajax.
* #param string is the header name, header value, variable name or variable value to be modified.
* #return an ajax compatible String.
*/
public static String getCompatibleString(String string)
{
return "'" + string + "'";
}
/**
* This adds additional single quotes to enclose two strings representing either a String header name and its value, or posted variable name and its value, so that they are both still recognised as Strings when they are passed into Ajax.
* #param name is the header or variable name, header value, variable name or variable value to be modified.
* #param value is the header or variable value to be modified.
* #return an ajax compatible HashMap which can also be added into any existing HashMaps as necessary.
*/
public static HashMap<String, String> getCompatibleHashMapEntry(String name, String value)
{
HashMap<String, String> result = new HashMap<String, String>();
result.put(getCompatibleString(name), getCompatibleString(value));
return result;
}
/**
* This adds additional single quotes to enclose an arbitrary number of two string arrays, each representing String header name and its value, or a posted variable name and its value so that they are both still recognised as Strings when they are passed into Ajax. This is returned as a hashmap ready to be set as the necessary headers or variables for the request.
* #param nameValuePairs is the header or variable name, header value, variable name or variable value to be modified.
* #return an ajax compatible HashMapEntry.
*/
public static HashMap<String, String> getCompatibleHashMap(String[] ... nameValuePairs)
{
HashMap<String, String> results = new HashMap<>();
for (String[] nameValuePair: nameValuePairs)
{
results.put(getCompatibleString(nameValuePair[0]), getCompatibleString(nameValuePair[1]));
}
return results;
}
/**
* This deals with the results of the Ajax API request.
* The handleResults method of this abstracted Ajax Javascript Interface should be implemented in order to define your app's behaviour when the request completes.
* The ConvertResultToObject method can also be used within the handleResults method in order to convert the received json string into the corresponding class.
* The handleFailure method is called if the Ajax request fails.
*/
public abstract static class AjaxHandler
{
private static final String TAG = "ajaxHandler";
private final Context context;
public AjaxHandler(Context context)
{
this.context = context;
}
/**
* Overwrite this method to handle the response to your request
* #param results is a String representation of the result from the WebView Query
*/
#android.webkit.JavascriptInterface
public abstract void handleResults(String results);
/**
* Overwrite this method to handle any failed requests
* #param message is a String representation of the failure message from the WebView Query
*/
#android.webkit.JavascriptInterface
public abstract void handleFailure(String message);
/**
* This should generate a predefined class object from a JSON response. It was not used in the final implementation, so it has not been tested and may need tweaking
* #param json is the json representation of the class
* #param classOfT is the class of the object to be populated
* #return an object of class T created from the JSON string
*/
public <T> T ConvertResultToObject(String json, Class<T> classOfT)
{
Gson gson = new Gson();
return gson.fromJson(json, classOfT);
}
}
}
An example implementation would be as follows:
// Example Ajax Implementation
// Setup basic variables
// Set up context
Context context = this.context;
// Set the beginning of the URL which is the same for all API requests
String API_URL = "https://mywebsite.com/myapi/";
// Set up the url from which requests should originate
String BASE_URL = "www.mywebsite.com";
// Set up variables to pass into the requests
// Header names and values
String API_KEY_NAME = "{API_KEY_NAME}";
String API_KEY_VALUE = "{API_KEY_VALUE}";
// Parameter names and values
String USER_NAME = "User";
String USER_VALUE = "{User name}";
String PASSWORD_NAME = "Password";
String PASSWORD_VALUE = "{User Password}";
String USER_ID_NAME = "User_id";
int USER_ID_VALUE = 7;
// Note that Strings need an extra set of single quotes to be recognised as a String when passed into Ajax. Methods have been provided which do that.
// Get the headers as a HashMap, and convert both name and value to an Ajax compatible String
HashMap<String, String> headers = BackupAPIService.getCompatibleHashMapEntry(API_KEY_NAME, API_KEY_VALUE);
// Get the parameters as a HashMap, and convert the appropriate names and values to Ajax compatible Strings
HashMap<String, String> parameters = BackupAPIService.getCompatibleHashMap(new String[] {USER_NAME, USER_VALUE}, new String[] {PASSWORD_NAME, PASSWORD_VALUE});
// Add the parameters where the values are not Strings
parameters.put(BackupAPIService.getCompatibleString(USER_ID_NAME), USER_ID_VALUE+"");
// Create or get service
BackupAPIService backupService = BackupAPIService.getService(context, headers, BASE_URL, API_URL);
// Post a request with the necessary addition to the API url set earlier for this request, the parameters set above, and an implementation of AjaxHandler which handles the results and failures as you desire
backupService.Post("user", parameters, new BackupAPIService.AjaxHandler(context){
#Override
public void handleResults(String results)
{
Log.e(TAG, "Success!! " + results);
}
#Override
public void handleFailure(String message)
{
Log.e(TAG, "Failure :( " + message);
}
});
Related
Background
SSLv3 protocol is insecure,after i read some articles, i use this solution to remove this protocol.
The method remove sslv3:
#Override
public void setEnabledProtocols(String[] protocols) {
if (protocols != null && protocols.length == 1 && "SSLv3".equals(protocols[0])) {
// no way jose
// see issue https://code.google.com/p/android/issues/detail?id=78187
List<String> enabledProtocols = new ArrayList<String>(Arrays.asList(delegate.getEnabledProtocols()));
for (String pro : enabledProtocols) {
VolleyLog.d(pro);
}
if (enabledProtocols.size() > 1) {
enabledProtocols.remove("SSLv3");
VolleyLog.d("Removed SSLv3 from enabled protocols");
} else {
VolleyLog.d("SSL stuck with protocol available for " + String.valueOf(enabledProtocols));
}
protocols = enabledProtocols.toArray(new String[enabledProtocols.size()]);
}
super.setEnabledProtocols(protocols);
}
I use Volley as http client, here is my code to initialize a requestqueue:
HttpStack stack;
if (Build.VERSION.SDK_INT >= 9) {
// Use a socket factory that removes sslv3
// https://code.google.com/p/android/issues/detail?id=78187
stack = new HurlStack(null, new NoSSLv3Compat.NoSSLv3Factory());
} else {
// Prior to Gingerbread, HttpUrlConnection was unreliable.
// See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
}
Device and Environment
I am using Xiaomi M3 with MIUI ROM, which is based on Android 4.4.4.
When the setEnabledProtocols method is called, i print some log:
D/Volley: [1444] NoSSLv3SSLSocket.setEnabledProtocols: SSLv3
D/Volley: [1444] NoSSLv3SSLSocket.setEnabledProtocols: TLSv1
D/Volley: [1444] NoSSLv3SSLSocket.setEnabledProtocols: Removed SSLv3 from enabled protocols
Problem
When i try to load this image, failed, output:
NoConnectionError: javax.net.ssl.SSLHandshakeException:
javax.net.ssl.SSLProtocolException: SSL handshake terminated:
ssl=0x77f49768: Failure in SSL library, usually a protocol error
E/CachedHttp: error:1409443E:SSL routines:SSL3_READ_BYTES:tlsv1 alert
inappropriate fallback (external/openssl/ssl/s3_pkt.c:1256
0x77f4c280:0x00000003)
this image server supports the following protocols:
TLS 1.2、 TLS 1.1、 TLS 1.0、 SSL 3
Could you please help me to figure it out?
Did you check the size of keys. Enable debug log to see the exact issue. Probably is being caused by insufficient key size by the backend you are trying to connect to.
Enable JCE Unlimited for java 7
Identify handshake errors
I am trying to test ssl mutual certification of android webview.
I setup an environment as below and tested via PC web browser, everything is OK. But I met the signature related issue during android webview test.
Can anybody help analyze which part of my test could be the cause?
The test environment:
Android SDK 5.1 (since client certificate event callback API is opened since 5.0)
tomcat 8.0 https server, the type of server keystore is jks.
client keystore type is PKCS12, Both client private key and ca trust certificate(self-signed) are save in it.
I customize WebviewClient which code is shown below.
public class SSLWebViewClient extends WebViewClient {
private X509Certificate[] certificatesChain;
private PrivateKey clientCertPrivateKey;
private InputStream certfile_p12;
private String certfile_password = "";
private Context context;
public SSLWebViewClient(Context context) throws Exception {
super();
this.context = context;
initPrivateKeyAndX509Certificate();
}
private void initPrivateKeyAndX509Certificate() throws Exception {
KeyStore keyStore;
certfile_password = "123456";
certfile_p12 = context.getResources().getAssets().open("client.p12");
keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(certfile_p12, certfile_password.toCharArray());
clientCertPrivateKey = (PrivateKey) keyStore.getKey("client", certfile_password.toCharArray());
certificatesChain = new X509Certificate[1];
certificatesChain[0] = (X509Certificate)keyStore.getCertificate("server");
}
#Override
public void onReceivedClientCertRequest(WebView view, ClientCertRequest handler) {
if((null != clientCertPrivateKey) && ((null!=certificatesChain) && (certificatesChain.length !=0))){
handler.proceed(this.clientCertPrivateKey, this.certificatesChain);
}else{
handler.cancel();
}
}
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
#Override
public void onReceivedSslError(final WebView view, SslErrorHandler handler,
SslError error) {
handler.proceed();
}
}
The handshake procedure I captured by ssldump is as follows.
2 1 0.0094 (0.0094) C>S Handshake ClientHello
2 2 0.0369 (0.0274) S>C Handshake ServerHello
Certificate
ServerKeyExchange
CertificateRequest
Not enough data. Found 266 bytes (expecting 32767)
ServerHelloDone
2 0.1244 (0.0874) C>S TCP FIN
2 0.1247 (0.0003) S>C TCP FIN
New TCP connection #3: 222.130.170.32(17617) <-> worknode(8443)
3 1 0.0074 (0.0074) C>S Handshake ClientHello
3 2 0.0373 (0.0299) S>C Handshake ServerHello
Certificate
ServerKeyExchange
CertificateRequest
Not enough data. Found 266 bytes (expecting 32767)
ServerHelloDone
3 3 0.1089 (0.0715) C>S Handshake Certificate
3 4 0.1089 (0.0000) C>S Handshake ClientKeyExchange
3 5 0.1089 (0.0000) C>S Handshake CertificateVerify
Not enough data. Found 258 bytes (expecting 16384)
3 6 0.1089 (0.0000) C>S ChangeCipherSpec
3 7 0.1089 (0.0000) C>S Handshake
3 8 0.1514 (0.0424) S>C Alert fatal bad_certificate
3 0.1514 (0.0000) S>C TCP FIN
3 0.3448 (0.1933) C>S TCP FIN
I notice that there are twice of ClientHello exists.
From the tomcat log, I find the first round of handshake is ended at
*** ServerHelloDone
http-nio-8443-exec-2, WRITE: TLSv1.2 Handshake, length = 1683
http-nio-8443-exec-4, called closeOutbound()
http-nio-8443-exec-4, closeOutboundInternal()
http-nio-8443-exec-4, SEND TLSv1.2 ALERT: warning, description = close_notify
http-nio-8443-exec-4, WRITE: TLSv1.2 Alert, length = 2
The second round handshake is ended at
*** CertificateVerify
Signature Algorithm SHA512withRSA
http-nio-8443-exec-6, fatal error: 42: certificate verify message signature error
javax.net.ssl.SSLHandshakeException: certificate verify message signature error
%% Invalidated: [Session-29, TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA]
http-nio-8443-exec-6, SEND TLSv1.2 ALERT: fatal, description = bad_certificate
http-nio-8443-exec-6, WRITE: TLSv1.2 Alert, length = 2
http-nio-8443-exec-6, fatal: engine already closed. Rethrowing javax.net.ssl.SSLHandshakeException: certificate verify message signature error
The detailed log of tomcat is posted on the link below.
https://raw.githubusercontent.com/watanuoli/ssllog/master/README.md
Well its about 5 years old but I had the same issue.
This line is important:
%% Invalidated: [Session-29, TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA]
Whereat TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA is a ciphre!
This line means that the cipher has been invalidated by an server internal decision!
So the request is incomming from the browser to the server, the certificates are shared and the server decide - no matter if the certificate is valid, no matter if the chain-of-trust is valid no matter if access is granted or not, to reject the connection because the cipher has been invalidated by the server's internal configuration.
In order to allow this chiphre to be valid for data-transfer, add this chiphre to the allowed ciphers in the server's configuration.
For Apache Tomcat you have to edit the server.xml. The xml-node Server->Service->Connector(usually port 443)->SSLHostConfig has a xml-attribute called ciphers, add your invalidated cipher to this xml-attribute (,-seperated).
I guess you have to restart the server then.
i am constantly monitoring my app errors and I see the following error too many times
javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0xb8f0fc28: Failure in SSL library, usually a protocol error
error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure (external/openssl/ssl/s23_clnt.c:741 0xaa48cd5c:0x00000000)-javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0xb8f0fc28: Failure in SSL library, usually a protocol error
error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure (external/openssl/ssl/s23_clnt.c:741 0xaa48cd5c:0x00000000)
You can see the the error is about SSLV3 and my server support only TLSV1.2.
It seems like that on some clients Volley falls back to use SSLV3 (for some reason) and they get an error.
Users that get this error are on Android 4.4.2, 4.4.4 and 4.1.1 and more.
Interestingly enough I also use DefaultHttpClient in the same application, but it does not seem to report the same issue.
I am using the default HurlStack in Volley
I have seen the following...
Disable SSL as a protocol in HttpsURLConnection
and
https://code.google.com/p/android/issues/detail?id=78187
So what are my options?
Is my assumption correct that Volley falls back to SSLV3?
Why does volley fallback to SSLV3? In other words, what was the original failure that caused the fallback and how to resolve it?
i I downloaded Volley recently, but I am not sure it is the latest. How do I find which version I have?.
Any thoughts?
Your server does well not supporting SSLv3 since it has some security issues and should not be used.
When using Android versions prior to Kitkat you must use a socket factory that removes SSLv3 to be used as default configuration:
public class VolleyToolboxExtension extends Volley {
/** Default on-disk cache directory. */
private static final String DEFAULT_CACHE_DIR = "volley";
/**
* Creates a default instance of the worker pool and calls {#link RequestQueue#start()} on it.
*
* #param context A {#link Context} to use for creating the cache dir.
* #param stack An {#link HttpStack} to use for the network, or null for default.
* #return A started {#link RequestQueue} instance.
*/
public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
String userAgent = "volley/0";
try {
String packageName = context.getPackageName();
PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
userAgent = packageName + "/" + info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
}
if (stack == null) {
if (Build.VERSION.SDK_INT >= 9) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
// Use a socket factory that removes sslv3
stack = new HurlStack(null, new NoSSLv3Compat.NoSSLv3Factory());
} else {
stack = new HurlStack();
}
} else {
// Prior to Gingerbread, HttpUrlConnection was unreliable.
// See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
}
}
Network network = new BasicNetwork(stack);
RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
queue.start();
return queue;
}
/**
* Creates a default instance of the worker pool and calls {#link RequestQueue#start()} on it.
*
* #param context A {#link Context} to use for creating the cache dir.
* #return A started {#link RequestQueue} instance.
*/
public static RequestQueue newRequestQueue(Context context) {
return newRequestQueue(context, null);
}
}
NoSSLv3Compat class can be found here:
https://github.com/Floens/volley/blob/master/src/com/android/volley/compat/NoSSLv3Compat.java
Use this extension to create your request queue:
/**
* #return The Volley Request queue, the queue will be created if it is null
*/
public RequestQueue getRequestQueue() {
// lazy initialize the request queue, the queue instance will be
// created when it is accessed for the first time
if (mRequestQueue == null) {
// Create the request queue
mRequestQueue = VolleyToolboxExtension.newRequestQueue(getApplicationContext());
}
return mRequestQueue;
}
You could also use Retrofit instead of Volley, since Square released the 2.1 version of this library that supports TLS version configuration:
http://square.github.io/retrofit/
I'm trying to implement a Client Certificates communication for an Android App, so far without much success - and it seems that this feature is, if at all possible, very hard. The full flow I'm implementing is described in my previous question.
I followed the code there and code from this blog post, describing the same scenario, more or less, without results.
What doesn't work: Opening an SSL Connection (HttpsURLConnection) between the Android Client and the Server causes the server to return an 403 status code.
AFAIK, this 403 is because the server doesn't get or doesn't trust the Client Certificate that it gets, and I'm not sure how to debug it.
What does work:
Creating a PKCS#10 request, sending it to the CA and getting a signed PKCS#7 (P7B)
Storing the received P7B with the private key in a KeyStore, and exporting it to a PKCS#12 (P12)
(Most annonying) picking the P12 from the device, installing it on windows, contacting the server and getting a coherent (200 HTTP-OK) response.
What I've changed: From the code samples I got (from here and here), I had to change a few things. I'm using HttpsURLConnection and not OkHttpClient as #Than used there (but it shouldn't matter), I can't provide the Certificates as Rich Freedman did (he had the certificate, and I'm obtaining it via PKCS#10 and #7), so I've created a CustomTrustManager that would trust the server's certificate, and for this reason I use SpongyCastle (v1.5.0.0 if it matters, set as a provider inserted at 0) and also don't persist the certificate, but all is done in-memory.
Question is what to do next:
How can I tell what the server is expecting (client-certificate wise)?
How can I tell which client certificates (if any) is being sent to the server?
How to debug this scenario in general? (Proxies such as Fiddler are useless for the underlying SSL)
Thanks!
It's not good answer, but there is too much in here to post it as comment.
For logging, debugging you can create your own X509KeyManager which uses normal key manager obtained from KeyManagerFactory:
#DebugLog annotation comes from Hugo library created by Jake Wharton. It prints function arguments and what it return. You can use normal Log.d or whatever you want.
ex:
class MyKeyManager implements X509KeyManager {
private final X509KeyManager keyManager;
MyKeyManager(X509KeyManager keyManager) {
this.keyManager = keyManager;
}
#DebugLog
#Override
public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
return this.keyManager.chooseClientAlias(strings, principals, socket);
}
#DebugLog
#Override
public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
return keyManager.chooseServerAlias(s, principals, socket);
}
#DebugLog
#Override
public X509Certificate[] getCertificateChain(String s) {
return keyManager.getCertificateChain(s);
}
#DebugLog
#Override
public String[] getClientAliases(String s, Principal[] principals) {
return keyManager.getClientAliases(s, principals);
}
#DebugLog
#Override
public String[] getServerAliases(String s, Principal[] principals) {
return keyManager.getServerAliases(s, principals);
}
#DebugLog
#Override
public PrivateKey getPrivateKey(String s) {
return keyManager.getPrivateKey(s);
}
}
And use it to init SSLContext
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, password);
final X509KeyManager origKm = (X509KeyManager) kmf.getKeyManagers()[0];
X509KeyManager km = new MyKeyManager(origKm);
SSLContext sslCtx = SSLContext.getInstance("TLS");
sslCtx.init(new KeyManager[]{km}, tmf.getTrustManagers(), null);
You will see which method are called, what are the arguments (obtained from serwer certificate) and which certificate and private key your keymanager returns.
I want to disable GZipContent for a Google Cloud Endpoints class so that a POST can work on the local development server.
The latest GPE release generates this endpoint builder:
public static final class Builder extends AbstractGoogleJsonClient.Builder {
public Builder(HttpTransport transport, JsonFactory jsonFactory,
HttpRequestInitializer httpRequestInitializer) {
super(
transport,
jsonFactory,
...);
}
and Google documentation recommends using it like this:
Myendpoint.Builder endpointBuilder = new Myendpoint.Builder(
AndroidHttp.newCompatibleTransport(),
new GsonFactory(),
credential);
Does anyone know how to disable GZipContent for the endpoint?
Thanks.
You can use:
builder.setGoogleClientRequestInitializer(new TictactoeRequestInitializer() {
protected void initializeTictactoeRequest(TictactoeRequest<?> request) {
request.setDisableGZipContent(true);
}
});
Replace TictactoeRequest with the appropriate class for your application.
I'm not 100% why Dan's answer didn't work for me, but this GitHub project's code solved it for me.
/*
* TODO: Need to change this to 'true' if you're running your backend locally using
* the DevAppServer. See
* http://developers.google.com/eclipse/docs/cloud_endpoints for more
* information.
*/
protected static final boolean LOCAL_ANDROID_RUN = false;
/*
* The root URL of where your DevAppServer is running (if you're running the
* DevAppServer locally).
*/
protected static final String LOCAL_APP_ENGINE_SERVER_URL = "http://localhost:8080/";
/*
* The root URL of where your DevAppServer is running when it's being
* accessed via the Android emulator (if you're running the DevAppServer
* locally). In this case, you're running behind Android's virtual router.
* See
* http://developer.android.com/tools/devices/emulator.html#networkaddresses
* for more information.
*/
protected static final String LOCAL_APP_ENGINE_SERVER_URL_FOR_ANDROID = "http://10.0.2.2:8080";
/**
* Updates the Google client builder to connect the appropriate server based
* on whether LOCAL_ANDROID_RUN is true or false.
*
* #param builder Google client builder
* #return same Google client builder
*/
public static <B extends AbstractGoogleClient.Builder> B updateBuilder(
B builder) {
if (LOCAL_ANDROID_RUN) {
builder.setRootUrl(LOCAL_APP_ENGINE_SERVER_URL_FOR_ANDROID
+ "/_ah/api/");
}
// only enable GZip when connecting to remote server
final boolean enableGZip = builder.getRootUrl().startsWith("https:");
builder.setGoogleClientRequestInitializer(new GoogleClientRequestInitializer() {
public void initialize(AbstractGoogleClientRequest<?> request)
throws IOException {
if (!enableGZip) {
request.setDisableGZipContent(true);
}
}
});
return builder;
}
For those that are Googling for the error I saw, it was java.io.EOFException, but only in the development server. Here's how I was able to fix this, using the example stated in the OP's question:
Myendpoint myendpointClient = new Myendpoint.Builder(
AndroidHttp.newCompatibleTransport(),
new GsonFactory(),
credential).build();
EndpointService svcCall = myendpointClient.endpointService("firstArg");
// Note, I didn't call "execute()", as normal!
svcCall.setDisableGZipContent(true);
// This is also a handy place to set http headers, etc
svcCall.getRequestHeaders().set("x-oddballhdr","OddballValue");
// It's now time to call execute()
svcCall.execute();
That may be a bit simpler than the other helpful answers.