Search code examples
javaphpencryption

Unable to decrypt data using PHP which is encrypted with Java


We are using the following code to encrypt data in Java, and trying to convert the logic to PHP. The data encrypted with one language cannot be decrypted with the other language. Is there any difference?

My Java class

public class EncYes {
    
    private static final char[] hexDigits = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        
    public static void main(String[] args) {
        try {
            String encString=null;
            
            EncYes enc = new EncYes();
            
            switch(args[0]){
               case "e":
                 System.out.println(enc.encrypt(args[1],args[2])); 
               break;  
               case "d":
                 System.out.println(enc.decrypt(args[1],args[2])); 
               break; 
             }
        }  catch (Exception e) {
            System.out.println(e);
        }
    }
     
    public String encrypt(String json, String key) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
        Cipher cipher = null;
        EncYes enc = new EncYes();
        SecretKeySpec skeySpec = new SecretKeySpec(enc.hexfromString(key), "AES");
        byte[] ivSrc = new byte[12];
        GCMParameterSpec ivSpec = new GCMParameterSpec(128, ivSrc);
        cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(1, skeySpec, ivSpec);
        byte[] encstr = cipher.doFinal(json.getBytes());
        return enc.hextoString(encstr);
    }
      
    public String decrypt(String json, String key) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
        Cipher cipher = null;
        EncYes enc = new EncYes();
        SecretKeySpec skeySpec = new SecretKeySpec(enc.hexfromString(key), "AES");
        byte[] ivSrc = new byte[12];
        GCMParameterSpec ivSpec = new GCMParameterSpec(128, ivSrc);
        cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(2, skeySpec, ivSpec);
      
        byte[] encstr = cipher.doFinal(enc.hexfromString(json));
        return new String(encstr);
    }
      
      
    public static byte[] hexfromString(String s) {
        int i = s.length();
        byte[] abyte0 = new byte[(i + 1) / 2];
        int j = 0;
        int k = 0;
        if (i % 2 == 1) {
            abyte0[k++] = (byte)HexfromDigit(s.charAt(j++));
        }
        while(j < i) {
            abyte0[k++] = (byte)(HexfromDigit(s.charAt(j++)) << 4 | HexfromDigit(s.charAt(j++)));
        }
        return abyte0;
    }

    public static int HexfromDigit(char c) {
        if (c >= '0' && c <= '9')
            return c - 48;
        if (c >= 'A' && c <= 'F')
            return (c - 65) + 10;
        if (c >= 'a' && c <= 'f')
            return (c - 97) + 10;
        else
            throw new IllegalArgumentException("invalid hex digit: ");
    }

    public static String asHex(byte[] buf) {
        StringBuffer strbuf = new StringBuffer(buf.length * 2);

        for(int i = 0; i < buf.length; ++i) {
            if ((buf[i] & 255) < 16) {
                strbuf.append("0");
            }

            strbuf.append(Long.toString((long)(buf[i] & 255), 16));
        }

        return strbuf.toString();
    }

    public static String HextoString(byte abyte0[], int i, int j) {
        char ac[] = new char[j * 2];
        int k = 0;
        for (int l = i; l < i + j; l++) {
             byte byte0 = abyte0[l];
             ac[k++] = hexDigits[byte0 >>> 4 & 0xf];
             ac[k++] = hexDigits[byte0 & 0xf];
        }
        return new String(ac);
    }

    public static String hextoString(byte[] abyte0) {
        return HextoString(abyte0, 0, abyte0.length);
    }

    public static String generateIv() {
        UUID uId = UUID.randomUUID();
        return uId.toString().replace("-", "");
    }
                 
}

My PHP class

class EncYes {

