Base64算法剖析 JAVA8源码

898 阅读5分钟

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(含+,/)的情况,所以会跳过newlinelinemax相关部分

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--+
  • 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

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;
}