如何附加到AES加密文件

我正在编写某种生成加密日志文件的记录器。 不幸的是,密码学不是我的强项。 现在我可以写入文件,然后关闭文件。 然后我可以打开它,附加一些消息,再次关闭,解密后我在文件中间看到填充字节。 有没有办法处理加密文件,而不必每次我想附加一些消息时解密它?

编辑 :更多细节。 当前实现使用CipherOutputStream。 据我所知,没有办法寻求使用它。 如果我能控制输出数据大小可以被块大小整除,我可以使用’NoPadding’选项吗?

如果您在CBC模式下使用AES,则可以使用倒数第二个块作为IV来解密最后一个块,该块可能只是部分满,然后再次加密最后一个块的明文,然后是新的明文。

这是一个概念certificate:

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

我想指出的是,输出与在一次运行中加密所产生的输出没有什么不同。 这不是一种自定义的加密forms - 它是标准的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(); } } 

我喜欢“maybeWeCouldStealAVa”提供的解决方案。 但是这没有正确实现’flush()’,我发现每次添加消息时都需要关闭并重新打开文件,以确保不丢失任何内容。 所以我重写了它。 我的解决方案将在每次刷新时写出最后一个块,但在添加下一条消息时重写此块。 使用这个2步前进,1步后退的方法,不可能使用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. Eg "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 len bytes from the specified byte array * starting at offset off 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 StackOverflow 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(); } } 

AES是分组密码。 这意味着它不会逐个字符地加密消息,但会保存数据,直到它具有一定大小的块,然后写入。 这样本身就会给你带来麻烦,因为你的日志消息不太可能与块大小相匹配。 这是第一个问题。

第二个问题是“AES”本身并不是对你正在做的事情的完整描述。 块密码可以用于不同的“模式”(参见维基百科的这个好的描述 )。 其中许多模式将流中较早的信息与稍后出现的数据混合在一起。 这使得加密更加安全,但同样会导致问题(因为您需要存储在关闭和打开文件之间混合的信息)。

解决你想要一个流密码的第一个问题。 就像你对这个名字的期望一样,这适用于数据流。 现在事实certificate,上面描述的一些密码模式可以使块密码像流一样工作。

但是流密码可能无法帮助解决第二个问题 – 因为您需要在某个地方存储需要在两次使用之间传输的数据,以便您可以正确初始化附加的流。

真的,如果你问这一切,你有多确定最终的结果是安全的? 即使以上述为指导,您可能会犯很多错误。 我建议找到一个现有的库来执行此操作,或者减少您的要求,以便您解决一个更简单的问题(您真的需要追加 – 在这种情况下您是否可以不启动新文件?或者,如上所述,添加文件的某种标记,以便您可以找到不同的部分?)

是否可以将数据附加到密文取决于两个因素:

  1. 您需要对AES使用计数器(CTR)模式,因为它是唯一允许您在加密数据中随机搜索的模式。 在这种情况下,您希望寻找加密数据的末尾。 请注意,CTR模式不需要将密文填充到密码块大小。
  2. 您不能使用任何消息validation代码(MAC),而无需再次使用整个消息 – 请将其作为密文或纯文本。 这是设计 – 如果你能做到这一点,那么MAC就会被打破。

所以,如果您不需要任何身份validation,那么您只能这样做。 但是,没有任何身份validation的加密是没有意义的,因为攻击者可以轻松地修改您的加密数据。 只有非常有限的用例,您可以谨慎地牺牲身份validation。

任何方式使用加密文件,而不必每次我想附加一些消息解密它?

如果您使用某些方法加密加密文件,则可能无法解密。

您可以实现自定义加密,该加密可能具有某种指示,即下一部分是附加消息。 这样,它使用相同的方法解密每条消息。

您也可以尝试这个https://stackoverflow.com/a/629762/643500