Cifrado y descifrado lento de AES GCM con Java 8u20

Estoy tratando de cifrar y descifrar datos usando AES / GCM / NoPadding. Instalé los archivos de la política de fuerza ilimitada de JCE y ejecuté el punto de referencia (simple) a continuación. Hice lo mismo con OpenSSL y logré cifrar y descifrar más de 1 GB / s en mi PC.

Con el índice de referencia a continuación, solo puedo obtener un cifrado y descifrado de 3 MB / s con Java 8 en la misma PC. ¿Alguna idea de lo que estoy haciendo mal?

public static void main(String[] args) throws Exception { final byte[] data = new byte[64 * 1024]; final byte[] encrypted = new byte[64 * 1024]; final byte[] key = new byte[32]; final byte[] iv = new byte[12]; final Random random = new Random(1); random.nextBytes(data); random.nextBytes(key); random.nextBytes(iv); System.out.println("Benchmarking AES-256 GCM encryption for 10 seconds"); long javaEncryptInputBytes = 0; long javaEncryptStartTime = System.currentTimeMillis(); final Cipher javaAES256 = Cipher.getInstance("AES/GCM/NoPadding"); byte[] tag = new byte[16]; long encryptInitTime = 0L; long encryptUpdate1Time = 0L; long encryptDoFinalTime = 0L; while (System.currentTimeMillis() - javaEncryptStartTime < 10000) { random.nextBytes(iv); long n1 = System.nanoTime(); javaAES256.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(16 * Byte.SIZE, iv)); long n2 = System.nanoTime(); javaAES256.update(data, 0, data.length, encrypted, 0); long n3 = System.nanoTime(); javaAES256.doFinal(tag, 0); long n4 = System.nanoTime(); javaEncryptInputBytes += data.length; encryptInitTime = n2 - n1; encryptUpdate1Time = n3 - n2; encryptDoFinalTime = n4 - n3; } long javaEncryptEndTime = System.currentTimeMillis(); System.out.println("Time init (ns): " + encryptInitTime); System.out.println("Time update (ns): " + encryptUpdate1Time); System.out.println("Time do final (ns): " + encryptDoFinalTime); System.out.println("Java calculated at " + (javaEncryptInputBytes / 1024 / 1024 / ((javaEncryptEndTime - javaEncryptStartTime) / 1000)) + " MB/s"); System.out.println("Benchmarking AES-256 GCM decryption for 10 seconds"); long javaDecryptInputBytes = 0; long javaDecryptStartTime = System.currentTimeMillis(); final GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(16 * Byte.SIZE, iv); final SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); long decryptInitTime = 0L; long decryptUpdate1Time = 0L; long decryptUpdate2Time = 0L; long decryptDoFinalTime = 0L; while (System.currentTimeMillis() - javaDecryptStartTime < 10000) { long n1 = System.nanoTime(); javaAES256.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec); long n2 = System.nanoTime(); int offset = javaAES256.update(encrypted, 0, encrypted.length, data, 0); long n3 = System.nanoTime(); javaAES256.update(tag, 0, tag.length, data, offset); long n4 = System.nanoTime(); javaAES256.doFinal(data, offset); long n5 = System.nanoTime(); javaDecryptInputBytes += data.length; decryptInitTime += n2 - n1; decryptUpdate1Time += n3 - n2; decryptUpdate2Time += n4 - n3; decryptDoFinalTime += n5 - n4; } long javaDecryptEndTime = System.currentTimeMillis(); System.out.println("Time init (ns): " + decryptInitTime); System.out.println("Time update 1 (ns): " + decryptUpdate1Time); System.out.println("Time update 2 (ns): " + decryptUpdate2Time); System.out.println("Time do final (ns): " + decryptDoFinalTime); System.out.println("Total bytes processed: " + javaDecryptInputBytes); System.out.println("Java calculated at " + (javaDecryptInputBytes / 1024 / 1024 / ((javaDecryptEndTime - javaDecryptStartTime) / 1000)) + " MB/s"); } 

