OpenSSL Command Cheatsheets

生成临时密码

1
openssl rand -base64 48

会生成类似如下的字符串 6/4tsW7PR4bYjdY+zzZWEGIsuUz8RIwrNc8FTeQLoeouGO/C3RK/JeqNi8E6nR1l

1
tr -dc 'A-Za-z0-9!?%=' < /dev/urandom | head -c 16; echo

这样生成出来的字符串更适合需要特殊字符的场景, 例如 SYqx!6J3=M8jrUeh

流式加解密

1
2
3
4
5
6
7
8
9
10
# 加密

cat input.raw | openssl enc -aes-256-cbc -e -pbkdf2 -iter 100000 -salt > output.raw
enter AES-256-CBC encryption password:
Verifying - enter AES-256-CBC encryption password:

# 解密

cat output.raw | openssl enc -aes-256-cbc -d -pbkdf2 -iter 100000 -salt > decrypted.raw
enter AES-256-CBC decryption password:

分块传输(对网盘上传比较友好)

1
2
3
4
5
6
7
8
9
10
# 加密后分块

cat input.raw | openssl enc -aes-256-cbc -e -pbkdf2 -iter 100000 -salt | split -b 1G -d -a 3 - chunk_
enter AES-256-CBC encryption password:
Verifying - enter AES-256-CBC encryption password:

# 合并后解密

cat chunk_* | openssl enc -aes-256-cbc -d -pbkdf2 -iter 100000 -salt > decrypted.raw
enter AES-256-CBC decryption password:

CBC加密后分块可以配合tar命令使用, tar可以保证缺块的情况下仍然能够最大限度的提取出可用的文件.

split 命令, -d 为使用数字结尾(而不是aaa,aab这样), -a 2 表示补全到两位, 需要预估总输入大小确保不会出现 01, 02, ..., 99, 100 这样的情况. 否则 cat 的时候可能会错乱.

基于GPG的对称流式加解密

gpg能做非对称加解密已经是老生常谈了, 但是对称加密之前用到的不多

1
2
3
4
5
6
7
8
9
10
11
# 加密后分块
passphrase=$(openssl rand -base64 128 | tr -d '\n')
echo "Passphrase: $passphrase"
exec {passfd}<<<"$passphrase"
tar -cvf - input_dir | pv | gpg --symmetric --s2k-cipher-algo AES256 --s2k-digest-algo SHA512 --s2k-count 65536 --no-compress --batch --pinentry-mode loopback --passphrase-fd $passfd | split -b 1G -d -a 3 - chunk_
unset passphrase
exec $passfd>&-

# 合并后解密
exec {passfd}<<<"$passphrase"
cat chunk_* | gpg --decrypt --batch --pinentry-mode loopback --passphrase-fd $passfd | pv | tar -xvf -

GnuPG使用的是 aes-256-cfb 模式进行的加密(不能调整模式). exec {passfd}<<<"$passphrase" 是从字符串创建一个fd, 需要注意的是bash/zsh实际上会创建一个 /tmp/tmp.XXXXXX 的文件然后立刻删除. exec $passfd>&- 会关闭这个fd. 可以使用这个命令查看当前shell打开的fd: ls -l /proc/$$/fd

需要注意要加 --no-compress 选项. gpg默认是开启压缩的, 但是内部的压缩组件似乎有问题, 会导致莫名其妙的报错比如: gpg: Fatal: zlib inflate problem: invalid distance code. 在网上翻了一下没看到有什么特别好的解法, 大部分都是说数据本身出现了错乱才会报错. 在流式加密的场景里, 可以换成其他的压缩方式, 例如 tar ... | pv | zstd -19 -T0 | gpg ...

如果磁盘空间不能同时容纳 tar chunks 和提取出来的东西, 但是足够容纳部分 tar chunks 和全部提取出来的东西, 可以考虑FIFO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建命名管道(named pipe)
mkfifo $(mktemp -u | tee /dev/tty)

# 会返回类似 /tmp/tmp.iblsCdwJJo 的输出

file /tmp/tmp.iblsCdwJJo
# /tmp/tmp.iblsCdwJJo: fifo (named pipe)

# 先开启读端
cat /tmp/tmp.iblsCdwJJo | gpg --decrypt --batch --pinentry-mode loopback --passphrase-fd $passfd | pv | tar -xvf -

# 再开启写端
exec {chunkfd}>/tmp/tmp.iblsCdwJJo
cat chunk_000 >&3
cat chunk_001 >&3
cat chunk_002 >&3
...
# 关闭写端口
exec $chunkfd>&-

注意不能直接 cat chunk_000 > pipe 因为cat结束之后会关闭STDOUT, 进而导致读端收到EOF.

手动cat chunk这个过程可以使用一个简单的python脚本来半自动化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import os
import time

print("Opening pipe...")
pout = open("/tmp/tmp.iblsCdwJJo", "wb")

for i in range(0, 115): # 这里需要提前知道一共有多少个chunk
while True:
fname = "chunk_{:03d}".format(i)
if os.path.exists(fname.format(i)):
print("chunk {} exists".format(i))
time.sleep(3)
print("reading chunk {}...".format(i))
with open(fname, "rb") as f:
content = f.read()
print("{} bytes read".format(len(content)))
pout.write(content)
print("chunk {} written".format(i))
break
else:
print("chunk {} not exists, wait...".format(fname))
time.sleep(5)

# 记得关闭FIFO
pout.close()

参考资料

split(1) — Linux manual page

GnuPG - ArchWiki

GPG(1) manpage