Base64 编解码终结符问题
1. 问题场景
工作时和甲方对接时,甲方测试对一个接口造了一个错误数据进行测试。其中一个参数是对原文进行采用 OPENSSL_PKCS1_PADDING 填充的 RSA 加密后,再进行 Base64 编码输出的数据进行传输,例如:若原文为 password
则加密后密文为:
1
| D9aRbiP4P9GIOvZOF1dhKqrAvihxVOfTwuCAKMiRJxRipLkUyRB2ES5B1mis6v2Cc7RYoG7q/9H3fnQopaHfhUJPLxAnX0GCksbXUhNfi+o6YMHoaXGnro+oVBKEMyHhh0eEgMVvBI/1dhb/UxHuUmyRfhkCcSf8R3jVDCmdXHXq203tm/Rgug7mLAGJvVB/D/dGcsHbu+HH956iPckRuD+p0e6KXGItV9BRQLRmuOPJmcMaDRgV4iN3lzCDFjbBJ2uysmjEroZNQMc00MwQbWWisB1i/blHJ1ruSe5opUtRMVfrRVekNPvRVxgjydGG4rxi7ZEFo+aPJvvZ+tmnyA==
|
所造错误数据即在加密密文后多加了一个字符,但是依然可以解密成功。测试代码如下:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher; import java.nio.charset.StandardCharsets; import java.security.Key; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.spec.PKCS8EncodedKeySpec;
public class RSA { private final static String CIPHER_ALGORITHM = "RSA/ECB/PKCS1Padding"; private final static String PRIVATE_KEY = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDxxrtVFMNqwVrbjfYHzPbTt8Osd1cFiY1zAq/zcEdUmRoS+ujK8Qpw22mTWsAM4DqpGYr2qPKMHFmlZnj/3jG2ZLDKTMB1p2P06cgGWkl6M7TJp62aA9bNj1v/hK7wqEK1xXVkqROc+4Har4l5FZW/r6hAj2ireOdyPjx2++m11LGAJ5Cwprv6vGijNWLd4Qnd0dPFBd5mAGcFcnF9MvdcyME4xUHrOwmpLmALftLVWss/UZaUZxGiEdaBWdwAVaYb4yDnHTCvLbgVLRXQwChaf66YMASt975HlX3I3GijedQJmTfVhY94Z3qc5sqDakXvZ3YI6ZkMdZ4HXO0bS+C7AgMBAAECggEAFLbxL+3yfEAKt8rm7G4sK6GP+0PSSeAqJVNyncnd4qqnaD7lGRYjzd2OoxhgYfoILJrKpC1/cm+vYpNwBIQWAEmKOBrxVmM8Fiy9fYXYy8aIU8qw/gQcMEp7GF5W2rmf1ZEQaMpvqsCFtKXbgmtOBDlZkgZ3clGOiuQ4K/2TXYer9Sqk5LV9ctF6Ktpkq+a2PawRhmBlHpy34fvE7B9uQ39foRaS7m+YmYmjhZVQX7jICG82TSA/O9o2z4bwj/yIYYLBD15nllavWqNDj8wWOoAFPwYa2Lc93vZpJvpKJK5wFcsaz1fsrmMcrXntrt/kivfosmmt1qulwi+GCnxpuQKBgQD+gohiIhAJNFC+w5J+BQYR6qLlJqpaAGr/QsMRr91kmGgYWuQ5nNfapxVZgP2FfPZsS94liswGIr+xphqvrElo8p4MoAyE5deQX7qNzCidBNtxcii2Z7cFBai7u6Pzt/0qt34K1izzvFYg0c1weJ7hOcr3V9yhhMhAqmUI1CdpjQKBgQDzMR0PIL1acvHXirfXqFPrB6Pd9Qgm7rtzw8uN8j9xiJ9vK+xt+e08LaCdKtw3AOTFBaGMGpYmcBKallKjDb2Hs9F6H/ixCpEHUtlVImEyfobAlrdje6MhYXY2kUfoLZ1clB1tiReXwQBL3GV7vmbAoSpY7TcVJiIm+rRQVLVNZwKBgQCu0EwLU6g+GkAH999sTdkQf2DqEvfZoAXeVSYVxP1FtmVxrSSr6e5d0nwYoUAB64Z7dlUc5kwjPsT6qcQUvDskKdmjhF90/UZmdUp3US7oQ0jTkH0kZPLSMUPnxwfjRJJRP/4ERX5U4B0sp877nO5Md1zRLfluu/ysZh3FxatYlQKBgAPIALaqgKc2YFJEouUkheGCpeael7jbP2jmY3Tajmf6gtgcq7luCGVGJFgtQW1Ng0EY/FEMXMdOOMvUiIZmgUrp3djzRE+kZWriu+RZ+37ofrnh3goa8wdi146zpZWTl/3Hg8mfNxGx+4oybBWHeVuHZfwp/BBFHoTSoxkYqBUDAoGAWRE9Dv0mDHBhFLxQ2UTPJfE0w9bcMD5uN8nUIzzxlv+r/P2X20ImsEPNE97YGjUqZcps7Bpuo3WtMyKC5VPiNzH4U8E6b7hj3YGYP4b7SKYmh2MP4du+efyItwfOR7sptkMj6+vyCDRsZVBIWGfdsA4vrtLVb0kQJaD/v/WcjlI=";
public static PrivateKey generatePrivateKey(String privateKeyStr) { try { byte[] privateKeyBytes = Base64.decodeBase64(privateKeyStr); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); return keyFactory.generatePrivate(keySpec); } catch (Throwable t) { throw new RuntimeException("generate private key error", t); } }
public static String decryptFromBase64(String ciphertext, Key pk) { String result; try { byte[] data = Base64.decodeBase64(ciphertext); Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, pk); byte[] decryptData = cipher.doFinal(data); result = new String(decryptData, StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); result = ciphertext; } return result; }
public static void main(String[] args) { String res = "D9aRbiP4P9GIOvZOF1dhKqrAvihxVOfTwuCAKMiRJxRipLkUyRB2ES5B1mis6v2Cc7RYoG7q/9H3fnQopaHfhUJPLxAnX0GCksbXUhNfi+o6YMHoaXGnro+oVBKEMyHhh0eEgMVvBI/1dhb/UxHuUmyRfhkCcSf8R3jVDCmdXHXq203tm/Rgug7mLAGJvVB/D/dGcsHbu+HH956iPckRuD+p0e6KXGItV9BRQLRmuOPJmcMaDRgV4iN3lzCDFjbBJ2uysmjEroZNQMc00MwQbWWisB1i/blHJ1ruSe5opUtRMVfrRVekNPvRVxgjydGG4rxi7ZEFo+aPJvvZ+tmnyA=="; String resAdd1 = "D9aRbiP4P9GIOvZOF1dhKqrAvihxVOfTwuCAKMiRJxRipLkUyRB2ES5B1mis6v2Cc7RYoG7q/9H3fnQopaHfhUJPLxAnX0GCksbXUhNfi+o6YMHoaXGnro+oVBKEMyHhh0eEgMVvBI/1dhb/UxHuUmyRfhkCcSf8R3jVDCmdXHXq203tm/Rgug7mLAGJvVB/D/dGcsHbu+HH956iPckRuD+p0e6KXGItV9BRQLRmuOPJmcMaDRgV4iN3lzCDFjbBJ2uysmjEroZNQMc00MwQbWWisB1i/blHJ1ruSe5opUtRMVfrRVekNPvRVxgjydGG4rxi7ZEFo+aPJvvZ+tmnyA==1"; String resAdd3 = "D9aRbiP4P9GIOvZOF1dhKqrAvihxVOfTwuCAKMiRJxRipLkUyRB2ES5B1mis6v2Cc7RYoG7q/9H3fnQopaHfhUJPLxAnX0GCksbXUhNfi+o6YMHoaXGnro+oVBKEMyHhh0eEgMVvBI/1dhb/UxHuUmyRfhkCcSf8R3jVDCmdXHXq203tm/Rgug7mLAGJvVB/D/dGcsHbu+HH956iPckRuD+p0e6KXGItV9BRQLRmuOPJmcMaDRgV4iN3lzCDFjbBJ2uysmjEroZNQMc00MwQbWWisB1i/blHJ1ruSe5opUtRMVfrRVekNPvRVxgjydGG4rxi7ZEFo+aPJvvZ+tmnyA==123"; String resAdd10 = "D9aRbiP4P9GIOvZOF1dhKqrAvihxVOfTwuCAKMiRJxRipLkUyRB2ES5B1mis6v2Cc7RYoG7q/9H3fnQopaHfhUJPLxAnX0GCksbXUhNfi+o6YMHoaXGnro+oVBKEMyHhh0eEgMVvBI/1dhb/UxHuUmyRfhkCcSf8R3jVDCmdXHXq203tm/Rgug7mLAGJvVB/D/dGcsHbu+HH956iPckRuD+p0e6KXGItV9BRQLRmuOPJmcMaDRgV4iN3lzCDFjbBJ2uysmjEroZNQMc00MwQbWWisB1i/blHJ1ruSe5opUtRMVfrRVekNPvRVxgjydGG4rxi7ZEFo+aPJvvZ+tmnyA==0123456789"; String resAddF1 = "D9aRbiP4P9GIOvZOF1dhKqrAvihxVOfTwuCAKMiRJxRipLkUyRB2ES5B1mis6v2Cc7RYoG7q/9H3fnQopaHfhUJPLxAnX0GCksbXUhNfi+o6YMHoaXGnro+oVBKEMyHhh0eEgMVvBI/1dhb/UxHuUmyRfhkCcSf8R3jVDCmdXHXq203tm/Rgug7mLAGJvVB/D/dGcsHbu+HH956iPckRuD+p0e6KXGItV9BRQLRmuOPJmcMaDRgV4iN3lzCDFjbBJ2uysmjEroZNQMc00MwQbWWisB1i/blHJ1ruSe5opUtRMVfrRVekNPvRVxgjydGG4rxi7ZEFo+aPJvvZ+tmnyA1==";
PrivateKey privateKey = generatePrivateKey(PRIVATE_KEY);
System.out.println(res); System.out.println(decryptFromBase64(res, privateKey)); System.out.println(resAdd1); System.out.println(decryptFromBase64(resAdd1, privateKey)); System.out.println(resAdd3); System.out.println(decryptFromBase64(resAdd3, privateKey)); System.out.println(resAdd10); System.out.println(decryptFromBase64(resAdd10, privateKey)); System.out.println(resAddF1) System.out.println(decryptFromBase64(resAddF1, privateKey)); } }
|
测试结果如下:
问题出现了,似乎在密文最后的 ==
后面不管加多少位都不影响解密过程,但是在 ==
前加就会报错。
对流程进行分析,RSA 加解密的流程似乎并没有在 ==
后加了数据而受到影响,向公司的前辈请教,似乎是最后进行 Base64 编码导致的问题。
2. Base64 的原理
此部分大量参考了阮一峰前辈的博客:Base64笔记 - 阮一峰的网络日志 (ruanyifeng.com)
2.1 Base64 是什么
Base64(基底 64 )是一种基于 64 个可打印字符来表示二进制数据的表示方法。由于 log264=6,所以每 6 个比特为一个单元,对应某个可打印字符。3 个字节相当于 24 个比特,对应于 4 个Base64单元,即 3 个字节可由 4 个可打印字符来表示。^1
所谓 Base64,就是说选出64个字符:小写字母 a-z、大写字母 A-Z、数字 0-9、符号 “+”、“/”(再加上作为垫字的 “=”,实际上是65个字符)作为一个基本字符集。然后,其他所有符号都转换成这个字符集中的字符。^2
2.2 Base64 有什么用
- 所有的二进制文件,都可以因此转化为可打印的文本编码,使用文本软件进行编辑;
- 能够对文本进行简单的加密^2
2.3 Base64 怎么进行编解码
Base64 的编解码可以分为如下四步:
- 将每 3 个字节作为一组,1 个字节有 8 个二进制位,共 24 个二进制位
- 将 24 个二进制位分为 4 组,每组有 6 个二进制位
- 在每组前面加 “00”,扩展为 32 个二进制位,共 4 个字节
- 根据下表,得到扩展后的每个字节的对应符号,则为 Base64 的编码值
- 解码过程反之
字节 |
符号 |
字节 |
符号 |
字节 |
符号 |
字节 |
符号 |
0 |
A |
16 |
Q |
32 |
g |
48 |
w |
1 |
B |
17 |
R |
33 |
h |
49 |
x |
2 |
C |
18 |
S |
34 |
i |
50 |
y |
3 |
D |
19 |
T |
35 |
j |
51 |
z |
4 |
E |
20 |
U |
36 |
k |
52 |
0 |
5 |
F |
21 |
V |
37 |
l |
53 |
1 |
6 |
G |
22 |
W |
38 |
m |
54 |
2 |
7 |
H |
23 |
X |
39 |
n |
55 |
3 |
8 |
I |
24 |
Y |
40 |
o |
56 |
4 |
9 |
J |
25 |
Z |
41 |
p |
57 |
5 |
10 |
K |
26 |
a |
42 |
q |
58 |
6 |
11 |
L |
27 |
b |
43 |
r |
59 |
7 |
12 |
M |
28 |
c |
44 |
s |
60 |
8 |
13 |
N |
29 |
d |
45 |
t |
61 |
9 |
14 |
O |
30 |
e |
46 |
u |
62 |
+ |
15 |
P |
31 |
f |
47 |
v |
63 |
/ |
因为 Base64 将三个字节转化成四个字节,因此经过 Base64 编码后的文本要比原文本大三分之一左右。^2
若最后不足三个字节怎么办?
- 若最后有两个字节
- 两个字节共 16 个二进制位,按照上述规则可以分为:6+6+4
- 对前两个 6 位二进制位组只需在前面补 “00” 即可,最后 4 位二进制位组除了在前面补 “00” 外,后面也要补上 “00” ,这样就可以得到 24 位二进制位。最后编码结果补 “=”
- 若最后只有一个字节
- 一个字节共 8 个二进制位,按照上述规则可以分为:6+2
- 最后 2 位二进制位组除了在前面补 “00” 外,后面也要补上 “0000”,这样就可以得到 16 位二进制位 最后编码结果补 “==”
2.4 举个例子
例如,将文本 “Base64!” 进行 Base64 编码:
原文 |
B |
a |
s |
e |
6 |
4 |
! |
ASCII 码值 |
66 |
97 |
115 |
101 |
54 |
52 |
33 |
二进制值 |
01000010 |
01100001 |
01110011 |
01100101 |
00110110 |
00110100 |
00100001 |
经过以上步骤得到文本 “Base64!” 的二进制值串:
01000010 01100001 01110011 01100101 00110110 00110100 00100001
三个字节为一组,进行转换:
01000010 01100001 01110011 || 01100101 00110110 00110100 || 00100001
010000|10 0110|0001 01|110011 || 011001|01 0011|0110 00|110100 || 001000|01
进行补 “0”,每组前面都有 “00”,下面省去了:
010000|10 0110|0001 01|110011 || 011001|01 0011|0110 00|110100 || 001000|010000
再转换回 ASCII 码值:
二进制值 |
010000 |
100110 |
000101 |
110011 |
011001 |
010011 |
011000 |
110100 |
001000 |
010000 |
ASCII 码值 |
16 |
38 |
5 |
51 |
25 |
19 |
24 |
52 |
8 |
16 |
符号 |
Q |
m |
F |
z |
Z |
T |
Y |
z |
I |
Q |
转换结果为:QmFzZTYzIQ
,因为最后单独剩了一个字符,所以最后补 ==
,即最终结果为:QmFzZTYzIQ==
3. 总结
明显,在进行 RSA 加密后再用 Base64 编码输出的结果最后的 “=” 为终结符,终结符后不管加什么都不会再去进行解码,因此在 “=” 后面加字符制造的错误数据本身就是错误的…在等号前加就会改变原本的 Base64 编码结果,自然不能被 RSA 解密成功。
后续会再写写阮一峰前辈写的 RSA 加解密部分的总结😁。