Secure serializing objects using sealedobject class
In this article we’ll take the approach of serializing and deserializing objects, with symmetric cryptographic key usage. Most articles describing that topic, require from developer to store inside application additional data, which are required to decrypt data. I’m going to show you how we can achieve the same goal with easier approach, by using SealedObject class.
Data encryption is taking place in few steps. Firstly, we are going to generate cryptographic key using AES standard. It’ll give us the possibility to easily encrypt large amount of data.
static final String ENCRYPTION_STANDARD = "AES"; static final String KEY_PROVIDER = "AndroidKeyStore"; static final String KEY_ALIAS = "key"; KeyGenerator keyGenerator = KeyGenerator.getInstance(ENCRYPTION_STANDARD, KEY_PROVIDER); KeyGenParameterSpec specs = new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .build(); keyGenerator.init(specs); SecretKey key = keyGenerator.generateKey();
We have passed a few parameters here:
- KEY_PROVIDER – provider instance in which Android should look up for AES implementation
- KEY_ALIAS – alias under which key could be found later we gave our key a purpose to encrypt & decrypt data
- BLOCK_MODE_CBC – cryptographic block mode with which our key can be used
- ENCRYPTION_PADDING_PKCS7 – default padding scheme with which our key can be used
On Android 6.0 and above our newly generated key would become automatically saved in provided Keystore for further use. Android Keystore System gives the possibility to store cryptographic keys in a secure way, without any chance of exporting them from the device.
Our next step is going to initialize Cipher object, which will encrypt our data. We’ll create it by passing transformation parameters used when generating our AES key. Then, we’ll initialize it with our key in Encryption mode.
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); cipher.init(Cipher.ENCRYPT_MODE, key);
Up to that point, everything is pretty similar to what you can find in other articles, but here comes the difference. In code above we intentionally omitted passing Initialization Vector parameter, which is required when using CBC block mode. When encrypting data using CBC mode, each data block is being encrypted with block from previous iteration, as presented in diagram below. For first iteration however, we have to pass a set of random values called Initialization Vector.
There are many disadvantages for this solution:
- It’s required to store used IV, so that data decryption would be possible.
- We should never use the same vector more than once for the same key.
Because we haven’t passed IV intentionally, we’re letting Cipher to generate new vector each time when it tries to encrypt data, by using random data provided by SecureRandom object.
So how we’re going to decrypt our data without remembering Initialization Vector then? Here’s when SealedObject comes into play.
This is how SealedObject constructor looks like:
SealedObject(Serializable data, Cipher c)
First parameter is object data, which implement Serializable interface. Second is Cipher object, which we initialized before using our SecretKey. Data which we’re going to pass will become encrypted inside SealedObject. It’ll also remember parameters used to initialize Cipher. Because SealedObject class also implements Serializable interface, we can easily replace our current saving data implementation, e.g.:
static final String FILE_NAME = "sealed_file"; FileOutputStream fos = applicationContext.openFileOutput(FILE_NAME, Context.MODE_PRIVATE); ObjectOutputStream os = new ObjectOutputStream(fos); os.writeObject(new SealedObject(data, cipher)); os.close();
Decoding data is even simpler. Because SealedObject stores all parameters required to reinitialize Cipher instance, we only have to pass our SecretKey stored inside device into SealedObject:
FileInputStream fis = context.openFileInput(FILE_NAME); ObjectInputStream is = new ObjectInputStream(fis); SealedObject obj = (SealedObject) is.readObject(); Object data = obj.getObject(key); is.close();
And that’s it! All of our data would remain encrypted in device memory, and thanks to storing SecretKey inside Keystore, and generating new Initialization Vector with each encryption we minimize the risk of decoding our data.