Search code examples
javaphpencryption

How to get the same encrypted string from php to java


We have encryption and decryption done in php using following code

    protected $key    = 'myKey';
    protected $iv     = 'myIv';
    protected $method = 'AES-256-CBC';
public function decrypt($value, AbstractPlatform $platform=null)
    {
        if (gettype($value) === 'NULL') {
            return null;
        }

        if (!base64_decode($value)) {
            return $value;
        }

        try {
            $key = hash('sha256', $this->key);
            $iv  = substr(hash('sha256', $this->iv), 0, 16);
            return openssl_decrypt(base64_decode($value), $this->method, $key, 0, $iv);
        } catch (\Exception $exception) {
            throw new \Error($exception->getMessage());
        }
    }
public function encrypt($value, AbstractPlatform $platform = null)
    {
        if (gettype($value) === 'NULL') {
            return null;
        }

        try {
            $key = hash('sha256', $this->key);
            $iv  = substr(hash('sha256', $this->iv), 0, 16);
            $encryptValue = openssl_encrypt($value, $this->method, $key, 0, $iv);

            if ($encryptValue) {
                return base64_encode($encryptValue);
            }
        } catch (\Exception $exception) {
            throw new \Error($exception->getMessage());
        }

        return false;
    }

Is it possible to write similar code in java and get the same encrypted string as with php and also decrypt it with java using same key and iv?

I have tried using cipher in java like this:

public static String encrypt(String stringToEncrypt){

        try {

            byte[] iv = new byte[16];
            IvParameterSpec ivspec = new IvParameterSpec(iv);
            /* Create factory for secret keys. */
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            /* PBEKeySpec class implements KeySpec interface. */
            KeySpec spec = new PBEKeySpec(KEY.toCharArray(), IV.getBytes(), 65536, 256);
            SecretKey tmp = factory.generateSecret(spec);
            SecretKeySpec secretKey = new SecretKeySpec(tmp.getEncoded(), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivspec);
            /* Retruns encrypted value. */
            return Base64.getEncoder()
                    .encodeToString(cipher.doFinal(stringToEncrypt.getBytes(StandardCharsets.UTF_8)));
        }
        catch (InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException e)
        {
            System.out.println("Error occured during encryption: " + e);
        }
        return null;

    }
    public static String decrypt(String stringToDecrypt){

        try {
            byte[] iv = new byte[16];
            IvParameterSpec ivSpec = new IvParameterSpec(iv);
            /* Create factory for secret keys. */
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            /* PBEKeySpec class implements KeySpec interface. */
            KeySpec spec = new PBEKeySpec(KEY.toCharArray(), IV.getBytes(), 65536, 256);
            SecretKey tmp = factory.generateSecret(spec);
            SecretKeySpec secretKey = new SecretKeySpec(tmp.getEncoded(), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
            /* Retruns decrypted value. */
            return new String(cipher.doFinal(Base64.getDecoder().decode(stringToDecrypt)));
        }
        catch (InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException e)
        {
            System.out.println("Error occured during decryption: " + e);
        }
        return null;

    }

I've tried this also:

public static String encrypt(String stringToEncrypt) {
        try {
            IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes("UTF-8"),0,16);
            SecretKeySpec skeySpec = new SecretKeySpec(KEY.getBytes("UTF-8"), "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, ivSpec);
            return Base64.getEncoder()
                    .encodeToString(cipher.doFinal(stringToEncrypt.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }
public static String decrypt(String encrypted) {
        try {
            IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes("UTF-8"),0,16);
            SecretKeySpec skeySpec = new SecretKeySpec(KEY.getBytes("UTF-8"), "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, ivSpec);
            byte[] decypted = cipher.doFinal(Base64.getDecoder().decode(encrypted));

            return new String(decypted);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }

but however aything I have tried I got different encrypted string than the one done with php method. Thanks a lot!


Solution

  • The PHP code does the following:

    • The key and IV are derived using SHA-256. The hash() function of the PHP code returns the hex encoded data by default. The key is implicitly truncated to 32 bytes due to the AES-256-CBC specification, the IV is explicitly truncated to 16 bytes. On the Java side, this can be implemented as follows:

      MessageDigest md = MessageDigest.getInstance("SHA-256");
      byte[] key = HexFormat.of().formatHex(md.digest("myKey".getBytes(StandardCharsets.UTF_8))).substring(0, 32).getBytes(StandardCharsets.UTF_8);
      byte[] iv = HexFormat.of().formatHex(md.digest("myIv".getBytes(StandardCharsets.UTF_8))).substring(0, 16).getBytes(StandardCharsets.UTF_8);
      IvParameterSpec ivspec = new IvParameterSpec(iv);
      SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
      

      In contrast to the PHP code, the key in the Java code must be explicitly shortened to 32 bytes, as here the key size determines the AES variant (a 32 bytes key specifies AES-256).

      Note that it is a vulnerability to use a fast digest for key derivation. Instead, a dedicated key derivation function, e.g. at least PBKDF2 in conjunction with a random salt, should be applied.
      Also, the direct use of hex encoded values as key (and IV) is a vulnerability, since it reduces the key space (from 256 to 16 values per byte). In addition, depending on whether upper or lower case is used for the hex digits, this can lead to compatibility problems (in this case it is not critical, as both Java and PHP apply lower case by default).
      A third vulnerability is that the code does not reliably protect against the reuse of key/IV pairs. One approach to avoid this is to use a random IV for each encryption (as already outlined in the comments).

    • PHP/OpenSSL applies PKCS#7 padding by default. On the Java side, this must be specified explicitly (as PKCS5Padding):

      Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
      cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivspec);
      
    • PHP/OpenSSL performs a Base64 encoding by default (4th parameter, $options = 0) during encryption. Since the PHP code also explicitly Base64 encodes, it is Base64 encoded twice. On the Java side, this can be implemented as follows:

      byte[] ciphertext = cipher.doFinal(stringToEncrypt.getBytes(StandardCharsets.UTF_8));
      byte[] ciphertextB64  = Base64.getEncoder().encode(ciphertext);
      return Base64.getEncoder().encodeToString(ciphertextB64);
      

      The same applies for decryption. Here, the ciphertext must be Base64 decoded twice before decryption:

      byte[] ciphertextB64 = Base64.getDecoder().decode(stringToDecrypt);
      byte[] ciphertext = Base64.getDecoder().decode(ciphertextB64);
      return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
      

      Note that double Base64 encoding is pointless and inefficient: it has no benefit, only reduces performance and increases the amount of data.


    Test: With the posted key material (myKey) and IV material (myIv), the adapted Java code generates the following ciphertext from the plaintext The quick brown fox jumps over the lazy dog:

    ZlUxZUMrTlNpa2FpWG5RdHdRYUhsUk83a0dvci9Pc0xlSjRXVjJ3b1FZSnRTK1Zpd2NVQkYzSDQ2MlVaQmlpTA==
    

    in accordance with the PHP code.