Using RSA and AES encryption to secure communication between Android client and Go server

Abhishek Sinha
9 min readOct 9, 2017

--

Most of the Android apps today are meant to work seamlessly in offline-online mode. So at times copy of data needs to be stored locally till better internet connections is available. However, a problem often arises when some data generated on client needs to stored safely and made inaccessible to other apps. For example, let’s say your app helps to create some User Generated content using tools provided by your app and you want this content to be unreadable by other apps on the device. One way to do it could be to have a public-private key encryption scheme. Public key is available with the client with which it encrypts the data and sends it to server once it gets connectivity. The data is then decrypted on server using the private key. This encryption scheme is called asymmetric encryption as it uses a different key for encryption and decryption. RSA is one of the ways of doing this. This also eliminates possibility of someone snooping on network from getting access to data specially if you are using unsecured protocol for communication. (Security experts may argue that anything stored on Android devices cannot be totally secure but then any encryption scheme is meant to make data difficult to access than impossible)

Problem with using RSA for data encryption

RSA is a relatively slow algorithm, specially when dealing with larger amount of data. Also, with less computation power of mobile platform using RSA for encryption of large amount of data like images or files is very expensive on resources.

Combine AES with RSA for better performance

AES is an encryption scheme based on combination of substitution and permutation and is fast encryption algorithm whether in hardware or software. It is a symmetric encryption scheme and needs same key for encryption and description. So, to solve our problem of protecting client data we use the following scheme

  • Generate a Random 16 byte encryption key and 16 byte IV (initialization vector ) on the client
  • Encrypt the data using this key and IV using AES
  • Now using the public key encrypt the key and IV using RSA
  • Send the encrypted Key, encrypted IV and encrypted data to server
  • On server, first decrypt the encrypted key and encrypted IV using the private key with server using RSA
  • Now using the decrypted key and decrypted IV decrypt the data using AES decryption

That’s it. Now let’s see how we do it on Android client and Go server using the standard crypto packages provided by these platforms

Generating your public and private keys

First and foremost we need to generate the key pair for public-private encryption scheme. We use openssl to generate our keys. It is recommended to do it on a secured system

First we create the private key.

openssl genrsa -out rsa_priv_key.pem 2048

This key needs to be on server where our golang server can access it. Now using this key we generate our public key to be used by client. This key needs to be in der format as Java clients work with that format of public key.

openssl rsa -in rsa_priv_key.pem -pubout -outform DER -out rsa_pub_key.der

Now, as you have your public and private key. Place the private key at a secure place where only your golang server can read it. Place the public key in your Android workspace in res/raw folder.

Part 1: Encryption on your Android client

To do the ecnryption on android we will be using javax.crypto and java.security packages. These packages are part of standard Android SDK since API level 1, however the snippets given here are tested only above API level 17.

We create a class EncryptionUtility to handle our encryption

public class EncryptionUtility { 
public static String encrypt(Context context, String data) {
//TODO:
return data;
}
}

So it has a static method which takes a string and returns encrypted string which we can send to server. To start with our encryption, we first need to read the public key. So we define a private method to return our key. Here is the way we can do it

.... 
.....
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.SecureRandom; import java.security.spec.X509EncodedKeySpec;
....
....
private static PublicKey getPublicKey(Context context) throws IOException, GeneralSecurityException { InputStream filename = context.getResources().openRawResource (R.raw.rsa_pub_key);
byte[] keyBytes = new byte[2048];
BufferedInputStream buf = new BufferedInputStream(filename);
buf.read(keyBytes, 0, keyBytes.length);
buf.close();
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(spec);
}

Remember, we had created public key and place it in raw folder in our workspace. We read the file using a BufferedInputStream and generate the public key using KeyFactory. Please read the documentation linked above for java.security package to understand what each function does.

Now that we have our public key, next step would be to generate the random key and IV. We will use SecureRandom class to generate our random 16 bytes needed. So we will define another function in the class EncryptionUtility to generate 16 byte random string