    private static $hexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];

    public static function main($args) {
        try {
            $enc = new EncYes();

            switch ($args[0]) {
                case "e":
                    return $enc->encrypt($args[1], $args[2]) . "\n";
                    break;
                case "d":
                    return $enc->decrypt($args[1], $args[2]) . "\n";
                    break;
            }
        } catch (Exception $e) {
            echo $e->getMessage() . "\n";
        }
    }

    public function encrypt($json, $key) {
        $iv = str_repeat("\0", 12);
        $cipher = "aes-128-gcm";
        $tag = "";

        $encrypted = openssl_encrypt($json, $cipher, $this->hexfromString($key), OPENSSL_RAW_DATA, $iv, $tag,"",12);
        if ($encrypted === false) {
            throw new Exception("Encryption failed");
        }

        return $this->hextoString($encrypted . $tag);
    }

    public function decrypt($json, $key) {
        $iv = str_repeat("\0", 12); // 12-byte IV filled with zeros
        $cipher = "aes-128-gcm";

        $data = $this->hexfromString($json);
        $encrypted = substr($data, 0, -12);
        $tag = substr($data, -12);

        $decrypted = openssl_decrypt($encrypted, $cipher, $this->hexfromString($key), OPENSSL_RAW_DATA, $iv, $tag);
        if ($decrypted === false) {
            throw new Exception("Decryption failed");
        }

        return $decrypted;
    }

    public static function hexfromString($s) {
        $i = strlen($s);
        $abyte0 = array_fill(0, (int)(($i + 1) / 2), 0);
        $j = 0;
        $k = 0;

        if ($i % 2 == 1) {
            $abyte0[$k++] = self::HexfromDigit($s[$j++]);
        }

        while ($j < $i) {
            $abyte0[$k++] = (self::HexfromDigit($s[$j++]) << 4) | self::HexfromDigit($s[$j++]);
        }

        return implode(array_map("chr", $abyte0));
    }

    public static function HexfromDigit($c) {
        if ($c >= '0' && $c <= '9') {
            return ord($c) - ord('0');
        }
        if ($c >= 'A' && $c <= 'F') {
            return (ord($c) - ord('A')) + 10;
        }
        if ($c >= 'a' && $c <= 'f') {
            return (ord($c) - ord('a')) + 10;
        }
        throw new InvalidArgumentException("invalid hex digit: " . $c);
    }

    public static function asHex($buf) {
        $strbuf = "";

        for ($i = 0; $i < strlen($buf); $i++) {
            $byte = ord($buf[$i]);
            if (($byte & 255) < 16) {
                $strbuf .= "0";
            }
            $strbuf .= dechex($byte & 255);
        }

        return $strbuf;
    }

    public static function HextoString2($abyte0, $i, $j) {
        $ac = array_fill(0, $j * 2, '0');
        $k = 0;

        for ($l = $i; $l < $i + $j; $l++) {
            $byte0 = ord($abyte0[$l]);
            $ac[$k++] = self::$hexDigits[$byte0 >> 4 & 0xf];
            $ac[$k++] = self::$hexDigits[$byte0 & 0xf];
        }

        return implode("", $ac);
    }

    public static function hextoString($abyte0) {
        return self::HextoString2($abyte0, 0, strlen($abyte0));
    }

    public static function generateIv() {
        return str_replace("-", "", uuid_create());
    }
}

Sample data for the Java code:

Encryption ------------------- 
java EncYes "e" "Hello" "a59d23ac19020c989cd8566a4ea16646ad4e02f67516cedc3bd7d833efda516a" 
B062024E2B9C4D4FFB204AB3622459CEFEAEFF969D 

Decryption ------------------ 
java EncYes "d" "B062024E2B9C4D4FFB204AB3622459CEFEAEFF969D" "a59d23ac19020c989cd8566a4ea16646ad4e02f67516cedc3bd7d833efda516a" 
Hello

