使用Java内置的AES加密非常缓慢

3

我有很多非常小的数据(19字节)需要加密并以加密格式通过TCP发送到远程服务器。 我正在使用以下代码执行此操作。

package aesclient;

import java.io.OutputStream;
import java.net.Socket;

import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class AESClient {
    static byte[] plaintext = new byte[] {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53};
    public static void main(String[] args) {
        try {
            Socket socket = new Socket("127.0.0.1", 1337); // connecting to server on localhost
            OutputStream outputStream = socket.getOutputStream();
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            String s_key = "Random09" + "Random09"; // 16 Byte = 128 Bit Key
            byte[] b_key = s_key.getBytes();
            SecretKeySpec sKeySpec = new SecretKeySpec(b_key, "AES");
            SecureRandom random = SecureRandom.getInstanceStrong();
            byte[] IV = new byte[16]; // initialization vector
            int num = 10000;
            long start = System.nanoTime();
            for (int i = 0; i < num; ++i) {
                random.nextBytes(IV);
                IvParameterSpec ivSpec = new IvParameterSpec(IV);
                cipher.init(Cipher.ENCRYPT_MODE, sKeySpec, ivSpec);
                byte[] msg = new byte[16 + 32];
                System.arraycopy(IV, 0, msg, 0, IV.length);
                byte[] encrypted = cipher.doFinal(plaintext);
                System.arraycopy(encrypted, 0, msg, IV.length, encrypted.length);
                outputStream.write(msg);
                outputStream.flush();      
            }
            long end = System.nanoTime();
            long duration = end - start;
            double drate = ((double)plaintext.length*(double)num)/((double)duration/1000000000);
            System.out.println("Verschlüsselung:\n" + num + " mal 19 Bytes in " + ((double)duration/1000000000) + " s\nData Rate = " + drate/1000.0 + " kBytes/s");
        }
        catch (Exception e) {
            System.err.println(e.getMessage());
        }
    } 
}

我想知道为什么它运行得非常缓慢。我得到了这样的输出:
Verschlüsselung:
10000 mal 19 Bytes in 2.566016627 s
Data Rate = 74.04472675694785 kBytes/s

这意味着我在发送原始(未加密)数据的情况下,拥有每秒74千字节的数据传输速率。如果我不通过TCP发送数据,这个速率只会略微增加(大约100千字节/秒)。根据我的阅读,有一些数据传输速率可达到20兆字节/秒甚至更高。我的电脑运行Windows 10操作系统,搭载i5处理器。非常感谢任何帮助。正如我所说,我只需要传输许多小的加密数据包(19字节)。


为什么不使用缓冲流?为什么要在每次迭代中刷新?在结尾处刷新并使用缓冲流以提高性能。还要阅读已接受答案中的评论。 - Saptarshi Basu
我展示的代码只是我的问题的一个例子。实际上,我必须每次强制调用flush()命令,因为我无法确定我需要等待下一次发送到服务器的19个字节需要多长时间。我的应用程序只是一个“翻译器”,它加密并将数据发送到服务器。数据非常小(19个字节),可以有无限多个,并且在事先不知道它们到达我的间隔。 - Lukas Nothhelfer
3个回答

5

SecureRandom 甚至在 PRNG 模式下速度缓慢,当熵不足时甚至会被阻塞。

我建议只在开始时获取随机的 IV 并在迭代中按类似 CTR 模式递增它。或者直接使用 CTR 模式。

public class Test {
    static byte[] plaintext = new byte[] { 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51,
            0x52, 0x53 };

    public static void main(String[] args) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CTR/PKCS5PADDING");
            String s_key = "Random09" + "Random09"; // 16 Byte = 128 Bit Key
            byte[] b_key = s_key.getBytes();
            SecretKeySpec sKeySpec = new SecretKeySpec(b_key, "AES");
            SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
            byte[] IV = new byte[16]; // initialization vector
            random.nextBytes(IV);
            int num = 10000;
            long start = System.nanoTime();
            for (int i = 0; i < num; ++i) {
                IvParameterSpec ivSpec = new IvParameterSpec(IV);
                cipher.init(Cipher.ENCRYPT_MODE, sKeySpec, ivSpec);
                byte[] msg = new byte[16 + 32];
                System.arraycopy(IV, 0, msg, 0, IV.length);
                byte[] encrypted = cipher.doFinal(plaintext);
                System.arraycopy(encrypted, 0, msg, IV.length, encrypted.length);
                increment(IV);
            }
            long end = System.nanoTime();
            long duration = end - start;
            double drate = ((double) plaintext.length * (double) num) / ((double) duration / 1000000000);
            System.out.println("Verschlüsselung:\n" + num + " mal 19 Bytes in " + ((double) duration / 1000000000) + " s\nData Rate = " + drate
                    / 1000.0 + " kBytes/s");
        } catch (Exception e) {
            System.err.println(e.getMessage());
        }
    }

    private static void increment(byte[] iv) {
        for (int i=0; i<4; ++i) {
            if (++iv[i] != 0)
                break;
        }
    }
}

输出:

Verschlüsselung:
10000 mal 19 Bytes in 0.0331898 s
Data Rate = 5724.650344382912 kBytes/s

在我的机器上,至少快了30倍。