..... 
import java.security.SecureRandom;
......
......
private static byte[] generateRandom16Bytes() {
SecureRandom r = new SecureRandom();
byte[] bytes = new byte[16];
r.nextBytes(bytes);
return bytes;
}
....

Now, we would need two more functions to do our AES and RSA encryption. Let’s write those

.... 
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import javax.crypto.spec.SecretKeySpec;
....
private static String encryptRSA(byte[] plainText, PublicKey publicKey) throws Exception {
byte[] encryptedData = null;
String encryptedDataString = "";
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
encryptedData = cipher.doFinal(plainText);
encryptedDataString = Base64.encodeToString(encryptedData, Base64.NO_WRAP);
return encryptedDataString;
}
private static String encryptAES(byte[] plainText, byte[] key, byte[] IV) throws Exception { String encryptedDataString = "";
SecretKeySpec keySpec = new SecretKeySpec(deviceKey, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.ENCRYPT_MODE, key,ivSpec);
byte[] encrypted = cipher.doFinal(plainText);
encryptedDataString= Base64.encodeToString(encrypted,Base64.NO_WRAP);
return encryptedDataString;
}
....

It is recommended to go through the documentation of javax.crypto.Cipher to better understand this code. Basically to initialize the Cipher we need to pass transformation string in one of two formats

  • “algorithm/mode/padding”
  • “algorithm”

As you see in case of RSA we passed "RSA/ECB/PKCS1Padding" and in case of AES we passed "AES/CBC/PKCS5Padding". The detailed description of padding, modes and providers in itself is a advanced topic and has not been covered here. They can be referenced from wikipedia or other such sources.

Important point to note is whatever padding or mode we have used on client we need to ensure to use the same while decryption on client. Also note, we have done a base64 encoding on our encrypted data. So we need to ensure that we do the same on server.

Getting back to our implementation now we have our building blocks we can write the code for encrypt()

public static String encrypt(Context context, String data) 
{
String cipher;
try {
PublicKey publicKey = getPublicKey(context);
byte[] key = generateRandom16Bytes();
byte[] iv = generateRandom16Bytes();
String cipherData = encryptAES(data.getBytes(), key, iv);
String encryptedKey = encryptRSA(key, publicKey);
String encryptedIV = encryptRSA(iv, publicKey);
cipher = cipherData + ":#:#:#" + key + ":#:#:#" + iv;
return cipher;
} catch(Exception e) {
e.printStackTrace();
}
}

So what we have done is

  • Encrypted the data using random key and iv using AES and did a base64 encode on that
  • Encrypted the key and iv using public key and also separately encoded them using base64 encoded
  • Suffixed the encrypted key and iv to the encrypted data to form our encrypted blob to send to server

Now the data returned by this EncryptionUtility.encrypt() is ready to be sent to server. How to send it to server depends on your use case. It is reommended to use SSL/TLS while sending. I have not covered how to send this to server. You can use HTTPUrlConnection or Retrofit or any such library to make client to server connection and pass this blob as body of http POST request. You can use any other way like websockets or raw sockets to send this data. Important thing is you also need to know how to get this blob on the server.

Once you are able to get the string on server, you can go ahead with next part of tutorial

Part 2: Decryption on your Go server

So, now you have got the encrypted blob on your go server. To get the decrypted data, we need to do the following

  • Get the private key
  • extract the encrypted key and iv from the string
  • Base64 decode the key and IV and decrypt them using the private key on server using RSA
  • Base64 decode the data part and decrypt using the key and IV using AES
  • Remove the padding from data that is added as part of AES encryption

So let’s begin with a empty go function called decrypt

func Decrypt(encryptedData string) (string,error) {
var plainText string
var err error
....
....
return plainText, err
}

Golang standard installation provides crypto package to deal with standard encryption and decryption algorithms. As with Android client, here too we first need a way to get the private key

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"io/ioutil"
"log"
)
const (
PrivateKeyPath string = "rsa_priv_key.pem"
)
func getPrivateKey() (*rsa.PrivateKey,error)
{
keyFile, err := ioutil.ReadFile(PrivateKeyPath)
if err != nil
{
log.Println(err)
return nil, err
}
block, _ := pem.Decode(keyFile)
if block == nil
{
log.Println("can't read key file")
return nil, errors.New("can't read key file")
}
privateKey, err1 := x509.ParsePKCS1PrivateKey(block.Bytes)
if err1 != nil
{
log.Println("Could not decode private key")
}
return privateKey,err1
}

