Создание зашифрованного файла журнала - PullRequest
8 голосов
/ 10 марта 2009

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

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

12.03.2009 08:34:21 -> Пользователь 'Bob' вошел в систему 03.12.2009 08:34:28 -> Перешел на страницу конфигурации 03.12.2009 08:34:32 -> Опция x изменена на y

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

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

Ответы [ 10 ]

9 голосов
/ 11 марта 2009

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

Original Encrypted using ECB mode Encrypted using other modes

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

6 голосов
/ 10 марта 2009

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

Симметричный «ключ файла журнала» затем шифровался с помощью открытого ключа и сохранялся в начале файла журнала, а отдельный считыватель журналов использовал закрытый ключ для расшифровки «ключа файла журнала» и считывания записей.

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

4 голосов
/ 10 марта 2009

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

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

3 голосов
/ 10 марта 2009

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

1 голос
/ 09 октября 2014

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

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

1 голос
/ 10 марта 2009

Мне не ясно, заботится ли ты о безопасности или орудии.

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

StreamEncryptor<AES_128> encryptor;
encryptor.connectSink(new std::ofstream("app.log"));
encryptor.write(line);
encryptor.write(line2);
...
1 голос
/ 10 марта 2009

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

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

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

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

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

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

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

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

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

1 голос
/ 10 марта 2009

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

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

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

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

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

[РЕДАКТИРОВАТЬ] Эта история имеет больше последствий, которые вы могли бы подумать на первый взгляд. Это фактически означает, что пользователь не может видеть, что находится в файле. «Пользователь» не обязательно включает «взломщик». Взломщик сосредоточится на зашифрованных файлах (так как они, вероятно, более важны). Вот почему старая поговорка гласит: как только кто-то получает доступ к машине, он не может помешать ему что-либо делать с ней. Или сказать это по-другому: просто потому, что вы не знаете, как это не означает, что кто-то другой также не делает. Если вы думаете, что вам нечего скрывать, вы не думали о себе.

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

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

0 голосов
/ 22 сентября 2014

У меня точно такая же потребность, как и у вас. Какой-то парень под названием «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();
    }
}
0 голосов
/ 10 марта 2009

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

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

...