EDITAR: Lo dejo como un ejercicio divertido para mejorar este punto de referencia simple.

He probado un poco más usando ServerVM, eliminado las llamadas de nanoTime e introducido el calentamiento, pero como esperaba, nada de esto tuvo ninguna mejora en los resultados del benchmark. Es de líneas planas a 3 megabytes por segundo.

Dejando a un lado la micro-evaluación comparativa, el rendimiento de la implementación de GCM en JDK 8 (al menos hasta 1.8.0_25) está paralizado.

Puedo reproducir consistentemente los 3MB / s (en una computadora portátil Haswell i7) con una micro-referencia más madura.

Desde una inmersión de código , esto parece deberse a una implementación de multiplicador ingenua y sin aceleración de hardware para los cálculos de GCM.

En comparación, AES (en modo ECB o CBC) en JDK 8 usa un AES-NI acelerado intrínseco y es (para Java al menos) muy rápido (del orden de 1GB / s en el mismo hardware), pero el AES / GCM general el rendimiento está completamente dominado por el rendimiento roto de GCM.

Hay planes para implementar la aceleración de hardware , y ha habido envíos de terceros para mejorar el rendimiento , pero estos aún no han llegado a un lanzamiento.

Otra cosa a tener en cuenta es que la implementación de JDK GCM también almacena todo el texto encriptado al descifrarlo hasta que se verifica la etiqueta de autenticación al final del texto cifrado, lo que lo paraliza para usarlo con mensajes grandes.

Bouncy Castle tiene (en el momento de la escritura) implementaciones de GCM más rápidas (y OCB si está escribiendo software de código abierto no gravado por las leyes de patentes de software).


Actualizado en julio de 2015 – 1.8.0_45 y JDK 9

JDK 8+ obtendrá una implementación de Java mejorada (y constante) (contribuida por Florian Weimer de RedHat) – esto ha aterrizado en las comstackciones de JDK 9 EA, pero aparentemente aún no en 1.8.0_45. JDK9 (ya que EA b72 al menos) también tiene intrínsecos GCM – La velocidad AES / GCM en b72 es de 18MB / s sin intrínsecos habilitados y 25MB / s con intrínsecos habilitados, los cuales son decepcionantes – para la comparación el más rápido (no el tiempo constante) BC la implementación es ~ 60MB / sy la más lenta (tiempo constante, no totalmente optimizado) es ~ 26MB / s.


Actualizado en enero de 2016 – 1.8.0_72:

Algunas correcciones de rendimiento aterrizaron en JDK 1.8.0_60 y el rendimiento en el mismo punto de referencia ahora es de 18 MB / s, una mejora de 6 veces respecto al original, pero aún mucho más lenta que las implementaciones de BC.

Esto ahora se ha abordado parcialmente en Java 8u60 con JDK-8069072 . Sin esta corrección, obtengo 2.5M / s. Con esta solución obtengo 25M / s. Desactivar completamente el GCM me da 60M / s.

Para desactivar GCM, cree completamente un archivo llamado java.security con la siguiente línea:

 jdk.tls.disabledAlgorithms=SSLv3,GCM 

Luego, inicie su proceso de Java con:

 java -Djava.security.properties=/path/to/my/java.security ... 

Si esto no funciona, es posible que necesite habilitar las propiedades de seguridad anulando /usr/java/default/jre/lib/security/java.security (la ruta real puede ser diferente según el sistema operativo) y agregando:

 policy.allowSystemProperty=true 

La implementación de OpenSSL se optimiza mediante la rutina de ensamblaje utilizando la instrucción pclmulqdq (plataforma x86). Es muy rápido debido al algoritmo paralelo.

La implementación de Java es lenta. pero también se optimizó en Hotspot utilizando la rutina de ensamblaje (no en paralelo). tienes que calentar el jvm para usar el Hotspot intrínseco. El valor predeterminado de -XX: CompileThreshold es 10000.

// pseudocódigo

warmUp_GCM_cipher_loop10000_times ();

do_benchmark ();

Intereting Posts