Base64 Encoding详解

前言

Base64是由64个字符的字母表定义的基数为64的编码/解码方案,可以将二进制数据转换为字符传输,是网络上最常见的用于传输8Bit字节码的编码方式之一。注意:采用Base64编码具有不可读性,需要解码后才能阅读。

目前Base64被广泛应用于计算机的各个领域,由于不同场景下对特殊字符的处理(+,/)不同,因此又根据应用场景又出现了Base64的各种改进的“变种”。因此在使用时,必须先确认使用的是哪种encoding类型,才能正确编/解码。

编/解码原理

base64编码基本规则:

把每3个8Bit的字节转换为4个6Bit的字节(3 x 8 = 4 x 6 = 24),然后把6Bit再添两位高位0,组成四个8Bit的字节,也就是说,转换后的字符串理论上将要比原来的长1/3。不足3个的byte,根据具体规则决定是否填充。

6bit意味着总共有2^6即64种情况,与base64的字符表可以一一对应。

解码则是一个反向的过程,则需要将每4个byte的数据根据base64转换表,转换为3个byte的数据。

具体Encoding类型

不同的语言中实现过程及Encoding的种类,可能并不一致,本文中主要以Golang的实现为例。

以Go encoding/base64 package为例,根据使用的特殊字符及是否填充,具体分为以下四种类型。

StdEncoding

适用环境:标准环境

根据RFC 4648标准定义实现,包含特殊字符'+''/',不足的部分采用'='填充,根据规则,最多有2个'='

const encodeStd = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
// StdEncoding is the standard base64 encoding, as defined in
// RFC 4648.
var StdEncoding = NewEncoding(encodeStd)

URLEncoding

适用环境:url传输

因为URL编码器会把标准Base64中的'/''+'字符变为形如"%XX"的形式,而这些"%"号在存入数据库时还需要再进行转换,因此采用'-''_'代替'/''+',不足的部分采用'='填充,根据规则,最多有2个'='

const encodeURL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
// URLEncoding is the alternate base64 encoding defined in RFC 4648.
// It is typically used in URLs and file names.
var URLEncoding = NewEncoding(encodeURL)

RawStdEncoding

除了不填充'='外,与StdEncoding一致。

// RawStdEncoding is the standard raw, unpadded base64 encoding,
// as defined in RFC 4648 section 3.2.
// This is the same as StdEncoding but omits padding characters.
var RawStdEncoding = StdEncoding.WithPadding(NoPadding)

RawURLEncoding

除了不填充'='外,与URLEncoding一致。

// RawURLEncoding is the unpadded alternate base64 encoding defined in RFC 4648.
// It is typically used in URLs and file names.
// This is the same as URLEncoding but omits padding characters.
var RawURLEncoding = URLEncoding.WithPadding(NoPadding)

整体比较

Encoding 特殊字符 是否填充
StdEncoding '+''/'
RawStdEncoding '+''/'
URLEncoding '-''_'
RawURLEncoding '-''_'

编/解码实现

编码-Encode

整体实现代码如下:

func (enc *Encoding) Encode(dst, src []byte) {
    if len(src) == 0 {
        return
    }
    // enc is a pointer receiver, so the use of enc.encode within the hot
    // loop below means a nil check at every operation. Lift that nil check
    // outside of the loop to speed up the encoder.
    _ = enc.encode

    di, si := 0, 0
    n := (len(src) / 3) * 3
    for si < n {
        // Convert 3x 8bit source bytes into 4 bytes
        // 将3x8 bit转换为4bytes
        val := uint(src[si+0])<<16 | uint(src[si+1])<<8 | uint(src[si+2])//通过移位运算实现,前8位为src[si+0],中间8位为src[si+1],最后8位为src[si+2]

        dst[di+0] = enc.encode[val>>18&0x3F]
        dst[di+1] = enc.encode[val>>12&0x3F]
        dst[di+2] = enc.encode[val>>6&0x3F]
        dst[di+3] = enc.encode[val&0x3F]

        si += 3
        di += 4
    }

    remain := len(src) - si
    if remain == 0 {
        return
    }
    // Add the remaining small block
    val := uint(src[si+0]) << 16
    if remain == 2 {
        val |= uint(src[si+1]) << 8
    }

    dst[di+0] = enc.encode[val>>18&0x3F]
    dst[di+1] = enc.encode[val>>12&0x3F]

    switch remain {//填充
    case 2:
        dst[di+2] = enc.encode[val>>6&0x3F]
        if enc.padChar != NoPadding {
            dst[di+3] = byte(enc.padChar)
        }
    case 1:
        if enc.padChar != NoPadding {
            dst[di+2] = byte(enc.padChar)
            dst[di+3] = byte(enc.padChar)
        }
    }
}

