Search code examples
javaencryptioncryptographyaescryptojs

Given Final Block not properly padded while AES decryption


First, I'll tell what is my primary goal. I'm going to use AES to encrypt some content in the client side, then use RSA public key to encrypt the important AES specs and sending both AES encrypted data and RSA encrypted AES specs to server. So at server, I'll decrypt the AES key specs using RSA private key, then using those AES specs, I'll decrypt the AES encrypted data. I've successfully made the RSA part working by test encrypting and decrypting. I've to made this AES art working before implementing RSa in this.

For client side, I'm using crypto-js

<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js"></script>
<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/pbkdf2.js"></script>
<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/components/enc-base64-min.js"></script>
<script type="text/javascript" src="jquery-1.7.1.js"></script>
<script type="text/javascript">

    $("#submit").click(function() {
        var salt = CryptoJS.lib.WordArray.random(16);
        var iv = CryptoJS.lib.WordArray.random(16);
        var pass = CryptoJS.lib.WordArray.random(16);
        var message = "Test Message for encryption";
        var key128Bits = CryptoJS.PBKDF2(pass, salt, { keySize: 128 }); 
        var key128Bits10Iterations = CryptoJS.PBKDF2(pass, salt, { keySize: 128, iterations: 10 });
        var encrypted = CryptoJS.AES.encrypt(message, key128Bits10Iterations, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7  });
        var cipherData = encrypted.toString()+":"+salt.toString()+":"+iv.toString()+":"+pass.toString();
        console.log(cipherData);

        $.ajax({
            url: 'encryption',
            type: 'POST',
            data: {
                cipherData: cipherData
            },
            success: function(data) {
                console.log(data);
            },
            failure: function(data) {

            }
        });
    });

</script>

And this is the code I'm using in server side

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String encryptedData = request.getParameter("cipherData");
    String data[] = encryptedData.split(":");

    String encrypted = data[0];     
    String salt = data[1];
    String iv = data[2];
    String password = data[3];

    byte[] saltBytes = hexStringToByteArray(salt);
    byte[] ivBytes = hexStringToByteArray(iv);
    IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);        
    SecretKeySpec sKey = null;
    try {
        sKey = (SecretKeySpec) generateKeyFromPassword(password, saltBytes);
    } catch (GeneralSecurityException e) {
        e.printStackTrace();
    }
    try {
        System.out.println( decrypt( encrypted , sKey ,ivParameterSpec));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static SecretKey generateKeyFromPassword(String password, byte[] saltBytes) throws GeneralSecurityException {

    KeySpec keySpec = new PBEKeySpec(password.toCharArray(), saltBytes, 10, 128);
    SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
    SecretKey secretKey = keyFactory.generateSecret(keySpec);

    return new SecretKeySpec(secretKey.getEncoded(), "AES");
}

public static byte[] hexStringToByteArray(String s) {

    int len = s.length();
    byte[] data = new byte[len / 2];

    for (int i = 0; i < len; i += 2) {
        data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                + Character.digit(s.charAt(i+1), 16));
    }

    return data;

}

public static String decrypt(String encryptedData, SecretKeySpec sKey, IvParameterSpec ivParameterSpec) throws Exception { 

    Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
    c.init(Cipher.DECRYPT_MODE, sKey, ivParameterSpec);
    byte[] decordedValue = Base64.decodeBase64(encryptedData);
    byte[] decValue = c.doFinal(decordedValue);
    String decryptedValue = new String(decValue);

    return decryptedValue;
}

First I've to make sure that the server is receiving the same data which I'm sending. So I tested it through sysout for encrypted,salt,iv and password. It's received the same data. But I got the exception at line

byte[] decValue = c.doFinal(decordedValue);

javax.crypto.BadPaddingException: Given final block not properly padded
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:966)
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:824)
at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:436)
at javax.crypto.Cipher.doFinal(Cipher.java:2121)
at com.Encryption.decrypt(Encryption.java:95)
at com.Encryption.doPost(Encryption.java:60)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:644)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:291)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:219)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:503)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:136)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:610)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:526)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1078)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:655)
at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.process(Http11NioProtocol.java:222)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1566)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1523)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)

You can see that in Javascript side, it's CryptoJS.pad.Pkcs7 and at server side it's AES/CBC/PKCS5Padding , I've done some searches on that and found that both are identical. Neither I can change it to CryptoJS.pad.Pkcs5 nor AES/CBC/PKCS7Padding which is not defined for both Crypto-js libraries and Java inbuild libraries.

And also I'm having the following thought. In javascript, I'm using random salt and pass to generate a 128bits key. And using the same salt and pass, I'm generating the same key in Java by defining appropriate Iteration count and key size. Why should I have to lengthen the process in Java by generating the same key again? I can simply send the key (encrypted.key), encrytedData(encrypted.toString()) and Iv (encrypted.iv) to server and decrypt the data instantly without have to go through the process of generating the key again. Am I right about this..? I've tried this too, where I'm getting "Invalid AES key length exception". For maintaining security, I'll encrypt the key and Iv in client side using RSA public key. One of the reasons for Implementing Symmetric with Asymmetric is due to RSA's limited size of data to be encrypted. But I cant Implement it if I can't clear this BadPaddingException.


Solution

  • Since you want to use RSA and did already implement it, there is no need to use a password derivation. Create a random key and random iv:

    var key = CryptoJS.lib.WordArray.random(16); // 128bit
    var iv = CryptoJS.lib.WordArray.random(16);  // 128bit
    var encrypted = CryptoJS.AES.encrypt(message, key, { iv: iv }); // CBC/PKCS#7 is default
    

    Then you send iv.toString(Crypto.enc.Base64), encrypted.ciphertext.toString(Crypto.enc.Base64) and "RSAencrypt(key)" to the server, decode the base64 encoded iv and ciphertext, decrypt the RSA ciphertext to get the AES key and combine all of them to decrypt the ciphertext.


    Your original problem probably lies in the sizes that you use. CryptoJS has an internal representation which consists of 4 bytes per word. That is why you need to divide by 32 for example to get a 128-bit hash:

    var key128Bits = CryptoJS.PBKDF2(pass, salt, { keySize: 128/32 });
    

    The WordArray on the other hand works only on bytes which is why you divide by 8:

    var key = CryptoJS.lib.WordArray.random(128/8);