1. 分析数据
import java.util.Base64;
//加密
final String s = Base64.getEncoder().encodeToString("阿斯顿123dd11".getBytes(StandardCharsets.UTF_8));
//解密
final byte[] decode = Base64.getDecoder().decode(s.getBytes(StandardCharsets.UTF_8));
- 本次只分析
Table 1: The Base 64 Alphabet
(含+
,/
)的情况,所以会跳过newline
,linemax
相关部分
2. 分析规则
The encoding process represents 24-bit groups of input bits as output strings of 4 encoded characters. Proceeding from left to right, a 24-bit input group is formed by concatenating 3 8-bit input groups. These 24 bits are then treated as 4 concatenated 6-bit groups, each of which is translated into a single character in the base 64 alphabet.
译:编码过程将24位组的输入位表示为4个编码字符的输出字符串。从左到右,连接3个8位输入组形成一个24位输入组。然后将这24位处理为4个连接的6位组,每个6位组被翻译成以64为基数的字母表中的一个字符。
- 例
+--first octet--+-second octet--+--third octet--+
|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|
+-----------+---+-------+-------+---+-----------+
|5 4 3 2 1 0|5 4 3 2 1 0|5 4 3 2 1 0|5 4 3 2 1 0|
+--1.index--+--2.index--+--3.index--+--4.index--+
3. 跟踪源码
3.1 加密过程
3.1.1 创建Encoder
public static Encoder getEncoder() {
return Encoder.RFC4648;
}
static final Encoder RFC4648 = new Encoder(false, null, -1, true);
/**
* @param isURL 是否使用“URL和文件名安全”Base 64字母表
* Table 1: The Base 64 Alphabet
* Table 2: The "URL and Filename safe" Base 64 Alphabet
* @param newline & linemax 多用途因特网邮件扩展(MIME)[4]经常作为base64的参考而不考虑后果用于换行或非字母字符。
* Multipurpose Internet Mail Extensions (MIME) [4] is often used as a reference for base64 without considering the consequences for line-wrapping or non-alphabet characters.
* @param doPadding 在某些情况下,在基础编码数据中使用填充("=")不是必需的或使用的。在一般情况下,当假设无法确定传输数据的大小,需要填充生成正确的解码数据。
* 实现必须在末尾包含适当的填充字符编码数据,除非参考本文档的规范另有明确说明。
* In some circumstances, the use of padding ("=") in base-encoded data is not required or used. In the general case, when assumptions about the size of transported data cannot be made, padding is required to yield correct decoded data.
* Implementations MUST include appropriate pad characters at the end of encoded data unless the specification referring to this document explicitly states otherwise.
*/
private Encoder(boolean isURL, byte[] newline, int linemax, boolean doPadding) {
this.isURL = isURL;
this.newline = newline;
this.linemax = linemax;
this.doPadding = doPadding;
}
3.1.2 方法入口
/**
* @param src 入参数据的字节数组
* @return 经过base64加密后的字节数组
*/
public byte[] encode(byte[] src) {
//计算加密后的数据该有多长
int len = outLength(src.length); // dst array size
byte[] dst = new byte[len];
//计算Base64的值,放入dst中
int ret = encode0(src, 0, src.length, dst);
if (ret != dst.length)
return Arrays.copyOf(dst, ret);
return dst;
}
3.1.3 计算加密后的数据该有多长
int len = outLength(src.length); // dst array size
/**
* @param srclen 入参数据的字节数组的长度
* @return 加密后的数据该有的长度
*/
private final int outLength(int srclen) {
int len = 0;
if (doPadding) {
// 走这个if分支
len = 4 * ((srclen + 2) / 3);
} else {
int n = srclen % 3;
len = 4 * (srclen / 3) + (n == 0 ? 0 : n + 1);
}
//这个if分支不会走
if (linemax > 0) // line separators
len += (len - 1) / linemax * newline.length;
return len;
}
该环境下 doPadding 必然为 true
由 2. 分析规则 可得加密后长度:
(srclen + 2)
:末尾有0-2个=
填充,考虑最多的情况填充两个,保证除3之后能按大了1来算((srclen + 2) / 3)
:3个数组一组- 计算有多少组像 2. 分析规则中的
+--first octet--+-second octet--+--third octet--+
- 计算有多少组像 2. 分析规则中的
4 * ((srclen + 2) / 3)
:计算有多少个index,这便是加密后的长度- 像 2. 分析规则中,一组有4个index
+--1.index--+--2.index--+--3.index--+--4.index--+
举个例子就是: - 长度0 -> 长度0
- 长度1 -> 长度4
- 长度2 -> 长度4
- 长度3 -> 长度4
- 长度4 -> 长度8
- 长度5 -> 长度8
- 长度6 -> 长度8
- 长度7 -> 长度12
- 像 2. 分析规则中,一组有4个index
3.1.4 计算Base64的值
int ret = encode0(src, 0, src.length, dst);
private int encode0(byte[] src, int off, int end, byte[] dst) {
//选择 toBase64 ,使用 `Table 1: The Base 64 Alphabet`(含`+`,`/`)
char[] base64 = isURL ? toBase64URL : toBase64;
int sp = off;
//主要目的是计算出整数3的长度,方便后面不足3的地方补'='
//还有就是假设偏移量不为0的情况,重新校准长度
int slen = (end - off) / 3 * 3;
int sl = off + slen;
//这个if分支不会走
if (linemax > 0 && slen > linemax / 4 * 3)
slen = linemax / 4 * 3;
int dp = 0;
while (sp < sl) {
int sl0 = Math.min(sp + slen, sl);
//这个for循环判断,一次取三个值,并拼接成如[2.分析规则]所描述的一样
//将第一个值左移16位,第二个值左移8位,第三个值还在原地。拼成24位数据存在bits里面
for (int sp0 = sp, dp0 = dp ; sp0 < sl0; ) {
int bits = (src[sp0++] & 0xff) << 16 |
(src[sp0++] & 0xff) << 8 |
(src[sp0++] & 0xff);
//'& 0x3f':'0011_1111','与'上它把除了低6位的数据全部变成0
//给第一个index赋值:将bits的 高6(24-18)位 转换成byte赋值
//给第二个index赋值:将bits的 高12(24-12)位中的低6位 转换成byte赋值
//给第三个index赋值:将bits的 高18(24-6)位中的低6位 转换成byte赋值
//给第四个index赋值:将bits的 bits中的低6位 转换成byte赋值
//ps: 个人觉得,这里通过base64查索引已经可以取得base64String了
dst[dp0++] = (byte)base64[(bits >>> 18) & 0x3f];
dst[dp0++] = (byte)base64[(bits >>> 12) & 0x3f];
dst[dp0++] = (byte)base64[(bits >>> 6) & 0x3f];
dst[dp0++] = (byte)base64[bits & 0x3f];
}
int dlen = (sl0 - sp) / 3 * 4;
dp += dlen;
sp = sl0;
if (dlen == linemax && sp < end) {
for (byte b : newline){
dst[dp++] = b;
}
}
}
//如果没有被整3分割,那么进入循环,处理剩余的byte
if (sp < end) { // 1 or 2 leftover bytes
//获取剩余位的第一个byte
int b0 = src[sp++] & 0xff;
//[1.index]先用前面6个bit
dst[dp++] = (byte)base64[b0 >> 2];
//判断末尾是否只有一个tyte,是则if,不是则else
if (sp == end) {
//[2.index]用后面2个bit拼上4个0
//然后base64中最后两个byte补'='
//那么由此可得:两个bit有三个情况:00、01、11
//所以可以推断出:base64串以'=='结尾的话,前面一个字符只可能是'A'、'g'、'w'
dst[dp++] = (byte)base64[(b0 << 4) & 0x3f];
if (doPadding) {
dst[dp++] = '=';
dst[dp++] = '=';
}
} else {
//获取剩余位的最后一个byte
int b1 = src[sp++] & 0xff;
//[2.index]用'第一个byte后面2个bit'拼接上'最后一个byte后面4个bit'
dst[dp++] = (byte)base64[(b0 << 4) & 0x3f | (b1 >> 4)];
//[3.index]用拼接上'最后一个byte后面4个bit'拼上2个0
//然后base64中最后一个byte补'='
//所以可以推断出:base64串以'='结尾的话,前面一个字符也有迹可循,就是有点多,不列出来了
dst[dp++] = (byte)base64[(b1 << 2) & 0x3f];
if (doPadding) {
dst[dp++] = '=';
}
}
}
return dp;
}
3.2 解密过程
3.2.1 创建Decoder
public static Decoder getDecoder() {
return Decoder.RFC4648;
}
static final Decoder RFC4648 = new Decoder(false, false);
private Decoder(boolean isURL, boolean isMIME) {
this.isURL = isURL;
this.isMIME = isMIME;
}
3.2.2 方法入口
/**
* @param src 入参Base64数据的字节数组
* @return 经过解密后的字节数组
*/
public byte[] decode(byte[] src) {
//计算解密后的数据该有多长
byte[] dst = new byte[outLength(src, 0, src.length)];
//解密Base64,放入dst中
int ret = decode0(src, 0, src.length, dst);
if (ret != dst.length) {
dst = Arrays.copyOf(dst, ret);
}
return dst;
}
3.2.3 计算解密后的数据该有多长
/**
* @param src 入参Base64数据的字节数组
* @param sp 偏移量
* @param sl 入参base64的长度
* @return 经过解密后的字节数组
*/
private int outLength(byte[] src, int sp, int sl) {
//选择 fromBase64
int[] base64 = isURL ? fromBase64URL : fromBase64;
int paddings = 0;
int len = sl - sp;
//长度为0就直接过了
if (len == 0)
return 0;
//长度<2,考虑是不是走MIME规则,不是的话就报错,因为不符合`Table 1: The Base 64 Alphabet`,至少有一组3个byte
if (len < 2) {
if (isMIME && base64[0] == -1)
return 0;
throw new IllegalArgumentException(
"Input byte[] should at least have 2 bytes for base64 bytes");
}
if (isMIME) {
// scan all bytes to fill out all non-alphabet. a performance
// trade-off of pre-scan or Arrays.copyOf
int n = 0;
while (sp < sl) {
int b = src[sp++] & 0xff;
if (b == '=') {
len -= (sl - sp + 1);
break;
}
if ((b = base64[b]) == -1)
n++;
}
len -= n;
} else {
//不是MIME规则,所以走else分支
//判断与计算base64串末尾是否有'='填充
if (src[sl - 1] == '=') {
paddings++;
if (src[sl - 2] == '=')
paddings++;
}
}
//根据[2.分析规则],加密后的数据长度必然为4的倍数
//我认为:此处判断 (len & 0x3) != 0,来兼容与清除可能出现的脏数据
// 或者是用来处理MIME规则的
if (paddings == 0 && (len & 0x3) != 0)
paddings = 4 - (len & 0x3);
return 3 * ((len + 3) / 4) - paddings;
}
/**
* Lookup table for decoding unicode characters drawn from the
* "Base64 Alphabet" (as specified in Table 1 of RFC 2045) into
* their 6-bit positive integer equivalents. Characters that
* are not in the Base64 alphabet but fall within the bounds of
* the array are encoded to -1.
*
* 将取自“Base64字母表”(如rfc2045的表1所述)的unicode字符解码为等效的6位正整数的查找表。
* 不在Base64字母表中但在数组范围内的字符被编码为-1。
*/
private static final int[] fromBase64 = new int[256];
static {
Arrays.fill(fromBase64, -1);
for (int i = 0; i < Encoder.toBase64.length; i++)
fromBase64[Encoder.toBase64[i]] = i;
fromBase64['='] = -2;
}
3.2.4 解密Base64
private int decode0(byte[] src, int sp, int sl, byte[] dst) {
//选择 fromBase64
int[] base64 = isURL ? fromBase64URL : fromBase64;
int dp = 0;
int bits = 0;
//准备好起始的位运算的偏移量,同时也是循环赋值的条件
int shiftto = 18; // pos of first byte of 4-byte atom
while (sp < sl) {
int b = src[sp++] & 0xff;
//末尾校验
if ((b = base64[b]) < 0) {
//上面将'-'位置赋值为了-2,在此处用作判断
if (b == -2) { // padding byte '='
// = shiftto==18 unnecessary padding
// x= shiftto==12 a dangling single x
// x to be handled together with non-padding case
// xx= shiftto==6&&sp==sl missing last =
// xx=y shiftto==6 last is not =
//根据规则判断,末尾是否不符合规范
if (shiftto == 6 && (sp == sl || src[sp++] != '=') ||
shiftto == 18) {
throw new IllegalArgumentException(
"Input byte array has wrong 4-byte ending unit");
}
//符合就跳出while了,去执行文末处理
break;
}
if (isMIME) // skip if for rfc2045
continue;
else
throw new IllegalArgumentException(
"Illegal base64 character " +
Integer.toString(src[sp - 1], 16));
}
//将b左移偏移量个位置放在当前index,然后偏移量-6指向下一个index
//当shiftto<0时正好执行了4次,就正好还原了一组加密后的bit数据,放在了bits里面
bits |= (b << shiftto);
shiftto -= 6;
if (shiftto < 0) {
//代表执行获取了一组完整的bit数据
//first octet 赋值为bits的高8位的补码,这里也是在处理中文
dst[dp++] = (byte)(bits >> 16);
dst[dp++] = (byte)(bits >> 8);
dst[dp++] = (byte)(bits);
//该组数据处理完毕,重新赋值为初始状态
shiftto = 18;
bits = 0;
}
}
//文末处理
// reached end of byte array or hit padding '=' characters.
if (shiftto == 6) {
dst[dp++] = (byte)(bits >> 16);
} else if (shiftto == 0) {
dst[dp++] = (byte)(bits >> 16);
dst[dp++] = (byte)(bits >> 8);
} else if (shiftto == 12) {
// dangling single "x", incorrectly encoded.
throw new IllegalArgumentException(
"Last unit does not have enough valid bits");
}
// 如果不是MIME,不会做剩余操作。
// anything left is invalid, if is not MIME.
// if MIME, ignore all non-base64 character
while (sp < sl) {
if (isMIME && base64[src[sp++]] < 0)
continue;
throw new IllegalArgumentException(
"Input byte array has incorrect ending byte at " + sp);
}
return dp;
}