Scenario

I am currently working on a software for a client, in which there are multiple Android devices involved. One of them works as a server while the other devices work as clients, fetching and pushing data from and to the server to keep all the data in sync. The software is supposed to be deployed on trusted devices in a trusted wireless network specifically set up for this application. Nevertheless, experience tells that in reality, it probably will be used in wireless networks which are not isolated.

The software is to be used at event locations and may not require internet connection at any time. It should work with any set of Android (4.0+) devices with having the app installed being the only prerequisite, wheras the same app should work as both client and server. The software should assume that the connection between the devices might be broken at any time and configuration should be simple for non-technical people.

As I'm pretty sure I'm not the only one having this requirements for an Android app project, I'm gonna talk a bit about how I've done it.

Implementation idea

The protocol used between the Android devices is HTTP. As I must assume that the app is being used in unisolated WiFi networks, the communication has to be SSL encrypted.

It is quite easy to run a Jetty server inside an Android app1, and it is also possible to use SSL encryption with Jetty. However, all documentation and examples on this I managed to find, were suggesting creating a SSL cert with keytool on your computer, storing it in a BKS keystore and shipping it with your application, or, having your users do this and letting them specify a path on the SD card to the keystore.

Neither of those is a real option for me: I cannot assume any of my users ever will create his own certificate with keytool and I also cannot ship a hard-coded private key with my application, as the app itself might not be considered a secret and using SSL with known keys is not even slightly better than not using encryption at all. Therefore, I must generate the SSL key and certificate on each device seperately on the first use of the app. I will present my code for this in the next section.

After the certificate is generated, the clients need to know the certificate's fingerprint: If the client would just accept any certificate, I could have avoided all the key generation work above, because a client accepting all certificates is nearly as bad as shipping the same keys on every device. As the system has to work offline and ad-hoc, there is no way to use something like a CA infrastructure.

Luckily, there is an elegant way to solve both the certificate and the configuration problem at once: The server device shows a QR code containing ip address, port and SSL fingerprint of the server as well as an authentication token (being in a public network, we want both encryption and authentication). The client just has to scan this QR code and gets all informaction necessary for etablishing a secure connection.

Implementation details

Dependencies

This effort has introduced a bunch of new dependencies to my application

  • My webserver component is built on Jetty 8.1.15, of which I created my own jar bundle (using jar xf and jar cf) containing:
    • jetty-continuation-8.1.15.v20140411.jar
    • jetty-http-8.1.15.v20140411.jar
    • jetty-io-8.1.15.v20140411.jar
    • jetty-security-8.1.15.v20140411.jar
    • jetty-server-8.1.15.v20140411.jar
    • jetty-servlet-8.1.15.v20140411.jar
    • jetty-util-8.1.15.v20140411.jar
    • jetty-webapp-8.1.15.v20140411.jar
    • jetty-xml-8.1.15.v20140411.jar
    • servlet-api-3.0.jar
  • Bouncycastle's bcprov-jdk15on-146.jar for keystore handling
  • Apache Commons Codec for fingerprinting keys
  • ZXing's core.jar for QR code generation or David Lazaro's wonderful QRCodeReaderView for easy QR code scanning (already includes core.jar)

Key generation

The hardest part was generating the keypair and certificate. I've got2 some3 inspiration4 from the web, but as I did not find an example ready to work on Android, here's mine:

/**
 * Creates a new SSL key and certificate and stores them in the app's
 * internal data directory.
 * 
 * @param ctx
 *            An Android application context
 * @param keystorePassword
 *            The password to be used for the keystore
 * @return boolean indicating success or failure
 */
