简体   繁体   English

从客户端到服务器的Java NIO + AES加密 - ByteBuffer问题

[英]Java NIO + AES Encryption from Client to Server - ByteBuffer issue

I'm quite a newbie regarding encryption and NIO, I have the following code for client: 我是一个关于加密和NIO的新手,我有以下客户端代码:

    String key1 = "1234567812345678";
    byte[] key2 = key1.getBytes();
    SecretKeySpec secret = new SecretKeySpec(key2, "AES");
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE, secret);
    byte[] encrypted = cipher.doFinal(msg.getBytes());
    System.out.println("Encrypted info: " + encrypted);

    String send = encrypted.toString();
    bytebuf = ByteBuffer.allocate(48);
    bytebuf.clear();
    bytebuf.put(send.getBytes());

    bytebuf.flip();

    while(bytebuf.hasRemaining()) {
        nBytes += client.write(bytebuf);
    }

and the following code for server: 以及服务器的以下代码:

    // Server receives data and decrypts

    SocketChannel socket = (SocketChannel) key.channel();
    ByteBuffer buf = ByteBuffer.allocate(1024);
    nBytes = socket.read(buf);
    String data = new String(buf.array()).trim();
    String key1 = "1234567812345678";
    byte[] key2 = key1.getBytes();
    SecretKeySpec secret = new SecretKeySpec(key2, "AES");

    Cipher cipher = Cipher.getInstance("AES");

    cipher.init(Cipher.DECRYPT_MODE, secret);
    byte[] decrypted = cipher.doFinal(data.getBytes());
    System.out.println("Decrypted Info: " + new String(decrypted));

