(本文内容针对 1.0.1c 版本的OpenSSL,尽管该版本的OpenSSL存在Heartbleed漏洞,但是Heartbleed漏洞与base64编解码的实现无关)
对于 base64 编码,OpenSSL在 evp.h 中声明了相关函数,在 encode.c 中给出了实现。有两种方法:
1. 调用 EVP_EncodeBlock() 函数;用这种方法生成的编码需要解码时,应该调用 EVP_DecodeBlock()。
2. 依次调用 EVP_EncodeInit(), EVP_EncodeUpdate(), EVP_EncodeFinal()函数。EVP_EncodeUpdate(), EVP_EncodeFinal()中调用了 EVP_EncodeBlock()。
用这种方法生成的编码需要解码时,应该依次调用 EVP_DecodeInit(), EVP_DecodeUpdate(), EVP_DecodeFinal()。EVP_DecodeUpdate(), EVP_DecodeFinal()中调用了 EVP_DecodeBlock()。
上面两种方法会在编码末尾添加一个字符串结束符'\0',但是解码时不会在解码输出字符串的末尾添加'\0'。
RFC 2045中规定在 Multipurpose Internet Mail Extensions (MIME) 中使用 base64 编码时,生成的编码一行字符数上限为76。也就是说,最多在每 76 个 base64 编码字符后就要插入换行。
上面第 1 种编码方法一次性地对所有输入数据进行编码,不会插入换行。
如果编码结果要用于 MIME,必须考虑换行,这时应采用上面第 2 中编码方法。但 OpenSSL 不是每 76 个字符才换行,而是每 64 个字符就换行了,因为 76 是上限,只要不超过 76 就行,所以这样做也符合规范的要求。
换行有两种:Unix 及 Linux 下的换行是 0x0A;Windows 下的换行是 0x0D 0x0A(回车符和换行符)。OpenSSL 插入的换行是 0x0A。
base64 编码将 3 字节输入编码生成 4 字节输出,如果输入字符串长度不是 3 的整数倍,在编码时会进行填充。
解码时,调用 EVP_DecodeBlock() 获得的解码长度中包括了编码时填充的字符个数。用户为了获得精确长度,需要手动去掉了编码时填充的字符个数。
去除的方法也很简单:使用一个初值为 0 的计数器,查看 base64 编码的倒数第一个字符,如果是 = 将计数器值加 1,再查看倒数第二个字符是否是 = ,如果是 = 再将计数器的值加 1。由于 base64 编码的末尾最多会有两个 =,所以不用再往前查看了。用 EVP_DecodeBlock() 的返回值(表示未去除填充的解码结果字符串长度)减去计数器的值,就是编码前的原始数据的精确长度了。
而依次调用 EVP_DecodeInit(), EVP_DecodeUpdate(), EVP_DecodeFinal() 获得的解码结果长度中不包括编码时填充的字符个数。用户不需要手动去掉填充字符个数。
在测试程序时有一个发现:依次调用 EVP_DecodeInit(), EVP_DecodeUpdate(), EVP_DecodeFinal() 时,EVP_DecodeFinal() 只执行了一条 return(1); 语句,在 EVP_DecodeUpdate() 执行完毕后其实就完成了全部的解码操作,所以其后可以不必再调用 EVP_DecodeFinal(),只要依次调用 EVP_DecodeInit(), EVP_DecodeUpdate() 就够了。
OpenSSL编解码的两种实现方式有没有可能统一成一种呢?
为了试着做到这一点,我在 EVP_EncodeBlock() 和 EVP_DecodeBlock() 的基础上写了 EncodeBase64() 和 DecodeBase64() 两个函数,通过给 EncodeBase64() 的输入参数设置不同的值,可以得到不同的编码结果:不插入换行、插入换行、插入回车换行。还可以通过给 EncodeBase64() 的输入参数设置不同的值决定一行编码中的字符个数(例如 76 或 64)。 不管编码过程中有没有插入换行,插入何种换行, DecodeBase64() 都可以解码,因为它会忽略编码中所有的 0x0D 和 0x0A。
最后附上代码,相关函数能独立使用,在编译时不需要包含 OpenSSL 的头文件,也不用链接到 OpenSSL 的库。分为三个文件:
第1个文件
/************************************************** * File name: base64.h * Author: HAN Wei * Author's blog: http://blog.csdn.net/henter/ * Date: Oct 31th, 2013 * Description: declare base64 encode and decode functions When encoding is needed, EncodeBase64() function is invoked. The user can decide the count of characters in a full line (the max count is set to 76 in RFC 2045). The user can decide to insert 0x0A (line feed) or 0x0D (carriage return) 0x0A into the end of each full line, and can also choose insert nothing. When decoding is needed, DecodeBase64() function is invoked. 0x0A or 0x0D 0x0A in base64 codes will be ignored. * Note: EncodeBase64() and DecodeBase64() are derived from EVP_EncodeBlock() and EVP_DecodeBlock() in encode.c of OpenSSL 1.0.1c. OpenSSL web site: http://www.openssl.org/ **************************************************/ #ifndef BASE64_ENCODE_AND_DECODE_H #define BASE64_ENCODE_AND_DECODE_H #ifdef __cplusplus extern "C" { #endif /************************************************** *函数名称:CalcBase64EncodeBufLen *功能: 在使用Base64编码函数EncodeBase64()之前,该函数可以为函数调用者预先算出 存放编码结果的缓冲区大小 *参数: raw_data_len[in] 等待编码的原始数据长度,以字节为单位 line_feed_flag[in] 为0表示编码时不插入换行符, 为1表示编码时在换行处插入Unix或Linux下的换行0x0A 为2表示编码时在换行处插入Windows下的换行0x0D,0x0A characters_count_per_base64_codes_line[in] 一行中能容纳的base64编码字符个数上限, line_feed_flag值为0时该参数被忽略, 可设为任意值,因为此时base64编码不分行。 line_feed_flag值为1或2时,该参数才有意义。 根据RFC 2045的规定,该值最大可以设为76。 要得到与OpenSSL中依次调用EVP_EncodeInit(), EVP_EncodeUpdate(),EVP_EncodeFinal()函数后 生成的编码结果相同的编码时,将此值设为64, 因为EVP_EncodeUpdate()中将一行能容纳的 base64编码字符个数上限设为64 *返回值: 所需存放base64编码结果的缓冲区大小,以字节为单位。当返回值为 -1 时表示出错 **************************************************/ unsigned int CalcBase64EncodeBufLen(unsigned int raw_data_len, int line_feed_flag, unsigned int characters_count_per_base64_codes_line); /************************************************** *函数名称:CalcBase64DecodeBufLen *功能: 在使用Base64解码函数DecodeBase64()之前,该函数可以为函数调用者预先算出 存放解码结果的缓冲区大小 *参数: base64_codes[in] 指向待解码的base64字符串首地址的指针 base64_codes_Len[in] 等待解码的base64字符串长度,以字节为单位。当base64字符串以'\0'结尾时,该输入参数表示的字符串长度不包含'\0'所占的1个字节,仅为base64字符的总数(如果其中有回车或换行,在统计base64字符总数时要把回车及换行也计入) *返回值: 存放base64解码结果的缓冲区大小,以字节为单位 **************************************************/ unsigned int CalcBase64DecodeBufLen(unsigned char *base64_codes, unsigned int base64_codes_Len); /************************************************** *函数名称:EncodeBase64 *功能: base64编码函数 *参数: t[out] 指向输出缓冲区首地址的指针 f[in] 指向输入缓冲区首地址的指针 n[in] 待编码的字符串长度,如果该字符串以'\0'结尾,统计该长度时不计入'\0' line_feed_flag[in] 是否插入换行的标志,为0表示编码时不插入换行, 为1表示在一行编码字符末尾插入Unix或Linux下的换行0x0A, 为2表示在一行编码字符末尾插入Windows下的换行0x0D 0x0A characters_count_per_base64_codes_line[in] 一行中能容纳的base64编码字符个数上限, line_feed_flag值为0时该参数可设为任意值, 因为此时base64编码不分行。 line_feed_flag值为1或2时,该参数才有意义, 根据RFC 2045的规定,该值最大可以设为76。 要得到与OpenSSL中依次调用EVP_EncodeInit(),EVP_EncodeUpdate(),EVP_EncodeFinal() 生成的编码结果相同的编码时,应将此值设为64 *返回值: base64编码结果字符串长度,以字节为单位。。当返回值为 -1 时表示出错。 如果在编码过程中插入了换行,那么像回车0x0D, 换行0x0A这样的符号的个数也计入长度, 该函数会在编码结果字符串末尾加上一个'\0',但在计算返回值时,即统计编码字符串 长度时不计入'\0' **************************************************/ int EncodeBase64(unsigned char *t, unsigned char *f, int n, int line_feed_flag, unsigned int characters_count_per_base64_codes_line); /************************************************** *函数名称:DecodeBase64 *功能: base64解码函数 *参数: t 指向输出缓冲区首地址的指针 f 指向输入缓冲区首地址的指针 n 待解码的base64字符串长度,统计该长度时如果字符串以'\0'结尾,不计入'\0',但是 回车0x0D, 换行0x0A符号的个数也要计入长度,它们不会影响解码 注意: 对于编码时在一行末尾插入的换行可能有两种:Windows下的换行符0x0D 0x0A,或Unix (及Linux)下的换行符0x0A。这两种换行该函数都能正确解码,因为该函数会忽略这些符号 *返回值: 存放base64解码结果字符串长度,即未编码的字符串长度。 如果编码时进行过填充,填充的字符个数不被计入,用户无需再手动去除填充字符个数 **************************************************/ int DecodeBase64(unsigned char *t, unsigned char *f, int n); #ifdef __cplusplus } #endif #endif /* end of BASE64_ENCODE_AND_DECODE_H */
第2个文件
/************************************************** * File name: base64.c * Author: HAN Wei * Author's blog: http://blog.csdn.net/henter/ * Date: Oct 31th, 2013 * Description: implement base64 encode and decode functions * Note: EncodeBase64() and DecodeBase64() are derived from EVP_EncodeBlock() and EVP_DecodeBlock() in encode.c of OpenSSL 1.0.1c. OpenSSL web site: http://www.openssl.org/ **************************************************/ #include "base64.h" #include <stdio.h> #define conv_bin2ascii(a) (data_bin2ascii[(a)&0x3f]) #define conv_ascii2bin(a) (data_ascii2bin[(a)&0x7f]) #define B64_EOLN 0xF0 #define B64_CR 0xF1 #define B64_EOF 0xF2 #define B64_WS 0xE0 #define B64_ERROR 0xFF #define B64_NOT_BASE64(a) (((a)|0x13) == 0xF3) static const unsigned char data_bin2ascii[65]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; static const unsigned char data_ascii2bin[128]={ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xE0,0xF0,0xFF,0xFF,0xF1,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xE0,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0x3E,0xFF,0xF2,0xFF,0x3F, 0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B, 0x3C,0x3D,0xFF,0xFF,0xFF,0x00,0xFF,0xFF, 0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, 0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, 0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, 0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,0x20, 0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28, 0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30, 0x31,0x32,0x33,0xFF,0xFF,0xFF,0xFF,0xFF, }; unsigned int CalcBase64EncodeBufLen(unsigned int raw_data_len, int line_feed_flag, unsigned int characters_count_per_base64_codes_line) { unsigned int pure_base64_encode_len, pem_len, total=0; pure_base64_encode_len = (raw_data_len + 2) / 3 * 4; if (pure_base64_encode_len % characters_count_per_base64_codes_line == 0) pem_len = pure_base64_encode_len / characters_count_per_base64_codes_line; else pem_len = pure_base64_encode_len / characters_count_per_base64_codes_line + 1; switch (line_feed_flag) { case 0: total = pure_base64_encode_len+1; break; case 1: total = pure_base64_encode_len+pem_len+1; break; case 2: total = pure_base64_encode_len+pem_len*2 + 1; break; default: printf("invalid input parameter line_feed_flag=%d at %s, line %d!\n", line_feed_flag, __FILE__, __LINE__); return (-1); } return total; } unsigned int CalcBase64DecodeBufLen(unsigned char *base64_codes, unsigned int base64_codes_Len) { unsigned int i, line_feed_count=0, carriage_return_count=0, buffer_len=0; unsigned char *p; p=base64_codes; for (i=0; i<base64_codes_Len; i++) { if (p[i]==0x0D) carriage_return_count++; else if (p[i]==0x0A) line_feed_count++; } buffer_len = (base64_codes_Len - carriage_return_count-line_feed_count) / 4 * 3; return buffer_len; } int EncodeBase64(unsigned char *t, unsigned char *f, int n, int line_feed_flag, unsigned int characters_count_per_base64_codes_line) { int i, ret=0, counter=0; unsigned long l; int characters_count_per_raw_data_line, pure_base64_encode_len; characters_count_per_raw_data_line = characters_count_per_base64_codes_line / 4 * 3; pure_base64_encode_len = (n+2)/3*4; for (i=n; i>0; i-=3) { if (i >= 3) { l = ( ( (unsigned long)f[0] ) << 16L ) | ( ( (unsigned long)f[1] ) << 8L) | f[2]; *(t++)=conv_bin2ascii(l>>18L); *(t++)=conv_bin2ascii(l>>12L); *(t++)=conv_bin2ascii(l>> 6L); *(t++)=conv_bin2ascii(l ); } else { l=((unsigned long)f[0])<<16L; if (i == 2) l|=((unsigned long)f[1]<<8L); *(t++)=conv_bin2ascii(l>>18L); *(t++)=conv_bin2ascii(l>>12L); *(t++)=(i == 1)?'=':conv_bin2ascii(l>> 6L); *(t++)='='; } f+=3; ret+=4; counter+=3; if (line_feed_flag) { if (counter % characters_count_per_raw_data_line == 0) { switch (line_feed_flag) { case 1: *t=0x0a; t++; ret++; break; case 2: *t=0x0D; t++; *t=0x0a; t++; ret+=2; break; default: printf("invalid input parameter line_feed_flag=%d at %s, line %d!\n", line_feed_flag, __FILE__, __LINE__); return (-1); } } } } if (line_feed_flag) { if (pure_base64_encode_len % characters_count_per_base64_codes_line != 0) { switch (line_feed_flag) { case 1: *t=0x0a; t++; ret++; break; case 2: *t=0x0D; t++; *t=0x0a; t++; ret+=2; break; default: printf("invalid input parameter line_feed_flag=%d at %s, line %d!\n", line_feed_flag, __FILE__, __LINE__); return (-1); } } } *t='\0'; return(ret); } int DecodeBase64(unsigned char *t, unsigned char *f, int n) { int i, ret=0, a, b, c, d; unsigned long l; unsigned char *p, *encode_buf=f; int pad_count=0; /* trim white space from the start of the input buffer. */ while ((conv_ascii2bin(*f) == B64_WS) && (n > 0)) { f++; n--; } /* trim B64_WS, B64_EOLN, B64_CR, B64_EOF at the end of the input buffer */ while ((n > 3) && (B64_NOT_BASE64(conv_ascii2bin(f[n-1])))) n--; if (n<4) return (-1); /* count the padding */ p=&encode_buf[n-1]; for (i=0; i<2; i++) { if (*p == '=') { pad_count++; p--; } } #ifdef _DEBUG printf("pad_count=%d\n", pad_count); #endif /* base64 decoding */ for (i=0; i<n; i+=4) { a=conv_ascii2bin(*(f++)); b=conv_ascii2bin(*(f++)); c=conv_ascii2bin(*(f++)); d=conv_ascii2bin(*(f++)); if ((a & 0x80) || (b & 0x80) || (c & 0x80) || (d & 0x80)) return(-1); l=( (((unsigned long)a)<<18L) | (((unsigned long)b)<<12L) | (((unsigned long)c)<< 6L) | (((unsigned long)d)) ); *(t++)=(unsigned char)(l>>16L)&0xff; *(t++)=(unsigned char)(l>> 8L)&0xff; *(t++)=(unsigned char)(l )&0xff; ret+=3; /* ignore CR (0x0D) and LF (0x0A) */ if (*f == 0x0D) { f++; n--; } if (*f == 0x0A) { f++; n--; } } ret-=pad_count; return(ret); }
第3个文件
/************************************************** * File name: sample.c * Author: HAN Wei * Author's blog: http://blog.csdn.net/henter/ * Date: Mar 5th, 2013 * Description: This program demostrates how to perform base64 encoding and decoding. **************************************************/ #include "base64.h" #include <stdio.h> #include <stdlib.h> #include <string.h> #define MY_TEST_INPUT_DATA_LENGTH (unsigned int)(1024) /* 这是一个演示程序,先对一个字符数组中的全部元素进行base64编码,再解码 最后比较解码出来的结果是否与编码前的原始数据一致 */ int main(void) { unsigned char input[MY_TEST_INPUT_DATA_LENGTH]; unsigned char *encode_buffer=NULL, *decode_buffer=NULL; int i, input_len; int encode_buf_len, encode_len=0, decode_buf_len, decode_len=0; int my_line_feed_flag; /* 变量 my_line_feed_flag 值为0时,编码时不插入换行符; 值为1时,编码时插入Unix下的换行0x0A; 值为2时,编码时插入Windows下的回车换行0x0D 0x0A */ int max_value_per_base64_codes_line; my_line_feed_flag=2; max_value_per_base64_codes_line=76; /* 可以修改上面两个变量的值,分别将my_line_feed_flag的值设为0,1,2,再分别将 max_value_per_base64_codes_line的值设为64和76,看看base64编码结果有何区别 */ for (i=0; i<MY_TEST_INPUT_DATA_LENGTH; i++) memset(&input[i], i, 1); input_len=sizeof(input); printf("input data length is %d bytes.\n", input_len); printf("input data:\n"); for (i=0; i<MY_TEST_INPUT_DATA_LENGTH; i++) printf("0x%x ", input[i]); printf("\n"); // get the encode buffer length encode_buf_len=CalcBase64EncodeBufLen(input_len, my_line_feed_flag, max_value_per_base64_codes_line); printf("the encode buffer size is %d bytes.\n", encode_buf_len); if ( !(encode_buffer=(unsigned char *)malloc(encode_buf_len)) ) { printf("malloc function failed at %s, line %d!\n", __FILE__, __LINE__); #if defined(_WIN32) || defined(_WIN64) system("pause"); #endif return (-1); } encode_len=EncodeBase64(encode_buffer, input, input_len, my_line_feed_flag, max_value_per_base64_codes_line); printf("base64 codes length is %d bytes.\n", encode_len); printf("base64 codes:\n"); printf("%s\n", encode_buffer); // get the decode buffer length decode_buf_len = CalcBase64DecodeBufLen(encode_buffer, encode_len); printf("\nthe decode buffer size is %d bytes\n", decode_buf_len); if ( !(decode_buffer=(unsigned char *)malloc(decode_buf_len)) ) { printf("malloc function failed at %s, line %d!\n", __FILE__, __LINE__); free(encode_buffer); #if defined(_WIN32) || defined(_WIN64) system("pause"); #endif return (-1); } // base64 decoding if ( (decode_len=DecodeBase64(decode_buffer, encode_buffer, encode_len)) == -1 ) { printf("my_decode_base64 function failed!\n"); free(encode_buffer); free(decode_buffer); #if defined(_WIN32) || defined(_WIN64) system("pause"); #endif return (-1); } printf("decode result length is %d bytes.\n", decode_len); printf("decode result:\n"); for (i=0; i<decode_len; i++) printf("0x%x ", decode_buffer[i]); printf("\n"); // compare the decoding result with input if (decode_len != input_len) { printf("decode result length does not match input length at %s, line %d!\n", __FILE__, __LINE__); free(encode_buffer); free(decode_buffer); #if defined(_WIN32) || defined(_WIN64) system("pause"); #endif return (-1); } if ( memcmp(input, decode_buffer, input_len) ) { printf("decode result does not match input data at %s, line %d!\n", __FILE__, __LINE__); free(encode_buffer); free(decode_buffer); #if defined(_WIN32) || defined(_WIN64) system("pause"); #endif return (-1); } printf("base64 encoding and decoding succeed!\n"); free(encode_buffer); free(decode_buffer); #if defined(_WIN32) || defined(_WIN64) system("pause"); #endif return 0; }
提醒:
如果base64编码是存放在一个文件中,当编码中包含换行或回车时,一定要用二进制方式打开文件,再用fread( )之类的函数将文件内容读入内存,再进行解码。
在Windows平台上试验后发现:如果使用文本方式打开文件,使用fread( )函数时会自动忽略0x0D,只读入0x0A,这就会导致解码失败。
如果要把base64编码写入到一个文件中去,当编码中包含换行或回车时,一定要用二进制方式打开文件,再用fwrite( )之类的函数将编码写入文件。
在Windows平台上试验后发现:如果用文本方式打开文件,当要写入的base64编码只使用0x0A作为换行符时,使用fwrite( )函数时会自动在0x0A前面插入一个0x0D,导致实际写入的编码与期望不符。
如果要把base64解码结果写入到一个文件中去,一定要用二进制方式打开文件,再用fwrite( )之类的函数将解码结果写入文件。
在Windows平台上试验后发现:如果用文本方式打开文件,当解码结果中包含0x0A时,使用fwrite( )函数时会自动在0x0A前面插入一个0x0D,导致写入到文件中的解码结果出错。