public static boolean genSSLKey(Context ctx, String keystorePassword) {
    try {
        // Create a new pair of RSA keys using BouncyCastle classes
        RSAKeyPairGenerator gen = new RSAKeyPairGenerator();
        gen.init(new RSAKeyGenerationParameters(BigInteger.valueOf(3),
                new SecureRandom(), 1024, 80));
        AsymmetricCipherKeyPair keypair = gen.generateKeyPair();
        RSAKeyParameters publicKey = (RSAKeyParameters) keypair.getPublic();
        RSAPrivateCrtKeyParameters privateKey = (RSAPrivateCrtKeyParameters) keypair
                .getPrivate();

        // We also need our pair of keys in another format, so we'll convert
        // them using java.security classes
        PublicKey pubKey = KeyFactory.getInstance("RSA").generatePublic(
                new RSAPublicKeySpec(publicKey.getModulus(), publicKey
                        .getExponent()));
        PrivateKey privKey = KeyFactory.getInstance("RSA").generatePrivate(
                new RSAPrivateCrtKeySpec(publicKey.getModulus(), publicKey
                        .getExponent(), privateKey.getExponent(),
                        privateKey.getP(), privateKey.getQ(), privateKey
                                .getDP(), privateKey.getDQ(), privateKey
                                .getQInv()));

        // CName or other certificate details do not really matter here
        X509Name x509Name = new X509Name("CN=" + CNAME);

        // We have to sign our public key now. As we do not need or have
        // some kind of CA infrastructure, we are using our new keys
        // to sign themselves

        // Set certificate meta information
        V3TBSCertificateGenerator certGen = new V3TBSCertificateGenerator();
        certGen.setSerialNumber(new DERInteger(BigInteger.valueOf(System
                .currentTimeMillis())));
        certGen.setIssuer(new X509Name("CN=" + CNAME));
        certGen.setSubject(x509Name);
        DERObjectIdentifier sigOID = PKCSObjectIdentifiers.sha1WithRSAEncryption;
        AlgorithmIdentifier sigAlgId = new AlgorithmIdentifier(sigOID,
                new DERNull());
        certGen.setSignature(sigAlgId);
        ByteArrayInputStream bai = new ByteArrayInputStream(
                pubKey.getEncoded());
        ASN1InputStream ais = new ASN1InputStream(bai);
        certGen.setSubjectPublicKeyInfo(new SubjectPublicKeyInfo(
                (ASN1Sequence) ais.readObject()));
        bai.close();
        ais.close();

        // We want our keys to live long
        Calendar expiry = Calendar.getInstance();
        expiry.add(Calendar.DAY_OF_YEAR, 365 * 30);

        certGen.setStartDate(new Time(new Date(System.currentTimeMillis())));
        certGen.setEndDate(new Time(expiry.getTime()));
        TBSCertificateStructure tbsCert = certGen.generateTBSCertificate();

        // The signing: We first build a hash of our certificate, than sign
        // it with our private key
        SHA1Digest digester = new SHA1Digest();
        AsymmetricBlockCipher rsa = new PKCS1Encoding(new RSAEngine());
        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
        DEROutputStream dOut = new DEROutputStream(bOut);
        dOut.writeObject(tbsCert);
        byte[] signature;
        byte[] certBlock = bOut.toByteArray();
        // first create digest
        digester.update(certBlock, 0, certBlock.length);
        byte[] hash = new byte[digester.getDigestSize()];
        digester.doFinal(hash, 0);
        // and sign that
        rsa.init(true, privateKey);
        DigestInfo dInfo = new DigestInfo(new AlgorithmIdentifier(
                X509ObjectIdentifiers.id_SHA1, null), hash);
        byte[] digest = dInfo.getEncoded(ASN1Encodable.DER);
        signature = rsa.processBlock(digest, 0, digest.length);
        dOut.close();
        
        // We build a certificate chain containing only one certificate
        ASN1EncodableVector v = new ASN1EncodableVector();
        v.add(tbsCert);
        v.add(sigAlgId);
        v.add(new DERBitString(signature));
        X509CertificateObject clientCert = new X509CertificateObject(
                new X509CertificateStructure(new DERSequence(v)));
        X509Certificate[] chain = new X509Certificate[1];
        chain[0] = clientCert;

        // We add our certificate to a new keystore
        KeyStore keyStore = KeyStore.getInstance("BKS");
        keyStore.load(null);
        keyStore.setKeyEntry(KEY_ALIAS, (Key) privKey,
                keystorePassword.toCharArray(), chain);
        
        // We write this keystore to a file
        OutputStream out = ctx.openFileOutput(FILE_NAME,
                Context.MODE_PRIVATE);
        keyStore.store(out, keystorePassword.toCharArray());
        out.close();
        return true;
    } catch (Exception e) {
        // Do your exception handling here
        // There is a lot which might go wrong
        e.printStackTrace();
    }
    return false;
}

The key generation takes roughly fifteen seconds on my Motorola Moto G, so it is strongly discouraged to do this in the UI thread – do it in your Service (you should have one for your server!) or in an AsyncTask.

FILE_NAME is the name of your key store (I use keystore.bks) and KEY_ALIAS the alias of the new key inside the keystore (I use ssl).

