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

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