创建加密的日志文件

时间:2009-03-10 11:31:13

标签: encryption logging

我正在创建一个客户端应用程序,需要创建用户活动的日志,但由于各种原因,这个日志必须是人类可读的。

目前,对于我的开发,我正在创建一个纯文本日志,如下所示:

12/03/2009 08:34:21 - >用户'Bob'已登录 12/03/2009 08:34:28 - >导航到配置页面 12/03/2009 08:34:32 - >选项x更改为y

当我部署我的应用程序时,日志不能是纯文本,因此所有文本都必须加密。这看起来并不简单,因为我需要在添加每个条目时动态更新日志文件。我正在考虑的方法是创建一个二进制文件,单独加密每个日志条目,然后将它附加到二进制文件,并在每个条目之间进行适当的划分。

有没有人知道这个问题的常见方法,我相信必须有更好的解决方案!

10 个答案:

答案 0 :(得分:9)

不要单独加密单个日志条目,并按照其他海报的建议将其写入文件,因为攻击者可以轻松识别日志文件中的模式。请参阅block cipher modes Wikipedia entry以了解有关此问题的更多信息。

Original Encrypted using ECB mode Encrypted using other modes

相反,请确保日志条目的加密取决于以前的日志条目。虽然这有一些缺点(你不能解密单个日志条目,因为你总是需要解密整个文件),但它使加密更加强大。对于我们自己的日志库SmartInspect,我们使用AES加密和CBC模式来避免模式问题。如果商业解决方案合适,请随意尝试SmartInspect

答案 1 :(得分:6)

FWIW,有一次我需要一个加密的记录器,我使用对称密钥(出于性能原因)来加密实际的日志条目。

然后,对称的“日志文件密钥”在公钥下加密并存储在日志文件的开头,单独的日志阅读器使用私钥解密“日志文件密钥”并读取条目。

整个过程是使用log4j和XML日志文件格式实现的(为了方便读者解析),每次日志文件都滚动到新的“日志文件密钥”时生成。

答案 2 :(得分:4)

这不是我的事情,我会很容易地承认,但是你不能单独加密每个条目然后将它附加到日志文件中吗?如果您不加密时间戳,您可以轻松找到您要查找的条目,并在需要时解密。

我的观点主要是将单个加密条目附加到文件中不一定需要将二进制条目附加到二进制文件中。使用(例如)gpg进行加密将产生可以附加到ascii文件的ascii乱码。这会解决你的问题吗?

答案 3 :(得分:3)

假设您正在使用某种日志框架,例如log4j等,那么您应该能够创建Appender(或类似)的自定义实现来加密每个条目,如@wzzrd建议。

答案 4 :(得分:1)

我想知道你写的是什么类型的应用程序。病毒还是特洛伊木马?无论如何...

单独加密每个条目,将其转换为某个字符串(例如Base64),然后将该字符串记录为“消息”。

这使您可以保留部分文件的可读性,并仅加密重要部分。

请注意,此硬币还有另一面:如果您创建一个完全加密的文件并询问用户,那么她无法知道您将从该文件中学到什么。因此,您应该尽可能少地加密(密码,IP地址,客户数据),以便法律部门能够验证剩下的数据。

更好的方法是使用日志文件的混淆器。这只是用“XXX”替换某些模式。你仍然可以看到发生了什么,当你需要一个特定的数据时,你可以要求它。

[编辑]这个故事乍一看你的想法更多。这实际上意味着用户无法看到文件中的内容。 “用户”不一定包括“破解者”。一个破解者将专注于加密文件(因为它们可能更重要)。这就是旧话的原因:一旦有人进入机器,就没有办法阻止他对它做任何事情。或者用另一种方式说:只是因为你不知道怎么不意味着别人也没有。如果你认为你没有什么可隐瞒的,你就没有想过自己。

此外,还有责任问题。比如说,在获得日志副本后,Internet上的某些数据会泄漏。由于用户不知道日志文件中的内容,您如何在法庭上证明您不是泄密?老板可以要求日志文件监视他们的棋子,要求对其进行编码,以便农民不会注意到并抱怨它(或起诉,人渣!)。

或者从一个完全不同的角度来看待它:如果没有日志文件,没有人可以滥用它。如何在紧急情况下启用调试?我已经配置了log4j以将最后200条日志消息保存在缓冲区中。如果记录了ERROR,我会将200条消息转储到日志中。理由:我真的不在乎白天会发生什么。我只关心错误。使用JMX,可以很容易地将调试级别设置为ERROR,并在需要更多详细信息时在运行时远程降低它。

答案 5 :(得分:1)

单独加密每个日志条目会大大降低密文的安全性,尤其是因为您使用的是非常可预测的明文。

以下是您可以做的事情:

  1. 使用对称加密(最好是AES)
  2. 选择随机主密钥
  3. 选择安全窗口(5分钟,10分钟等)
  4. 然后,在每个窗口的开头(每5分钟,每10分钟等)选择一个随机的临时密钥。

    使用临时密钥单独加密每个日志项,并附加到临时日志文件。

    当窗口关闭(预定时间结束)时,使用临时密钥解密每个元素,使用主密钥解密主日志文件,合并文件,并使用主密钥加密。

    然后,选择一个新的临时密钥并继续。

    此外,每次旋转主日志文件(每天,每周等)时都要更改主密钥。

    这应该提供足够的安全性。

答案 6 :(得分:1)

我不清楚你对安全性或工具的关注。

一个简单的工具是与流加密器连接。流加密器维护自己的状态,可以动态加密。

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

答案 7 :(得分:1)

非常古老的问题,我确信科技界取得了很大进展,但是FWIW Bruce Schneier和John Kelsey写了一篇关于如何做到这一点的论文:https://www.schneier.com/paper-auditlogs.html

如果承载日志/审核文件的系统受到威胁,上下文不仅仅是安全性,还可以防止现有日志文件数据的损坏或更改。

答案 8 :(得分:0)

对于.Net,请参阅Microsoft应用程序块以获取日志和加密功能: http://msdn.microsoft.com/en-us/library/dd203099.aspx

我会将加密的日志条目附加到平面文本文件中,使用每个条目之间的适当分界来使解密工作。

答案 9 :(得分:0)

我和你有完全相同的需求。有些人打电话来说,也许是我们可以解决的问题。在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();
    }
}