@rustyx 非常感谢您的回复。为了验证您的说法,我只是注释掉了“random.nextBytes(IV)”这一行,并始终使用相同的初始化向量进行加密。结果证明,加密速度提高了5倍。我的真正关注点是能够提供两个函数,一个用于加密,一个用于解密,然后在LabVIEW环境中的.dll中使用。LabVIEW环境是我试图传输数据到的服务器。因此,我想用C#编写.dll。如果我现在在Java中使用CTR的AES,那么我必须考虑如何在C#中编写对应的程序。 - Lukas Nothhelfer
递增IV是一个非常好的主意。谢谢。这就是我所需要的。 - Lukas Nothhelfer
1
这看起来不安全。CBC IV在加密时必须是不可预测的。@LukasNothhelfer要小心。由于IV不是一个秘密,攻击者可以轻易地知道消息的IV,并且由于你正在递增,攻击者可以预测所有后续的IV。另外,为什么不使用认证加密模式(如GCM)?顺便问一下,为什么密钥是硬编码的?为什么密钥在一个String中?为什么不使用像PBKDF2这样的密钥派生函数将这个单词“Random09”转换为密钥?根本不安全。 - Saptarshi Basu
@SaptarshiBasu 如果攻击者可以预测初始向量(IV),只要他不知道密钥,这为什么会成为一个问题呢?这只是一个例子。密钥不会被硬编码。 - Lukas Nothhelfer
@LukasNothhelfer 参考 https://en.m.wikipedia.org/wiki/Block_cipher_mode_of_operation。CBC 和 CTR 不同。在开发软件时,应坚持算法或模式规范要求的内容。建议不要为软件发明自己的解决方案,因为这些模式、算法及其要求已经被数百人多年审查和测试过。过去发现定制解决方案存在漏洞。顺便问一下,为什么不使用多线程来加速处理过程呢?尝试找出替代方案,而不是牺牲安全性。 - Saptarshi Basu
显示剩余4条评论

2

事实上,所使用的SecureRandom非常缓慢,甚至会阻塞。这是瓶颈所在。 因此,在可行的情况下,加密更大的缓冲区,多个消息。

否则,仍有一些小细节需要考虑:

        OutputStream outputStream = socket.getOutputStream();
        int bufSize = Math.min(socket.getSendBufferSize(), 1024);
        outputStream = new BufferedOutputStream(sock, bufSize);

        byte[] b_key = s_key.getBytes(StandardCharsets.ISO_8859_1);

        byte[] msg = new byte[16 + 32];
        for (int i = 0; i < num; ++i) {
            random.nextBytes(IV);
            IvParameterSpec ivSpec = new IvParameterSpec(IV);
            cipher.init(Cipher.ENCRYPT_MODE, sKeySpec, ivSpec);
            System.arraycopy(IV, 0, msg, 0, IV.length);
            byte[] encrypted = cipher.doFinal(plaintext);
            System.arraycopy(encrypted, 0, msg, IV.length, encrypted.length);
            outputStream.write(msg);
        }
        outputStream.flush();      

使用重载的doFinal有更好的方法来处理字节数组。 然后清理代码,删除此处的arraycopy

另外,在出现异常、超时等异常情况下,我会使用try-with-resources来关闭套接字和其他东西。


1
谢谢你的回答。你提供的解决方案很好,但是小数据(19字节)的数量是无限的,没有限制。因此我无法提前分配任何内容。在这个例子中,典型的套接字过程被省略了,以避免代码膨胀。 - Lukas Nothhelfer
这是正确的答案。但据我理解,arraycopy非常快。它使用memcpy来复制内存块。 - Saptarshi Basu
1
@SaptarshiBasu 这是一个样式问题:重载的 doFinal 可以做现在所做的一切(还可以在内部执行 memcopy)。优点仅在于代码更小,错误更少 = 更易读。因此,我没有重写那部分;现在代码正在工作,由 OP 决定是否需要修改。 - Joop Eggen

0

你需要安全性还是需要AES?块密码听起来不是一个好的选择,因为它会将你的数据从19到48个字节膨胀。

被接受的答案给出了两个建议,其中一个据我所知是安全灾难:在CBC模式中递增计数器几乎没有任何有用的作用。

另一个建议,即使用计数器模式,据我所知是可以的。它有效地将块密码转换为流密码,并允许您发送仅16+19个字节。很可能,您可以使用少于16个字节的计数器。

另一个低效率来自于密码初始化循环。如果我没记错的话,这比加密你的两个块还要花费更多。

数据非常小(19个字节),可以无限制地到达,而且事先不知道它们到达我的时间间隔。

尽管如此,您可以更有效地处理它。一次性读取所有字节。当只有19个字节时,加密并发送它。如果少于19个字节,则继续读取。如果超过19个字节,则全部加密并发送。这样您就可以更有效率...即使是非常慢的SecureRandom也不会成为问题,因为您只需要一个IV来处理大块数据(处理时间越长,一次获取的数据量就越多)。


我需要安全性。我收到大小为19字节的数据包,必须加密并发送到服务器。 19字节数据包的最大数据传输速率为1 MBit/s。与服务器之间有一个1000TX以太网连接,因此这里不应该有瓶颈。如果由于加密而导致消息变得更大,只要在1 MBit/s的数据传输速率下可行即可。为什么任何消息都简单地增加IV会成为问题?据我所知,IV不必保密。 - Lukas Nothhelfer
@LukasNothhelfer IV不需要保密,但必须是不可预测的。看一下我链接中的图像,它会发生什么:它只是与数据进行异或运算。因此,当您的数据仅递减时,这种变化会取消随机数的增量,然后您会发送相同的加密数据两次。假设攻击者知道您的加密方案,则他们会确切地知道发生了什么。如果您认为这是不可能的,那么您就是依赖于数据的不可预测性而不是加密。 - maaartinus
我的数据由4字节头、12字节用户数据和7字节时间戳组成。如果只计算IV,则很难使其与数据相互补偿。纯粹地计算IV可能不被推荐,因此我将采用@SaptarshiBasu的建议并使用SHA1PRNG。谢谢你的帮助。 - Lukas Nothhelfer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接