编码转换

根据规则,是先将3个8bit的数据拆成4个6bit,再将每个6bit高位填充两个0,即变成4个base64字符。核心实现:

    val := uint(src[si+0])<<16 | uint(src[si+1])<<8 | uin(src[si+2])//通过移位运算实现,前8位为src[si+0],中间8位src[si+1],最后8位为src[si+2]

    dst[di+0] = enc.encode[val>>18&0x3F]
    dst[di+1] = enc.encode[val>>12&0x3F]
    dst[di+2] = enc.encode[val>>6&0x3F]
    dst[di+3] = enc.encode[val&0x3F]

由具体实现可以发现源码采用了非常巧妙的方式实现规则:

  • 选取前3位byte,分别左移16、8、0位,然后进行逻辑或,得到的结果的前8位、中8位和最后8位分别对应原始的3个byte数据,如此组成新的24 bit数据val。

  • 由val数据分别右移18、12、6、0,可以得到前6、12、18、24位数据,所有目前的数据均在后低6位中。

  • 为获取低6位,与0x3F(00111111)进行逻辑与&操作即可。

  • 根据得到的结果,查找在encode得到对应位置上的字符。

填充

每3个byte进行相关的4byte转换,当有剩余的byte不足3个时,此时如果需要填充,缺几个byte则补几个'='

解码-Decode

整体实现代码如下:

func (enc *Encoding) Decode(dst, src []byte) (n int, err error) {
    if len(src) == 0 {
        return 0, nil
    }

    // Lift the nil check outside of the loop. enc.decodeMap is directly
    // used later in this function, to let the compiler know that the
    // receiver can't be nil.
    _ = enc.decodeMap

    si := 0
    for strconv.IntSize >= 64 && len(src)-si >= 8 && len(dst)-n >= 8 {
        if dn, ok := assemble64(//是否有效的base64字符
            enc.decodeMap[src[si+0]],
            enc.decodeMap[src[si+1]],
            enc.decodeMap[src[si+2]],
            enc.decodeMap[src[si+3]],
            enc.decodeMap[src[si+4]],
            enc.decodeMap[src[si+5]],
            enc.decodeMap[src[si+6]],
            enc.decodeMap[src[si+7]],
        ); ok {
            binary.BigEndian.PutUint64(dst[n:], dn)
            n += 6
            si += 8
        } else {
            var ninc int
            si, ninc, err = enc.decodeQuantum(dst[n:], src, si)
            n += ninc
            if err != nil {
                return n, err
            }
        }
    }

    for len(src)-si >= 4 && len(dst)-n >= 4 {
        if dn, ok := assemble32(
            enc.decodeMap[src[si+0]],
            enc.decodeMap[src[si+1]],
            enc.decodeMap[src[si+2]],
            enc.decodeMap[src[si+3]],
        ); ok {
            binary.BigEndian.PutUint32(dst[n:], dn)
            n += 3
            si += 4
        } else {
            var ninc int
            si, ninc, err = enc.decodeQuantum(dst[n:], src, si)
            n += ninc
            if err != nil {
                return n, err
            }
        }
    }

    for si < len(src) {
        var ninc int
        si, ninc, err = enc.decodeQuantum(dst[n:], src, si)
        n += ninc
        if err != nil {
            return n, err
        }
    }
    return n, err
}

