本文分别通过 「OpenSSL 命令行」和「php」实现「AES-256-CBC PKCS7 SHA256 SALT PBKDF2」加密与解密,并附有 AutoHotkey 和 Hammerspoon 分别调用 OpenSSL 命令行实现的加密与解密的示例。
关于 OpenSSL 的几点注意事项:
- 加密后的密文格式为「Salted__ + 8位salt + 密文」,并不是「8位salt + 16位iv + 4位iter + 密文」。
- iv 和 key 是根据 salt 算出来的,并未存于密文中;
- 密文中没有 iter,也不通过 salt 计算,而是多方约定的值,解密时的次数值需要与加密时相同;
- 密码原则上需要32位,但由于 sha256 后使用,所以密码长度可以不是32位;
- 密文需要 chunk,即每行 64 字符,openssl 命令行解不了超长的单行密文,会报「error reading input file」;
OpenSSL
# windows 命令行时 echo 后的字符串不要加引号!加引号后解密的字符串也有引号。macOS/Linux 加不加引号都可以,解密后没有引号; # windows 使用 echo 方式只能加密单行文本;macOS/Linux 可以使用 echo -e "Line1\nLine2" 方式使用多行文本; echo "secret" | openssl enc -aes-256-cbc -base64 -md sha256 -pbkdf2 -iter 1000 -salt -pass pass:YOUR_PASSCODE_STRING echo "U2FsdGVkX1+U0osoymcY15yBcO+g9i42tg/0EQqaYfc=" | openssl enc -aes-256-cbc -base64 -md sha256 -pbkdf2 -iter 1000 -salt -pass pass:YOUR_PASSCODE_STRING -d # -p 参数可显示加解密过程中产生的 iv, key # 以文件的方式加密与解密(去掉 -d 为加密,加上为解密): openssl enc -aes-256-cbc -base64 -md sha256 -pbkdf2 -iter 1000 -salt -pass pass:YOUR_PASSCODE_STRING -in "/documents/secret.txt" -d
AutoHotkey
v2:
crypt(message, password, isEncrypt := true) {
tempFile := A_Temp . "\input.txt"
outFile := A_Temp . "\output.txt"
if FileExist(tempFile) {
FileDelete(tempFile)
}
if FileExist(outFile) {
FileDelete(outFile)
}
FileAppend(message, tempFile)
cmd := "cmd /c openssl enc -aes-256-cbc -base64 -md sha256 -pbkdf2 -iter 1000 -salt -pass pass:" . password . ' -in "' . tempFile . '" -out "' . outFile . '"' . (isEncrypt ? " -e" : " -d")
RunWait(cmd, , "Hide")
FileRead(&encrypted, outFile)
FileDelete(tempFile)
FileDelete(outFile)
return encrypted
}
; 示例用法
msg := "your_message_here"
pwd := "your_password_here"
result := crypt(msg, pwd, true)
result := result != "" ? result : msg
MsgBox("加密结果:`n" . result)
decrypted := crypt(result, pwd, false) ; 密文需要 chunked (每行64字符换行)用于解密外部的密文
decrypted := decrypted != "" ? decrypted : result
MsgBox("解密结果:`n" . decrypted)
v1:
crypt(message, password, isEncrypt := true) {
tempFile := A_Temp . "\input.txt"
FileDelete, %tempFile%
FileAppend, %message%, %tempFile%
outFile := A_Temp . "\output.txt"
FileDelete, %outFile%
cmd := "cmd /c openssl enc -aes-256-cbc -base64 -md sha256 -pbkdf2 -iter 1000 -salt -pass pass:" . password . " -in """ . tempFile . """ -out """ . outFile . """" . (isEncrypt ? " -e" : " -d")
RunWait, %cmd%, , Hide
FileRead, encrypted, %outFile%
FileDelete, %tempFile%
FileDelete, %outFile%
return encrypted
}
; 示例用法
msg := "your_message_here"
pwd := "your_password_here"
result := crypt(msg, pwd, true)
result := result != "" ? result : msg
MsgBox, 加密结果:`n%result%
decrypted := crypt(result, pwd, false) ; 密文需要 chunked (每行64字符换行)用于解密外部的密文
decrypted := decrypted != "" ? decrypted : result
MsgBox, 解密结果:`n%decrypted%
注:文章 同步剪贴板内容 AutoHotkey 需要这里的代码。
为什么使用文件而不是 echo “string” ?因为 windows 的 echo 没有 -e 参数,处理不了有换行的多行文本。echo 多行文本时遇到换行就执行命令了,走不到后边的 openssl 。
另,以上代码使用 RunWait 调用 OpenSSL 实现,如果必须用纯 AutoHotkey 实现(不调用 openssl.exe),需要:
- 实现 PBKDF2-HMAC-SHA256(可用 AHK 脚本或 DLL)
- 实现 AES-256-CBC(可用 AHK 脚本或 DLL)
- 实现 PKCS7 填充
- 拼接 Salted__ + salt + 密文
AutoHotkey 原生不支持上边这些算法,通常需要调用外部 DLL(如 OpenSSL DLL、Crypt32.dll、bcrypt.dll),或者用 COM 对象(如 CAPICOM),但 CAPICOM 不支持 PBKDF2-HMAC-SHA256。所以最终决定用 openssl.exe 命令行加密与解密而 AutoHotkey 只负责自动化。
Hammerspoon lua
function crypt(message, password, isEncrypt)
-- 1. 写入临时文件
local tmpfile = os.tmpname()
local f = io.open(tmpfile, "w")
f:write(message)
f:close()
-- 2. 构造 openssl 命令
local mode = isEncrypt == false and "-d" or "-e"
local cmd = string.format(
[[openssl enc -aes-256-cbc -base64 -md sha256 -pbkdf2 -iter 1000 -salt -pass pass:%s -in "%s" %s]],
password, tmpfile, mode
)
-- 3. 执行命令
local output, status = hs.execute(cmd, true)
if nil == status then
local message = hs.styledtext.new(
'Error: crypt failed. [code:' .. rc .. ']',
{
color = hs.drawing.color.asRGB(
{
hex = '#FF0000',
alpha = 1
}
),
font = {
name = 'Monaco',
size = 14
}
}
)
hs.alert.show(message);
end
-- 4. 删除临时文件
os.remove(tmpfile)
-- 5. 返回结果
return status and output or nil
end
-- 示例用法
local message = [[
line1
line2
line3
]]
local password = "your_password_here"
-- 加密
local encrypted = crypt(message, password)
if encrypted then
hs.alert.show("加密结果:\n" .. encrypted)
print("加密结果:\n" .. encrypted)
else
hs.alert.show("加密失败")
print("加密失败")
end
-- 解密
local decrypted = crypt(encrypted, password, false)
if decrypted then
hs.alert.show("解密结果:\n" .. decrypted)
else
hs.alert.show("解密失败")
end
注:使用 openssl.exe 命令加密与解决,Hammerspoon 只负责自动化,理由同上边 AutoHotkey。另,同步剪贴板内容 Hammerspoon需要这里的代码。
PHP
// 加密函数
function encrypt($data, $password, $iterations = 1000) {
$salt = random_bytes(8); // 生成指定长度的随机盐值
$derived = deriveKeyAndIV($password, $salt, $iterations); // 派生密钥和 IV
$key = $derived['key'];
$iv = $derived['iv'];
$encrypted = openssl_encrypt($data, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv); // 加密数据
$encryptedBase64 = base64_encode("Salted__" . $salt . $encrypted); // 构造 OpenSSL 格式的密文并 Base64 编码
$encryptedWithNewLine = chunk_split($encryptedBase64, 64, "\n"); // 必须 chunk 否则命令行的 openssl 不认识
return $encryptedWithNewLine;
}
// 解密函数
function decrypt($encryptedData, $password, $iterations = 1000) {
$data = base64_decode($encryptedData); // 解码 Base64 数据
if (substr($data, 0, 8) !== "Salted__") {
throw new Exception("Invalid encrypted data format.");
}
$salt = substr($data, 8, 8); // 提取盐值(8 字节)
$encrypted = substr($data, 16); // 提取加密数据
$derived = deriveKeyAndIV($password, $salt, $iterations); // 派生密钥和 IV
$key = $derived['key'];
$iv = $derived['iv'];
return openssl_decrypt($encrypted, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv); // 解密数据
}
// 使用 PBKDF2 派生密钥和 IV
function deriveKeyAndIV($password, $salt, $iterations = 1000, $keySize = 32, $ivSize = 16) {
$derived = hash_pbkdf2('sha256', $password, $salt, $iterations, $keySize + $ivSize, true);
$key = substr($derived, 0, $keySize);
$iv = substr($derived, $keySize, $ivSize);
return ['key' => $key, 'iv' => $iv];
}