Создание зашифрованного файла журнала


Я создаю клиентское приложение, которое должно создавать журнал действий пользователя, но по разным причинам этот журнал не должен быть читаемым человеком.

В настоящее время для моей разработки я создаю простой текстовый журнал, который выглядит примерно так:

12/03/2009 08:34:21 -> пользователь ' Bob ' вошел в систему 12/03/2009 08: 34: 28 - > переход на страницу конфигурации 12/03/2009 08: 34: 32 - > опция x изменена на y

При развертывании приложения журнал не должен быть в виде обычного текста, поэтому весь текст должно быть, зашифровано. Это не кажется простым для достижения, поскольку мне нужно, чтобы файл журнала динамически обновлялся по мере добавления каждой записи. Подход, о котором я думал, состоял в том, чтобы создать двоичный файл, зашифровать каждую запись журнала изолированно, а затем добавить ее к двоичному файлу с некоторой подходящей демаркацией между каждой записью.

Кто-нибудь знает какие-либо общие подходы к этой проблеме, я уверен, что должно быть лучшее решение!

10 7

10 ответов:

На самом деле это не мое дело, я признаю это с готовностью, но разве вы не можете зашифровать каждую запись по отдельности, а затем добавить ее в файл журнала? Если вы воздержитесь от шифрования метки времени, вы можете легко найти записи, которые ищете, и расшифровать их, когда это необходимо.

Я имею в виду, что добавление отдельных зашифрованных записей в файл не обязательно должно быть двоичными записями, добавленными к двоичному файлу. Шифрование с помощью (например) gpg приведет к искажению ascii, что может быть добавлен в файл ascii. Это решит вашу проблему?

Не шифруйте отдельные записи журнала отдельно и не записывайте их в файл, как это предлагается другими плакатами, потому что злоумышленник легко сможет идентифицировать шаблоны в файле журнала. Смотрите разделрежимы блочного шифра Wikipedia entry , чтобы узнать больше об этой проблеме.

ОригиналШифрование с использованием режима ЕЦБШифрование с использованием других режимов

Вместо этого убедитесь, что шифрование записи журнала зависит от предыдущих записей журнала. Хотя это имеет некоторые недостатки (вы не можете расшифровать отдельные записи журнала, как всегда нужно расшифровать весь файл), это делает шифрование намного сильнее. Для нашей собственной библиотеки журналов, SmartInspect , мы используем шифрование AES и режим CBC, чтобы избежать проблемы с шаблоном. Не стесняйтесь датьSmartInspect попробовать, если коммерческое решение будет подходящим.

FWIW, один раз, когда мне понадобился зашифрованный регистратор, я использовал симметричный ключ (из соображений производительности) для шифрования фактических записей журнала.

Затем симметричный "ключ файла журнала" был зашифрован под открытым ключом и сохранен в начале файла журнала, а отдельный считыватель журнала использовал закрытый ключ для расшифровки "ключа файла журнала" и чтения записей.

Все это было реализовано с использованием log4j и формата XML log file (чтобы читателю было легче анализировать), и каждый при прокате файлов журнала создавался новый "ключ файла журнала".

Предполагая, что вы используете какой-то фреймворк ведения журнала, например, log4j и др., то вы должны иметь возможность создать пользовательскую реализацию Appender (или аналогичную), которая шифрует каждую запись, как предложил @wzzrd.

Шифрование каждой записи журнала по отдельности значительно снизит безопасность вашего шифротекста, особенно потому, что вы работаете с очень предсказуемым открытым текстом.

Вот что вы можете сделать:

  1. Используйте симметричное шифрование (предпочтительно AES)
  2. Выберите случайный мастер-ключ
  3. выберите окно безопасности (5 минут, 10 минут и т. д.)

Затем выберите случайный временный ключ в начале каждого окна (каждые 5 минут, каждые 10 минут, и т.д.)

Зашифруйте каждый элемент журнала отдельно с помощью временного ключа и добавьте к временному файлу журнала.

Когда окно закрыто (заданное время истекло), расшифруйте каждый элемент с помощью временного ключа, расшифруйте главный файл журнала с помощью главного ключа, объедините файлы и зашифруйте с помощью главного ключа.

Затем выберите новый временный ключ и продолжайте.

