0

So usually i use one java file to encrypt and decrypt a string to hex with AES, then my angular app want to consume api, that use the result of it.

this is my old java code

package decryptoor;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.util.Formatter;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;


public class CryptoAndroidKoplak {

    private static final String TEXT_ENCODING = "UTF-8";
    private static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";
    private static final String ENCRYPTION_ALGORITM = "AES";
    private static final String TAG = "Crypto";

    private Cipher cipher;
    private IvParameterSpec initialVector;

//    private static void DEBUG(String msg){
//        if(IDefines.DEBUG_LOG_TRACE){
//            Log.i(TAG, msg);
//        }
//    }
    public CryptoAndroidKoplak() {
        try {
            cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
            initialVector = new IvParameterSpec(new byte[16]);
        } catch (Exception e) {
            System.out.println(e.toString());
        }
    }

    public String encryptString(String plainText, String key) throws Exception {
        return toHexString(encrypt(plainText, key)).toUpperCase();
    }

    public byte[] encrypt(String plainText, String key) throws Exception {
        byte[] byteKey = getKeyBytes(key);
        byte[] plainData = plainText.getBytes(TEXT_ENCODING);
        SecretKeySpec keySpec = new SecretKeySpec(byteKey, ENCRYPTION_ALGORITM);
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, initialVector);
        return cipher.doFinal(plainData);
    }

    public String decryptString(String encryptedText, String key) throws Exception {
        return new String(decrypt(encryptedText, key));
    }

    public byte[] decrypt(String encryptedText, String key) throws Exception {
        byte[] byteKey = getKeyBytes(key);
        byte[] encryptData = hexToAscii(encryptedText);
        SecretKeySpec keySpec = new SecretKeySpec(byteKey, ENCRYPTION_ALGORITM);
        cipher.init(Cipher.DECRYPT_MODE, keySpec, initialVector);
        return cipher.doFinal(encryptData);
    }

    public static String toMD5(String text) throws Exception {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] data = text.getBytes(TEXT_ENCODING);
        return toHexString(md.digest(data));
    }

    public static String toSHA1(String text) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        byte[] data = text.getBytes(TEXT_ENCODING);
        return toHexString(md.digest(data));
    }

    private static String toHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);

        Formatter formatter = new Formatter(sb);
        for (byte b : bytes) {
            formatter.format("%02x", b);
        }

        return sb.toString();
    }

    private static byte[] hexToAscii(String hexStr) {
        byte[] buff = new byte[hexStr.length() / 2];
        int offset = 0;
        for (int i = 0; i < hexStr.length(); i += 2) {
            String str = hexStr.substring(i, i + 2);
            buff[offset++] = (byte) Integer.parseInt(str, 16);
        }

        return buff;
    }

    private static byte[] getKeyBytes(String key) throws UnsupportedEncodingException {
        byte[] keyBytes = new byte[16];
        byte[] parameterKeyBytes = key.getBytes("UTF-8");
        System.arraycopy(parameterKeyBytes, 0, keyBytes, 0, Math.min(parameterKeyBytes.length, keyBytes.length));
        return keyBytes;
    }

}



and this is my code in angular

import { Injectable } from '@angular/core';
import * as CryptoJS from 'crypto-js';

@Injectable({
  providedIn: 'root'
})
export class Encryption {
  constructor() {}

  encryptAesToString(stringToEncrypt: string, key: string): string {
    // first way
    // let encrypted;
    // try {
    //   encrypted = CryptoJS.AES.encrypt(JSON.stringify(stringToEncrypt), key);
    // } catch (e) {
    //   console.log(e);
    // }
    // encrypted = CryptoJS.enc.Hex.stringify(encrypted.ciphertext);
    // return encrypted;

    // second way
    // var b64 = CryptoJS.AES.encrypt(stringToEncrypt, key).toString();
    // var e64 = CryptoJS.enc.Base64.parse(b64);
    // var eHex = e64.toString(CryptoJS.enc.Hex);
    // return eHex;

    // third way
    const key2 = CryptoJS.enc.Utf8.parse(key);
    const iv = CryptoJS.enc.Utf8.parse(key);
    const encrypted = CryptoJS.AES.encrypt(stringToEncrypt, key2, {
      keySize: 16,
      iv: iv,
      mode: CryptoJS.mode.ECB,
      padding: CryptoJS.pad.Pkcs7,
    });
    let eHex = CryptoJS.enc.Hex.stringify(encrypted.ciphertext);
    return encrypted;
  }

  decryptAesformString(stringToDecrypt: string, key: string): string {
    let decrypted: string = '';
    try {
      const bytes = CryptoJS.AES.decrypt(stringToDecrypt, key);
      if (bytes.toString()) {
        decrypted = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
      }
    } catch (e) {
      console.log(e);
    }
    return decrypted;
  }
}

i have try three code, the first one doesn't return hex, so i try 2 more ways but it doesn't show same encrypted string with the old java code so i cant consume the api.

any idea why this happen? if you have better way to encrypt and decrypt with key that more simple both in angular and java, it will really help.

many thanks

