2

I'm attempting to write a web app that uses AES encryption over AJAX to interact with a Java backend.

I've spent some time looking for and testing libraries and none of them have proven fruitful.

I have Java <-> PHP working correctly with the following Java code:

public static String encrypt(String input, String key){
    IvParameterSpec ips = new IvParameterSpec("sixteenbyteslong".getBytes());
    try {
        key = md5(key);
    } catch (NoSuchAlgorithmException e1) {
        e1.printStackTrace();
    }
    byte[] crypted = null;
    try{
        SecretKeySpec skey = new SecretKeySpec(key.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, skey, ips);
        crypted = cipher.doFinal(input.getBytes());
    }catch(Exception e){
        System.out.println(e.toString());
    }
    return new String(Base64.encodeBase64(crypted));
}

public static String decrypt(String input, String key){
    IvParameterSpec ips = new IvParameterSpec("sixteenbyteslong".getBytes());
    try {
        key = md5(key);
    } catch (NoSuchAlgorithmException e1) {
        // TODO Auto-generated catch block
        e1.printStackTrace();
    }
    byte[] output = null;
    try{
        SecretKeySpec skey = new SecretKeySpec(key.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, skey,ips);
        output = cipher.doFinal(Base64.decodeBase64(input));
    }catch(Exception e){
        System.out.println(e.toString());
    }
    return new String(output);
}

Base64 is org.apache.commons.codec.binary.Base64.

I tried SlowAES, but it didn't support "PKCS5Padding", but even if this was present the actual encryption may not have been working.

halfer
  • 19,824
  • 17
  • 99
  • 186
Marcus
  • 778
  • 9
  • 20
  • I am creating an API that is easily accessible via HTTP and is required to be fast. SSL would slow down the request and complicate setup for clients. One inherent advantage I have over SSL with an AES setup is that there is a shared secret, both the client and server know the key so the request is a simple call->response rather than performing the handshake, certificate check and then eventually the transfer. For a long-lived connection SSL is a sensible option, but I value quick response times. AES makes sense, but I suppose I could be convinced otherwise. – Marcus Aug 23 '11 at 21:42
  • 1
    I would follow the KISS first, and then optimize *if* there is a performance problem, and *if* it has been proven that it comes from the SSL layer. Moreover, how will you distribute your keys? What if a key is compromised? How will the clients protect them? If they must hard-code them in their HTML pages, they'll become public very quickly. – JB Nizet Aug 23 '11 at 21:50
  • The API is username/password based. The password will never be stored in the page upon load, but will be entered by the user in the case of javascript. SSL is a large overhead for short requests, simply the principle of operation dictates this. Does anyone have any advice relating to the AES situation described in the question? – Marcus Aug 23 '11 at 22:08

1 Answers1

5

I looked at slowAes, and I think you are correct. It's broken.

The code intends to apply PKCS#7 padding when operating in CBC mode, but does not succeed. (PKCS#7 is just PKCS#5 extended to a 16-byte block encryption algorithm. I think java's use of the term PKCS#5 with AES is a msitake - they should call it PKCS#7, because they are doing 16-byte padding).

I modified slowAes to do the padding correctly, and with that modification, I got good interoperability between slowAes and your Java code. But you need to modify your Java code. More about that later.

here's the java code I used: (Demonstration ONLY; not suitable for use in real apps)

private static MessageDigest md;
static {
    try {
        md = MessageDigest.getInstance("MD5");
    }
    catch(Exception e) {
        md = null;
    }
}

private static byte[] md5(String source) {
    byte[] bytes = source.getBytes();
    byte[] digest = md.digest(bytes);
    return digest;
}

public static String encrypt(String input, String key){
    byte[] ivbytes = "sixteenbyteslong".getBytes();  // <- NO NO NO NO  !!!!
    IvParameterSpec ips = new IvParameterSpec(ivbytes);
    System.out.println("plaintext: " + input);
    byte[] keybytes = md5(key);  // <- NO NO NO NO !!!!
    System.out.println("key      : " + Hex.encodeHexString(keybytes));
    System.out.println("iv       : " + Hex.encodeHexString(ivbytes));
    byte[] crypted = null;
    try{
        SecretKeySpec skey = new SecretKeySpec(keybytes, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, skey, ips);
        byte[] ptext = input.getBytes();
        crypted = cipher.doFinal(ptext);
    }catch(Exception e){
        System.out.println(e.toString());
    }
    return new String(Hex.encodeHexString(crypted));
}

public static String decrypt(String input, String key){
    IvParameterSpec ips = new IvParameterSpec("sixteenbyteslong".getBytes());  // <- NO !!!
    byte[] keybytes = md5(key);  // <- BAD BAD BAD!!!
    System.out.println("key      : " + Hex.encodeHexString(keybytes));
    byte[] output = null;
    try{
        SecretKeySpec skey = new SecretKeySpec(keybytes, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, skey, ips);
        output = cipher.doFinal(Hex.decodeHex(input.toCharArray()));
    }catch(Exception e){
        System.out.println(e.toString());
    }
    return new String(output);
}

