2018-04-24 Base64 + Base58编码

Base64
Base58

Base64

Base64是一种用64个字符表示任意二进制数据的方法。

编码前,可以是各种各样的字符,中文、法语、日语等,先把这些字符转成字节,然后对这些字节进行编码,编码后就都是限定了的64个字符了。

好处是编码后文本数据可以显示出来了,可以用在邮件、网页、URL上。
编码后,3字节的二进制数变成了4字节的文本数据,长度增加了33%,所以适合传送少量二进制数据。

编码

首先准备一个含有64个字符的数组,一般是 “A~Za~z0~9+/”;
然后原先的二进制数据每3个字节一组,就是3*8=24个比特,然后分成四组,每组6个比特,每6个比特就对应了一个0~63之间的数字;
把这4个范围在0~63的数字当作索引,在上面的字符数组里查找相应的字符;
这样就是编码后的字符串了。

特殊处理:
如果要编码的二进制数据不是3的倍数,就用\x00字节在末尾补足,然后再在编码的末尾加上1到2个等号(=),表示补了多少字节,这样解码的时候就可以自动去掉了。

特别注意,Base64编码后的文本的长度总是4的倍数,但是如果再加上1到2个=不就不是4的倍数了吗?
所以并不是先编码,再加上1到2个=,而是编码之后,把最后的1到2个字符(这个字符肯定是A替换=

解码

与编码相反,首先去除末尾的等号(=),然后比对初始的64字符的数组,把编码后的文本转成各字符在数组里的索引值,再然后转成6比特的二进制数,最后删除多余的\x00

改进

  1. 标准Base64里是包含 +/ 的,在URL里不能直接作为参数,所以出现一种 “url safe” 的Base64编码,其实就是把 +/ 替换成 -_
  2. 同样的,=也会被误解,所以编码后干脆去掉=,解码时,自动添加一定数量的等号,使得其长度为4的倍数即可正常解码了。

思考

编码时,=添加的逻辑是:原始二进制数据的长度不是3的倍数时,要补\x00,补了多少个\x00,就在编码的末尾添加几个=
而解码时,自动添加=的逻辑是:把编码后的文本的长度补齐到4的倍数。
这两个的逻辑明明是不同的,为什么可以工作呢?

解答

因为这里的=的意义只在于表示编码时添加了多少个\x00,哪怕缺失了=,也可以知道缺失了多少个,那么在解码后就要去掉多少个\x00
而且编码时添加的=其实是替换,并不会破坏编码后长度为4的倍数的这个特性,所以解码时,把=换成A,然后正常解码,再删除掉尾部的X个\x00即可(X=1或着2)。

Base58

除了Base64,还有Base16、Base32、Base58、Base85等编码方式,这里介绍一下Base58是因为:

  1. 它的编码思路和Base64看起来不太一样(虽然实质是一样的)
  2. 删除了 + / 0 O I l,让编码后的结果更加清晰,且不容易看错
  3. 比特币、Monero、Ripple、Flickr都在用这个Base58的编码方式

Base58的本质就是把256进制的值转成58进制的值。
所以它在编码时不需要考虑补\x00的问题,直接转换即可。
把字节流转成一个256进制的大数,然后不断除以58,保留余数,最后余数当作索引,再倒序,即为转换后的结果。

特殊处理:
不同于一个普通的数字转成某个进制,普通数字最高位是不会为0的,而我们要编码的对象是字节流,那么如果字节流的最前面是0(\x00),那么就会丢失这个信息。所以编码时要特殊记录一下,字节流的开端有多少个\x00,就直接在转换后的编码前面加上多少个b58Alphabet[0],同理,解码的时候先记录一下前面的b58Alphabet[0]的个数,然后解码之后再在解码的前面加上相同数量的0x00

示例代码

base58.go

var b58Alphabet = []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")

// Base58Encode encodes a byte array to Base58
func Base58Encode(input []byte) []byte {
    var result []byte

    x := big.NewInt(0).SetBytes(input)

    base := big.NewInt(int64(len(b58Alphabet)))
    zero := big.NewInt(0)
    mod := &big.Int{}

    for x.Cmp(zero) != 0 {
        x.DivMod(x, base, mod)
        result = append(result, b58Alphabet[mod.Int64()])
    }

    ReverseBytes(result)
    for _, b := range input {
        if b == 0x00 {
            result = append([]byte{b58Alphabet[0]}, result...)
        } else {
            break
        }
    }

    return result
}

// Base58Decode decodes Base58-encoded data
func Base58Decode(input []byte) []byte {
    result := big.NewInt(0)
    zeroBytes := 0

    for _, b := range input {
        if b == b58Alphabet[0] {
            zeroBytes++
        } else {
            break
        }
    }

    payload := input[zeroBytes:]
    for _, b := range payload {
        charIndex := bytes.IndexByte(b58Alphabet, b)
        result.Mul(result, big.NewInt(58))
        result.Add(result, big.NewInt(int64(charIndex)))
    }

    decoded := result.Bytes()
    decoded = append(bytes.Repeat([]byte{byte(0x00)}, zeroBytes), decoded...)

    return decoded
}

base58_test.go

func TestBase58Encode(t *testing.T) {
    data := []byte{0x00, 0x00, 0x00}
    encoded := Base58Encode(data)
    assert.Equal(t, []byte{'1', '1', '1'}, encoded)

    data = []byte{'a'}
    encoded = Base58Encode(data)
    assert.Equal(t, []byte{'2', 'g'}, encoded)

    data = []byte{'1', 0x00}
    encoded = Base58Encode(data)
    assert.Equal(t, []byte{'4', 'j', 'H'}, encoded)
}

func TestBase58Decode(t *testing.T) {
    data := []byte{0x00, 0x00, 0x00}
    assert.Equal(t, data, Base58Decode(Base58Encode(data)))

    data = []byte{'a'}
    assert.Equal(t, data, Base58Decode(Base58Encode(data)))

    data = []byte{'1', 0x00}
    assert.Equal(t, data, Base58Decode(Base58Encode(data)))
}

总结

不管是Base64还是Base58,都会造成信息的冗余,使得需要传输的数据量增大,所以不会用在很大的数据上。

  1. 使用Base64最普遍的是URL、邮件文本、图片;
  2. 相比于Base64直接切割比特的方法(3个比特变为4个比特),Base58采用的大数进制转换,效率更低,所以使用场景的数据更少,例如上面提到的比特币的地址的编码。

参考文档

  1. https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/001431954588961d6b6f51000ca4279a3415ce14ed9d709000
  2. https://www.jianshu.com/p/e002931bb38b
  3. https://github.com/Jeiwan/blockchain_go/blob/part_7/base58.go (这里的代码有问题)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 字符是用户可以读写的最小单位。计算机所能支持的字符组成的集合,就叫做字符集。字符集通常以二维表的形式存在。二维表的...
    刘惜有阅读 7,926评论 2 14
  • 最近学Python的时候遇到了编码问题,顺带了解了一下base64编码,首先阅读了咱CSDN上的一篇文章Base6...
    连命都给你了阅读 812评论 0 3
  • 上一周听了武老师有关梦的主题分享,很有感触,因为我也总是很关注我的梦。其中梦到底表达了什么,以及如何解梦都是让我很...
    学习思考行动阅读 723评论 2 1
  • 临夏最淳朴的年味_摇钱枣树!临夏方言中枣树与早富谐音。过年插摇钱枣树是雷打不动的春节习俗。寓意来年五谷丰登,早日致...
    金手银胳膊阅读 556评论 0 1
  • 额房间里有两个小门 贴着地面 只能让小猫通过 我按下手中按钮 两个门同时打开 第一个门里爬出小蚂蚁 ...
    是他631阅读 133评论 0 0