Solution

  • The main issue is that the Java code uses a tag length of 128 bit (1st parameter in new GCMParameterSpec(128, ivSrc)), while the PHP code applies 12 byte = 96 bit (last parameter in openssl_encrypt($json, $cipher, $this->hexfromString($key), OPENSSL_RAW_DATA, $iv, $tag,"",12) and when separating ciphertext and tag during decryption).

    128 bit guarantees the greatest security, so this tag length should be used (unless there are valid reasons for a smaller tag length).


    Other possible problems, vulnerabilities or inefficiencies are:

    • The algorithm specified in the PHP code is aes-128-gcm, i.e. the two codes are only compatible if a 16 byte key, i.e. AES-128, is used. For a different key length or AES variant, the specification in the PHP code must be adapted accordingly. This is not required on the Java side, as the AES variant is implicitly derived from the key length.
    • A zero IV is used, which is a critical vulnerability for CTR based modes like GCM (at least with a fixed key), see here. A possible solution is to generate a random IV for each encryption. The IV is not secret and is passed to the decrypting side along with the ciphertext and tag (usually concatenated: IV|ciphertext|tag).
    • In the Java code, a charset encoding should be explicitly specified in getBytes() or new String(), as otherwise platform-dependent default values are applied (at least in older Java versions). In addition, hex encoding has been supported since Java 17, s. HexFormat.

    Update regarding your test data:
    Your test data shows that you are using a 32 bytes key, i.e. AES-256. Therefore, in the PHP code aes-256-gcm must be used instead of aes-128-gcm.
    In addition, the tag size in the PHP code must be changed from 12 bytes to 16 bytes so that the PHP code is compatible with the Java code.
    Furthermore, the PHP code can be shortened considerably if the built-in bin2hex() and hex2bin() are used instead of hextoString() and hexfromString() respectively.

    Complete code (without main($args)):

    class EncYes {
        const TAG_SIZE = 16;            // Fix 1: Set the tag length to 16 bytes (note that 16 bytes is the PHP/OpenSSL default).
        const CIPHER =  "aes-256-gcm";  // Fix 2: set AES-256 as the algorithm, as a 32 bytes key is used.
        public function encrypt($json, $key) {
            $iv = str_repeat("\0", 12); // Keep in mind: A static IV is a serious vulnerability for GCM with a fixed key.
            $tag = "";
            $encrypted = openssl_encrypt($json, self::CIPHER, hex2bin($key), OPENSSL_RAW_DATA, $iv, $tag, "", self::TAG_SIZE); 
            if ($encrypted === false) {
                throw new Exception("Encryption failed");
            }
            return bin2hex($encrypted . $tag);
        }
        public function decrypt($json, $key) {
            $iv = str_repeat("\0", 12); // Keep in mind: A static IV is a serious vulnerability for GCM with a fixed key.
            $data = hex2bin($json);
            $encrypted = substr($data, 0, -self::TAG_SIZE); 
            $tag = substr($data, -self::TAG_SIZE);
            $decrypted = openssl_decrypt($encrypted, self::CIPHER, hex2bin($key), OPENSSL_RAW_DATA, $iv, $tag);
            if ($decrypted === false) {
                throw new Exception("Decryption failed");
            }
            return $decrypted;
        }
    }
    

    Test:

    // decryption of your sample data
    $aesenc = new EncYes();
    $dt = $aesenc->decrypt("B062024E2B9C4D4FFB204AB3622459CEFEAEFF969D", "a59d23ac19020c989cd8566a4ea16646ad4e02f67516cedc3bd7d833efda516a");
    print($dt . PHP_EOL); // Hello
    
    // complete encryption/decryption cycle
    $ct = $aesenc->encrypt("The quick brown fox jumps over the lazy dog", "a59d23ac19020c989cd8566a4ea16646ad4e02f67516cedc3bd7d833efda516a");
    $dt = $aesenc->decrypt($ct, "a59d23ac19020c989cd8566a4ea16646ad4e02f67516cedc3bd7d833efda516a");
    print($dt . PHP_EOL); // The quick brown fox jumps over the lazy dog