利用 AES 对 log4j 日志文件加密

2023-10-29

总览

本文简要介绍了 AES 算法加密的方式,以及如何利用 AES 对 log4j 输出的日志进行加密。

背景

在互联网时代下,JAVA 大多用来做后端开发,由于后端的程序大多都部署在自己的服务器上,客户接触不到程序的日志文件,因此,多数情况下,日志是没有加密的必要,log4j 本身也没有提供加密的方法。但有些客户端软件仍然是用 java 编写,客户端安装在客户的 PC上,我们想要了解软件的运行状态以及出错原因,就必须记下日志,这些日志可能包含有一些敏感的信息,我们不希望用户能直接看到,因此对日志加密是很有必要的。

AES 加密

既然要进行加密,那么首先得选择一个可靠的加密算法,网上搜索了下,大概有这三种:DES、AES、RSA。,其中 RSA 的解密是基于大数的因式分解,虽然安全性极高,但解密效率比其它两种低得多,不太适合。 DES是美国联邦政府采用过的一种加密方式,由于它的密钥只有56位,因此算法的理论安全强度是 2 56 2^{56} 256,但随着计算机的飞速发展,每秒能处理的密钥数越来越多,DES将不能够提供足够的安全性,因此美国国家标准技术研究所开始征集了 AES 用以取代 DES,研究所对 AES的要求是速度快(比三重 DES 速度快)、安全性高(至少与三重 DES一样安全),最终 Rijndael 算法脱颖而出。因此对日志的加密解密选用 AES 比较适合。

JAVA 中的 AES

Java 早已提供标准的 AES 算法供大家使用,这里我提供一个简单的 AES 加密工具类(需要引入 apache 的 codec)

import java.nio.charset.Charset;
import java.security.GeneralSecurityException;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

/**
 * @Title: AESUtil.java
 * @Description: AES 加密解密工具类
 * @author: weekdragon
 * @date: 2018年7月18日 下午4:58:22
 * @version V1.0
 */
public class AESUtil {

    private static final String KEY_ALGORITHM = "ThisIsASecretKey";
    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";// 默认的加密算法

    private static Cipher cipher = null;
    
    static {
        try {
            cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
        } catch (Exception e) {}
    }
    
    /**
     * AES 加密操作
     *
     * @param content
     *            待加密内容
     * @param key
     *            加密密码
     * @return 返回Base64转码后的加密数据
     */
    public static String encrypt(String content, String key) throws GeneralSecurityException {
        if(cipher == null)return content;
        byte[] raw = key.getBytes(Charset.forName("UTF-8"));
        if (raw.length != 16) {
            throw new IllegalArgumentException("Invalid key size.");
        }
        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec, new IvParameterSpec(new byte[16]));
        byte[] doFinal = cipher.doFinal(content.getBytes(Charset.forName("UTF-8")));
        return Base64.encodeBase64String(doFinal);
    }

    /**
     * AES 解密操作
     *
     * @param content
     * @param key
     * @return
     */
    public static String decrypt(String content, String key) throws GeneralSecurityException {
        byte[] encrypted = Base64.decodeBase64(content);
        byte[] raw = key.getBytes(Charset.forName("UTF-8"));
        if (raw.length != 16) {
            throw new IllegalArgumentException("Invalid key size.");
        }
        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, skeySpec, new IvParameterSpec(new byte[16]));
        byte[] original = cipher.doFinal(encrypted);
        return new String(original, Charset.forName("UTF-8"));
    }

}

对于 AES 加密,可以选择不同的密钥长度(16的整数倍),还可以选择不同的补齐方法(加密数据不足16位时的补位策略),这里只做了个最简单的实现。

对日志加密

log4j 本身没有提供日志加密的方法,但是用户可以自定义日志的 Appender,这个 Appender 就是负责日志输出的东西,目前我研究的加密方式有两种:
第一种是对整个输出流加密,自定义一个加密的输出流

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

然后自定义一个 Appender ,重写 setFile 方法

public class AESRollingFileAppender extends RollingFileAppender {
	public final static byte[] keyBytes = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
	public final static SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
	public final static String cipherKey[] = new String[] { "AES/CBC/PKCS5Padding", "AES/CFB8/NoPadding" };
	public final static String Encoding = "UTF-8";
	
	private Writer fw;
	
	@Override
	public synchronized void setFile(String fileName, boolean append,
			boolean bufferedIO, int bufferSize) throws IOException {
		LogLog.debug("setFile called: " + fileName + ", " + append);
		
		// It does not make sense to have immediate flush and bufferedIO.
		if (bufferedIO) {
			setImmediateFlush(false);
		}

		reset();
		FlushableCipherOutputStream cstream = null;
		try {
			cstream = new FlushableCipherOutputStream(fileName, key, true, false);
		} catch (Exception ex) {
			LogLog.error("setFile error", ex);
			ex.printStackTrace();
		}

		setEncoding(Encoding);
		fw = createWriter(cstream);
		if (bufferedIO) {
			fw = new BufferedWriter(fw, bufferSize);
		}
		this.setQWForFiles(fw);
		this.fileName = fileName;
		this.fileAppend = append;
		this.bufferedIO = bufferedIO;
		this.bufferSize = bufferSize;
		writeHeader();
		LogLog.debug("setFile ended");
		
		if (append) {
			File f = new File(fileName);
			((CountingQuietWriter) qw).setCount(f.length());
		}
	}
}

这种方法是在 https://stackoverflow.com/questions/10283637/how-to-append-to-aes-encrypted-file 上找到的,实际测试中,发现客户端在某些极端情况下,日志输出的可能不完整,导致日志文件格式损坏,从损坏点开始起,日志不能解密,这是目前一个缺陷。但对于稳定运行的程序,并且日志并发量比较高,用这种方式加密可以节约时间。

第二种是对单条日志加密,由于会对访问到私有变量,因此直接拷贝 RollingFileAppender 类,重写 subAppend 方法即可

 /**
     * This method differentiates RollingFileAppender from its super class.
     * 
     * @since 0.9.0
     */
    protected void subAppend(LoggingEvent event) {
        this.qw.write(encrypt(this.layout.format(event)));
        if (layout.ignoresThrowable()) {
            String[] s = event.getThrowableStrRep();
            if (s != null) {
                int len = s.length;
                for (int i = 0; i < len; i++) {
                    this.qw.write(encrypt(s[i]));
                    this.qw.write(encrypt(Layout.LINE_SEP));
                }
            }
        }

        if (shouldFlush(event)) {
            this.qw.flush();
        }
        if (fileName != null && qw != null) {
            long size = ((CountingQuietWriter) qw).getCount();
            if (size >= maxFileSize && size >= nextRollover) {
                rollOver();
            }
        }
    }

    private String encrypt(String content) {
        try {
            return AESUtil.encrypt(content, AES_KEY) + "\n";
        } catch (Exception e) {
            e.printStackTrace();
            return content;
        }
    }

在 log4j 配置文件里替换掉默认的 Appender 即可

log4j.appender.AppenderName=package.AESRollingFileAppender

这种方式,日志是按行加密,每一条日志加密一行,相比前一种,加密时间有所增加,大概每 10w 条日志增加几秒钟的样子,对于客户端程序来说,日志记录得不会非常密集,并发量不会很高,完全可以满足要求,而且可以应对客户端在强退、断电等异常情况,即使日志记录不完整,也只会损坏单条,而不会影响全部。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

利用 AES 对 log4j 日志文件加密 的相关文章