public void Run() {
    String plaintext = CommandLineArgs.get("pt");
    String keystring = CommandLineArgs.get("k");

    if (plaintext == null || keystring == null) {
        Usage();
        return;
    }

    System.out.println("encrypting...");
    String crypto = encrypt(plaintext, keystring);
    System.out.println("crypto   : " + crypto);
    System.out.println("decrypting...");
    String decrypted = decrypt(crypto, keystring);
    System.out.println("decrypted: " + decrypted);
}

(the Hex class in the code above is org.apache.commons.codec.binary.Hex; It's in the same jar as the Base64 encoder you used. I swapped out your Base64 because I wanted to actually see the bytes.)

and here's the output:

encrypting...
plaintext: AbbaDabbaDo_Once_upon_a_time....
key      : 2e0160e078aa4b925e62b20610378253
iv       : 7369787465656e62797465736c6f6e67
crypto   : f353e4dd6fb11ea13254dfef670ad88f8fbebcd24217374c06daefbbfe152df504035ae2d82537392c9ab1f719993ec1
decrypting...
key      : 2e0160e078aa4b925e62b20610378253
decrypted: AbbaDabbaDo_Once_upon_a_time....

The output from the JS module:

key       : 2e0160e078aa4b925e62b20610378253
iv        : 7369787465656e62797465736c6f6e67
plaintext : AbbaDabbaDo_Once_upon_a_time....
ciphertext: f353e4dd6fb11ea13254dfef670ad88f8fbebcd24217374c06daefbbfe152df504035ae2d82537392c9ab1f719993ec1
decrypted : AbbaDabbaDo_Once_upon_a_time....

And the JS code:

var keystring = "keystring",
md5String = MD5.getDigest(keystring),
keybytes = cryptoHelpers.toNumbers(md5String), // <- NO NO NO!
iv = "sixteenbyteslong".getBytes(),  // <- NO NO NO
keysize, key = cryptoHelpers.toHex(keybytes),
plaintext, bytesToEncrypt, mode, result,
decrypted, recoveredText;
say("key       : " + key);
keysize = slowAES.aes.keySize.SIZE_128;
say("iv        : " + cryptoHelpers.toHex(iv));

plaintext = "AbbaDabbaDo_Once_upon_a_time....";

bytesToEncrypt = cryptoHelpers.convertStringToByteArray(plaintext);
mode = slowAES.modeOfOperation.CBC;
result = slowAES.encrypt(bytesToEncrypt,
                         mode,
                         keybytes,
                         keysize,
                         iv);

say( "plaintext : " + plaintext);
say( "ciphertext: " + cryptoHelpers.toHex(result.cipher));

decrypted = slowAES.decrypt(result.cipher,
                            result.mode,
                            keybytes,
                            keysize,
                            iv) ;

recoveredText = cryptoHelpers.convertByteArrayToString(decrypted);
say( "decrypted : " + recoveredText);

The modification I made to slowAes was in the encrypt() function. I added a new var called padLength,

    if (mode == this.modeOfOperation.CBC) {
        padLength = 16 - (bytesIn.length % 16);
    }
    // the AES input/output
    if (bytesIn !== null)
    {
        for (var j = 0;j < Math.ceil((bytesIn.length + padLength)/16); j++)
        {
        ....

You can get the modified AES source I used, here, along with my test program.


Important: you should not be using an MD5 of the passphrase to get the keybytes. Use PBKDF2. There's a java version of PBKDF2 out there, and it works, and there's a Javascript PBKDF2 that works. Also, some J2EE servers include a PBKDF2 class. Likewise for the IV bytes. These also should be derived from the passphrase. If you doubt this, read IETF RFC 2898 for the rationale.

Don't use the code I posted above for a real app. Modify it to use PBKDF2.


EDIT
About padding...

How would I go about removing the additional padding when decrypting a returned message because I wouldn't necessarily know the unencrypted length. I thought that the padding bytes were meant to be equal to the padding length, but they don't seem to be.

AES is a block encryptor; it encrypts blocks of exactly 16 bytes long. If you put in 32 bytes of plaintext, you get exactly 32 bytes of cryptotext out in return. If you put in 1024 bytes, you get 1024 bytes out. (not exactly true, you'll see why later. just assume this is true for now.)

As you have seen, when the plaintext is not an even multiple of 16 bytes, since AES requires 16 byte blocks, the question arises - what do I put in as "extra" stuff to make a full 16-byte block? The answer is padding.

There are different was to pad. In CBC mode, the typical way is PKCS#7 (Java calls it PKCS#5, as I said I think that is a misnomer). If you send in 25 bytes of plaintext, padding means AES will actually encrypt 32 bytes: 25 bytes of actual data, and 7 bytes of padding. Ok, but what values go into the 7 bytes of padding?

PKCS#7 says that the pad byte is the value 16-len, where len is the length of actual data bytes in the final block. In other words, the value is the same as the number of pad bytes, which is what you said. In the example above, if you encrypt 25 bytes, you need 7 pad bytes, and each one will take the value 7. These pad bytes get added to the end of the plaintext, before encryption. It results in a cryptostream that is a whole number of 16-byte blocks.

This is nice because on decryption, the decryptor can simply look at the final byte in the decrypted stream, and it now knows how many pad bytes to remove from that decrypted stream. With PKCS#7 padding, the application layer does not need to worry about removing padding on decryption or adding padding on encryption. The AES library should handle all that. Suppose the decryptor decrypts 32 bytes of cryptotext, and the last byte in the resulting plaintext is a value of 7. With PKCS#7 padding, the decryptor knows to slice off 7 bytes from the end of the last block, and deliver a partial block of 9 bytes for the final one, to the application, for a total of 25 bytes of plaintext.

Java does this correctly. slowAES was doing it correctly, except for the case where the plaintext length was a multiple of 16 bytes. PKCS#7 says that in that case, you need to add 16 bytes of padding, all with value 16. If you want to encrypt exactly 32 bytes, PKCS#7 for AES says, you need to add 16 bytes of pad, for a total of 48 bytes encrypted. This is so the decryptor can do the right thing. Think about it: If you don't add 16 bytes of padding, the decryptor cannot "tell" that the last byte of plaintext is not a padding byte.

SlowAES was not padding in this case, which was part of the reason you couldn't get it to interoperate with Java. I noticed this because the cryptostream was exactly 32 bytes for a 32-byte plaintext, and that means no padding. When I looked in the code the logic error was right there. (Reminds me: I need to verify that there is not a logic error in slowaes regarding padding on the decryption side)

So you are correct that your app does not know the unencrypted length. But the decryptor does know how many bytes to slice off, if PKCS#7 padding is used, and a correct decryptor will always return to you the correct number of bytes of plaintext. For this to work properly, you must use the same padding convention when decrypting as was used when encrypting. You normally need to tell the crypto library what padding to use, although some (like slowAES) don't give you the choice.

If you use "no padding" which is an option in some libraries but not in slowAES when in CBC mode, then, yes, your application must somehow "know" the size of the unencrypted data, so it can discard the last N bytes of plaintext. This is ok in some data formats and protocols. But often it's easier to use PKCS#7 padding.


EDIT

just looked again and yes, the decrypt logic in slowAES also has a problem with the padding. It wants you to pass in the "decrypted length" - I see that now, I see that was the reason for your question. This is unnecessary if it does PKCS#7 padding properly. It isn't now. Should be a simple fix. Will update back here, later.

EDIT

ok, the updated AES file is now available here; the updated test code is here. It does PKCS#7 padding correctly on encrypt and decrypt. I probably should send these changes back to the owners of slowAES.

EDIT

oh, and one more thing: Performing encryption within Javascript is considered harmful.

Cheeso
  • 189,189
  • 101
  • 473
  • 713
  • Absolutely amazing. You've saved me and probably a lot of other people who find this question a lot of time. I've now got Java <-> Javascript, but the PHP encryption no longer works. I assume it's just the padding I need to change? It worked with the previous implementation in Java. – Marcus Aug 24 '11 at 18:06
  • Well hmmm, that's a bummer. I didn't change the padding in the Java program; I left it as it was. but i did change the encoding of the string - in *my* Java code it uses Hex to encode, where you used Base64. So you may want to switch that back in the Java program, and also replace the `toHex()` in the JS program with base64 equivalents. You will need to get a Base64 encoder for Javascript. There's one at http://www.webtoolkit.info/javascript-base64.html . And be sure to get the PKBDF2 key generator. – Cheeso Aug 24 '11 at 18:26
  • You've helped me so much this far, it almost seems impolite to ask something else, but I don't think it's that complicated. How would I go about removing the additional padding when decrypting a returned message because I wouldn't necessarily know the unencrypted length. I thought that the padding bytes were meant to be equal to the padding length, but they don't seem to be. Thanks. – Marcus Aug 24 '11 at 18:51
  • My reply was too long to fit here, so I edited the answer above. Good luck. It's not impolite to ask. If I didn't want to answer, I wouldn't. Did I mention that you should use PBKDF2? – Cheeso Aug 24 '11 at 21:47
  • @Cheeso - I am unable to open the Test URL which you have shared. I am stuck with a similar problem where Java is sending me a Encrypted text and i need to decrypt it at UI using javascript. Can you share some place where you have the complete Javascript code which i can use for decryption. Are you using any library apart from your JS code – Achyut Oct 09 '14 at 15:07