Cipher 的 doFinal() 不写入字节
Cipher's doFinal() doesn't write bytes
这是我的完整代码:
import static java.nio.file.StandardOpenOption.READ;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Test {
public static void main(String[] args) throws Exception {
encrypt();
decrypt();
}
void encrypt() throws Exception {
Path file = Paths.get("path/to/file");
Path backupFile = file.getParent().resolve(file.getFileName().toString() + ".bak");
Files.deleteIfExists(backupFile);
Files.copy(file, backupFile);
SecureRandom secureRandom = new SecureRandom();
byte[] initializeVector = new byte[96 / Byte.SIZE];
secureRandom.nextBytes(initializeVector);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec p = new GCMParameterSpec(128, initializeVector);
try (FileChannel src = FileChannel.open(backupFile, READ);
FileChannel dest = FileChannel.open(file, WRITE, TRUNCATE_EXISTING)) {
SecretKeySpec secretKeySpec =
new SecretKeySpec(MessageDigest.getInstance("MD5").digest(new byte[]{0x00}), "AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, p);
ByteBuffer ivBuffer = ByteBuffer.allocate(Integer.BYTES + cipher.getIV().length);
ivBuffer.putInt(cipher.getIV().length);
ivBuffer.put(cipher.getIV());
ivBuffer.flip();
dest.write(ivBuffer);
ByteBuffer readBuf = ByteBuffer.allocateDirect(8192);
ByteBuffer writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(8192));
while (src.read(readBuf) >= 0) {
if (cipher.getOutputSize(8192) > writeBuf.capacity()) {
writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(8192));
}
readBuf.flip();
cipher.update(readBuf, writeBuf);
writeBuf.flip();
dest.write(writeBuf);
readBuf.clear();
writeBuf.clear();
}
if (cipher.getOutputSize(0) > writeBuf.capacity()) {
writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(0));
}
cipher.doFinal(ByteBuffer.allocate(0), writeBuf);
writeBuf.flip();
dest.write(writeBuf);
Files.delete(backupFile);
} catch (ShortBufferException e) {
//Should not happen!
throw new RuntimeException(e);
}
}
void decrypt() throws Exception {
Path file = Paths.get("path/to/file");
Path backupFile = file.getParent().resolve(file.getFileName().toString() + ".bak");
Files.deleteIfExists(backupFile);
Files.copy(file, backupFile);
try (FileChannel src = FileChannel.open(backupFile, READ);
FileChannel dest = FileChannel.open(file, WRITE, TRUNCATE_EXISTING)) {
ByteBuffer ivLengthBuffer = ByteBuffer.allocate(Integer.BYTES);
src.read(ivLengthBuffer);
ivLengthBuffer.flip();
int ivLength = ivLengthBuffer.getInt();
ByteBuffer ivBuffer = ByteBuffer.allocate(ivLength);
src.read(ivBuffer);
ivBuffer.flip();
byte[] iv = new byte[ivBuffer.limit()];
ivBuffer.get(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec p = new GCMParameterSpec(128, iv);
SecretKeySpec secretKeySpec =
new SecretKeySpec(MessageDigest.getInstance("MD5").digest(new byte[]{0x00}), "AES");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, p);
ByteBuffer readBuf = ByteBuffer.allocateDirect(8192);
ByteBuffer writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(8192));
while (src.read(readBuf) >= 0) {
if (cipher.getOutputSize(8192) > writeBuf.capacity()) {
writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(8192));
}
readBuf.flip();
cipher.update(readBuf, writeBuf);
writeBuf.flip();
dest.write(writeBuf);
readBuf.clear();
writeBuf.clear();
}
if (cipher.getOutputSize(0) > writeBuf.capacity()) {
writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(0));
}
cipher.doFinal(ByteBuffer.allocate(0), writeBuf);
writeBuf.flip();
dest.write(writeBuf);
Files.deleteIfExists(backupFile);
}
}
}
我发现了一个奇怪的问题:如果原始文件(未加密)大于 4KB
,在解密时,cipher.update(readBuf, writeBuf)
不会向缓冲区写入任何内容,cipher.doFinal(ByteBuffer.allocate(0), writeBuf)
也不会写入任何内容,最后我丢失了数据。每次调用 cipher.getOutputSize(8192)
,都会增加结果,我不知道为什么会这样,但它可能会有所帮助。
为什么会发生这种情况,我该如何解决?
.update()
很简单; SunJCE 实现了 GCM(和 CCM)的要求,如果认证失败,认证解密不会释放(任何)明文;见 How come putting the GCM authentication tag at the end of a cipher stream require internal buffering during decryption? and https://moxie.org/blog/the-cryptographic-doom-principle/ 。因为标签位于密文的末尾,这意味着它必须缓冲所有密文,直到调用 doFinal()
(的重载之一)。 (这就是为什么对于大文件,随着您不断读取和缓冲更多数据,您将 writeBuf
重新分配到 cipher.getOutputSize(8192)
会不断增加。)
.doFinal()
更难;它应该工作。但是,我已经缩小了失败范围:只有当您使用 ByteBuffer
而不是原始 byte[]
数组时才会发生这种情况——这是在 javax.crypto.CipherSpi.bufferCrypt
中实现的,而不是分派到实现 [=24] =];并且输出 ByteBuffer
没有支持数组(即直接分配);明文超过4096字节。我将尝试更深入地研究失败的原因,但与此同时更改前两个中的任何一个修复它(或将您的数据限制为 4096 字节,但大概您不希望这样)。
这是我的完整代码:
import static java.nio.file.StandardOpenOption.READ;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Test {
public static void main(String[] args) throws Exception {
encrypt();
decrypt();
}
void encrypt() throws Exception {
Path file = Paths.get("path/to/file");
Path backupFile = file.getParent().resolve(file.getFileName().toString() + ".bak");
Files.deleteIfExists(backupFile);
Files.copy(file, backupFile);
SecureRandom secureRandom = new SecureRandom();
byte[] initializeVector = new byte[96 / Byte.SIZE];
secureRandom.nextBytes(initializeVector);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec p = new GCMParameterSpec(128, initializeVector);
try (FileChannel src = FileChannel.open(backupFile, READ);
FileChannel dest = FileChannel.open(file, WRITE, TRUNCATE_EXISTING)) {
SecretKeySpec secretKeySpec =
new SecretKeySpec(MessageDigest.getInstance("MD5").digest(new byte[]{0x00}), "AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, p);
ByteBuffer ivBuffer = ByteBuffer.allocate(Integer.BYTES + cipher.getIV().length);
ivBuffer.putInt(cipher.getIV().length);
ivBuffer.put(cipher.getIV());
ivBuffer.flip();
dest.write(ivBuffer);
ByteBuffer readBuf = ByteBuffer.allocateDirect(8192);
ByteBuffer writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(8192));
while (src.read(readBuf) >= 0) {
if (cipher.getOutputSize(8192) > writeBuf.capacity()) {
writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(8192));
}
readBuf.flip();
cipher.update(readBuf, writeBuf);
writeBuf.flip();
dest.write(writeBuf);
readBuf.clear();
writeBuf.clear();
}
if (cipher.getOutputSize(0) > writeBuf.capacity()) {
writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(0));
}
cipher.doFinal(ByteBuffer.allocate(0), writeBuf);
writeBuf.flip();
dest.write(writeBuf);
Files.delete(backupFile);
} catch (ShortBufferException e) {
//Should not happen!
throw new RuntimeException(e);
}
}
void decrypt() throws Exception {
Path file = Paths.get("path/to/file");
Path backupFile = file.getParent().resolve(file.getFileName().toString() + ".bak");
Files.deleteIfExists(backupFile);
Files.copy(file, backupFile);
try (FileChannel src = FileChannel.open(backupFile, READ);
FileChannel dest = FileChannel.open(file, WRITE, TRUNCATE_EXISTING)) {
ByteBuffer ivLengthBuffer = ByteBuffer.allocate(Integer.BYTES);
src.read(ivLengthBuffer);
ivLengthBuffer.flip();
int ivLength = ivLengthBuffer.getInt();
ByteBuffer ivBuffer = ByteBuffer.allocate(ivLength);
src.read(ivBuffer);
ivBuffer.flip();
byte[] iv = new byte[ivBuffer.limit()];
ivBuffer.get(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec p = new GCMParameterSpec(128, iv);
SecretKeySpec secretKeySpec =
new SecretKeySpec(MessageDigest.getInstance("MD5").digest(new byte[]{0x00}), "AES");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, p);
ByteBuffer readBuf = ByteBuffer.allocateDirect(8192);
ByteBuffer writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(8192));
while (src.read(readBuf) >= 0) {
if (cipher.getOutputSize(8192) > writeBuf.capacity()) {
writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(8192));
}
readBuf.flip();
cipher.update(readBuf, writeBuf);
writeBuf.flip();
dest.write(writeBuf);
readBuf.clear();
writeBuf.clear();
}
if (cipher.getOutputSize(0) > writeBuf.capacity()) {
writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(0));
}
cipher.doFinal(ByteBuffer.allocate(0), writeBuf);
writeBuf.flip();
dest.write(writeBuf);
Files.deleteIfExists(backupFile);
}
}
}
我发现了一个奇怪的问题:如果原始文件(未加密)大于 4KB
,在解密时,cipher.update(readBuf, writeBuf)
不会向缓冲区写入任何内容,cipher.doFinal(ByteBuffer.allocate(0), writeBuf)
也不会写入任何内容,最后我丢失了数据。每次调用 cipher.getOutputSize(8192)
,都会增加结果,我不知道为什么会这样,但它可能会有所帮助。
为什么会发生这种情况,我该如何解决?
.update()
很简单; SunJCE 实现了 GCM(和 CCM)的要求,如果认证失败,认证解密不会释放(任何)明文;见 How come putting the GCM authentication tag at the end of a cipher stream require internal buffering during decryption? and https://moxie.org/blog/the-cryptographic-doom-principle/ 。因为标签位于密文的末尾,这意味着它必须缓冲所有密文,直到调用 doFinal()
(的重载之一)。 (这就是为什么对于大文件,随着您不断读取和缓冲更多数据,您将 writeBuf
重新分配到 cipher.getOutputSize(8192)
会不断增加。)
.doFinal()
更难;它应该工作。但是,我已经缩小了失败范围:只有当您使用 ByteBuffer
而不是原始 byte[]
数组时才会发生这种情况——这是在 javax.crypto.CipherSpi.bufferCrypt
中实现的,而不是分派到实现 [=24] =];并且输出 ByteBuffer
没有支持数组(即直接分配);明文超过4096字节。我将尝试更深入地研究失败的原因,但与此同时更改前两个中的任何一个修复它(或将您的数据限制为 4096 字节,但大概您不希望这样)。