如何附加到 AES 加密文件
Posted
技术标签:
【中文标题】如何附加到 AES 加密文件【英文标题】:How to append to AES encrypted file 【发布时间】:2012-05-04 05:55:57 【问题描述】:我正在编写某种生成加密日志文件的记录器。不幸的是,密码学并不是我的强项。现在我可以写入文件几条消息,然后关闭文件。然后我可以打开它,附加一些消息,再次关闭,解密后我在文件中间看到填充字节。有什么方法可以处理加密文件,而不必每次我想附加一些消息时都对其进行解密?
编辑:更多细节。当前实现使用 CipherOutputStream。据我了解,没有办法寻求使用它。 如果我将控制输出数据大小可被块大小整除,我可以使用“NoPadding”选项吗?
【问题讨论】:
您的问题是填充出现在解密文件的中间吗?你在使用 FileOutputStream 和 CipherOutputStream 吗? @edralzar:是的,我使用 FileOutputStream 和 CipherOutputStream 您为什么不直接轮换日志文件而不是附加到它们?然后你可以完全跳过这个黑客攻击。 @ChristianSchlichtherle :首先 - 我需要所有将写入日志的数据。所以,没有轮回。第二 - 如果我每分钟重新启动一次程序怎么办?每分钟都会创建一个新文件。不太好…… 每分钟创建一个新的日志文件可能比将加密限制在某些模式(CBC 或 CTR,如下所述)并完全放弃身份验证(见下文)更好。 【参考方案1】:我喜欢maybeWeCouldStealAVan 提供的解决方案。但这并没有正确实现“flush()”,我发现每次附加消息时都必须关闭并重新打开文件,以确保不会丢失任何内容。所以我重写了它。我的解决方案将在每次刷新时写出最后一个块,但在添加下一条消息时重写此块。使用这种 2-steps-forward, 1-step-back 的方法,不可能使用 OutputStream 的,而是直接在 RandomAccessFile 之上实现它。
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.security.*;
public class FlushableCipherOutputStream extends OutputStream
private static int HEADER_LENGTH = 16;
private SecretKeySpec key;
private RandomAccessFile seekableFile;
private boolean flushGoesStraightToDisk;
private Cipher cipher;
private boolean needToRestoreCipherState;
/** the buffer holding one byte of incoming data */
private byte[] ibuffer = new byte[1];
/** the buffer holding data ready to be written out */
private byte[] obuffer;
/** Each time you call 'flush()', the data will be written to the operating system level, immediately available
* for other processes to read. However this is not the same as writing to disk, which might save you some
* data if there's a sudden loss of power to the computer. To protect against that, set 'flushGoesStraightToDisk=true'.
* Most people set that to 'false'. */
public FlushableCipherOutputStream(String fnm, SecretKeySpec _key, boolean append, boolean _flushGoesStraightToDisk)
throws IOException
this(new File(fnm), _key, append,_flushGoesStraightToDisk);
public FlushableCipherOutputStream(File file, SecretKeySpec _key, boolean append, boolean _flushGoesStraightToDisk)
throws IOException
super();
if (! append)
file.delete();
seekableFile = new RandomAccessFile(file,"rw");
flushGoesStraightToDisk = _flushGoesStraightToDisk;
key = _key;
try
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
byte[] iv = new byte[16];
byte[] headerBytes = new byte[HEADER_LENGTH];
long fileLen = seekableFile.length();
if (fileLen % 16L != 0L)
throw new IllegalArgumentException("Invalid file length (not a multiple of block size)");
else if (fileLen == 0L)
// new file
// You can write a 16 byte file header here, including some file format number to represent the
// encryption format, in case you need to change the key or algorithm. E.g. "100" = v1.0.0
headerBytes[0] = 100;
seekableFile.write(headerBytes);
// Now appending the first IV
SecureRandom sr = new SecureRandom();
sr.nextBytes(iv);
seekableFile.write(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
else if (fileLen <= 16 + HEADER_LENGTH)
throw new IllegalArgumentException("Invalid file length (need 2 blocks for iv and data)");
else
// file length is at least 2 blocks
needToRestoreCipherState = true;
catch (InvalidKeyException e)
throw new IOException(e.getMessage());
catch (NoSuchAlgorithmException e)
throw new IOException(e.getMessage());
catch (NoSuchPaddingException e)
throw new IOException(e.getMessage());
catch (InvalidAlgorithmParameterException e)
throw new IOException(e.getMessage());
/**
* Writes one _byte_ to this output stream.
*/
public void write(int b) throws IOException
if (needToRestoreCipherState)
restoreStateOfCipher();
ibuffer[0] = (byte) b;
obuffer = cipher.update(ibuffer, 0, 1);
if (obuffer != null)
seekableFile.write(obuffer);
obuffer = null;
/** Writes a byte array to this output stream. */
public void write(byte data[]) throws IOException
write(data, 0, data.length);
/**
* Writes <code>len</code> bytes from the specified byte array
* starting at offset <code>off</code> to this output stream.
*
* @param data the data.
* @param off the start offset in the data.
* @param len the number of bytes to write.
*/
public void write(byte data[], int off, int len) throws IOException
if (needToRestoreCipherState)
restoreStateOfCipher();
obuffer = cipher.update(data, off, len);
if (obuffer != null)
seekableFile.write(obuffer);
obuffer = null;
/** The tricky stuff happens here. We finalise the cipher, write it out, but then rewind the
* stream so that we can add more bytes without padding. */
public void flush() throws IOException
try
if (needToRestoreCipherState)
return; // It must have already been flushed.
byte[] obuffer = cipher.doFinal();
if (obuffer != null)
seekableFile.write(obuffer);
if (flushGoesStraightToDisk)
seekableFile.getFD().sync();
needToRestoreCipherState = true;
catch (IllegalBlockSizeException e)
throw new IOException("Illegal block");
catch (BadPaddingException e)
throw new IOException("Bad padding");
private void restoreStateOfCipher() throws IOException
try
// I wish there was a more direct way to snapshot a Cipher object, but it seems there's not.
needToRestoreCipherState = false;
byte[] iv = cipher.getIV(); // To help avoid garbage, re-use the old one if present.
if (iv == null)
iv = new byte[16];
seekableFile.seek(seekableFile.length() - 32);
seekableFile.read(iv);
byte[] lastBlockEnc = new byte[16];
seekableFile.read(lastBlockEnc);
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
byte[] lastBlock = cipher.doFinal(lastBlockEnc);
seekableFile.seek(seekableFile.length() - 16);
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] out = cipher.update(lastBlock);
assert out == null || out.length == 0;
catch (Exception e)
throw new IOException("Unable to restore cipher state");
public void close() throws IOException
flush();
seekableFile.close();
您可以查看如何使用它并使用以下方法对其进行测试:
import org.junit.Test;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.io.BufferedWriter;
public class TestFlushableCipher
private static byte[] keyBytes = new byte[]
// Change these numbers lest other *** readers can read your log files
-53, 93, 59, 108, -34, 17, -72, -33, 126, 93, -62, -50, 106, -44, 17, 55
;
private static SecretKeySpec key = new SecretKeySpec(keyBytes,"AES");
private static int HEADER_LENGTH = 16;
private static BufferedWriter flushableEncryptedBufferedWriter(File file, boolean append) throws Exception
FlushableCipherOutputStream fcos = new FlushableCipherOutputStream(file, key, append, false);
return new BufferedWriter(new OutputStreamWriter(fcos, "UTF-8"));
private static InputStream readerEncryptedByteStream(File file) throws Exception
FileInputStream fin = new FileInputStream(file);
byte[] iv = new byte[16];
byte[] headerBytes = new byte[HEADER_LENGTH];
if (fin.read(headerBytes) < HEADER_LENGTH)
throw new IllegalArgumentException("Invalid file length (failed to read file header)");
if (headerBytes[0] != 100)
throw new IllegalArgumentException("The file header does not conform to our encrypted format.");
if (fin.read(iv) < 16)
throw new IllegalArgumentException("Invalid file length (needs a full block for iv)");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
return new CipherInputStream(fin,cipher);
private static BufferedReader readerEncrypted(File file) throws Exception
InputStream cis = readerEncryptedByteStream(file);
return new BufferedReader(new InputStreamReader(cis));
@Test
public void test() throws Exception
File zfilename = new File("c:\\WebEdvalData\\log.x");
BufferedWriter cos = flushableEncryptedBufferedWriter(zfilename, false);
cos.append("Sunny ");
cos.append("and green. \n");
cos.close();
int spaces=0;
for (int i = 0; i<10; i++)
cos = flushableEncryptedBufferedWriter(zfilename, true);
for (int j=0; j < 2; j++)
cos.append("Karelia and Tapiola" + i);
for (int k=0; k < spaces; k++)
cos.append(" ");
spaces++;
cos.append("and other nice things. \n");
cos.flush();
tail(zfilename);
cos.close();
BufferedReader cis = readerEncrypted(zfilename);
String msg;
while ((msg=cis.readLine()) != null)
System.out.println(msg);
cis.close();
private void tail(File filename) throws Exception
BufferedReader infile = readerEncrypted(filename);
String last = null, secondLast = null;
do
String msg = infile.readLine();
if (msg == null)
break;
if (! msg.startsWith(""))
secondLast = last;
last = msg;
while (true);
if (secondLast != null)
System.out.println(secondLast);
System.out.println(last);
System.out.println();
【讨论】:
嗨,它工作得很好,谢谢。如何在 PC 上查看这些加密文件?有没有办法做到这一点。谢谢。【参考方案2】:如果您在CBC mode 中使用 AES,您可以使用倒数第二个块作为 IV 来解密最后一个块,该块可能只是部分已满,然后再次加密最后一个块的明文,然后新的明文。
这是一个概念证明:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class AppendAES
public static void appendAES(File file, byte[] data, byte[] key) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException
RandomAccessFile rfile = new RandomAccessFile(file,"rw");
byte[] iv = new byte[16];
byte[] lastBlock = null;
if (rfile.length() % 16L != 0L)
throw new IllegalArgumentException("Invalid file length (not a multiple of block size)");
else if (rfile.length() == 16)
throw new IllegalArgumentException("Invalid file length (need 2 blocks for iv and data)");
else if (rfile.length() == 0L)
// new file: start by appending an IV
new SecureRandom().nextBytes(iv);
rfile.write(iv);
// we have our iv, and there's no prior data to reencrypt
else
// file length is at least 2 blocks
rfile.seek(rfile.length()-32); // second to last block
rfile.read(iv); // get iv
byte[] lastBlockEnc = new byte[16];
// last block
// it's padded, so we'll decrypt it and
// save it for the beginning of our data
rfile.read(lastBlockEnc);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key,"AES"), new IvParameterSpec(iv));
lastBlock = cipher.doFinal(lastBlockEnc);
rfile.seek(rfile.length()-16);
// position ourselves to overwrite the last block
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key,"AES"), new IvParameterSpec(iv));
byte[] out;
if (lastBlock != null) // lastBlock is null if we're starting a new file
out = cipher.update(lastBlock);
if (out != null) rfile.write(out);
out = cipher.doFinal(data);
rfile.write(out);
rfile.close();
public static void decryptAES(File file, OutputStream out, byte[] key) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException
// nothing special here, decrypt as usual
FileInputStream fin = new FileInputStream(file);
byte[] iv = new byte[16];
if (fin.read(iv) < 16)
throw new IllegalArgumentException("Invalid file length (needs a full block for iv)");
;
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key,"AES"), new IvParameterSpec(iv));
byte[] buff = new byte[1<<13]; //8kiB
while (true)
int count = fin.read(buff);
if (count == buff.length)
out.write(cipher.update(buff));
else
out.write(cipher.doFinal(buff,0,count));
break;
fin.close();
public static void main(String[] args) throws Exception
byte[] key = new byte[]0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15;
for (int i = 0; i<1000; i++)
appendAES(new File("log.aes"),"All work and no play makes Jack a dull boy. ".getBytes("UTF-8"),key);
decryptAES(new File("log.aes"), new FileOutputStream("plain.txt"), key);
我想指出,输出与一次性加密所有输出的结果没有什么不同。这不是一种自定义的加密形式——它是标准的 AES/CBC/PKCS5Padding。唯一特定于实现的细节是,在空白文件的情况下,我在开始数据之前编写了 iv。
编辑:使用CipherOutputStream
改进(根据我的口味)解决方案:
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class AppendAES
public static CipherOutputStream appendAES(File file, SecretKeySpec key) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException
return appendAES(file, key, null);
public static CipherOutputStream appendAES(File file, SecretKeySpec key, SecureRandom sr) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException
RandomAccessFile rfile = new RandomAccessFile(file,"rw");
byte[] iv = new byte[16];
byte[] lastBlock = null;
if (rfile.length() % 16L != 0L)
throw new IllegalArgumentException("Invalid file length (not a multiple of block size)");
else if (rfile.length() == 16)
throw new IllegalArgumentException("Invalid file length (need 2 blocks for iv and data)");
else if (rfile.length() == 0L)
// new file: start by appending an IV
if (sr == null) sr = new SecureRandom();
sr.nextBytes(iv);
rfile.write(iv);
else
// file length is at least 2 blocks
rfile.seek(rfile.length()-32);
rfile.read(iv);
byte[] lastBlockEnc = new byte[16];
rfile.read(lastBlockEnc);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
lastBlock = cipher.doFinal(lastBlockEnc);
rfile.seek(rfile.length()-16);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] out;
if (lastBlock != null)
out = cipher.update(lastBlock);
if (out != null) rfile.write(out);
CipherOutputStream cos = new CipherOutputStream(new FileOutputStream(rfile.getFD()),cipher);
return cos;
public static CipherInputStream decryptAES(File file, SecretKeySpec key) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException
FileInputStream fin = new FileInputStream(file);
byte[] iv = new byte[16];
if (fin.read(iv) < 16)
throw new IllegalArgumentException("Invalid file length (needs a full block for iv)");
;
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
CipherInputStream cis = new CipherInputStream(fin,cipher);
return cis;
public static void main(String[] args) throws Exception
byte[] keyBytes = new byte[]
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
;
SecretKeySpec key = new SecretKeySpec(keyBytes,"AES");
for (int i = 0; i<100; i++)
CipherOutputStream cos = appendAES(new File("log.aes"),key);
cos.write("All work and no play ".getBytes("UTF-8"));
cos.write("makes Jack a dull boy. \n".getBytes("UTF-8"));
cos.close();
CipherInputStream cis = decryptAES(new File("log.aes"), key);
BufferedReader bread = new BufferedReader(new InputStreamReader(cis,"UTF-8"));
System.out.println(bread.readLine());
cis.close();
【讨论】:
非常感谢 - 我也有同样的需求,我已经确认此代码有效。但是:每次我附加一条消息并调用flush()时,数据都不会被写出——最后一个块上的填充可能只在我调用close()时才被写入。如果要求在 flush()d 后消息是完整的,并且我不想 close() 并在每条消息上重新打开文件,我该怎么做?【参考方案3】:是否可以将数据附加到密文取决于两个因素:
-
您需要对 AES 使用计数器 (CTR) 模式,因为它是唯一允许您在加密数据中随机查找的模式。在这种情况下,您要查找到加密数据的末尾。请注意,CTR 模式不需要将密文填充到密码块大小。
您不能使用任何消息验证码 (MAC),除非再次将其与整个消息一起提供 - 让它成为密文或纯文本。这是设计使然 - 如果你能做到这一点,那么 MAC 就会被破坏。
因此,只有在不需要任何身份验证的情况下才能完成您尝试做的事情。但是,没有任何身份验证的加密是毫无意义的,因为攻击者可以轻松修改您的加密数据。只有非常有限的用例可以明智地牺牲身份验证。
【讨论】:
但是对文件的追加操作与加密数据有什么关系呢?当您使用附加进行文件写入时,数据可能是二进制等。在这种情况下,它是加密的。加密如何改变这里的东西? @user384706 附加数据会更改 MAC,这是整个文件的指纹。 @andrewcooke:OP 没有提到任何关于 MAC 的内容。他只想加密 这是答案所讨论的。【参考方案4】:AES 是一种分组密码。这意味着它不会逐个字符地加密消息,而是保存数据,直到它具有一定大小的块,然后再写入。所以这本身会给您带来问题,因为您的日志消息不太可能与块大小匹配。这是第一个问题。
第二个问题是“AES”本身并不是对您正在做的事情的完整描述。分组密码可以用于不同的“模式”(参见this good description at wikipedia)。其中许多模式将流中较早的信息与较晚的数据混合在一起。这使加密更加安全,但同样会导致问题(因为您需要存储将在关闭和打开文件之间混合的信息)。
要解决您需要流密码的第一个问题。就像您对名称所期望的那样,这适用于数据流。现在事实证明,上面描述的一些密码模式可以使分组密码像流密码一样工作。
但流密码可能无助于解决第二个问题 - 因为您需要在某处存储需要在两次使用之间传递的数据,以便您可以正确初始化附加的流。
真的,如果您要问这一切,您有多确定最终结果是安全的?即使以上述为指导,您也可能会犯很多错误。我建议要么找到一个现有的库来执行此操作,要么减少您的要求,以便您解决一个更简单的问题(您真的需要追加 - 在这种情况下您可以不开始一个新文件吗?或者,就像上面建议的那样,添加文件的某种标记,以便您可以找到不同的部分?)
【讨论】:
向 AES 提供多少数据并不重要。该块总是填充到 128 位。这是固定的 没错。而且,如果我理解正确,如果它的大小不等于块大小,使用“NoPadding”将强制 CipherOutputStream 忽略最后一个数据块。【参考方案5】:有什么方法可以处理加密文件,而不必每次我想附加一些消息时都对其进行解密?
如果您对加密文件进行加密,那么使用某些方法可能无法解密。
您可以实现自定义加密,该加密可能具有某种指示符,表明下一部分是附加消息。这样,它会使用相同的方法解密每条消息。
你也可以试试这个https://***.com/a/629762/643500
【讨论】:
以上是关于如何附加到 AES 加密文件的主要内容,如果未能解决你的问题,请参考以下文章