2009-03-10 5 views
7

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

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

12/03/2009 8:34:21 -> User «Bob» вошли в 12/03/2009 08 : 34: 28 -> Навигатор на страницу конфигурации 12/03/2009 08:34:32 -> Опция x изменена на y

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

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

+0

У вас есть особое окружение? в общем, я бы использовал базу данных и применял безопасность на уровне базы данных, не работал бы, если вы не используете базу данных или если вам нужно легко экспортировать журналы. –

ответ

3

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

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

+0

Вы можете предоставить нам ссылку на пример в Интернете – shareef

3

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

5

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

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

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

+0

Отличное решение! – erickson

+0

За исключением одного: симметричный ключ должен храниться где-то в текстовом редакторе в любом случае, если вы хотите что-то зашифровать/расшифровать. Этот факт убирает все преимущества безопасности от асимметричного шифрования. – bytefu

+0

@bytefu: Nope - симметричный ключ генерируется «на лету» и хранится в зашифрованном виде под открытым ключом в файле журнала. – tonys

1

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

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

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

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

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

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

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

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

+0

К сожалению, у меня нет времени и возможностей для создания вируса/трояна. Зашифрованный файл журнала действительно защищает от доступа кого-либо к машине и проверки того, что сделал пользователь (т. Е. Для защиты конфиденциальности пользователей). – JamieH

+0

JamieH: Интересно, что ответит вирус/троянский писатель;) Но остается вопрос: кто защищает пользователя от вас? Или сказать это наоборот: как вы будете защищать себя, когда пользователи будут судиться с вами из-за вторжения в их личную жизнь? –

+0

Тангенциально связан с OP, но мне очень нравится идея буфера событий, который только попадает в файл, когда есть ошибка. – erickson

0

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

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

1

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

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

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

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

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

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

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

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

Это должно обеспечить достаточный уровень безопасности.

+0

Вы могли бы предоставить дополнительную информацию о том, почему вы рекомендуете это решение? в частности, зачем использовать симметричные, почему временные ключи повышают безопасность и почему смена главного ключа на каждом ротации? – sazary

+0

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

1

Непонятно, почему вы беспокоитесь о безопасности или реализации.

Простой инструмент - это подключение к потоковому шифру. Шифрование потока поддерживает свое собственное состояние и может зашифровать «на лету».

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

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

Original Encrypted using ECB mode Encrypted using other modes

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

+0

Хотя мне нравится идея шифрования записи журнала B с информацией из входа в журнал A, я не уверен, действительно ли подходит ваш собственный продукт ... (Если это будет сочтено целесообразным, я буду первым смиренно удалите этот комментарий, конечно ;-)) – wzzrd

+3

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

+0

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

0

У меня есть то же самое, что и у вас. Некоторый парень под названием «maybeWeCouldStealAVa» написал хорошую реализацию в: How to append to AES encrypted file, однако это страдал от того, что вы не были легкомысленным - вам придется закрывать и открывать файл каждый раз, когда вы очищаете сообщение, чтобы не потерять ничего.

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

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

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

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

Смежные вопросы