使用 Java 8u20 进行慢速 AES GCM 加密和解密

Posted

技术标签:

【中文标题】使用 Java 8u20 进行慢速 AES GCM 加密和解密【英文标题】:Slow AES GCM encryption and decryption with Java 8u20 【发布时间】:2014-11-17 11:37:56 【问题描述】:

我正在尝试使用 AES/GCM/NoPadding 加密和解密数据。我安装了 JCE Unlimited Strength Policy Files 并运行了下面的(简单的)基准测试。我使用 OpenSSL 完成了相同的操作,并且能够在我的 PC 上实现超过 1 GB/s 的加密和解密。

通过以下基准测试,我只能在同一台 PC 上使用 Java 8 获得 3 MB/s 加密和解密。知道我做错了什么吗?

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");

编辑: 我把它作为一个有趣的练习来改进这个简单的基准。

我已经使用 ServerVM 进行了更多测试,删除了 nanoTime 调用并引入了预热,但正如我预期的那样,这些都没有对基准测试结果有任何改进。它以每秒 3 兆字节的速度平线。

【问题讨论】:

首先,基准测试是错误的:没有预热、单次迭代、过多的 nanoTime 调用。 Hotspot 的 AES-NI 内在函数仅与优化的 JIT 编译器一起使用,您必须在评估性能之前到达那里。其次,尝试 AES/CBC。你真的用 OpenSSL 测量 aes-gcm,它给了你 1 GB/s? 另请注意,要使用 AES-NI 内在函数,需要使用服务器 VM,这是一种支持的现代 Intel CPU,有一个预热序列。请注意,OpenSSL 是目前最快的库之一,字节码对于业务逻辑可能相对较快,但对于密码学,您看到与良好实现的 C/C++ 库的差异。 是的,我知道这不是最强大的基准测试,但 3 MB/s 与 1 GB/s 仍然非常重要,我觉得这个简单的基准测试足以说明这一点。我已经尝试过 AES/CBC,我能够获得超过 400 MB/s 的加密速度和超过 1 GB/s 的使用 Java 密码的解密速度。 我正在编写我自己的 JavaFX/Java EE 测试应用程序,它带有一个很棒的 GUI,它通过使用 SRP 验证用户的整个过程,然后使用 AES/GCM 通过 WebSocket 发送加密文件。应用程序完成后,我将返回一个链接。但是现在,我只想说,与未加密的文件传输相比,使用 AES/GCM 对我来说慢了大约 10 倍(96 位身份验证标签,128 位密钥和 IV)。 @atrioom 你没有安装Unlimited Strength Jurisdiction Policy Files。 【参考方案1】:

抛开微基准测试不谈,JDK 8(至少高达 1.8.0_25)中 GCM 实现的性能被削弱了。

我可以通过更成熟的微基准持续重现 3MB/s(在 Haswell i7 笔记本电脑上)。

来自code dive,这似乎是由于简单的乘法器实现和 GCM 计算没有硬件加速。

相比之下,JDK 8 中的 AES(ECB 或 CBC 模式)使用 AES-NI 加速内在函数,并且(至少对于 Java)非常快(在同一硬件上大约 1GB/s),但总体AES/GCM 性能完全由损坏的 GCM 性能支配。

有 plans to implement hardware acceleration 和 there have been third party submissions to improve the performance with,但这些还没有发布。

另外需要注意的是,JDK GCM 实现还在解密时缓冲整个明文,直到验证密文末尾的身份验证标签,这会削弱它用于大消息的能力。

Bouncy Castle(在撰写本文时)具有更快的 GCM 实施(如果您正在编写不受软件专利法约束的开源软件,还有 OCB)。


2015 年 7 月更新 - 1.8.0_45 和 JDK 9