When a message is sent from the Client to the Server, "HELLO" for example is encrypted to [B@34d74aa5 and on the Server side I get *Data packet found as [B@34d74aa5 . 当一条消息从客户端发送到服务器时,“HELLO”例如被加密到[B @ 34d74aa5,而在服务器端我得到*数据包被发现为[B @ 34d74aa5]

Till here everything looks fine, but I get the following exception: 直到这里一切看起来都很好,但我得到以下异常:

javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher

I suspect that I have some issue with the way the data is coming out of the buffer on the server side? 我怀疑数据从服务器端缓冲区出来的方式有问题吗? Any ideas on this? 有什么想法吗?

UPDATE: 更新:

**Based on Erickson's answer this is the final solution **根据Erickson的回答,这是最终的解决方案

javax.crypto.BadPaddingException: Given final block not properly padded

Client Code: 客户代码:

            String key1 = "1234567812345678";
        byte[] key2 = key1.getBytes();
            byte[] iv = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
        IvParameterSpec ivspec = new IvParameterSpec(iv);
        SecretKeySpec secret = new SecretKeySpec(key2, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, secret, ivspec);

        byte[] encrypted = cipher.doFinal(msg.getBytes(StandardCharsets.UTF_8));
        String text = DatatypeConverter.printBase64Binary(encrypted);

        System.out.println("Encrypted info: " + text);

        bytebuf = ByteBuffer.allocate(32);
        bytebuf.clear();

        bytebuf.put(text.getBytes());

        bytebuf.flip();

        while(bytebuf.hasRemaining()) {
            nBytes += client.write(bytebuf);
        }

Server Code: 服务器代码:

            LOGGER.info("Confirming write");
        String data = new String(buf.array());

        LOGGER.info("Data packet found as {}", data);

        /*******************************************************/
        byte[] iv = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
        IvParameterSpec ivspec = new IvParameterSpec(iv);
        String key1 = "1234567812345678";
        byte[] key2 = key1.getBytes();
        SecretKeySpec secret = new SecretKeySpec(key2, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

        cipher.init(Cipher.DECRYPT_MODE, secret, ivspec);

        byte[] encrypted = DatatypeConverter.parseBase64Binary(data);
        byte[] decrypted = cipher.doFinal(encrypted);

        System.out.println("Decrypted Info: " + new String(decrypted, StandardCharsets.UTF_8));

Your cipher text, encrypted , is a byte[] , and invoking toString() on an array doesn't render the array content, it returns type ( [B ) and hash code ( @34d74aa5 ) information as described by Object.toString() . 您的密文, encrypted ,是一个byte[]和调用toString()的阵列不呈现阵列内容上,则返回类型( [B和散列码( @34d74aa5 )的信息由所描述Object.toString()

You can't just use new String(encrypted) either. 您不能只使用new String(encrypted) When a byte array is decoded to text, the decoder will replace any invalid byte sequences with the replacement character, \� ( ). 当字节数组被解码为文本时,解码器将用替换字符\� ( )替换任何无效字节序列。 Thus, information is lost and subsequent decryption will fail. 因此,信息丢失,随后的解密将失败。

Use an encoding like base-64 to convert byte sequences to printable characters instead. 使用类似base-64的编码将字节序列转换为可打印字符。 Don't junk up your code with third-party libraries for this; 不要为此使用第三方库来破解您的代码; you can use javax.xml.bind.DatatypeConverter . 你可以使用javax.xml.bind.DatatypeConverter

/* Client: */
byte[] encrypted = cipher.doFinal(msg.getBytes(StandardCharsets.UTF_8));
String text = DatatypeConverter.printBase64Binary(encrypted);
…

/* Server: */
byte[] encrypted = DatatypeConverter.parseBase64Binary(data);
byte[] decrypted = Cipher.doFinal(encrypted);
System.out.println(new String(decrypted, StandardCharsets.UTF_8);

You should also be explicit in selecting your mode and padding (like "AES/CBC/PKCS5Padding") because there's no guarantee the recipient will use the same provider, or that the same provider will use the same defaults over time. 您还应明确选择模式和填充(如“AES / CBC / PKCS5Padding”),因为无法保证收件人将使用相同的提供程序,或者同一提供程序将使用相同的默认值。 Same goes for specifying character encodings, like UTF-8. 指定字符编码也是如此,例如UTF-8。

You are converting the ciphertext, which is a byte[] , to a String here: 您正在将密文( byte[] )转换为String

byte[] encrypted = cipher.doFinal(msg.getBytes());
String send = encrypted.toString();

This is incorrect. 这是不正确的。 You also cannot do new String(byte[]) because the byte[] is random, not a stream of character data in the platform default encoding assumed by new String(byte[]) . 您也不能执行new String(byte[])因为byte[]是随机的,而不是new String(byte[])假定的平台默认编码中的字符数据流。 You should convert the byte[] data to a String by using a hex or base64 encoding (I recommend Apache Commons Codec ) eg 您应该使用十六进制或base64编码将byte[]数据转换为String (我建议使用Apache Commons Codec ),例如

hexEncodedCipherText = new String(Hex.encodeHex(binaryCipherText))

On the server-side, use the opposite operation to convert the hex or base64 encoded data back to a byte[] before decryption eg 在服务器端,使用相反的操作在解密之前将十六进制或base64编码数据转换回byte[] ,例如

binaryCipherText = Hex.decodeHex(hexEncodedCipherText.toCharArray());

UPDATE: 更新:

The updated question is not working during decryption because of the incorrect use of the initialization vector. 由于初始化向量的使用不正确,更新的问题在解密期间不起作用。 You don't specify an IV during encryption, which means Java will generate a random one. 在加密期间不指定IV,这意味着Java将生成随机IV。 You need to obtain this random IV from the cipher by calling cipher.getIV() after the encryption (or specify it explicitly, though generating a random one is more secure). 您需要通过在加密后调用cipher.getIV()从密码中获取此随机IV(或明确指定它,尽管生成随机的更安全)。 Then, during the decryption, create the IvParameterSpec using the IV created during encryption. 然后,在解密期间,使用加密期间创建的IV创建IvParameterSpec In addition, you will need to encode/decode the IV in the same manner as the ciphertext, since it is also binary data. 此外,您需要以与密文相同的方式对IV进行编码/解码,因为它也是二进制数据。

UPDATE 2: 更新2:

I see you have updated your question with the IV, but you are using a null IV. 我看到你用IV更新了你的问题,但你使用的是null IV。 Generally, this is only "safe" when you have a unique key for every message you send. 通常,当您为发送的每封邮件都有唯一的密钥时,这只是“安全”。 If your key is fixed or re-used for any significant length of time, you should generate a unique IV for each encryption/decryption. 如果您的密钥被修复或重复使用了很长时间,您应该为每个加密/解密生成一个唯一的IV。 Otherwise, you are leaving yourself open to cryptanalysis based on multiple ciphertexts encrypted with the same key and IV. 否则,您将基于使用相同密钥和IV加密的多个密文进行密码分析。

The AES scheme is a "block cipher" it works on fixed-size blocks of data. AES方案是一种“分组密码”,它适用于固定大小的数据块。 You are creating a "raw" Cipher instance, which will expect you to make sure that every byte array that you pass to the cipher is aligned to the cipher's "native" block length. 您正在创建一个“原始”Cipher实例,它希望您确保传递给密码的每个字节数组都与密码的“本机”块长度对齐。 That's usually not what you want to do. 这通常不是你想要做的。

An additional problem that you are exposing yourself to in using the cipher "raw", although it's not causing an actual error, is that if you were to pass it the same block of data on separate occasions, each time, that block would be encrypted identically, therefore giving an attacker clues as to the structure of the data. 您在使用密码“raw”时遇到的另一个问题是,虽然它不会导致实际错误,但如果您要在不同的时间将相同的数据块传递给它,那么每次都会加密该块同样地,因此给攻击者提供了关于数据结构的线索。 Again, that's usually not what you want to do in a practical application. 同样,这通常不是您在实际应用中想要做的事情。

So usually, you need to specify two extra things: a padding scheme , which determines what happens when sections of data are not exactly aligned to a block size, and a block mode , which determines what scheme the cipher will use to avoid identical input blocks being encrypted to identical output blocks. 通常,您需要指定两个额外的东西: 填充方案 ,用于确定当数据部分未与块大小完全对齐时发生的情况;以及块模式 ,用于确定密码将使用哪种方案来避免相同的输入块被加密到相同的输出块。 The block mode generally needs initialising with a "starting state" called the initialisation vector (you could use a default state of "all zero", but that's less secure). 块模式通常需要使用称为初始化向量的“起始状态”进行初始化 (您可以使用默认状态“全零”,但这不太安全)。

So you need to do two things: 所以你需要做两件事:

  • You need to initialise you cipher with a padding scheme and block mode, eg "AES/CBC/PKCS5PADDING" 您需要使用填充方案和块模式初始化密码,例如“AES / CBC / PKCS5PADDING”

  • For additional security, you would also usually set up (and transmit before the data) a random initialisation vector. 为了提高安全性,您通常还会设置(并在数据之前传输)随机初始化向量。 See this example for more information. 有关更多信息, 请参阅此示例

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM