2009-03-10 32 views
7

Tôi đang tạo ứng dụng phía ứng dụng khách cần tạo nhật ký hoạt động của người dùng nhưng vì nhiều lý do, nhật ký này không phải là con người có thể đọc được.Tạo một tệp nhật ký được mã hóa

Hiện nay đối với sự phát triển của tôi, tôi đang tạo ra một bản ghi văn bản đơn giản mà trông giống như sau:

12/03/2009 08:34:21 -> Người dùng 'Bob' logged in 12/03/2009 08 : 34: 28 -> Điều hướng đến trang cấu hình 12/03/2009 08:34:32 -> Tùy chọn x đổi thành y

Khi tôi triển khai ứng dụng, nhật ký không được ở dạng văn bản thuần túy, vì vậy tất cả văn bản phải được mã hóa. Điều này dường như không đơn giản để đạt được khi tôi cần tệp nhật ký để cập nhật động khi mỗi mục nhập được thêm vào. Cách tiếp cận mà tôi đã nghĩ đến là tạo một tệp nhị phân, mã hóa mỗi mục nhập nhật ký một cách riêng biệt và sau đó nối nó vào tệp nhị phân với một số ranh giới phù hợp giữa mỗi mục nhập.

Có ai biết về bất kỳ cách tiếp cận phổ biến nào cho vấn đề này không, tôi chắc rằng phải có giải pháp tốt hơn!

+0

Bạn đã có một môi trường cụ thể? nói chung tôi sẽ sử dụng cơ sở dữ liệu và áp dụng bảo mật ở cấp cơ sở dữ liệu, sẽ không hoạt động nếu bạn không sử dụng cơ sở dữ liệu hoặc nếu bạn cần phải dễ dàng xuất nhật ký. –

Trả lời

3

Đây không phải là điều thực sự của tôi, tôi sẽ thừa nhận rằng dễ dàng, nhưng bạn không thể mã hóa mỗi mục riêng lẻ và sau đó nối nó vào logfile? Nếu bạn kiềm chế không mã hóa dấu thời gian, bạn có thể dễ dàng tìm thấy các mục bạn đang tìm kiếm và giải mã chúng khi cần.

Điểm của tôi chủ yếu là việc gắn các mục được mã hóa riêng lẻ vào một tệp không nhất thiết phải là mục nhị phân được nối vào tệp nhị phân. Mã hóa với (ví dụ) gpg sẽ tạo ra ascii garble có thể được thêm vào một tệp ascii. Điều đó có giúp bạn giải quyết vấn đề không?

+0

bạn có thể cung cấp cho chúng tôi liên kết ví dụ trên internet – shareef

3

Giả sử bạn đang sử dụng một số loại khung ghi nhật ký, ví dụ: log4j và các cộng sự, thì bạn có thể tạo triển khai tùy chỉnh Appender (hoặc tương tự) mã hóa từng mục nhập như @wzzrd đề xuất.

5

FWIW, một lần tôi cần một trình ghi nhật ký được mã hóa, tôi đã sử dụng khóa đối xứng (vì lý do hiệu suất) để mã hóa các mục nhật ký thực tế.

Khoá tệp nhật ký 'đối xứng' sau đó được mã hóa dưới khóa công khai và được lưu trữ ở đầu tệp nhật ký và trình đọc nhật ký riêng sử dụng khóa riêng để giải mã 'khóa tệp nhật ký' và đọc các mục nhập.

Toàn bộ điều được thực hiện bằng cách sử dụng log4j và định dạng tệp nhật ký XML (để người đọc dễ dàng phân tích cú pháp hơn) và mỗi lần tệp nhật ký được cuộn qua 'khóa tệp nhật ký' mới được tạo.

+0

Giải pháp tuyệt vời! – erickson

+0

Ngoại trừ một điều: khóa đối xứng phải được lưu trữ ở đâu đó trong văn bản thuần túy, nếu bạn muốn mã hóa/giải mã một cái gì đó. Điều này thực tế whittles đi tất cả các lợi ích bảo mật từ mã hóa bất đối xứng. – bytefu

+0

@bytefu: Không - một khóa đối xứng được tạo ra khi đang bay và được lưu trữ được mã hóa dưới khóa công khai trong tệp nhật ký. – tonys

1

Tôi tự hỏi bạn đang viết loại ứng dụng nào. Một loại virus hay một con ngựa Trojan? Dù sao ...

Mã hóa từng mục nhập, chuyển đổi nó thành một số chuỗi (ví dụ Base64) và sau đó ghi chuỗi đó làm "thông báo".

Điều này cho phép bạn giữ các phần của tệp có thể đọc được và chỉ mã hóa các phần quan trọng.