Jetty initialization

In the initialization code of our jetty servlet, we have to load our newly created keystore into a SslContextFactory, which is quite easy:

SslContextFactory sslContextFactory = new SslContextFactory();
InputStream in = openFileInput(SSLUtils.FILE_NAME);
KeyStore keyStore = KeyStore.getInstance("BKS");
try {
    keyStore.load(in, KEYSTORE_PASSWORD.toCharArray());
} finally {
    in.close();
}
sslContextFactory.setKeyStore(keyStore);
sslContextFactory.setKeyStorePassword(KEYSTORE_PASSWORD);
sslContextFactory.setKeyManagerPassword(KEYSTORE_PASSWORD);
sslContextFactory.setCertAlias(SSLUtils.KEY_ALIAS);
sslContextFactory.setKeyStoreType("bks");
// We do not want to speak old SSL and we only want to use strong ciphers
// In the original version of this blog post, I only set
// sslContextFactory.setIncludeProtocols("TLS");
// which does not seem to work with current Android/jetty/whatever versions
sslContextFactory.setIncludeProtocols("TLSv1", "TLSv1.1", "TLSv1.2");
sslContextFactory.setExcludeProtocols("SSLv3");
sslContextFactory.setIncludeCipherSuites("TLS_DHE_RSA_WITH_AES_128_CBC_SHA");

Server server = new Server();
SslSelectChannelConnector sslConnector = new SslSelectChannelConnector(
        sslContextFactory);
sslConnector.setPort(PORT);
server.addConnector(sslConnector);

// As before:
server.setHandler(handler); // where handler is an ``AbstractHandler`` instance
server.start();

QR Code generation

In order to display the QR code, we first need to create a SHA1 hash of our certificate:

public static String getSHA1Hash(Context ctx, String keystorePassword) {
    InputStream in = null;
    KeyStore keyStore;
    try {
        in = ctx.openFileInput(FILE_NAME);
        keyStore = KeyStore.getInstance("BKS");
        keyStore.load(in, keystorePassword.toCharArray());
        return new String(Hex.encodeHex(DigestUtils.sha1(keyStore
                .getCertificate(KEY_ALIAS).getEncoded())));
    } catch (Exception e) {
        // Should not go wrong on standard Android devices
        // except possible IO errors on reading the keystore file
        e.printStackTrace();
    } finally {
        try {
            if (in != null)
                in.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    return null;
}

Using this method, we can generate a QR code containing ip, port and certificate information and draw it onto an ImageView:

protected void genQrCode(ImageView view) {
    QRCodeWriter writer = new QRCodeWriter();
    try {
        WifiManager wifiManager = (WifiManager) getActivity()
                .getSystemService(WIFI_SERVICE);
        int ipAddress = wifiManager.getConnectionInfo().getIpAddress();
        final String formatedIpAddress = String.format(Locale.GERMAN,
                "%d.%d.%d.%d", (ipAddress & 0xff),
                (ipAddress >> 8 & 0xff), (ipAddress >> 16 & 0xff),
                (ipAddress >> 24 & 0xff));

        JSONObject qrdata = new JSONObject();
        qrdata.put("server", formatedIpAddress);
        qrdata.put("port", ServerService.PORT);
        qrdata.put("cert", getSHA1Hash(getActivity(), KEYSTORE_PASSWORD));
        qrdata.put("secret", SECRET); // for authentitication. Generate yourself ;)

        BitMatrix bitMatrix = writer.encode(qrdata.toString(),
                BarcodeFormat.QR_CODE, 500, 500);
        int width = bitMatrix.getWidth();
        int height = bitMatrix.getHeight();
        Bitmap bmp = Bitmap.createBitmap(width, height,
                Bitmap.Config.RGB_565);
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                bmp.setPixel(x, y, bitMatrix.get(x, y) ? Color.BLACK
                        : Color.WHITE);
            }
        }
        view.setImageBitmap(bmp);

    } catch (WriterException e) {
        e.printStackTrace();
    } catch (JSONException e) {
        e.printStackTrace();
    }
}

Client side

On the client side, HttpsURLConnection is being used for the connection. This works roughly like this:

// My application saves server address, port and certificate
// in a SharedPreferences store
SharedPreferences sp = getSharedPreferences("server",
        Context.MODE_PRIVATE);