ikiSiTamvaaan
  • 127
  • 1
  • 14
  • 2
    That is easy to notice. Java uses CBC mode Angular uses ECB mode. Turn Angular into `mode: CryptoJS.mode.CBC`. Note: there might be other errors, too. – kelalaka Jul 11 '19 at 13:16
  • thanks @kelalaka, it produce diferent result from ecb mode, but stil not give same result with java, pcks5 cant be used in angular, but pcks7 cant be use in java. but from what i read they are both is same. is there another thing that i miss? – ikiSiTamvaaan Jul 12 '19 at 03:11
  • are you sure that the Encryption keys are same? Also, you need to use the same IV for both. – kelalaka Jul 12 '19 at 07:37
  • IVs must be unpredictable, especially for CBC. Your Java code even uses a zero IV! Your key seems to be from the UTF-8 space, which is far smaller than proper random bytes. It's unclear what you expect to get out of all this. Having the key and ciphertext on the client is the same as having the plaintext to begin with. Just use TLS (i.e. HTTPS) and call it a day. – Peter Jul 12 '19 at 10:44
  • thanks everyone i decide to make new java file, and i think this one better hehe – ikiSiTamvaaan Jul 12 '19 at 12:02

1 Answers1

2

after give up on how to make it same with my old java code, finally i try to make a new one hehe...

so after i read this answer, then i understand CryptoJS (library that i use in angular) implements the same key derivation function as OpenSSL. so i choose to use basic CryptoJS function to encrypt my string like this

var text = "The quick brown fox jumps over the lazy dog.  ";
var secret = "René Über";
var encrypted = CryptoJS.AES.encrypt(text, secret);
encrypted = encrypted.toString();
console.log("Cipher text: " + encrypted);

after that, what i need to do is make new java file to encrypt and decrypt aes OpenSsl, and i get what i need here in this answer. i use robert answer, cause accepted answer not really give me what i need.

but like the first answer mentioned, to encrypt and decrypt in this way, we have to install the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy. Otherwise, AES with key size of 256 won't work and throw an exception:(you won't need JCE with up-to-date java version)

so i add some functionality to force using AES with key size of 256 without to install JCE here. note to use this, actually isnt recomended, please read the comment in ericson answer

then this is my final code to encrypt and decrypt like OpenSsl

package decryptoor;

import groovy.transform.CompileStatic;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URLEncoder;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.security.SecureRandom;
import static java.nio.charset.StandardCharsets.*;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Permission;
import java.security.PermissionCollection;
import java.util.Arrays;
import java.util.Base64;
import java.util.Map;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

/**
* Mimics the OpenSSL AES Cipher options for encrypting and decrypting messages using a shared key (aka password) with symetric ciphers.
*/
@CompileStatic
class OpenSslAes {

/** OpenSSL's magic initial bytes. */
private static final String SALTED_STR = "Salted__";
private static final byte[] SALTED_MAGIC = SALTED_STR.getBytes(US_ASCII);


static String encryptAndURLEncode(String password, String clearText) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, InvalidAlgorithmParameterException, BadPaddingException, UnsupportedEncodingException {

    String encrypted = encrypt(password, clearText);
    return URLEncoder.encode(encrypted, UTF_8.name() );
}

/**
 *
 * @param password  The password / key to encrypt with.
 * @param data      The data to encrypt
 * @return  A base64 encoded string containing the encrypted data.
 */
static String encrypt(String password, String clearText) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, InvalidAlgorithmParameterException, BadPaddingException {
    removeCryptographyRestrictions();
    final byte[] pass = password.getBytes(US_ASCII);
    final byte[] salt = (new SecureRandom()).generateSeed(8);
    final byte[] inBytes = clearText.getBytes(UTF_8);

    final byte[] passAndSalt = array_concat(pass, salt);
    byte[] hash = new byte[0];
    byte[] keyAndIv = new byte[0];
    for (int i = 0; i < 3 && keyAndIv.length < 48; i++) {
        final byte[] hashData = array_concat(hash, passAndSalt);
        final MessageDigest md = MessageDigest.getInstance("MD5");
        hash = md.digest(hashData);
        keyAndIv = array_concat(keyAndIv, hash);
    }

    final byte[] keyValue = Arrays.copyOfRange(keyAndIv, 0, 32);
    final byte[] iv = Arrays.copyOfRange(keyAndIv, 32, 48);
    final SecretKeySpec key = new SecretKeySpec(keyValue, "AES");

    final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
    byte[] data = cipher.doFinal(inBytes);
    data =  array_concat(array_concat(SALTED_MAGIC, salt), data);
    return Base64.getEncoder().encodeToString( data );
}

/**
 * @see http://stackoverflow.com/questions/32508961/java-equivalent-of-an-openssl-aes-cbc-encryption  for what looks like a useful answer.  The not-yet-commons-ssl also has an implementation
 * @param password
 * @param source The encrypted data
 * @return
 */
static String decrypt(String password, String source) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
    removeCryptographyRestrictions();
    final byte[] pass = password.getBytes(US_ASCII);

    final byte[] inBytes = Base64.getDecoder().decode(source);

