Click here to Skip to main content
15,885,767 members
Articles / Mobile Apps / Android
Tip/Trick

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

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
13 Mar 2024CPOL1 min read 13.3K   15   6
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)


Written By
Technical Lead
United States United States
Doing this stuff for nearly 40 years now. Lots of GUIs, drivers, services, all starting on Windows 3.1 all the way through 11 with some CE / Windows Mobile thrown in way back when. I've been doing Android in addition to Windows for over 10 years. My stuff has all been one-of-a-kind for various external customers and in-house use, nothing commercial. Problem domain is usually data gathering, hardware control, "allow the user to control it easily," and data analysis / manipulation.

Prior MO was to get an assignment of "build us something to do X," create requirements after a huddle with the hardware engineers, get customer approval, and build it. If it took more than 9 months, it was unusual! Now, I'm working on the "same" thing on both Windows and Android -- not true cross platform (except for the C underneath doing data manipulation) but a project that does the same thing on both Windows and Android, and can exchange data between the two platforms. The product runs on a 9 - 12 month release cycle, so it's still mostly the same MO, just now features / enhancements to the same product instead of creating a new product each time.

I work mostly in Windows and Android but I did have a 2.5 year period where I was Linux heavy with the requirement of cross-platform with Embedded Windows XP - a full system from PCI driver through service / daemon to UI. A controller / test bench running on the single-board-computer sitting on a proprietary board with some cutting-edge hardware.

Comments and Discussions

 
Questioni like to connect. Will you be interested Pin
SERGUEIK20-Mar-24 12:25
SERGUEIK20-Mar-24 12:25 
GeneralMy vote of 5 Pin
Member 209926914-Mar-24 1:23
Member 209926914-Mar-24 1:23 
QuestionNice Code, but... Pin
Kevin Marois13-Mar-24 8:13
professionalKevin Marois13-Mar-24 8:13 
AnswerRe: Nice Code, but... Pin
JudyL_MD13-Mar-24 11:09
JudyL_MD13-Mar-24 11:09 
QuestionIs there a GitHub repo for this code? Pin
Lars Fosdal13-Mar-23 22:19
Lars Fosdal13-Mar-23 22:19 
AnswerRe: Is there a GitHub repo for this code? Pin
JudyL_MD14-Mar-23 2:31
JudyL_MD14-Mar-23 2:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.