配置文件加解密工具

项目要求服务端的配置文件需要加密存放,写个工具处理一下。

一、目标

  • 一个命令实现明文编辑、密文保存
  • 必要时可以通过微调做职责分离,能够让工作人员只生成新的密文配置,而不能读取之前的配置
  • 程序(Java)解密读取方便,密钥易于保存,不容易泄密

二、思路

  • 通过 Shell 脚本串联解密、编辑、加密的过程

  • 加密算法:RSA

  • 工具:OpenSSL

  • 步骤:

    1. 通过 OpenSSL 生成密钥对
    2. 通过私钥解密原密文配置文件(存在且允许的情况下)
    3. 通过编辑器进行明文编辑
    4. 公钥加密保存

    对于步骤 2-4,本想通过管道直接完成,避免明文数据写入磁盘,但没找到合适的办法,暂时先用临时文件

三、实现

1、生成密钥对

# 生成密钥,OpenSSL 采用的默认私钥格式为 PKCS#1
$ openssl genrsa -out prv_pkcs1.key 2048

# 私钥用于后面 Java 程序解密,为了不引入三方库,转换为 PKCS#8 的密钥格式
# -nocrypt 用于指定密钥不加密,这里只是演示,实际使用过程中去掉这个参数,根据提示设置密码
# 假设密码为 123456
$ openssl pkcs8 -topk8 -inform PEM -in prv_pkcs1.key -outform pem -nocrypt -out prv_pkcs8.key

# 生成公钥
$ openssl rsa -in prv_pkcs1.key -pubout -out pub.key

2、串联解密、编辑和加密保存

#!/bin/bash

# 遇到错误退出,使用未定义变量时退出
set -eu

PRIVATE_KEY_FILE=prv_pkcs8.key
PUBLIC_KEY_FILE=pub.key

# 读取要编辑的文件
config_file=${1-""}

if [[ -z "${config_file}" ]]; then
    echo '请指定要编辑的文件名'
    exit -1
fi

# 生成临时文件用于存放明文文件
tmp_file=$(mktemp)

# 如果配置文件存在
if [[ -e "${config_file}" ]]; then
    # 解密到临时文件
    openssl rsautl -decrypt -inkey "${PRIVATE_KEY_FILE}" -in "${config_file}" -out "${tmp_file}"
fi

# 获取临时文件编辑前的 HASH
tmp_file_hash_old=$(md5 -q "${tmp_file}")

# 明文编辑明文临时文件
vim "${tmp_file}"

# 获取临时文件编辑后的 HASH
tmp_file_hash_new=$(md5 -q "${tmp_file}")

# 临时文件编辑前后哈希值不同,说明修改了内容
if [[ "${tmp_file_hash_new}" != "${tmp_file_hash_old}" ]]; then
    # 如果配置文件存在
    if [[ -e "${config_file}" ]]; then
        # 以当前时间作为时间戳
        timestamp_cmd='date +%Y%m%d%H%M%S'
        timestamp=$($timestamp_cmd)

        # 备份原配置文件
        cp "${config_file}" "${config_file}_${timestamp}"
    fi
    # 编辑后加密存储
    openssl rsautl -encrypt -inkey "${PUBLIC_KEY_FILE}" -pubin -in "${tmp_file}" -out "${config_file}"
fi

# 删除临时文件
rm "${tmp_file}"

3、Java 解密

public class Sample {
    // 算法
    private static final String RSA_ALGORITHM = "RSA";
    // 私钥密码
    private static final String RSA_PRIVATE_KEY_PASSWORD = "123456";
  