Lưu ý rằng có một mặt khác của đồng xu này: Nếu bạn tạo tệp được mã hóa hoàn toàn và yêu cầu người dùng, bạn không thể biết bạn sẽ học được gì từ tệp. Vì vậy, bạn nên mã hóa càng ít càng tốt (mật khẩu, địa chỉ IP, dữ liệu chi phí) để làm cho bộ phận pháp lý có thể xác minh dữ liệu nào được để lại.

Một cách tiếp cận tốt hơn sẽ là một obfuscator cho tệp nhật ký. Điều đó chỉ đơn giản là thay thế các mẫu nhất định bằng "XXX".Bạn vẫn có thể thấy điều gì đã xảy ra và khi bạn cần một phần dữ liệu cụ thể, bạn có thể yêu cầu điều đó.

[EDIT] Câu chuyện này có nhiều tác động hơn mà bạn nghĩ ngay từ cái nhìn đầu tiên. Điều này có nghĩa là người dùng không thể xem nội dung trong tệp. "Người dùng" không nhất thiết phải bao gồm "cracker". Một cracker sẽ tập trung vào các tập tin được mã hóa (vì chúng có lẽ quan trọng hơn). Đó là lý do cho câu nói cũ: Ngay sau khi ai đó được truy cập vào máy, không có cách nào để ngăn anh ta làm bất cứ điều gì trên đó. Hay nói theo cách khác: Chỉ vì bạn không biết làm thế nào không có nghĩa là người khác cũng không. Nếu bạn nghĩ rằng bạn không có gì để ẩn, bạn đã không nghĩ về bản thân mình.

Ngoài ra, có vấn đề trách nhiệm pháp lý. Giả sử, một số dữ liệu bị rò rỉ trên Internet sau khi bạn nhận được bản sao nhật ký. Vì người dùng không có ý tưởng gì trong các tệp nhật ký, làm thế nào bạn có thể chứng minh tại tòa án rằng bạn không bị rò rỉ? Các ông chủ có thể yêu cầu các tệp nhật ký theo dõi những con tốt của họ, yêu cầu nó được mã hóa để nông dân không thể nhận ra và rên rỉ về nó (hoặc kiện, cặn bã!).

Hoặc nhìn từ góc độ hoàn toàn khác: Nếu không có tệp nhật ký, không ai có thể lạm dụng nó. Làm cách nào để bật gỡ lỗi chỉ trong trường hợp khẩn cấp? Tôi đã cấu hình log4j để giữ 200 thông điệp tường trình cuối cùng trong bộ đệm. Nếu một ERROR được ghi lại, tôi đổ 200 tin nhắn vào nhật ký. Lý do: Tôi thực sự không quan tâm những gì xảy ra trong ngày. Tôi chỉ quan tâm đến lỗi. Sử dụng JMX, thật đơn giản để đặt mức gỡ lỗi thành ERROR và giảm mức độ điều khiển từ xa khi chạy khi bạn cần thêm chi tiết.

+0

Đáng buồn là tôi không có thời gian hoặc khả năng tạo ra virus/trojan. Tệp nhật ký được mã hóa thực sự bảo vệ khỏi người nào đó truy cập vào máy và kiểm tra những gì người dùng đã thực hiện (tức là để bảo vệ quyền riêng tư của người dùng). – JamieH

+0

JamieH: Tôi tự hỏi những gì một nhà văn virus/trojan sẽ trả lời;) Nhưng câu hỏi vẫn là: Ai bảo vệ người dùng chống lại bạn? Hay nói theo cách khác: Làm cách nào * bạn * tự bảo vệ mình khi người dùng kiện bạn vì xâm phạm quyền riêng tư của họ? –

+0

Có liên quan liên quan đến OP, nhưng tôi thực sự thích ý tưởng về bộ đệm sự kiện chỉ được chuyển sang tệp khi có lỗi. – erickson

0

Đối với Net xem các khối ứng dụng Microsoft cho chức năng đăng nhập và mã hóa: http://msdn.microsoft.com/en-us/library/dd203099.aspx

tôi sẽ thêm mục log được mã hóa vào một tập tin văn bản bằng phẳng sử dụng ranh giới phù hợp giữa mỗi mục nhập cho giải mã để làm việc.

1

Mã hóa từng mục nhập nhật ký riêng lẻ sẽ làm giảm tính bảo mật của bản mã của bạn rất nhiều, đặc biệt là vì bạn đang làm việc với bản rõ nguyên bản có thể đoán trước được.

Đây là những gì bạn có thể làm:

  1. Sử dụng mã hóa đối xứng (tốt nhất là AES)
  2. Chọn một bậc thầy ngẫu nhiên chìa khóa
  3. Chọn một cửa sổ bảo mật (5 phút, 10 phút, vv)

Sau đó, chọn một khóa tạm thời ngẫu nhiên ở đầu mỗi cửa sổ (mỗi 5 phút, cứ 10 phút, v.v.)

Mã hóa từng mục nhật ký một cách riêng biệt bằng cách sử dụng khóa tạm thời và nối thêm vào tệp nhật ký tạm thời.

Khi đóng cửa sổ (thời gian xác định trước), giải mã từng phần tử bằng khóa tạm thời, giải mã tệp nhật ký chính bằng khóa chính, hợp nhất tệp và mã hóa bằng khóa chính.

Sau đó, chọn một khóa tạm thời mới và tiếp tục.

Ngoài ra, thay đổi khóa thạc sĩ mỗi khi bạn xoay tập tin của bạn log thạc sĩ (mỗi ngày, mỗi tuần, vv)

này nên cung cấp đủ an ninh.

+0

bạn có thể cung cấp thêm một số thông tin về lý do bạn giới thiệu giải pháp này không? cụ thể, tại sao sử dụng đối xứng, tại sao các phím tạm thời tăng tính bảo mật và tại sao thay đổi khóa chính trên mỗi vòng quay? – sazary

+0

nếu hiệu suất không phải là vấn đề, thì tại sao lại sử dụng khóa tạm thời? và nếu có, thì giải mã toàn bộ tệp bằng khóa chính và mã hóa lại nó vẫn là một vấn đề, mặc dù ở cuối mỗi cửa sổ. – sazary

1

Tôi không quan tâm đến vấn đề bảo mật của bạn hoặc việc thực thi.

Triển khai đơn giản là kết nối với bộ mã hóa luồng. Mã hóa luồng duy trì trạng thái riêng và có thể mã hóa khi đang di chuyển.

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

Không mã hóa riêng từng mục nhập nhật ký và ghi chúng vào tệp theo đề xuất của áp phích khác, vì kẻ tấn công có thể dễ dàng xác định mẫu trong tệp nhật ký. Xem block cipher modes Wikipedia entry để tìm hiểu thêm về vấn đề này.

Original Encrypted using ECB mode Encrypted using other modes

Thay vào đó, hãy chắc chắn rằng mã hóa của một bản ghi phụ thuộc vào các mục đăng nhập trước. Mặc dù điều này có một số nhược điểm (bạn không thể giải mã các mục nhật ký riêng lẻ như bạn luôn cần giải mã toàn bộ tệp), nó làm cho mã hóa mạnh hơn rất nhiều. Đối với thư viện khai thác riêng của chúng tôi, SmartInspect, chúng tôi sử dụng mã hóa AES và chế độ CBC để tránh sự cố mẫu. Vui lòng thử dùng thử SmartInspect nếu một giải pháp thương mại phù hợp.

+0

Mặc dù tôi thích ý tưởng mã hóa mục nhập B với thông tin từ mục nhập A, tôi không chắc liệu việc cắm sản phẩm của riêng bạn ở đây có thực sự thích hợp không ... (Nếu được coi là phù hợp, tôi sẽ là người đầu tiên khiêm nhường loại bỏ bình luận này, tất nhiên ;-)) – wzzrd

+3

Tôi không chắc chắn wzzrd. Nếu tôi đang tìm kiếm một giải pháp cho một vấn đề và có một công cụ đã làm những gì tôi đang tìm kiếm, tôi sẽ rất vui nếu có ai đó chỉ ra (ngay cả khi nó là thương mại). Bên cạnh đó, tôi hy vọng rằng câu trả lời của tôi là hữu ích ngay cả khi bạn không quan tâm đến công cụ của chúng tôi. –

+0

Đọc một tệp nhật ký khổng lồ ngay từ đầu để tìm một dấu thời gian nhất định không hiệu quả. Nếu đây là trường hợp sử dụng, có các chế độ mã hóa tốt hơn để sử dụng. – erickson

0

Tôi có cùng nhu cầu giống như bạn. Một số người được gọi là 'maybeWeCouldStealAVa' đã viết một cách thực hiện tốt ở: How to append to AES encrypted file, tuy nhiên điều này không bị xóa - bạn sẽ phải đóng và mở lại tệp mỗi khi bạn tuôn ra một tin nhắn, để chắc chắn không mất bất cứ thứ gì.

Vì vậy, tôi đã viết lớp của riêng tôi để làm điều này:

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

Dưới đây là một ví dụ của việc sử dụng nó:

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

Câu hỏi rất cũ và tôi chắc chắn rằng thế giới công nghệ đã làm cho nhiều tiến bộ, nhưng FWIW Bruce Schneier và John Kelsey đã viết một bài báo về cách thực hiện điều này: https://www.schneier.com/paper-auditlogs.html

Ngữ cảnh không chỉ là an ninh mà còn ngăn chặn tham nhũng hoặc thay đổi của e dữ liệu tệp nhật ký xisting nếu hệ thống lưu trữ tệp nhật ký/kiểm tra bị xâm phạm.

Các vấn đề liên quan