如何附加到 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 加密文件的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 aes 算法加密 sqlite 文件?

如何使用 AES 在 Java 中加密文件 [重复]

CryptoJS<AES-CBC加密和解密>

aes加密安全吗

用php加密大文件的最佳方法

Js AES(ECB模式)加密和解密