digitalbazaar / forge

A native implementation of TLS in Javascript and tools to write crypto-based and network-heavy webapps
https://digitalbazaar.com/
Other
5.07k stars 784 forks source link

Forge AES Encryption/Decryption compatibility with default jdk 8 Java AES Encryption/Decryption #627

Open fgharo opened 5 years ago

fgharo commented 5 years ago

Hi,

I am trying to write some Javascript AES encryption/decryption code that is compatible with the defaults of a Java AES/ECB/PKCS5Padding encryption/decryption implementation at my job.

Let me share the implementation code more/less in a junit to show you what I mean:

import static org.junit.Assert.*;

import org.junit.Test;

import com.google.common.base.Charsets;

import static com.google.common.base.Strings.nullToEmpty;
import static com.google.common.base.Strings.padEnd;
import static com.google.common.io.BaseEncoding.base64;

import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

public class EncryptionDecryptionTest {
    AES specificAesImpl = new AES("secret");
        /* I want javascript code to behave the same way in terms of input/output as shown in the following tests: */
    @Test public void encryption() throws Exception {
        assertEquals("{ENCRYPT_AES}4XZCA28Y4krd5k/XblolMg==",specificAesImpl.encrypt("password"));
    }

    @Test public void decryption() throws Exception {
        assertEquals("password" , specificAesImpl.decrypt("{ENCRYPT_AES}4XZCA28Y4krd5k/XblolMg=="));
    }
}

class AES {
    static final String PREFIX = "{ENCRYPT_AES}";
    private final String passphrase;
    private final Key key;

    public AES(String passphrase)  {
        String formatted = nullToEmpty(passphrase);

        if (formatted.getBytes(Charsets.UTF_8).length != 16) {
            System.out.format("AES Encryption passphrase was %s to 16 bytes.\n", formatted.length() < 16 ? "padded" : "truncated");
        }

        this.passphrase = padEnd(formatted, 16, 'X').substring(0, 16);
        // Code for SecretKeySpec is in source [4].
        this.key = new SecretKeySpec(this.passphrase.getBytes(Charsets.UTF_8), 0, this.passphrase.length(), "AES");

    }

    public String encrypt(String value) {
        try {
            Cipher cipher = Cipher.getInstance(this.key.getAlgorithm());
               cipher.init(Cipher.ENCRYPT_MODE, this.key);

            final byte[] bytes = value.getBytes(Charsets.UTF_8);
            final byte[] encrypted = cipher.doFinal(bytes);

            return PREFIX + base64().encode(encrypted);

        } catch (IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
            System.out.println("Could not encrypt value with provided passphrase.");
            throw new RuntimeException();
        }
    }

    public String decrypt(String value) {
        try {
               Cipher cipher = Cipher.getInstance(this.key.getAlgorithm());
               cipher.init(Cipher.DECRYPT_MODE, this.key);

            String encrypted = value.substring(PREFIX.length());

            final byte[] decoded = base64().decode(encrypted);
            final byte[] decrypted = cipher.doFinal(decoded);

            return new String(decrypted, Charsets.UTF_8);

        } catch (GeneralSecurityException e) {
            System.out.println("Could not decrypt value with provided passphrase.");
            throw new RuntimeException();        
        }
    }
}

According to this source [1] under section "Creating a Cipher Object" , The default transformation settings or modes of operation in java is AES/ECB/PKCS5Padding. So when creating cipher object in encrypt/decrypt methods above it should be set to this default.

Some points I find confusing to get over: a. Java Strings are represented using unicode and can be converted to a utf 8 byte array with "hi".getBytes(Charsets.UTF_8); however javascript strings are stored as utf 16 strings. Would this function do me any good? I could call it like utf16ToUtf8ByteArr("secret")? Or would I have to call it with Xs to match the AES cstr above? utf16ToUtf8ByteArr("secretXXXXXXXXXX")?

var utf16ToUtf8ByteArr = (utf16Str) => {
  var utf8 = unescape(encodeURIComponent(utf16Str));

  var arr = [];
  for (var i = 0; i < utf8.length; i++) {
      arr.push(utf8.charCodeAt(i));
  }
  return arr;
};

b. I printed out the initialization vector in java System.out.println(Arrays.toString(cipher.getIV())); and it returned null. This contradicts my understanding for doing pkcs5 with your code example here [3]. It almost seems like my jobs implementation doesn't really use or take advantage of using a password for pkcs5? Or a salt? Or number of iterations for that matter?

Here is my attempt at the Javascript implementation. Hope you can understand it as I cannot :P I used [5] as a reference guide.

var utf16ToUtf8ByteArr = (utf16Str) => {
  var utf8 = unescape(encodeURIComponent(utf16Str));

  var arr = [];
  for (var i = 0; i < utf8.length; i++) {
      arr.push(utf8.charCodeAt(i));
  }
  return arr;
};
// Encryption
var cipher = forge.cipher.createCipher('AES-ECB', utf16ToUtf8ByteArr('secretXXXXXXXXXX'));
cipher.start(); // start without iv.
cipher.update(forge.util.createBuffer(utf16ToUtf8ByteArr('password')));
cipher.finish();
var encrypted = cipher.output; // cipher.output has nothing in it. Its like it threw away the input :(

// Decryption
var decipher = forge.cipher.createDecipher('AES-ECB', utf16ToUtf8ByteArr('secretXXXXXXXXXX'));
decipher.start();
decipher.update(encrypted);
decipher.finish(); // true
console.log(decipher.output.toHex()); // This too prints out nothing what was I expecting considering above :(

I assumed something was wrong with key so I tried changing that part as follows and passing it to createCipher/creatDecipher functions but it had no effect.

var salt = forge.random.getBytesSync(128); 
var derivedKey = forge.pkcs5.pbkdf2('secretXXXXXXXXXX', salt, 0, 16);

Please let me know if you can spot any mistakes.

[1] https://docs.oracle.com/javase/8/docs/technotes/guides/security/crypto/CryptoSpec.html#Cipher [2] http://es5.github.io/#x4.3.16 [3] https://github.com/digitalbazaar/forge#pkcs5 [4] https://github.com/frohoff/jdk8u-dev-jdk/blob/master/src/share/classes/javax/crypto/spec/SecretKeySpec.java [5] https://github.com/digitalbazaar/forge#cipher [6] https://github.com/frohoff/jdk8u-dev-jdk/blob/master/src/share/classes/javax/crypto/Cipher.java

johnmanko commented 5 years ago

You might find a better response for support questions at Stackoverflow: https://stackoverflow.com/search?q=forge+%5Bcryptography%5D