随机推荐

  • Linux学习-17-rpm查询软件包命令(-q、-qa、-i、-p、-l、-f、-R)

    7 4 Linux rpm查询软件包命令 q qa i p l f R rpm 命令还可用来对 RPM 软件包做查询操作 具体包括 查询软件包是否已安装 查询系统中所有已安装的软件包 查看软件包的详细信息 查询软件包的文件列表 查询某系统文
  • JAVA编程基础:第九章 swing类

    如何创建更好看的界面 1 导入swing包 里面有更好看的组件 2 创建各个组件的实例 然后添加到面板 import java awt import javax swing swing包中的组件是从awt包扩展而来的 这些组件更好看 知识点
  • Linux下使用apt安装mysql

    Ubuntu上安装MySQL非常简单只需要几条命令就可以完成 1 sudo apt get install mysql server 2 apt get isntall mysql client 3 sudo apt get install
  • 如何快速制作数据词典

    其实制作数据词典是一件非常麻烦费力的事情 如果有一条SQL能够帮你全都查询出来 那无疑会省力许多 今天呢我就给大家带来一条这样的SQL 源自大佬小梦想的亲笔之作 USE information schema SELECT 字段 字段说明 P
  • Wireshark 使用技巧

    一 数据包过滤 过滤需要的IP地址 ip addr 在数据包过滤的基础上过滤协议ip addr xxx xxx xxx xxx and tcp 过滤端口ip addr xxx xxx xxx xxx and http and tcp por
  • PHPExcel 学习笔记

    首先到phpexcel官网上http phpexcel codeplex com下载最新的phpexcel类 下周解压缩一个classes文件夹 里面包含了PHPExcel php和PHPExcel的文件夹 这个类文件和文件夹是我们需要的
  • 自动控制原理《传递函数》

    目录 文章目录 目录 摘要 1 传递函数的定义 2 传递函数的标准形式 3 传递函数的性质 4 传递函数的局限性 5 总结 摘要 本节主要学习自动控制原理中的传递函数相关知识 大部分内容参考西北工业大学课件 1 传递函数的定义 需要注意的是
  • JEESITE快速开发平台(五)用户-角色-部门-区域-菜单-权限表关系

    一 表关系 一共有8张表分别用来实现用户 角色 部门 区域 菜单 权限管理 详细如下 二 SQL语句 java view plain copy 一共八张表 select from sys user 用户表 select from sys m
  • Vue 打包优化之 生产环境删除 console 日志

    使用 vue cli 3 0 vue cli 脚手架构建的项目 一般在本地开发过程中 会有不少 console 调试信息 如果不处理这些日志信息 默认情况下 即使是构建生产环境的包 这些 console 打印也不会被移除 这显然是不够严谨的
  • 蓝桥杯-时间模拟

    蓝桥杯 时间模拟 引言 时间模拟 是蓝桥杯最常见的题型 我愿意把他称作小白和入门画的界限 接下来就让我来带大家入门把 一 模板 include
  • 操作系统学习提升篇——进程同步

    进程的线程共享进程资源 进程共享计算机资源 因此进程和线程一样都需要信息同步 共享内存 在某种程度上 多进程是共同使用物理内存的 由于操作系统的进程管理 进程间的内存空间是独立的 进程默认是不能访问进程空间之外的内存空间的 一个进程不能访问
  • 在java项目中如何使用Lucene搜索引擎(入门篇)

    什么是lucene 就是一个简单的工具包 java语言特有的 做全文检索用的 为什么不用数据库的模糊查询 两者都什么区别 1 模糊查询只适用于结构化数据 如数据库中存储的数据 非结构化数据就是文档 图片 音频等等 2 模糊查询速度慢 3 不
  • tcp/udp socket 网络通信中超时时间的设置

    1 connect函数的超时时间设置只对TCP有效 UDP由于是无连接的connect都会返回success 有两种方法 第一种方法 默认的socket是阻塞模式 我们只需要设置其为非阻塞模式 然后调用select去查询其状态 代码如下 s
  • 【实时更新】LaTeX公式编辑(希腊字母/分数/上下标/加粗/关系符/点乘/无穷大)

    一 基本用法 1 行内公式加 2 行间公式加 二 常用代码 1 常用小写希腊字母 希腊字母 代码 alpha alpha
  • vscode 终端集成bash

    windows 版本的 vs code 终端默认是没有集成bash的 虽然也能在vscode 终端可以提交git 但是没有高亮 没有提示 很不方便 这时候就需要我们将bash集成到vs code的终端 就可以愉快的使用git的分支高亮 提示
  • 为什么需要脉冲成形

    数字信号在传输的过程中难免会受到干扰 从而出现了波形失真 为了解决电报传输问题 提出了数字波形在无噪声线性信道传输时的无失真条件 称为奈奎斯特准则 其中奈奎斯特第一准则便是抽样点无失真准则 是关于接收机不产生码间串扰的问题 对于基带传输系统
  • win7官方原版iso镜像_教你从微软官网下载 Windows 10 原版 ISO 镜像

    到微软官网只能下载到Windows升级助手 或者Media Creation Tool 但这个工具制作U盘启动真是有点慢 不如直接下载Windows 10 的ISO镜像 再制作U盘工具 而且可以收藏 从第三方的渠道的确可以下载到Win10的
  • Subquery and Wrapping query

    Subquery Progressive query Into Wrapping query 1 Using fluent syntax string names Tom Dick Harry Mary Jay IEnumerable
  • odoo15 owl 组件实验

    视图有两种形式 一种是利用odoo MVC框架的QWeb模板引擎进行渲染 另一种是独立于odoo的模板引擎 利用前端框架搭建视图与用户交互 并调用odoo的控制器与odoo交互 odoo15提供了一套全新的前端框架owl 最主要的是owl的
  • 利用 AES 对 log4j 日志文件加密

    总览 本文简要介绍了 AES 算法加密的方式 以及如何利用 AES 对 log4j 输出的日志进行加密 背景 在互联网时代下 JAVA 大多用来做后端开发 由于后端的程序大多都部署在自己的服务器上 客户接触不到程序的日志文件 因此 多数情况