当前位置: 首页 > 科技观察

吃透Base64编码解码原理

时间:2023-03-14 00:29:39 科技观察

本文转载自微信公众号《大转转FE》,作者大转转FE。转载本文请联系大转转FE公众号。背景网上对base64编码原理的解释比较多,对解码原理的解释比较少,也没有分析内部实现原理。如果你想吃透base64的编解码原理,请耐心阅读本文,一定会有收获。涉及算法和逻辑运算的概念在探索base64编码原理和解码原理的过程中,首先需要了解下面会用到的算法和逻辑运算的概念,这样才能真正理解编码原理和解码原理base64的解码原理,在体验其算法的精妙之处,甚至在思考的过程中会有意想不到的收获。不知道base64编码表和ascII编码表的同学可以直接到文末查看。短除法短除法的运算方法是用一个能被它整除的质数除以一个约数,以此类推,直到商为质数。使用短除法,十进制数可以重复除以2以获得多个余数。最后将余数自下而上排列组合,得到二进制数。我们以字符n对应的ascII码110为例。110/2=55...055/2=27...127/2=13...113/2=6...16/2=3...03/2=1...11/2=0...1将余数从下往上排列组合,得到字符n对应的ascII码110转为二进制1101110,因为一个字节对应8位(bit),所以需要加上0向前补8位,得到01101110,其余字符同法得到。按权重展开求和按权重展开求和,8位二进制数从右到左从0递增到7,基数*基数乘以,从左到右累加,相加结果为对应的十进制数.我们以二进制数01101110转换为十进制为例:(01101110)2=0*20+1*21+1*22+1*23+0*24+1*25+1*26+0*27位概念在二进制数系中,每一个0或1都是一个位(bit,bit),也叫一个存储单元,位是数据存储的最小单位。其中,8bit称为一个字节(Byte)。移位运算符移位运算符是编程中的一种位运算运算符。移位运算符可以在二进制基础上移位数字。根据平移方向和填数规则,分为<<(左移)、>>(有符号右移)和>>>(无符号右移)三种。我们在base64的编解码过程中对正数进行操作,所以只用了<<(左移)、>>(有符号右移)两个运算符。左移运算:将一个二进制位操作数左移指定的移位位数,移出的位被舍弃,右移的所有空位补0。右移运算:将移位一个二进制位操作数右移指定个数的位,移位的位被舍弃,左移的空位全部补0或符号位,具体视不同机器而定。在使用二进制补码作为机器编号的机器中,正数的符号位为0,负数的符号位为1。我们用大白话来形容左移。一共8个座位,坐了8个人。如果8个座位不动,现在我让这8个人向左移动2个座位,这样最左边的两个人站起来,没有座位可以坐,最右边有两个空位。shift操作相当于人站起来出门,左边空格补0.//左移01101000<<2->101000(左边移位的位置舍弃)->10100000(rightspaceisfilledwith0)//右移01101000>>2->011010(右边移出的位被舍弃)->00011010(左边所有空位都填0)并运算,or运算和运算,或运算,是计算机中的一种基本逻辑运算方法。AND运算:符号表示为&。运算规则:两位同时为“1”,则结果为“1”,否则为0或运算:符号表示为|。运算规则:只要两位中有一位为“1”,则结果为“1”,否则为0。(bit)个字节(6位有效字节,最左边两位一直为0,实际上是8位字节)个子序列,然后在Base64编码索引表中查找得到的子序列,得到对应字符的编码方式连接成一个新的字符串。将每个3个8位字节子序列拆分成4个6位字节的过程如下图所示:base64为什么base64编码后的大小是原来的4/3倍?6和8的最大公倍数是24,所以三个8位的字节刚好可以拆分成四个6位的字节,38=64。在计算机中,因为一个字节需要8个存储单元来存储,所以我们需要加上6位前面加两个0,组成8位。如下图所示:显然,补充后需要32个存储单元,是原来24个的4/3倍。现在大家明白为什么base64编码后的大小是原来的4/3倍了。为什么叫base64呢?因为6位二进制数有2的6次方,即64个二进制数代表0-63之间的二进制数(00000000-00111111)。不是说一个字节用8位二进制表示,为什么不是2的8次方呢?因为我们得到的8位二进制数的前两位永远为0,而真正的有效位数只有6,所以我们可以得到的二进制数只有2的6次方。Base64字符是哪64个?Base64编码索引表,字符使用“A-Z、a-z、0-9、+、/”64个可打印字符来表示(00000000-00111111)这64个二进制数。也就是让base64EncodeChars='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'的编码原理,大家不妨先想一想,3个字节怎么拆分成4个字节?你的实现思路和我的有什么区别?又会擦出怎样的火花呢?流程图流程图思维分析映射关系:abc->xyzi。我们从高到低加上索引来分析这个过程x:(前面两个0)a的前六位=>00a7a6a5a4a3a2y:(前面两个0)a的最后两位+b的前四位=>00a1a0b7b6b5b4z:(前面两个0)b的后四位+c的前两位=>00b3b2b1b0c7c6i:(前面两个0)c的后六位=>00c5c4c3c2c1c0通过上面的映射关系,我们可以很容易得到以下实现思路:1.将字符对应的ascII码转换为8位二进制数2.对三个8位二进制数分别进行如下操作,将第一个数右移2位得到第一个6位有效位二进制数将第一个数&0x3左移4位得到第二个6位有效位二进制数的第一个和第二个有效位,右移第二个数&0xf04位,得到第二个6位二进制数的后四位有效位,取二得到第二个6位二进制数,将第二个数&0xf左移2位,得到第三个第一个6位二进制数的前四位有效位右移6位第三个数&0xC0后的位得到第三个6位二进制数的后两位有效位,取二得到将第三个数&0x3f转换为第三个6位二进制数得到第四个6-digitbinarynumber3.将得到的6位二进制数转换成十进制,找到对应的base64字符。我们以hao字符串为例子,观察base64编码过程,我们通过代码逻辑分析来实现上面的转换。代码实现//inputstringletstr='hao'//base64stringletbase64EncodeChars='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'//定义输入输出字节的二进制数letchar1,char2,char3,out1,out2,out3,out4,out//转换将字符对应的ascII码转化为8位二进制数char1=str.charCodeAt(0)&0xff//10401101000char2=str.charCodeAt(1)&0xff//9701100001char3=str.charCodeAt(2)&0xff//11101101111//输出6位有效字节二进制数6out1=char1>>2//26011010out2=(char1&0x3)<<4|(char2&0xf0)>>4//6000110out3=(char2&0xf)<<2|(char3&0xc0)>>6//5000101out4=char3&0x3f//47101111out=base64EncodeChars[out1]+base64EncodeChars[out2]+base64EncodeChars[out3]+base64EncodeChars[out4]//aGFv算法分析1.out1:char1>>201101000->000110102.out2=(char1&)<<4|(char2&0xf0)>>4//和运算01101000011000010000001111110000------------0000000001100000//移位运算后,0000000000000110//或运算0000000000-00-110-00-11000000110第三个字符和第四个字符同理以上代码组织,扩展为多字符字符串//inputstringletstr='haohaohao'//base64字符串letbase64EncodeChars='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'//获取字符串长度letlen=str.length//当前字符索引letindex=0//输出字符串letout=''while(index>2//26011010out2=(char1&0x3)<<4|(char2&0xf0)>>4//6000110out3=(char2&0xf)<<2|(char3&0xc0)>>6//5000101out4=char3&0x3f//47101111out=out+base64EncodeChars[out1]+base64EncodeChars[out2]+base64EncodeChars[out3]+base64EncodeChars[out4]//aGFv}原始字符串如果length不是3的整数倍,需要特殊处理...char1=str.charCodeAt(index++)&0xff//10401101000if(index==len){out2=(char1&0x3)<<4out=out+base64EncodeChars[out1]+base64EncodeChars[out2]+'=='returnout}char2=str.charCodeAt(index++)&0xff//9701100001if(index==len){out1=char1>>2//26011010out2=(char1&0x3)<<4|(char2&0xf0)>>4//6000110out3=(char2&0xf)<<2out=out+base64EncodeChars[out1]+base64EncodeChars[out2]+base64EncodeChars[out3]+'='returnout}...allcodefunctionbase64Encode(str){//base64stringletbase64EncodeChars='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'//获取字符串长度letlen=str.length//当前字符索引letindex=0//输出字符串letout=''while(index>2if(index==len){out2=(char1&0x3)<<4out=out+base64EncodeChars[out1]+base64EncodeChars[out2]+'=='returnout}char2=str.charCodeAt(index++)&0xffout2=(char1&0x3)<<4|(char2&0xf0)>>4if(index==len){out3=(char2&0xf)<<2out=out+base64EncodeChars[out1]+base64EncodeChars[out2]+base64EncodeChars[out3]+'='returnout}char3=str.charCodeAt(index++)&0xff//输出6个有效字节二进制数out3=(char2&0xf)<<2|(char3&0xc0)>>6out4=char3&0x3fout=out+base64EncodeChars[out1]+base64EncodeChars[out2]+base64EncodeChars[out3]+base64EncodeChars[out4]}returnout}base64Encode('haohao')//aGFvaGFvbase64Encode('haoha')//aGFvaGE=base64Encode('haoh')//aGFvaA==解码原理逆向推导,每4个6位有效位的二进制数组合成3个8位二进制数,根据ascII编码映射对应字符拼接后,分析拼接字符串的思路及映射关系xyzi->abca:x的后六位+y的三四位=>x5x4x3x2x1x0y5y4b:最后一位y的四位+z的三四五六位=>y3y2y1y0z5z4z3z21.c:z的后两位+i的后六位=>z1z0i5i4i3i2i1i02。将字符对应的base64字符集索引转换为6位二进制数。对四个6位二进制数中的每一个执行以下操作。二进制数左移2位得到新二进制数的前6位,在第二个二进制数&0x30后右移4位,或者运算后得到第一个新二进制数在第二个二进制数&0xf后左移4位,在第三个二进制数&0x3c后右移2位,或者在第二个二进制数&0x3后得到第二个新的二进制数,在第二个二进制数后左移6位&0x3,与第四个二进制数or运算后,得到第二个新的二进制数。3.根据ascII码映射到对应的字符拼接字符串编码实现//base64字符串letstr='aGFv'//base64字符集letbase64CharsArr='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')//得到索引值letchar1=base64CharsArr.findIndex(char=>char==str[0])&0xff//26011010letchar2=base64CharsArr.findIndex(char=>char==str[1])&0xff//6000110letchar3=base64CharsArr.findIndex(char=>char==str[2])&0xff//5000101letchar4=base64CharsArr.findIndex(char=>char==str[3])&0xff//47101111letout1,out2,out3,out//位运算out1=char1<<2|(char2&0x30)>>4out2=(char2&0xf)<<4|(char3&0x3c)>>2out3=(char3&0x3)<<6|char4console.log(out1,out2,out3)out=String.fromCharCode(out1)+String.fromCharCode(out2)+String.fromCharCode(out3)functionbase64decode遇到有用的'='补码时(str){//base64字符集letbase64CharsArr='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')letchar1=base64CharsArr.findIndex(char=>char==str[0])letchar2=base64CharsArr.find>chardex(char=str[1])letout1,out2,out3,outif(char1==-1||char2==-1)returnoutchar1=char1&0xffchar2=char2&0xffletchar3=base64CharsArr.findIndex(char=>char==str[2])//th时三位是不在base64对照表中,只拼接第一个字符串if(char3==-1){out1=char1<<2|(char2&0x30)>>4out=String.fromCharCode(out1)returnout}letchar4=base64CharsArr.findIndex(char=>char==str[3])//当第三位不在base64对照表中时,只拼接第一位和前两个字符串if(char4==-1){out1=char1<<2|(char2&0x30)>>4out2=(char2&0xf)<<4|(char3&0x3c)>>2out=String.fromCharCode(out1)+String。fromCharCode(out2)returnout}//位运算out1=char1<<2|(char2&0x30)>>4out2=(char2&0xf)<<4|(char3&0x3c)>>2out3=(char3&0x3)<<6|char4console.log(out1,out2,out3)out=String.fromCharCode(out1)+String.fromCharCode(out2)+String.fromCharCode(out3)returnout}解码整个字符串,完成后的代码functionbase64decode(str){//base64字符集letbase64CharsArr='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')leti=0letlen=str.lengthletout=''while(ichar==str[i])i++letchar2=base64CharsArr.findIndex(char=>char==str[i])i++letout1,out2,out3if(char1==-1||char2==-1)returnoutchar1=char1&0xffchar2=char2&0xffletchar3=base64CharsArr.findIndex(char=>char==str[i])i++//当第三位不在base64对照表中时,只拼接第一串out1=char1<<2|(char2&0x30)>>4if(char3==-1){out=out+String.fromCharCode(out1)returnout}letchar4=base64CharsArr.findIndex(char=>char==str[i])i++//第三位不在base64比较表时,只拼接第一串和第二串out2=(char2&0xf)<<4|(char3&0x3c)>>2if(char4==-1){out=out+String.fromCharCode(out1)+String.fromCharCode(out2)returnout}//位运算out3=(char3&0x3)<<6|char4console.log(out1,out2,out3)out=out+String.fromCharCode(out1)+String.fromCharCode(out2)+细绳。fromCharCode(out3)}returnout}base64decode('aGFvaGFv')//haohaobase64decode('aGFvaGE=')//haohabase64decode('aGFvaA==')//haoh上面解码的核心是字符与base64字符集的映射indexes,网上看的我一直在使用AccII编码索引映射base64字符索引的方法letbase64DecodeChars=[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1]//letchar1='hao'.charCodeAt(0)//h->104base64DecodeChars[char1]//base64编码表中的33->h可以看出base64DecodeChars中存储的是base64编码对着accII的indexencodingtable表中对应字符的索引汇总说到Base64编码可能有点陌生,因为大多数编码都是将字符转换为二进制的过程,而将二进制转换为字符的过程称为解码。Base64的概念正好相反。从二进制到字符的转换称为编码,从字符到二进制的转换称为解码。Base64是一种数据编码方式,可用于简单的加密。我们可以改变base64编码映射顺序,形成自己独特的加密算法进行加解密。代码表