Кроме того, меняйте главный ключ при каждом повороте главного файла журнала (каждый день, каждую неделю, и т.д.)

Это должно обеспечить достаточную безопасность.

Мне не ясно, что вас больше беспокоит-безопасность или орудие.

Простая реализация заключается в подключении к потоковому шифратору. Потоковый шифратор поддерживает свое собственное состояние и может шифровать на лету.

StreamEncryptor<AES_128> encryptor;
encryptor.connectSink(new std::ofstream("app.log"));
encryptor.write(line);
encryptor.write(line2);
...

Мне интересно, какое заявление вы пишете. Вирус или Троянский конь? В любом случае...

Зашифруйте каждую запись отдельно, преобразуйте ее в некоторую строку (например, Base64) и затем протоколируйте эту строку как "сообщение".

Это позволяет сохранять части файла читаемыми и шифровать только важные части.

Обратите внимание, что у этой монеты есть и другая сторона: если вы создадите полностью зашифрованный файл и попросите пользователя об этом, он не может знать, что вы узнаете из файла. Поэтому вы должны шифровать как можно меньше (пароли, IP-адреса, данные клиентов), чтобы юридический отдел мог проверить, какие данные уходят.

Гораздо лучшим подходом был бы обфускатор для файла журнала. Это просто заменяет некоторые шаблоны на "XXX". Вы все еще можете видеть, что произошло, и когда вам нужна конкретная часть данных, вы можете попросить об этом.

[EDIT] эта история имеет больше последствий, чем вы думаете на первый взгляд. Это эффективно означает, что пользователь не может видеть, что находится в файле. "Пользователь "не обязательно включает в себя"взломщик". Взломщик будет концентрироваться на зашифрованных файлах (поскольку они, вероятно, более важны). Вот в чем причина старой поговорки: как только кто-то получает доступ к машине, нет никакого способа помешать ему что-либо сделать на ней. Или сказать по-другому: просто потому, что вы не знаете, как это делается, не означает, что кто-то другой тоже не знает. себе.

Также существует проблема ответственности. Скажем, некоторые данные утекают в Интернет после того, как вы получаете копию журналов. Поскольку пользователь понятия не имеет, что находится в файлах журнала, как вы можете доказать в суде, что вы не были утечкой? Боссы могли бы попросить лог-файлы, чтобы следить за своими пешками, попросив закодировать их, чтобы крестьяне не заметили и не скулили об этом (или подать в суд, мразь!).

Или посмотрите на это с совершенно другой точки зрения: если бы не было файла журнала, никто не мог бы злоупотреблять оно. Как насчет включения отладки только в случае чрезвычайной ситуации? Я настроил log4j для хранения последних 200 сообщений журнала в буфере. Если регистрируется ошибка, Я сбрасываю 200 сообщений в журнал. Обоснование: мне действительно все равно, что происходит в течение дня. Меня интересуют только жуки. Используя JMX, легко установить уровень отладки на ошибку и снизить его удаленно во время выполнения, когда вам нужно больше деталей.

Очень старый вопрос, и я уверен, что технический мир добился большого прогресса, но Fwiw Брюс Шнайер и Джон Келси написали статью о том, как это сделать: https://www.schneier.com/paper-auditlogs.html

Контекст-это не только безопасность,но и предотвращение повреждения или изменения существующих данных файла журнала, если система, в которой размещены файлы журнала/аудита, находится под угрозой.

Для .Net смотрите раздел Microsoft Application blocks для функций ведения журнала и шифрования: http://msdn.microsoft.com/en-us/library/dd203099.aspx

Я бы добавил зашифрованные записи журнала в плоский текстовый файл, используя соответствующую демаркацию между каждой записью для работы дешифровки.

У меня точно такая же потребность, как и у тебя. Какой - то парень под названием "maybeWeCouldStealAVa" написал хорошую реализацию в: Как добавить к зашифрованному файлу AES , однако это страдало от того, что не было смыва-вам придется закрывать и снова открывать файл каждый раз, когда вы смываете сообщение, чтобы быть уверенным, что ничего не потеряете.

Поэтому я написал свой собственный класс, чтобы сделать это:

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 StackOverflow readers can decrypt your 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();
    }
}