Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Android

Windows / Android (C#/Java) Compatible Data Encryption with Compression

5.00/5 (5 votes)
13 Mar 2024CPOL1 min read 14.8K  
Code sample for passing encrypted compressed data between Windows and Android
I couldn't find an example of plain simple bytes-in and bytes-out data compression and encryption that was compatible in all directions between Windows and Android. This tip contains that code: .NET Framework C# code for the Windows side and Java for the Android side.

Introduction

I do a lot of cross-platform work passing data between Windows and Android, without the benefit of writing that data to a file or assuming that the data is text (aka strings). Most examples you will find use files and assume text input and output. I'm always working with plain bytes of data. When I was looking to compress and encrypt my data, I had to cobble together bits and pieces from a number of samples that pointed me in the right direction, but never used byte arrays as the input or output. My final code is the contents of this Tip.

Since .NET Framework does not support the current recommendation in encryption (GCM), this example uses AES-256-CBC with a manual hash to duplicate the authentication portion of GCM.

Using the Code

Four chunks of code are included:

  1. C# using .NET Framework to compress and encrypt
  2. C# using .NET Framework to decrypt and decompress
  3. Java to compress and encrypt
  4. Java to decrypt and decompress

Code has been tested on Windows 10 and 11, and Android 12 and 13 built to target API 29.

Four defines are used throughout each code block. These are the appropriate values for using AES-256 encryption and HMAC SHA256 hashing. I do not address management of either key or IV; the assumption is that you have those two values available. I also use AES in NoPadding mode since I'd rather do the padding myself.

C#
int BLOCK_SIZE = 16; 
int HASH_SIZE  = 32;
int KEY_SIZE   = 32; 
int IV_SIZE    = 16; 

C# to compress, then encrypt:

C#
private byte[] compressEncryptData (byte[] inputData, byte[] key, byte[] IV)
{
    //  validate
    if ((inputData == null) || (inputData.Length < 1) ||
        (key == null)       || (key.Length < KEY_SIZE) ||
        (IV == null)        || (IV.Length < IV_SIZE))
    {
        //  report error in whatever manner is appropriate
        return null;
    }

    // compress
    byte[] dataToEncrypt;
    try
    {
        using (MemoryStream memStr = new MemoryStream ())
        using (GZipStream   compr  = new GZipStream (memStr, CompressionMode.Compress))
        {
            compr.Write (inputData, 0, inputData.Length);
            compr.Close ();
            dataToEncrypt = memStr.ToArray ();
        }
    }
    catch (Exception)
    {
        //  report error in whatever manner is appropriate 
        return null;
    }

    // pad to block size OR pad if last byte is a pad value (1 to BLOCK_SIZE)
    if ((dataToEncrypt.Length % BLOCK_SIZE != 0) ||
        ((dataToEncrypt[dataToEncrypt.Length - 1] > 0) && 
         (dataToEncrypt[dataToEncrypt.Length - 1] <= BLOCK_SIZE)))
    {
        int  newLength = (dataToEncrypt.Length + BLOCK_SIZE) /  BLOCK_SIZE * BLOCK_SIZE;
        byte padValue  = (byte) (newLength - dataToEncrypt.Length);

        byte[] data = new byte[newLength];
        Array.Copy (dataToEncrypt, data, dataToEncrypt.Length); 

        for (int i = dataToEncrypt.Length; i < newLength; i++)
        {
            data[i] = padValue;
        }
        dataToEncrypt = data;
    }

    // encrypt
    byte[] encryptedData;
    try
    {
        using (MemoryStream             memStr   = new MemoryStream ())
        using (AesCryptoServiceProvider aesProv  = new AesCryptoServiceProvider () 
              { Padding = PaddingMode.None })
        using (CryptoStream             cryptStr = 
               new CryptoStream (memStr, aesProv.CreateEncryptor (key, IV), 
               CryptoStreamMode.Write))
        {
            cryptStr.Write (dataToEncrypt, 0, dataToEncrypt.Length);
            encryptedData = memStr.ToArray ();
        }
    }
    catch (Exception)
    {
        // report error in whatever manner is appropriate
        return null;
    }

    // calculate hash
    byte[] hashData;
    try
    {
        using (HMACSHA256 hmac = new HMACSHA256 (key)) 
        {
            hashData = hmac.ComputeHash (encryptedData);
        }
    }
    catch (Exception)
    {
        // report error in whatever manner is appropriate 
        return null;
    }

    // all done, build final buffer of hash followed by encrypted data
    byte[] outputData = new byte[encryptedData.Length + hashData.Length]; 
    Array.Copy (hashData,      0, outputData, 0,               hashData.Length);
    Array.Copy (encryptedData, 0, outputData, hashData.Length, encryptedData.Length); 

    return outputData;
}

C# to decrypt, then decompress:

C#
private byte[] decryptDecompressData (byte[] inputData, byte[] key, byte[] IV)
{
    // validate
    if ((inputData == null) || (inputData.Length < BLOCK_SIZE + HASH_SIZE) ||
        (key == null)       || (key.Length < KEY_SIZE) ||
        (IV == null)        || (IV.Length< IV_SIZE))
    {
        // report error in whatever manner is appropriate 
        return null;
    }

    int encryptedLength = inputData.Length - HASH_SIZE; 
    if (encryptedLength % BLOCK_SIZE != 0)
    {
        // report error in whatever manner is appropriate 
        return null;
    }

    // split the encrypted data and the hash into separate arrays
    byte[] hashData = new byte[HASH_SIZE];
    byte[] encrData = new byte[encryptedLength];

    Array.Copy (inputData, 0,         hashData, 0, HASH_SIZE);
    Array.Copy (inputData, HASH_SIZE, encrData, 0, encryptedLength);

    // verify hashes match
    byte[] calcedHash; 
    try
    {
        using (HMACSHA256 hmac = new HMACSHA256 (key))
        {
            calcedHash = hmac.ComputeHash (encrData);
        }
    }
    catch (Exception)
    {
        // report error in whatever manner is appropriate
        return null;
    }

    for (int i= 0; i < HASH_SIZE; i++)
    {
        if (calcedHash[i] != hashData[i])
        {
            // report error in whatever manner is appropriate
            return null;
        }
    }

    // decrypt
    byte[] decryptedData;
    try
    {
        using (MemoryStream             memStr   = new MemoryStream ())
        using (AesCryptoServiceProvider aesProv  = 
               new AesCryptoServiceProvider () { Padding = PaddingMode.None })
        using (CryptoStream             cryptStr = new CryptoStream 
              (memStr, aesProv.CreateDecryptor (key, IV), CryptoStreamMode.Write))
        {
            cryptStr.Write (encrData, 0, encrData.Length);
            decryptedData = memStr.ToArray ();
        }
    }
    catch (Exception)
    {
        // report error in whatever manner is appropriate
        return null;
    }

    //  remove padding
    if ((decryptedData[decryptedData.Length - 1] > 0) &&
        (decryptedData[decryptedData.Length - 1] <= BLOCK_SIZE))
    {
        int padValue = (int) decryptedData[decryptedData.Length - 1];

        byte[] data = new byte[decryptedData.Length - padValue];
        Array.Copy (decryptedData, data, decryptedData.Length - padValue);

        decryptedData = data;
    }

    // decompress
    byte[] plainData;
    try
    {
        using (MemoryStream outStr = new MemoryStream ())
        {
            using (MemoryStream inStr   = new MemoryStream (decryptedData))
            using (GZipStream   decompr = new GZipStream 
                                          (inStr, CompressionMode.Decompress))
            {
                inStr.Position = 0;
                decompr.CopyTo (outStr);
            }
            plainData = outStr.ToArray ();
        }
    }
    catch (Exception)
    {
        // report error in whatever manner is appropriate
        return null;
    }

    return plainData;
}

Java to compress, then encrypt:

Java
private byte[] compressEncryptData (byte[] inputData, byte[] key, byte[] IV)
{
    // validate 
    if ((inputData == null) || (inputData.length < 1) ||
        (key == null)       || (key.length < KEY_SIZE) ||
        (IV == null)        || (IV.length < IV_SIZE))
    {
        // report error in whatever manner is appropriate
        return null;
    }

    // compress
    byte[] dataToEncrypt; 
    try
    {
        ByteOutputStream memStr = new ByteOutputStream ();
        GZIPOutputStream compr  = new GZIPOutputStream (memStr);

        compr.write (inputData, 0, inputData.length);
        compr.finish ();
        dataToEncrypt = memStr.toByteArray ();
        memStr.close ();
    }
    catch (Exception ignore)
    {
        // report error in whatever manner is appropriate
        return null;
    }

    // pad to block size OR pad if last byte is a pad value (1 to BLOCK_SIZE)
    if ((dataToEncrypt.length % BLOCK_SIZE != 0) ||
        ((dataToEncrypt[dataToEncrypt.length - 1] > 0) && 
         (dataToEncrypt[dataToEncrypt.length - 1] <= BLOCK_SIZE)))
    {
        int  newLength = (dataToEncrypt.length + BLOCK_SIZE) / BLOCK_SIZE * BLOCK SIZE;
        byte padValue  = (byte) (newLength - dataToEncrypt.length);

        byte[] data = new byte[newLength];
        System.arraycopy (dataToEncrypt, 0, data, 0, dataToEncrypt.length); 

        for (int i = dataToEncrypt.length; i < newLength; i++)
        {
            data[i] = padValue;
        }
        dataToEncrypt = data;
    }

    // encrypt
    byte[] encryptedData;
    try
    {
        SecretKey       algoKey = new SecretKeySpec (key, "AES_256");
        IvParameterSpec algoIV  = new IvParameterSpec (IV);

        Cipher algo = Cipher.getinstance ("AES_256/CBC/NoPadding");
        algo.init (Cipher.ENCRYPT_MODE, algoKey, algoIV); 
        encryptedData = algo.doFinal (dataToEncrypt);
    }
    catch (Exception ignore)
    {
        // report error in whatever manner is appropriate
        return null;
    }

    // calculate hash
    byte[] hashData;
    try
    {
        SecretKey algoKey = new SecretKeySpec (key, "HmacSHA256");

        Mac algo = Mac.getinstance ("HmacSHA256");
        algo.init (algoKey);
        hashData = algo.doFinal (encryptedData);
    }
    catch (Exception ignore)
    {
        // report error in whatever manner is appropriate
        return null;
    }

    // all done, build final buffer of hash followed by encrypted data
    byte[] outputData = new byte[encryptedData.length + hashData.length];
    System.arraycopy (hashData,      0, outputData, 0,               hashData.length); 
    System.arraycopy (encryptedData, 0, outputData, 
                      hashData.length, encryptedData.length);

    return outputData;
} 

Java to decrypt then decompress:

Java
private byte[] decryptDecompressData (byte[] inputData, byte[] key, byte[] IV)
{
    if ((inputData == null) || (inputData.length < 1) ||
        (key == null)       || (key.length < KEY_SIZE) ||
        (IV == null)        || (IV.length < IV_SIZE))
    {
        // report error in whatever manner is appropriate
        return null;
    }

    int encryptedLength = inputData.length – HASH_SIZE;
    if (encryptedLength % BLOCK_SIZE != 0)
    {
        // report error in whatever manner is appropriate
        return null;
    }

    // move the encrypted data and the hash into separate byte arrays
    byte[] hashData = Arrays.copyOfRange (inputData, 0,          HASH_SIZE);
    byte[] encrData = Arrays.copyOfRange (inputData, HASH_SIZE, inputData.length);

    // verify hashes match
    byte[] calcedHash;
    try
    {
        SecretKey algoKey = new SecretKeySpec (key, "HmacSHA256");

        Mac algo = Mac.getinstance ("HmacSHA256");
        algo.init (algoKey);
        calcedHash = algo.doFinal (encrData);
    }
    catch (Exception ignore)
    {
        // report error in whatever manner is appropriate
        return null;
    }

    for (int i = 0; i < HASH_SIZE; i++) 
    {
        if (calcedHash[i] != hashData[i])
        {
            // report error in whatever manner is appropriate
            return null;
        }
    }

    // decrypt
    byte[] decryptedData;
    try
    {
        SecretKey       algoKey = new SecretKeySpec (key, "AES_256");
        IvParameterSpec algoIV  = new IvParameterSpec (IV);

        Cipher algo = Cipher.getinstance ("AES_256/CBC/NoPadding");
        algo.init (Cipher.DECRYPT_MODE, algoKey, algoIV);
        decryptedData = algo.doFinal (encrData);
    }
    catch (Exception ignore)
    {
        // report error in whatever manner is appropriate
        return null;
    }

    // remove padding
    if ((decryptedData[decryptedData.length - 1] > 0) &&
        (decryptedData[decryptedData.length - 1] <= BLOCK_SIZE))
    {
        int padValue = (int) decryptedData[decryptedData.length - 1];

        decryptedData = Arrays.copyOf (decryptedData, decryptedData.length - padValue);
    }

    // decompress
    byte[] plainData;
    try
    {
        ByteOutputStream outStr  = new ByteOutputStream ();
        ByteInputStream  inStr   = new ByteInputStream (decryptedData);
        GZIPInputStream  decompr = new GZIPInputStream (inStr);

        int    len;
        byte[] buffer = new byte[l024];
        while ((len = decompr.read (buffer)) > 0)
        {
            outStr.write (buffer, 0, len);
        }
        inStr.close ();
        decompr.close ();

        plainData = outStr.toByteArray ();
        outStr.close ();
    }
    catch (Exception ignore)
    {
        // report error in whatever manner is appropriate
        return null;
    }

    return plainData;
}

History

  • 10th March, 2023: Original version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)