Working with RSA in Android

One of the key concepts of one of the projects we are working on (We will show you some demo soon) is RSA. This project's backend application is built with Python/Django, Go and Node.js, so if you want to learn something about RSA on Python, you can read the previous article we wrote about this topic.

The first version of this "secret-but-cool-project" will have in its architecture an Android client that communicates with the server with RSA encryption.

When we started to work with RSA on Android we found one big limitation on the platform itself: We cannot use the standard Java Bouncy Castle provider.

To solve this, we can use the Spongy Castle library which makes the standard provider to work on Android with some tweaks:

  • all package names have been moved from org.bouncycastle.* to org.spongycastle.* - to avoid classloader conflicts
  • the Java Security API Provider name is now SC rather than BC
  • no class names change, so the BouncyCastleProvider class remains Bouncy, not Spongy, but moves to the org.spongycastle.jce.provider package.

To include it on your android project you just have to insert it as you would do it with any other dependency. This example needs four dependencies (this library is splitted on four packages) to work:

dependencies {  
    ...
    compile 'com.madgag.spongycastle:core:1.51.0.0'
    compile 'com.madgag.spongycastle:prov:1.51.0.0'
    compile 'com.madgag.spongycastle:pkix:1.51.0.0'
    compile 'com.madgag.spongycastle:pg:1.51.0.0'
    ...
}

In the process of any application that uses RSA there are 3 different phases:

  • Key Generation
  • Encryption / Decryption
  • Signing / Verifying

RSA┬┤s algorithm is beyond the scope of this article, but you can find how it works and it's mathematical background here.

All of this article comes from an example uploaded to Github, so in the bottom you can follow a link to the repo.