So as we see, we read the private key file and decode it using pem.Decode and parse it using x509 package. Now, we need few more utility functions for doing base64 decoding and RSA and AES decryption

func decodeBase64(input string) ([]byte, error) {
cipher, err := base64.StdEncoding.DecodeString(input)
return cipher, err
}
func DecryptRSA(cipherData string, isBase64Encoded bool) ([]byte, error) {
var cipher []byte
var out []byte
var err error
var privateKey *rsa.PrivateKey
privateKey, err = getPrivateKey()
if err != nil {
log.Println("Failed to read private key")
return out,err
}
if isBase64Encoded {
cipher, err = DecodeBase64(cipherData)
if err != nil {
return out, err
}
} else {
cipher = []byte(cipherData)
}
out, err = rsa.DecryptPKCS1v15(rand.Reader, privateKey, cipher)
if err != nil {
log.Println("failed decrypt ", err)
return out, err
}
return out, err
}
func DecryptAES(key []byte, iv []byte, cipherString string) ([]byte, error) {
cipheredMessage, err := base64.StdEncoding.DecodeString(cipherString)
if err != nil {
log.Println("error: decoding string (aes)")
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
log.Println(err)
return nil, err
}
if len(cipheredMessage) < aes.BlockSize {
log.Println("error: ciphertext too short")
return nil, errors.New("cipherText too short")
}
cipherText := cipheredMessage
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(cipherText, cipherText)
return cipherText, nil
}

Check the functions rsa.DecryptPKCS1v15 and cipher.NewCBCDecrypter. Note that they are using same scheme as what we used on Android. This is important to note. You can use any other padding scheme or mode as needed just ensure that you keep it symmetric on client and server.

Putting this all together

Now since we have our building blocks, let’s implement our incomplete function for decryption

func Decrypt(encryptedData string) (string,error) {
var plainText string
var err error
tokens := strings.Split(encryptedData, ":#:#:#")
if len(tokens) < 3 {
log.Println("Data malformed")
return "", errors.New("Data malformed")
}
data_enc = tokens[0]
key_enc = tokens[1]
iv_err = tokens[2]
var key, iv []byte
key, err = DecryptRSA(key_enc, true)
if err != nil {
log.Println("Failed to decrypt key")
return plainText, err
}
iv, err = DecryptRSA(iv_enc, true)
if err != nil {
log.Println("Failed to decrypt iv")
return plainText, err
}
var plainBytes []byte
plainBytes, err = DecryptAES(key, iv, data_enc)
plainText = string(plainBytes)
return plainText, err
}

That should be it by looks of it. However, one thing that we missed out was the PKCS1Padding that we had used while encrypting data using AES on client would have added some padding at the end of data. To get the exact data we need to remove the padded characters from decrypted data. This can be simply done

func PKCS5Trimming(encrypt []byte) []byte {
padding := encrypt[len(encrypt)-1]
return encrypt[:len(encrypt)-int(padding)]
}

So now changing our decrypt function a a bit

func Decrypt(encryptedData string) (string,error) {
var plainText string
var err error
......
......
var plainBytes []byte
plainBytes, err = DecryptAES(key, iv, data_enc)
plainText = string(PKCS5Trimming(plainBytes))
return plainText, err

That’s it! You have setup the framework to ensure encrypted communication between android and Go.

Disclaimers

This technique is mainly about encryption and does not handle authentication. An attacker might use the public key of the server to encrypt data and submit it. To ensure that server knows that the message is from intended client, one can choose to use any standard authentication mechanism like jwt based token authentication.

References

Originally published at gist.github.com.

--

--

Abhishek Sinha

Lazy Programmer. Likes to experiment new technologies. Android, Go, Full stack, AWS. github: sinha-abhishek