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
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.