How to append to AES encrypted file

前端 未结 5 1429
孤街浪徒
孤街浪徒 2020-12-02 19:10

I\'m writing some kind of logger that produces encrypted log-file. Unfortunately, cryptography is not my strong side. Now I can write to file several messages and then close

5条回答
  •  爱一瞬间的悲伤
    2020-12-02 20:02

    If you're using AES in CBC mode, you can use the second to last block as the IV to decrypt the last block, which may be only partially full, then again to encrypt the plaintext of the last block followed by the new plaintext.

    Here's a proof of concept:

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

    I'd like to point out that the output is no different than what would be produced by encrypting all in one run. This is not a custom form of encryption --- it's standard AES/CBC/PKCS5Padding. The only implementation-specific detail is that, in the case of a blank file, I've written the iv before beginning the data.

    EDIT: Improved (for my taste) solution using 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();
        }
    
    }
    

提交回复
热议问题