    /**
     * 读取 RSA 私钥
     * @param privateKeyStr PKCS#8 PEM 格式的私钥字符串,不包括首位标记符
     * @return RSA 私钥对象
     */
    public static RSAPrivateKey loadEncryptedPrivateKey(String privateKeyStr) throws IOException, InvalidKeyException, InvalidKeySpecException, NoSuchAlgorithmException {
        // 将 PEM 格式私钥数据转为 DER 格式
        byte[] buffer = Base64.getMimeDecoder().decode(privateKeyStr);
        // 获取加密的私钥信息
        EncryptedPrivateKeyInfo pkInfo = new EncryptedPrivateKeyInfo(buffer);
        // 密码
        PBEKeySpec keySpec = new PBEKeySpec(RSA_PRIVATE_KEY_PASSWORD.toCharArray());
        // 根据密钥信息获取密钥工厂
        SecretKeyFactory pbeKeyFactory = SecretKeyFactory.getInstance(pkInfo.getAlgName());
        // PKCS#8 格式的密钥
        PKCS8EncodedKeySpec encodedKeySpec = pkInfo.getKeySpec(pbeKeyFactory.generateSecret(keySpec));
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
        // 真正的密钥
        PrivateKey encryptedPrivateKey = keyFactory.generatePrivate(encodedKeySpec);
        return (RSAPrivateKey) encryptedPrivateKey;
    }

    /**
     * 使用私钥解密数据
     * @param privateKey 私钥
     * @param encrypted  密文数据
     * @return 解密后的明文数据
     */
    public static byte[] decrypt(PrivateKey privateKey, byte[] encrypted) throws InvalidKeyException, BadPaddingException, IllegalBlockSizeException, NoSuchPaddingException, NoSuchAlgorithmException {
        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        return cipher.doFinal(encrypted);
    }
}

4、补充说明

网上有很多对于 RSA 的处理代码都不能用,总结下来有如下几方面问题:

  • 没有弄清楚“公钥加密,私钥解密”的规则
  • 没有区分 PKCS#1 和 PKCS#8 的私钥格式,Java 默认只支持 PKCS#8 的私钥格式,否则需要引入三方库处理。OpenSSL 默认使用 PKCS#1 格式的私钥,这里提前做好转换即可。三方库的话,网上有些例子用的是 Bouncy Castle
  • 对于无密码私钥和带密码私钥没有做区分处理

本文中的 Shell 脚本在 macOS 10.15.7 和 CentOS 7.6 中测试通过,Java 代码在 OpenJDK 11 上测试通过。

5、使用说明

# 假设脚本命名为 confeditor.sh
# 增加可执行权限
$ chmod +x confeditor.sh
# 执行脚本
$ ./confeditor.sh 文件名

6、编写 vim 插件

如果习惯使用 vim 作为编辑器,可以通过编写插件,为特定类型文件在打开和保存事件做自定义处理。这样可以实现在打开文件时,提示输入密钥密码,保存时,自动把明文加密成密文。基本实现用户无感知。

代码不多,为了方便可以直接保存到 ~/.vimrc 中。vim 的插件可以放到 runtime 目录中,
如:~/.vim/
也可以根据作用,放到不同目录中,
如:~/.vim/plugin/encrypt_config/encrypt_config.vim

" 加载文件
function LoadFile(filename)
    " 如果文件存在
    if filereadable(a:filename)
        " 解密文件,把解密后的内容写入缓冲区
        silent! execute '%!openssl rsautl -decrypt -inkey prv_pkcs8.key -in %'
    endif
endfunction

" 打开 *.encryptcfg 文件时,执行自定义加载函数
:autocmd BufReadCmd *.encryptcfg :call LoadFile(bufname('%'))

" 保存文件
function SaveFile(outfile)
    " 获取临时文件路径
    let tmpfile = tempname()
    " 当前缓冲区内容写入临时文件
    silent! execute 'write' fnameescape(tmpfile)
    " 缓冲区状态指定为未修改
    let &modified = 0
    " 加密文件,删除临时文件
    silent! execute '!openssl rsautl -encrypt -inkey pub.key -pubin -in' tmpfile '-out' a:outfile '; rm ' tmpfile
endfunction

" 保存 *.encryptcfg 文件时,执行自定义加密函数
:autocmd BufWriteCmd *.encryptcfg :call SaveFile(bufname('%'))

脚本会自动解密、编辑、加密、保存,并备份原配置文件。

Java 读取加密文件的代码,根据注释调用即可。

四、参考资料

(完)

推荐阅读更多精彩内容