Search code examples
javaencryptionclientversion

java.security.InvalidKeyException thrown only for older java versions during AES decryption


My goal is to encrypt cache assets (in this case 3D models using a custom format) that are being decrypted by a client distributed to users. For this I used the following implementation:

public class AESTestStackoverflow {
    
    private final static byte[] secretKey = new byte[]{-74, 80, 22, 62, -70, -117, 22, 110, 57, -51, 2, 70, 68, -29, 14, -100, -24, 121, -122, 81, 5, 23, -90, 78, -99, -116, 29, -38, 118, 121, 126, 51};
    SecretKeySpec secretKeySpec;

    @Before
    public void setUp() {
        secretKeySpec = new SecretKeySpec(secretKey, "AES");
    }

    @Test
    public void testDecryption() {
        final byte[] raw = new byte[]{1,2,3,4,5,6,7,8,9,10};
        final EncryptionResult result = AES.encrypt(raw, secretKeySpec);

        Assert.assertNotNull(result);

        final byte[] encrypted = result.getData();
        final byte[] iv = result.getIv();
        final byte[] decrypted = AES.decrypt(encrypted, iv, secretKeySpec);

        Assert.assertArrayEquals(raw, decrypted);
    }
}

This implementation works perfectly fine for the latest java versions but fails for java versions equal to or below 1.8.131 (not sure on the exact version that it breaks, but the latest java versions in the 1.8.2xx range appear to work fine). Users running these java versions get the following exception:

java.security.InvalidKeyException: No installed provider supports this key: javax.crypto.spec.SecretKeySpec
    at javax.crypto.Cipher.chooseProvider(Cipher.java:893)
    at javax.crypto.Cipher.init(Cipher.java:1396)
    at javax.crypto.Cipher.init(Cipher.java:1327)

I did do some research on this error and I found several posts stating this might have to do with some part of the JCE library not being shipped with the JRE, and the provided solution was to manually install these into your JRE.

My manager does not want to force users to update their java version, and obviously we cannot ask users to modify their JRE manually, so I am at a bit of a loss on how to continue. I have tried doing some more research but I am not well versed in the security libraries or any related JVM settings. I would appreciate it if someone can shed some light on why this happens, and if it can be made compatible for older java version (from 1.8.xxx onwards).


Solution

  • I didn't test you your code but my simple program will test for something that is called "unlimited crypto policies". Due to US export restrictions the US companies were not allowed to ship their programs with an unlimited key format. You are using AES with a 32 bytes long key so it is called AES-256 (32 bytes * 8 bit = 256) but the government allowed only 16 bytes long keys ("AES 128").

    My program runs the test depending on the Java version that is in use. For an easier test I setup a JFiddle-link where you can test the program directly and for convinience change the Java version. The JFiddle is available under this link: https://www.jdoodle.com/a/2nZe

    When running the test the Java version is JDK 11.0.4 but you can change (please scroll down) to JDK 1.8.0_66.

    output for Java 11:

    Check for unlimited crypto policies
    restricted cryptography: false Notice: 'false' means unlimited policies
    Security properties: unlimited
    Max AES key length = 2147483647
    

    output for Java 1.8.0_66:

    restricted cryptography: true Notice: 'false' means unlimited policies
    Security properties: null
    Max AES key length = 128
    

    To enable the unlimited policy for older Java's like 1.8.0_66 your clients have to load two small files from Oracle's pages and place them in their Java VM.

    As you stated your manager does not allow this there is only one way left - you have to use (only) 16 byte long AES keys.

    Security warning: due to upcoming Quantum computers the actual recommendation for AES keys is 32 byte long keys.

    code:

    import javax.crypto.Cipher;
    import java.security.NoSuchAlgorithmException;
    import java.security.Security;
    
    public class UnlimitedCryptoPoliciesCheck {
        public static void main(String[] args) throws NoSuchAlgorithmException {
            System.out.println("Check for unlimited crypto policies");
            //Security.setProperty("crypto.policy", "limited"); // muss ganz am anfang gesetzt werden !
            System.out.println("restricted cryptography: " + restrictedCryptography() + " Notice: 'false' means unlimited policies"); // false mean unlimited crypto
            System.out.println("Security properties: " + Security.getProperty("crypto.policy"));
            int maxKeyLen = Cipher.getMaxAllowedKeyLength("AES");
            System.out.println("Max AES key length = " + maxKeyLen);
      }
    
        /**
         * Determines if cryptography restrictions apply.
         * Restrictions apply if the value of {@link Cipher#getMaxAllowedKeyLength(String)} returns a value smaller than {@link Integer#MAX_VALUE} if there are any restrictions according to the JavaDoc of the method.
         * This method is used with the transform <code>"AES/CBC/PKCS5Padding"</code> as this is an often used algorithm that is <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#impl">an implementation requirement for Java SE</a>.
         *
         * @return <code>true</code> if restrictions apply, <code>false</code> otherwise
         * https://stackoverflow.com/posts/33849265/edit, author Maarten Bodewes
         */
        public static boolean restrictedCryptography() {
            try {
                return Cipher.getMaxAllowedKeyLength("AES/CBC/PKCS5Padding") < Integer.MAX_VALUE;
            } catch (final NoSuchAlgorithmException e) {
                throw new IllegalStateException("The transform \"AES/CBC/PKCS5Padding\" is not available (the availability of this algorithm is mandatory for Java SE implementations)", e);
            }
        }
    }