I'm bulding an application that Encrypt in .NET and decrypt on Android App.
To test the correcteness of .NET, I also develope the Decrypt method on .NET, which is basically:
public static byte[] DecryptStreamWithEmbeddedHash(Stream inputStream, string password)
{
const int iterations = 10000;
const string customSalt = "myCustomSalt";
static HashAlgorithmName hashAlgorith = HashAlgorithmName.SHA256;
byte[] salt = Encoding.ASCII.GetBytes(password + customSalt);
using (var aesAlg = Aes.Create())
{
using (var keyDerivation = new Rfc2898DeriveBytes(password, salt, iterations, hashAlgorith))
{
aesAlg.Key = keyDerivation.GetBytes(32);
aesAlg.IV = keyDerivation.GetBytes(16);
// read hash length
byte[] hashLengthBytes = new byte[sizeof(int)];
inputStream.Read(hashLengthBytes, 0, sizeof(int));
int hashLength = BitConverter.ToInt32(hashLengthBytes, 0);
// read hash
byte[] embeddedHash = new byte[hashLength];
inputStream.Read(embeddedHash, 0, hashLength);
// decrypt content
using (var cryptoStream = new CryptoStream(inputStream, aesAlg.CreateDecryptor(), CryptoStreamMode.Read))
using (var memoryStream = new MemoryStream())
{
cryptoStream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
}
}
}
Here's my attempt on Java:
public static byte[] decryptStreamWithEmbeddedHash(InputStream inputStream, String password) throws Exception {
int iterations = 10000;
String customSalt = "myCustomSalt";
String hashAlgorithm = "SHA-256";
byte[] salt = (password + customSalt).getBytes(StandardCharsets.US_ASCII);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, 256);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
byte[] iv = Arrays.copyOfRange(salt, 0, 16); // IV is the first 16 bytes of salt
// read hash length
byte[] hashLengthBytes = new byte[4]; // sizeof(int)
inputStream.read(hashLengthBytes, 0, 4);
int hashLength = ByteBuffer.wrap(hashLengthBytes).order(ByteOrder.LITTLE_ENDIAN).getInt();
// read hash
byte[] embeddedHash = new byte[hashLength];
inputStream.read(embeddedHash, 0, hashLength);
// decrypt content
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
ByteArrayOutputStream decryptedStream = new ByteArrayOutputStream();
try (CipherInputStream cryptoStream = new CipherInputStream(inputStream, cipher)) {
byte[] buffer = new byte[8192];
int len;
while ((len = cryptoStream.read(buffer)) > 0) {
decryptedStream.write(buffer, 0, len);
}
} catch (Exception err) {
err.printStackTrace();
}
byte[] decryptedData = decryptedStream.toByteArray();
// verify hash
MessageDigest md = MessageDigest.getInstance(hashAlgorithm);
byte[] calculatedHash = md.digest(decryptedData);
if (!Arrays.equals(calculatedHash, embeddedHash)) {
throw new Exception("Hash verification failed.");
}
return decryptedData;
}
but on:
while ((len = cryptoStream.read(buffer)) > 0) {
decryptedStream.write(buffer, 0, len);
}
at some point I got this:java.io.IOException: javax.crypto.IllegalBlockSizeException: error:1e00007b:Cipher functions:OPENSSL_internal:WRONG_FINAL_BLOCK_LENGTH
Am I not really sure what's the concrete cause. Going in debug indexes and position of stream seems correct?
Any clues?
The PBKDF2 key derivation in both codes differs in 2 points:
So that the key derivation in the Java code is equivalent to that in the C# code, the following changes are required:
...
// derive key and IV via PBKDF2/HMAC/SHA256
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); // Fix 1: apply SHA 256
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, 256 + 128); // Fix 2: derive key and IV
byte[] keyIv = factory.generateSecret(spec).getEncoded();
ByteBuffer keyIvBuffer = ByteBuffer.wrap(keyIv);
byte[] key = new byte[32];
keyIvBuffer.get(key);
SecretKey secret = new SecretKeySpec(key, "AES");
byte[] iv = new byte[16];
keyIvBuffer.get(iv);
...
Note that the C# code does not contain the part that compares the hash values. Maybe this part was simply not posted.
Also consider the comment on the read()
calls in both codes: In general, a read()
call is not guaranteed to return as much data as was requested. More robust is e.g. on the Java side readNBytes()
.
Regarding the comparison of the hash values: Usually an HMAC (of the ciphertext) is used for authentication, see Encrypt-then-MAC.
Test (with a ciphertext that is successfully decrypted by the C# code):
ByteArrayInputStream bais = new ByteArrayInputStream(HexFormat.of().parseHex("20000000D7A8FBB307D7809469CA9ABCB0082E4F8D5651E46D3CDB762D02D0BF37C9E592682052AFDB5B011FCEDEBC9FBF65688ADB9C1D32551CDE3F505D35B693CAED7B189C1D6DAD14EFD5861DF1239CF1CB74"));
byte[] decrypted = decryptStreamWithEmbeddedHash(bais, "my password");
System.out.println(new String(decrypted, StandardCharsets.UTF_8)); // The quick brown fox jumps over the lazy dog