Before going deeper we have to define two things (I've done both on the RSA.java file):

  • We have to define a RSA Key size. I have setted it to 1024. The bigger this number is, the safer the algorithm will be. Also, as the number is bigger, the time used to do operations with this will grow.

    private static final int KEY_SIZE = 1024;
    
  • The new provider (Spongy Castle) has to be added to the list of available security providers.

    static {
        Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
    }
    

Key generation

The key generation is very simple

public static KeyPair generate() {  
    try {
        SecureRandom random = new SecureRandom();
        RSAKeyGenParameterSpec spec = new RSAKeyGenParameterSpec(KEY_SIZE, RSAKeyGenParameterSpec.F4);
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "SC");
        generator.initialize(spec, random);
        return generator.generateKeyPair();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

The only different thing we have to do comparing with the standard Java Bouncy Castle is that we have to specify the "SC" provider.

Remember to do this on a background thread because depending on the KEY_SIZE and the hardware specifications of the device you are using, it could take long time to generate de RSA key pair. In a Moto G, 2-3 seconds with 1024 bits and 30 seconds with 2048!

This method will return a KeyPair that contains the private and the public keys. But if we want to export them to a file or just storing them on Preference we will have to get their String representation with this two methods:

public void writePublicKeyToPreferences(KeyPair key) {  
    StringWriter publicStringWriter = new StringWriter();
    try {
      PemWriter pemWriter = new PemWriter(publicStringWriter);
      pemWriter.writeObject(new PemObject("PUBLIC KEY", key.getPublic().getEncoded()));
      pemWriter.flush();
      pemWriter.close();
      Preferences.putString(Preferences.RSA_PUBLIC_KEY, publicStringWriter.toString());
    } catch (IOException e) {
        Log.e("RSA", e.getMessage());
        e.printStackTrace();
    }
}

public void writePrivateKeyToPreferences(KeyPair keyPair) {  
      StringWriter privateStringWriter = new StringWriter();
      try {
        PemWriter pemWriter = new PemWriter(privateStringWriter);
        pemWriter.writeObject(new PemObject("PRIVATE KEY", keyPair.getPrivate().getEncoded()));
        pemWriter.flush();
        pemWriter.close();
        Preferences.putString(Preferences.RSA_PRIVATE_KEY, privateStringWriter.toString());
      } catch (IOException e) {
        Log.e("RSA", e.getMessage());
        e.printStackTrace();
      }
}

And here are the methods that you'll have to use to extract them from Preference (or simply a String):

public static PublicKey getRSAPublicKeyFromString(String publicKeyPEM) throws Exception {  
    publicKeyPEM = stripPublicKeyHeaders(publicKeyPEM);
    KeyFactory keyFactory = KeyFactory.getInstance("RSA", "SC");
    byte[] publicKeyBytes = Base64.decode(publicKeyPEM.getBytes("UTF-8"));
    X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(publicKeyBytes);
    return keyFactory.generatePublic(x509KeySpec);
}

public static PrivateKey getRSAPrivateKeyFromString(String privateKeyPEM) throws Exception {  
    privateKeyPEM = stripPrivateKeyHeaders(privateKeyPEM);
    KeyFactory fact = KeyFactory.getInstance("RSA", "SC");
    byte[] clear = Base64.decode(privateKeyPEM);
    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(clear);
    PrivateKey priv = fact.generatePrivate(keySpec);
    Arrays.fill(clear, (byte) 0);
    return priv;
}

As before, remember always the "SC" provider, otherwise, it will not work.

Encryption / Decryption

Now that we have the KeyPair, we would want to encrypt some message or decrypt one.

Before sending a message, the first thing we'll have to do (after generating RSA KeyPair) is to encrypt it so nobody can read it. To achieve this and send this message through the network, we have to work with Base64 strings.

     public static byte[] encrypt(Key publicKey, byte[] toBeCiphred) {
        try {
            Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding", "SC");
            rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey);
            return rsaCipher.doFinal(toBeCiphred);
        } catch (Exception e) {
            Log.e(TAG, "Error while encrypting data: " + e.getMessage());
            throw new RuntimeException(e);
        }
    }

    public static String encryptToBase64(Key publicKey, String toBeCiphred) {
        byte[] cyphredText = RSA.encrypt(publicKey, toBeCiphred.getBytes());
        return Base64.encodeToString(cyphredText, Base64.DEFAULT);
    }

The only thing you have to keep in mind is to use compatible encryption/decryption's types of algorithm in server and client side. In this case we're using "RSA/ECB/OAEPWithSHA1AndMGF1Padding" (Yes, there are no constants defining this and you have to write them manually).

The decryption algorithm is quite similar:

     public static byte[] decrypt(Key privateKey, byte[] encryptedText) {
        try {
            Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding", "SC");
            rsaCipher.init(Cipher.DECRYPT_MODE, privateKey);
            return rsaCipher.doFinal(encryptedText);
        } catch (Exception e) {
            Log.e(TAG, "Error while decrypting data: " + e.getMessage());
            throw new RuntimeException(e);
        }
    }

    public static String decryptFromBase64(Key key, String cyphredText) {
        byte[] afterDecrypting = RSA.decrypt(key, Base64.decode(cyphredText, Base64.DEFAULT));
        return stringify(afterDecrypting);
    }

Signing / Verifying

We only have one more step to do. We have received the message and want to verify if it's correct, or we want to sign it so the receiver can verify it.

This is the easiest of all the methods you can find in this article. One to sign, and other to verify:

     private byte[] sign(byte[] bytes, String privateKey) throws Exception {
        Signature signature = Signature.getInstance("SHA256withRSA", "SC");
        signature.initSign(Crypto.getRSAPrivateKeyFromString(privateKey));
        signature.update(bytes);
        return signature.sign();
    }


    private boolean verify(byte[] bytes, String privateKey) throws Exception {
        Signature signature = Signature.getInstance("SHA256withRSA", "SC");
        signature.initSign(Crypto.getRSAPrivateKeyFromString(privateKey));
        return signature.verify(bytes);
    }

You just have to remember two things here:
1. You have to work with the same algorithm. In this case "SHA256withRSA"
2. Your message has to be represented in byte format (after encryption/decryption)

Sample

screen-1

Because all of this is very abstract and It could be difficult to understand without watching all the code, here you can find a complete example source code so you can play around with it and watch how to use it.

comments powered by Disqus