CipherInputStream & CipherOutputStream - Secure access to your encrypted files in android

Two years ago we had to implement a HIPAA compliant android application. Out of a number of secure module implementations, I'd share my experience on working with images in the android app.
There were two sources from where our application would receive images viz. through HTTPS and secure XMPP. Merely transmitting the image on a secure network was not enough. In the android application, the image had to be stored and retrieved securely as well.
For storing the image, I had used Java's cryptography with "AES/CBC/PKCS5Padding" encryption. Now to display these encrypted images in an Imageview and some of our custom tiled zoomable views, one would simply decrypt the image & store in some temporary location. But that is not what is expected from a HIPAA compliant app.
With CipherInputStream & CipherOutputStream, there was no need to decrypt & temporarily store the image at some insecure location but to directly harness the streams to display the images. Yes, it would take some time to display the images due to the limited CPU & RAM on the devices back then but when checked on the latest android devices there is no noticeable lag with the above implementation.

Following is one of our util classes which we use for app lifecycle bound encryption & decryption.

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Random;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class StorageFactory {

    private static StorageFactory instance;
    public static final int AES_Key_Size = 256;
    private static String ENC_TYPE = "AES/CBC/PKCS5Padding";
    private byte[] iv;
    Cipher cipher;
    byte[] secretKey;
    SecretKeySpec aeskeySpec;
    IvParameterSpec ivSpec;

    static {
        instance = new StorageFactory();
    }

    public static StorageFactory getInstance() {
        return instance;
    }

    private StorageFactory() {
        // create AES cipher
        try {
            cipher = Cipher.getInstance(ENC_TYPE);
            createKey();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        }
    }

    private void createKey() throws NoSuchAlgorithmException {
        KeyGenerator kgen = KeyGenerator.getInstance("AES");
        kgen.init(AES_Key_Size);
        SecretKey key = kgen.generateKey();
        secretKey = key.getEncoded();

        iv = new byte[cipher.getBlockSize()];
        SecureRandom random = new SecureRandom();
        random.nextBytes(iv);

        ivSpec = new IvParameterSpec(iv);

        aeskeySpec = new SecretKeySpec(secretKey, "AES");
    }

    public synchronized CipherInputStream getInputStream(File in)
            throws IOException, InvalidKeyException,
            InvalidAlgorithmParameterException {
        cipher.init(Cipher.DECRYPT_MODE, aeskeySpec, ivSpec);

        return new CipherInputStream(new FileInputStream(in), cipher);
    }

    public synchronized byte[] getBytes(File in) throws IOException,
            InvalidKeyException, InvalidAlgorithmParameterException {
        cipher.init(Cipher.DECRYPT_MODE, aeskeySpec, ivSpec);

        CipherInputStream is = new CipherInputStream(new FileInputStream(in),
                cipher);
        ByteArrayOutputStream os = new ByteArrayOutputStream();

        copy(is, os);

        os.flush();
        is.close();
        os.close();

        return os.toByteArray();
    }

    public void encrypt(byte[] in, File out) throws IOException,
            InvalidKeyException, InvalidAlgorithmParameterException {
        cipher.init(Cipher.ENCRYPT_MODE, aeskeySpec, ivSpec);

        ByteArrayInputStream is = new ByteArrayInputStream(in);
        CipherOutputStream os = new CipherOutputStream(
                new FileOutputStream(out), cipher);

        copy(is, os);
        os.flush();
        is.close();
        os.close();
    }

    public void encrypt(File in, File out) throws IOException,
            InvalidKeyException, InvalidAlgorithmParameterException {
        cipher.init(Cipher.ENCRYPT_MODE, aeskeySpec, ivSpec);

        FileInputStream is = new FileInputStream(in);
        CipherOutputStream os = new CipherOutputStream(
                new FileOutputStream(out), cipher);

        copy(is, os);
        os.flush();
        is.close();
        os.close();
    }

    public void decrypt(byte[] in, File out) throws IOException,
            InvalidKeyException, InvalidAlgorithmParameterException {
        cipher.init(Cipher.DECRYPT_MODE, aeskeySpec, ivSpec);

        CipherInputStream is = new CipherInputStream(new ByteArrayInputStream(
                in), cipher);
        FileOutputStream os = new FileOutputStream(out);

        copy(is, os);

        os.flush();
        is.close();
        os.close();
    }

    public void decrypt(File in, File out) throws IOException,
            InvalidKeyException, InvalidAlgorithmParameterException {
        cipher.init(Cipher.DECRYPT_MODE, aeskeySpec, ivSpec);

        CipherInputStream is = new CipherInputStream(new FileInputStream(in),
                cipher);
        FileOutputStream os = new FileOutputStream(out);

        copy(is, os);

        os.flush();
        is.close();
        os.close();
    }

    private void copy(InputStream is, OutputStream os) throws IOException {
        int i;
        byte[] b = new byte[1024];
        while ((i = is.read(b)) != -1) {
            os.write(b, 0, i);
        }
    }

    public String makeCopy(String path) {
        try {
            File in = new File(path);
            File out = new File(in.getParent() + "/_" + in.getName());

            if (!out.exists()) {

                FileInputStream is = new FileInputStream(in);

                FileOutputStream os = new FileOutputStream(out);

                copy(is, os);

                os.flush();
                is.close();
                os.close();
            }
            return out.getAbsolutePath();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRandomString() {
        char[] chars = "1234567890abcdefghijklmnopqrstuvwxyz".toCharArray();
        StringBuilder sb = new StringBuilder();
        Random random = new Random();
        for (int i = 0; i < 20; i++) {
            char c = chars[random.nextInt(chars.length)];
            sb.append(c);
        }
        return sb.toString();
    }

}

Podcast

Michael Patterson sat down with the CEO of Boston Byte, Mustapha Shaikh to discuss the significance and rapid digitization of the healthcar...