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 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)
{
if ((inputData == null) || (inputData.Length < 1) ||
(key == null) || (key.Length < KEY_SIZE) ||
(IV == null) || (IV.Length < IV_SIZE))
{
return null;
}
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)
{
return null;
}
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;
}
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)
{
return null;
}
byte[] hashData;
try
{
using (HMACSHA256 hmac = new HMACSHA256 (key))
{
hashData = hmac.ComputeHash (encryptedData);
}
}
catch (Exception)
{
return null;
}
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)
{
if ((inputData == null) || (inputData.Length < BLOCK_SIZE + HASH_SIZE) ||
(key == null) || (key.Length < KEY_SIZE) ||
(IV == null) || (IV.Length< IV_SIZE))
{
return null;
}
int encryptedLength = inputData.Length - HASH_SIZE;
if (encryptedLength % BLOCK_SIZE != 0)
{
return null;
}
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);
byte[] calcedHash;
try
{
using (HMACSHA256 hmac = new HMACSHA256 (key))
{
calcedHash = hmac.ComputeHash (encrData);
}
}
catch (Exception)
{
return null;
}
for (int i= 0; i < HASH_SIZE; i++)
{
if (calcedHash[i] != hashData[i])
{
return null;
}
}
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)
{
return null;
}
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;
}
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)
{
return null;
}
return plainData;
}
Java to compress, then encrypt:
private byte[] compressEncryptData (byte[] inputData, byte[] key, byte[] IV)
{
if ((inputData == null) || (inputData.length < 1) ||
(key == null) || (key.length < KEY_SIZE) ||
(IV == null) || (IV.length < IV_SIZE))
{
return null;
}
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)
{
return null;
}
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;
}
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)
{
return null;
}
byte[] hashData;
try
{
SecretKey algoKey = new SecretKeySpec (key, "HmacSHA256");
Mac algo = Mac.getinstance ("HmacSHA256");
algo.init (algoKey);
hashData = algo.doFinal (encryptedData);
}
catch (Exception ignore)
{
return null;
}
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))
{
return null;
}
int encryptedLength = inputData.length – HASH_SIZE;
if (encryptedLength % BLOCK_SIZE != 0)
{
return null;
}
byte[] hashData = Arrays.copyOfRange (inputData, 0, HASH_SIZE);
byte[] encrData = Arrays.copyOfRange (inputData, HASH_SIZE, inputData.length);
byte[] calcedHash;
try
{
SecretKey algoKey = new SecretKeySpec (key, "HmacSHA256");
Mac algo = Mac.getinstance ("HmacSHA256");
algo.init (algoKey);
calcedHash = algo.doFinal (encrData);
}
catch (Exception ignore)
{
return null;
}
for (int i = 0; i < HASH_SIZE; i++)
{
if (calcedHash[i] != hashData[i])
{
return null;
}
}
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)
{
return null;
}
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);
}
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)
{
return null;
}
return plainData;
}
History
- 10th March, 2023: Original version