X509PinningTrustManager trustManager = new X509PinningTrustManager(
        sp.getString("cert", ""));

SSLContext sc = null;
DataSource data = getDataSource();
HttpsURLConnection urlConnection = null;
try {
    data.open();

    URL url = new URL("https://" + sp.getString("server", "") + ":"
            + sp.getInt("port", ServerService.PORT) + "/path");
    urlConnection = (HttpsURLConnection) url.openConnection();

    // Set our SSL settings settings
    urlConnection
            .setHostnameVerifier(trustManager.new HostnameVerifier());
    try {
        sc = SSLContext.getInstance("TLS");
        sc.init(null, new TrustManager[] { trustManager },
                new java.security.SecureRandom());
        urlConnection.setSSLSocketFactory(sc.getSocketFactory());
    } catch (NoSuchAlgorithmException e1) {
        // Should not happen...
        e1.printStackTrace();
    } catch (KeyManagementException e1) {
        // Should not happen...
        e1.printStackTrace();
    }

    // do HTTP POST or authentication stuff here...
    InputStream in = new BufferedInputStream(
            urlConnection.getInputStream());
    // process the response...
} catch (javax.net.ssl.SSLHandshakeException e) {
    // We got the wrong certificate
    // (or handshake was interrupted, we can't tell here)
    e.printStackTrace();
} catch (IOException e) {
    // other IO errors
    e.printStackTrace();
} finally {
    if (urlConnection != null)
        urlConnection.disconnect();
    try {
        data.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

If you /only/ connect to this server in your whole application, you can use the HttpsUrlConnection.setDefault* methods instead of specifying it with every request. With writing a clever HostnameVerifier and TrustManager you could also achieve that the pinning is only enforced for your server but the system defaults are used for other servers.

The above code example makes use of a TrustManager class I implemented myself. It only accepts exactly one certificate:

/**
 * This class provides an X509 Trust Manager trusting only one certificate.
 */
public class X509PinningTrustManager implements X509TrustManager {

    String pinned = null;

    /**
    * Creates the Trust Manager.
    * 
    * @param pinnedFingerprint
    *            The certificate to be pinned. Expecting a SHA1 fingerprint in
    *            lowercase without colons.
    */
    public X509PinningTrustManager(String pinnedFingerprint) {
        pinned = pinnedFingerprint;
    }

    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[] {};
    }

    public void checkClientTrusted(X509Certificate[] certs, String authType)
            throws CertificateException {
        checkServerTrusted(certs, authType);
    }

    public void checkServerTrusted(X509Certificate[] certs, String authType)
            throws CertificateException {
        for (X509Certificate cert : certs) {
            try {
                String fingerprint = new String(Hex.encodeHex(DigestUtils
                        .sha1(cert.getEncoded())));
                if (pinned.equals(fingerprint))
                    return;
            } catch (CertificateEncodingException e) {
                e.printStackTrace();
            }
        }
        throw new CertificateException("Certificate did not match, pinned to "
                + pinned);
    }

    /**
    * This hostname verifier does not verify hostnames. This is not necessary,
    * though, as we only accept one single certificate.
    *
    */
    public class HostnameVerifier implements javax.net.ssl.HostnameVerifier {
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    }
}

Security considerations

This approach should be very secure against most attackers, as all network traffic is encrypted and traditional man-in-the-middle is not possible, as the client exactly know which certificate it expects and does not accept others. We even have Perfect Forward Secrecy! The fact that this information is not being transferred via network but via a QR code adds additional security.

However, there are some security problems left:

  • Most important: We only have TLS 1.0 available. I did not find a possibility to enable TLS 1.2. This is sad. I suspect the reason is that Android is still based on Java 6 and TLS 1.2 was introduced with Java 7. There is a possibility of running Java 7 code starting with Android KitKat, but this did not help in my quick test.
  • The keystore password is hardcoded. The only other option would be to prompt the user for the password on every application startup, which is not desirable. This, however, is only important if someone gains access to the keystore file, which is only readable for the app itself and root. And if our potential attacker is root on your phone, I guess you've got bigger problems than this keystore… Remember, my application is supposed to run on phones dedicated to run this application (with not many 3rd-party applications introducing vulnerabilities being installed).
  • SecureRandom is not cryptographically secure in Android 4.3 or older, as Android Lint points out. This official blog post5 has some more details and a workaround, but if you care about security, you should not run an old operating system anyway ;)