Question Comment ajouter un fichier crypté AES


J'écris une sorte de logger qui produit un fichier journal crypté. Malheureusement, la cryptographie n'est pas mon fort. Maintenant, je peux écrire pour classer plusieurs messages puis fermer le fichier. Ensuite, je peux l'ouvrir, ajouter des messages, fermer à nouveau et après le déchiffrement, je vois des octets de remplissage au milieu du fichier. Est-il possible de travailler avec un fichier chiffré sans avoir à le déchiffrer chaque fois que je souhaite ajouter des messages?

MODIFIER: peu plus de détails. L'implémentation actuelle utilise CipherOutputStream. Si je comprends bien, il est impossible de chercher En l'utilisant. Puis-je utiliser l'option «NoPadding» si je veux contrôler que la taille des données de sortie est divisible par taille de bloc?


12
2018-04-23 15:34


origine


Réponses:


Si vous utilisez AES dans Mode CBC, vous pouvez utiliser le bloc dernier comme dernier pour décrypter le dernier bloc, qui peut être partiellement rempli, puis encore une fois pour chiffrer le texte en clair du dernier bloc suivi du nouveau texte en clair.

Voici une preuve de 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);
    }

}

Je tiens à souligner que la sortie n'est pas différente de ce qui serait produit en cryptant tout en un seul passage. C'est ne pas une forme de cryptage personnalisée - c'est le standard AES / CBC / PKCS5Padding. Le seul détail spécifique à l'implémentation est que, dans le cas d'un fichier vierge, j'ai écrit le fichier iv avant de commencer les données.

EDIT: Solution améliorée (à mon goût) en utilisant 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();
    }

}

15
2018-04-24 03:14



J'aime la solution fournie par "MaybeWeCouldStealAVa". Mais cela n'a pas correctement implémenté 'flush ()', et j'ai trouvé qu'il était nécessaire de fermer et de rouvrir le fichier chaque fois que vous ajoutez un message, pour être sûr de ne rien perdre. Donc je l'ai réécrit. Ma solution écrira le dernier bloc chaque fois que vous viderez, mais réécrivez ensuite ce bloc lorsque le message suivant est ajouté. Avec cette méthode en 2 étapes, il n'est pas possible d'utiliser OutputStream, mais je l'ai implémenté directement sur 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();
    }
}

Vous pouvez voir comment l'utiliser et le tester en utilisant:

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

5
2017-09-28 10:00



AES est un chiffrement de bloc. cela signifie qu'il ne crypte pas un caractère de message par caractère, mais enregistre les données jusqu'à ce qu'il ait un bloc d'une certaine taille, puis les écrit. de sorte que cela en soi va causer des problèmes pour vous, car vos messages de journal ne sont pas susceptibles de correspondre à la taille du bloc. c'est le premier problème.

Le deuxième problème est que "AES" en soi n'est pas une description complète de ce que vous faites. un bloc chiffrement peut être utilisé dans différents "modes" (voir cette bonne description à wikipedia). beaucoup de ces modes mélangent des informations plus anciennes dans le flux avec des données fournies ultérieurement. Cela rend le cryptage plus sûr, mais encore une fois, cela posera des problèmes (car vous devez stocker les informations qui seront mélangées entre la fermeture et l'ouverture du fichier).

pour résoudre le premier problème, vous voulez un chiffrement de flux. comme vous vous en doutez, cela fonctionne sur un flux de données. maintenant, il s'avère que certains des modes de chiffrement décrits ci-dessus peuvent faire fonctionner un chiffrement de bloc comme un chiffrement.

mais un chiffrement de flux ne vous aidera probablement pas à résoudre le deuxième problème - pour cela vous devez stocker, quelque part, les données qui doivent être transportées entre les utilisations afin de pouvoir initialiser correctement le flux ajouté.

vraiment, si vous demandez tout cela, dans quelle mesure êtes-vous sûr que le résultat final est sécurisé? Il y a beaucoup d'erreurs que vous pourriez commettre, même avec les conseils ci-dessus. Je suggérerais soit de trouver une bibliothèque existante qui le fait, soit de réduire vos besoins pour résoudre un problème plus simple (avez-vous vraiment besoin d’ajouter - pouvez-vous ne pas démarrer un nouveau fichier dans ce cas? ou, comme suggéré ci-dessus, ajouter une sorte de marqueur pour le fichier afin que vous puissiez trouver les différentes sections?)


2
2018-04-23 16:23



Qu'il soit ou non possible d'ajouter des données à un texte chiffré dépend de deux facteurs:

  1. Vous devrez utiliser le mode Compteur (CTR) pour l'AES car c'est le seul mode qui vous permet de rechercher de manière aléatoire dans les données chiffrées. Dans ce cas, vous souhaitez rechercher la fin des données chiffrées. Notez que le remplissage du texte chiffré à la taille du bloc de chiffrement n'est pas requis avec le mode CTR.
  2. Vous ne pouvez pas utiliser de code d'authentification de message (MAC) sans l'alimenter à nouveau avec le message entier - laissez-le être du texte chiffré ou du texte brut. C'est par conception - si vous pouviez faire cela, alors le MAC serait cassé.

Donc, ce que vous essayez de faire ne peut être fait que si vous n'avez pas besoin d'authentification. Cependant, le cryptage sans aucune authentification est inutile car un adversaire peut alors facilement modifier vos données cryptées. Il n'y a que des cas d'utilisation très limités où vous pouvez sacrifier de manière sûre l'authentification.


1
2018-04-23 16:39



Une façon de travailler avec un fichier crypté sans avoir à le décrypter chaque fois que je veux ajouter des messages?

Si vous chiffrez le fichier chiffré, il se peut que certaines méthodes ne soient pas déchiffrables.

Vous pouvez implémenter un chiffrement personnalisé qui pourrait indiquer que la prochaine partie est un message ajouté. De cette façon, il décrypte chaque message avec la même méthode.

Vous pouvez essayer cela aussi https://stackoverflow.com/a/629762/643500


0
2018-04-23 16:14