整体处理过程:

  • 先进行8转6,依次取前8个字符,根据decodeMap查找对应的字符位置(转换后值),确定全部字节是否是有效的(无效值为0xff)

  • 进行8转6操作,如果成功,将获取的前6个byte存入dst中;如果失败,返回之前的解析结果及错误。

  • 不足8个的部分进行4转3操作,与8转6处理逻辑一致,成功则将获取的前3个byte存入dst中;如果失败,返回之前的解析结果及错误。

8转6处理过程

关键代码如下:

func assemble64(n1, n2, n3, n4, n5, n6, n7, n8 byte) (dn uint64, ok bool) {
    // Check that all the digits are valid. If any of them was 0xff, their
    // bitwise OR will be 0xff.
    if n1|n2|n3|n4|n5|n6|n7|n8 == 0xff {
        return 0, false
    }
    return uint64(n1)<<58 |
            uint64(n2)<<52 |
            uint64(n3)<<46 |
            uint64(n4)<<40 |
            uint64(n5)<<34 |
            uint64(n6)<<28 |
            uint64(n7)<<22 |
            uint64(n8)<<16,
        true
}

func (bigEndian) PutUint64(b []byte, v uint64) {
    _ = b[7] // early bounds check to guarantee safety of writes below
    b[0] = byte(v >> 56)
    b[1] = byte(v >> 48)
    b[2] = byte(v >> 40)
    b[3] = byte(v >> 32)
    b[4] = byte(v >> 24)
    b[5] = byte(v >> 16)
    b[6] = byte(v >> 8)
    b[7] = byte(v)
}

具体解释:

  • 根据编码表获取字符的位置,其对应byte的前两位为0。解码是为了获取后6bit数据,因此我们依次将n1、n2、…、n8移位42、36、…、0位即可得到原6bit组成的数据。

  • 获取后新的数据仅有48位,即6个byte,我们在低位填充16位,即再左移16位,在转换为uint64后,获取的前6byte即为原始数据。

  • 因此n1、n2、…、n8的总移位数58、22、…、16。

4转3与8转6完全原理一致,只是使用uint32转换,此处不再复述。

包含无效字符的处理

func (enc *Encoding) decodeQuantum(dst, src []byte, si int) (nsi, n int, err error) {
    // Decode quantum using the base64 alphabet
    var dbuf [4]byte
    dlen := 4

    // Lift the nil check outside of the loop.
    _ = enc.decodeMap

    for j := 0; j < len(dbuf); j++ {
        if len(src) == si {//解析到最后
            switch {//说明是
            case j == 0:
                return si, 0, nil
            case j == 1, enc.padChar != NoPadding:
                return si, 0, CorruptInputError(si - j)
            }
            dlen = j
            break
        }
        in := src[si]
        si++

        out := enc.decodeMap[in]
        if out != 0xff {//获取合法字符至dbuf中
            dbuf[j] = out
            continue
        }

        if in == '\n' || in == '\r' {//换行跳过不处理
            j--
            continue
        }

        if rune(in) != enc.padChar {//如果是非法字符且不是填充,报错处理
            return si, 0, CorruptInputError(si - 1)
        }

        // We've reached the end and there's padding
        switch j {//填充处理
        case 0, 1://填充不能出现在最后四位的前两位
            // incorrect padding
            return si, 0, CorruptInputError(si - 1)
        case 2:
            // "==" is expected, the first "=" is already consumed.
            // skip over newlines
            // 判断下一个字符
            // 忽略换行符
            for si < len(src) && (src[si] == '\n' || src[si] == '\r') {
                si++
            }
            // 第三位出现填充符,则必然第四位是填充符,否则非法
            if si == len(src) {
                // not enough padding
                return si, 0, CorruptInputError(len(src))
            }
            if rune(src[si]) != enc.padChar {
                // incorrect padding
                return si, 0, CorruptInputError(si - 1)
            }

            si++
        }
        // 填充符出现在第四位,意味着到达结束点,则其后除非是换行,否则均为非法
        // skip over newlines
        for si < len(src) && (src[si] == '\n' || src[si] == '\r') {
            si++
        }
        // 填充符后
        if si < len(src) {
            // trailing garbage
            err = CorruptInputError(si)
        }
        dlen = j //2、3
        break
    }

    // Convert 4x 6bit source bytes into 3 bytes
    // 进行4转3,dbuf中可能并非完整的存入合法的4个字符,可能存在0-2个填充,这些位置的byte为0,因此不能复用之前合法字符的转换方式。
    // 依次多左移6位再右移8位,得到的前3个byte即为原始值
    // 填充的byte为0,转换后依然为0
    val := uint(dbuf[0])<<18 | uint(dbuf[1])<<12 | uint(dbuf[2])<<6 | uint(dbuf[3])
    dbuf[2], dbuf[1], dbuf[0] = byte(val>>0), byte(val>>8), byte(val>>16)
    // 根据填充的个数,依次将获取的byte放入dst中
    switch dlen {
    case 4://没有填充
        dst[2] = dbuf[2]
        dbuf[2] = 0
        fallthrough
    case 3://有一个填充符
        dst[1] = dbuf[1]
        // 如果存在一个填充,则dbuf[2]必然为0,否则就不是填充了
        if enc.strict && dbuf[2] != 0 {
            return si, 0, CorruptInputError(si - 1)
        }
        dbuf[1] = 0
        fallthrough
    case 2://有两个填充符
        dst[0] = dbuf[0]
        // 如果存在2个填充,则dbuf[1]、dbuf[2]必然为0,否则就不是填充了
        if enc.strict && (dbuf[1] != 0 || dbuf[2] != 0) {
            return si, 0, CorruptInputError(si - 2)
        }
    }

    return si, dlen - 1, err
}

