开发者

Javascript <-> Java AES

开发者 https://www.devze.com 2023-03-30 09:03 出处:网络
I\'m attempting to write a web app that uses AES encryption over AJAX to interact with a Java backend.

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

I've spent 开发者_StackOverflowsome 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.


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.

0

精彩评论

暂无评论...
验证码 换一张
取 消