JDK 8+ 将获得改进的(和恒定时间的)Java 实现(由 RedHat 的 Florian Weimer 提供)——这已在 JDK 9 EA 版本中实现,但显然尚未在 1.8.0_45 中实现。 JDK9(至少从 EA b72 开始)也具有 GCM 内在函数 - b72 上的 AES/GCM 速度为 18MB/s (未启用内在函数)和 25MB/s (启用内在函数),两者都令人失望 - 用于比较最快(非恒定时间)BC实现速度约为 60MB/s,最慢的(恒定时间,未完全优化)约为 26MB/s。


2016 年 1 月更新 - 1.8.0_72:

在 JDK 1.8.0_60 中进行了一些性能修复,现在在同一基准上的性能为 18MB/s - 比原来提高了 6 倍,但仍然比 BC 实现慢得多。

【讨论】:

GCM on bouncy 只是在解密期间缓存标签大小(因为它被用作密文的一部分),尽管我也在尝试新代码来获取它(并单独请求标签,因为它应该是)。重写允许稍后添加 AAD 所需的幂是相当痛苦的。 我已经做了一些测试来加密/解密 84 个文件。 JDK 实现:crypt = 15 秒/解密 = 111 秒。 BC:加密 = 14 秒 / 解密 = 23 秒!!!与 BC 相比,原生 JDK 的解密速度怎么可能这么慢? BC 只是一个只包含类文件的 jar 文件(所以是 java 语言文件)。 Oracle (sun) 可以使用 JRE/JDK 访问本机操作系统,因此他们可以用低级和更快的语言实现所有这些东西,并优化他们的代码(如 C 语言)!这对我来说是不可理解的:/ GCM 解密的 JDK 实现效率特别低 - 它在返回任何数据之前缓冲所有明文,而且最重要的是缓冲效率非常低,导致许多内存副本,这完全压倒了拥有的基本好处硬件加速 AES 和 GCM。【参考方案2】:

这已在 Java 8u60 中通过JDK-8069072 部分解决。如果没有这个修复,我会得到 2.5M/s。通过此修复,我得到 25M/s。完全禁用 GCM 给我 60M/s。

要完全禁用 GCM,请使用以下行创建一个名为 java.security 的文件:

jdk.tls.disabledAlgorithms=SSLv3,GCM

然后启动您的 Java 进程:

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

如果这不起作用,您可能需要通过编辑/usr/java/default/jre/lib/security/java.security(实际路径可能因操作系统而异)并添加以下内容来启用覆盖安全属性:

policy.allowSystemProperty=true

【讨论】:

我打开了主 java.security 并更改了 securerandom.source=file:/dev/random -> securerandom.source=file:/dev/urandom 。它对我有帮助 查看这里了解有关随机/随机数***.com/questions/21757653/…的一些详细信息 使用/dev/urandom的另一种方式:以java -Djava.security.egd=file:/dev/urandom …开头(来自&lt;java-root&gt;/jre/lib/security/java.security中的cmets)。 根据oracle docs,正确的属性名称是java.security.policy【参考方案3】:

OpenSSL 实现由assembly routine 使用 pclmulqdq 指令(x86 平台)进行了优化。由于并行算法,速度非常快。

java 实现很慢。但它也在 Hotspot 中使用汇编程序(非并行)进行了优化。您必须预热 jvm 才能使用 Hotspot 内在函数。 -XX:CompileThreshold 的默认值为 10000。

//伪代码

warmUp_GCM_cipher_loop10000_times();

do_benchmark();

【讨论】:

以上是关于使用 Java 8u20 进行慢速 AES GCM 加密和解密的主要内容,如果未能解决你的问题,请参考以下文章

使用 Java 的 AES-256-GCM 解密中的标签不匹配错误

在 JAVA 中使用 AES/GCM 检测不正确的密钥

Java AES/GCM/NoPadding 加密在 doFinal 之后不会增加 IV 的计数器

java JDK8 AES-GCM代码示例

AES GCM 使用 web 微妙加密进行加密并使用颤振加密进行解密

有没有办法过滤 aes 256 gcm 加密数据库中的数据?