包含无效字符的处理方式是:

  • 预定义4byte dbuf数组,用以存入获取到的合法字符

  • 获取si位置的字符,查找decodeMap

    • 若是合法字符,存入debuf,查找下一个字符
    • 若是换行符,跳过不处理
    • 若是非法字符,且不是不是填充字符,报错退出。
    • 若是填充字符,进行填充位置的判断,
      • 填充只可能出现在4个byte的后2个位置,否则不合法,报错退出
      • 若第三个位置是填充符,则后续的字符,除换行符外,则必然是填充符,否则不合法,报错退出。
      • 若第四位是填充符,后续到src末端,除换行符外,不应该存在其他字符。
      • 没有填充,dlen默认为4,有一个填充,dlen位,两个填充,dlen为2
    • 若当前位置已到达src末端
      • 若未获取到一个合法字符,则意味着,src当前中无有效信息需要解码,正常退出。
      • 若仅获取到一个字符,且encoding是需要填充的(填充最多2位),则意味着src非法,报错退出。
  • 根据debuf进行4转3处理(dbuf中可能并非完整的存入合法的4个字符,未存入的位置的byte为0,需要根据合法值的数量,确认数据的位置)

    • 依次多左移6位再右移8位,得到的前3个byte(0左/右移后仍为0)

    • 根据dlen(填充数量= 4-dlen)的确认数据,从低位到高位依次处理

      • 没有填充(或不填充时全部为有效值),取全部值,dst[2] = dbuf[2],继续
      • 有1个填充(或不填充时仅有2个有效值),取前2个值,dst[1] = dbuf[1];
        • 如果采用strict模式,则dbuf[2]必然为0,正常继续;否则异常,退出。
      • 有2个填充(或不填充时仅有1个有效值),取第一个值,dst[0] = dbuf[0];
        • 如果采用strict模式,则dbuf[1]、dbuf[2]必然为0,正常继续;否则异常,退出。

总结

本文主要以Go base64 package为例,详细介绍了4种Encoding的异同点及使用环境,同时对base64编/解码的详细实现过程进行了较深入的探讨。

Encoding的不同主要是因为使用环境对特殊字符的处理导致,如url传输就需要使用相关的URLEncoding,若使用StdEncoding会导致'+''/'符号异常。

编解码中则充分利用了左移、右移的特性及uint32、uint64与byte的转换,在简短的代码中即实现了byte数据的转换,而无需按照规则中对具体的bit位进行操作。这点可以给我们很多启示,在以后的代码中,不妨做下相关的思考,有没有更简单的方式实现。

公众号

鄙人刚刚开通了公众号,专注于分享Go开发相关内容,望大家感兴趣的支持一下,在此特别感谢。

gzh

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章