    final byte[] shouldBeMagic = Arrays.copyOfRange(inBytes, 0, SALTED_MAGIC.length);
    if (!Arrays.equals(shouldBeMagic, SALTED_MAGIC)) {
        throw new IllegalArgumentException("Initial bytes from input do not match OpenSSL SALTED_MAGIC salt value.");
    }

    final byte[] salt = Arrays.copyOfRange(inBytes, SALTED_MAGIC.length, SALTED_MAGIC.length + 8);

    final byte[] passAndSalt = array_concat(pass, salt);

    byte[] hash = new byte[0];
    byte[] keyAndIv = new byte[0];
    for (int i = 0; i < 3 && keyAndIv.length < 48; i++) {
        final byte[] hashData = array_concat(hash, passAndSalt);
        final MessageDigest md = MessageDigest.getInstance("MD5");
        hash = md.digest(hashData);
        keyAndIv = array_concat(keyAndIv, hash);
    }

    final byte[] keyValue = Arrays.copyOfRange(keyAndIv, 0, 32);
    final SecretKeySpec key = new SecretKeySpec(keyValue, "AES");

    final byte[] iv = Arrays.copyOfRange(keyAndIv, 32, 48);

    final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
    final byte[] clear = cipher.doFinal(inBytes, 16, inBytes.length - 16);
    return new String(clear, UTF_8);
}


private static byte[] array_concat(final byte[] a, final byte[] b) {
    final byte[] c = new byte[a.length + b.length];
    System.arraycopy(a, 0, c, 0, a.length);
    System.arraycopy(b, 0, c, a.length, b.length);
    return c;
}

private static void removeCryptographyRestrictions() {
    if (!isRestrictedCryptography()) {
        return;
    }
    try {
        /*
         * Do the following, but with reflection to bypass access checks:
         * 
         * JceSecurity.isRestricted = false; JceSecurity.defaultPolicy.perms.clear();
         * JceSecurity.defaultPolicy.add(CryptoAllPermission.INSTANCE);
         */
        final Class<?> jceSecurity = Class.forName("javax.crypto.JceSecurity");
        final Class<?> cryptoPermissions = Class.forName("javax.crypto.CryptoPermissions");
        final Class<?> cryptoAllPermission = Class.forName("javax.crypto.CryptoAllPermission");

        Field isRestrictedField = jceSecurity.getDeclaredField("isRestricted");
        isRestrictedField.setAccessible(true);
        setFinalStatic(isRestrictedField, true);
        isRestrictedField.set(null, false);

        final Field defaultPolicyField = jceSecurity.getDeclaredField("defaultPolicy");
        defaultPolicyField.setAccessible(true);
        final PermissionCollection defaultPolicy = (PermissionCollection) defaultPolicyField.get(null);

        final Field perms = cryptoPermissions.getDeclaredField("perms");
        perms.setAccessible(true);
        ((Map<?, ?>) perms.get(defaultPolicy)).clear();

        final Field instance = cryptoAllPermission.getDeclaredField("INSTANCE");
        instance.setAccessible(true);
        defaultPolicy.add((Permission) instance.get(null));
    }
    catch (final Exception e) {
        e.printStackTrace();
    }
}

static void setFinalStatic(Field field, Object newValue) throws Exception {
      field.setAccessible(true);

      Field modifiersField = Field.class.getDeclaredField("modifiers");
      modifiersField.setAccessible(true);
      modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

      field.set(null, newValue);
   }

private static boolean isRestrictedCryptography() {
    // This simply matches the Oracle JRE, but not OpenJDK.
    return "Java(TM) SE Runtime Environment".equals(System.getProperty("java.runtime.name"));
}
}
ikiSiTamvaaan
  • 127
  • 1
  • 14
  • Up-to-date Java installations do not require the unlimited crypto jurisdiction files anymore. And for security related code you do want to use an up-to-data Java version, *right*? – Maarten Bodewes Jul 12 '19 at 22:26
  • that's right, you won't need it after 8.1.6 CMIIW, but just in case, some one still need it. but if you already use higher version just comment this line removeCryptographyRestrictions(); – ikiSiTamvaaan Jul 15 '19 at 02:27
  • I've just reviewed a bit of your code, but note that you haven't implemented the password based key derivation function `EVP_BytesToKey` in OpenSSL correctly; your code claims an AES key size of 256 bits but only performs AES with a key size of 128 bits. This code is insecure and you should preferably not program your own cryptographic solutions. – Maarten Bodewes Jul 15 '19 at 08:18
  • i actually don't really know about cryptographic, only knowing that it making a sentence to another form of string that can be brought back by same key hehe.. any sugestion about how to do it right way? – ikiSiTamvaaan Jul 15 '19 at 08:55
  • You could implement `EVP_BytesToKey`, but to be honest, it is only useful to be compatible with command line `openssl`, which uses one iteration by default. You should really switch to PBKDF2, which is implemented by Oracle's Java. And you should only do this **if** you are required to use a password, because passwords are inherently insecure for encryption purposes. – Maarten Bodewes Jul 15 '19 at 09:04