
项目要求服务端的配置文件需要加密存放,写个工具处理一下。
一、目标
- 一个命令实现明文编辑、密文保存
- 必要时可以通过微调做职责分离,能够让工作人员只生成新的密文配置,而不能读取之前的配置
- 程序(Java)解密读取方便,密钥易于保存,不容易泄密
二、思路
-
通过 Shell 脚本串联解密、编辑、加密的过程
-
加密算法:RSA
-
工具:OpenSSL
-
步骤:
- 通过 OpenSSL 生成密钥对
- 通过私钥解密原密文配置文件(存在且允许的情况下)
- 通过编辑器进行明文编辑
- 公钥加密保存
对于步骤 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 读取加密文件的代码,根据注释调用即可。
四、参考资料
(完)
网友评论