Search code examples
java.netencryption

What's wrong on my Decrypt method translating it from .NET to Java?


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?


Solution

  • The PBKDF2 key derivation in both codes differs in 2 points:

    • In the .NET code SHA256 is used, in the Java code SHA1.
    • In the .NET code the key and IV are derived, in the Java code only the key. As IV the first 16 bytes of the salt are used.

    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