Windows / Android (C#/Java) Compatible Data Encryption with Compression
Code sample for passing encrypted compressed data between Windows and Android
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 string
s). 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:
- C# using .NET Framework to compress and encrypt
- C# using .NET Framework to decrypt and decompress
- Java to compress and encrypt
- 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.
int BLOCK_SIZE = 16;
int HASH_SIZE = 32;
int KEY_SIZE = 32;
int IV_SIZE = 16;
C# to compress, then encrypt:
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:
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:
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:
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