OpenSSL源码笔记--base64编解码
---基于openssl1.1.1g
源码
base64编解码
BASE64
编码是一种常用的将十六进制数据转换为可见字符的编码。与 ASCII
码相比,它占用的空间较小。BASE64
编码在 rfc3548
中定义。
原理
将数据编码成 BASE64 编码时,以 3 字节数据为一组,转换为 24bit 的二进制数,将 24bit的二进制数分成四组,每组 6bit。对于每一组,得到一个数字:0-63。然后根据这个数字查表即得到结果。表如下:
Value Encoding Value Encoding Value Encoding Value Encoding
0 A 17 R 34 i 51 z
1 B 18 S 35 j 52 0
2 C 19 T 36 k 53 1
3 D 20 U 37 l 54 2
4 E 21 V 38 m 55 3
5 F 22 W 39 n 56 4
6 G 23 X 40 o 57 5
7 H 24 Y 41 p 58 6
8 I 25 Z 42 q 59 7
9 J 26 a 43 r 60 8
10 K 27 b 44 s 61 9
11 L 28 c 45 t 62 +
12 M 29 d 46 u 63 /
13 N 30 e 47 v
14 O 31 f 48 w (pad) =
15 P 32 g 49 x
16 Q 33 h 50 y
比如有数据:0x30 0x82 0x02
编码过程如下:
1)得到 16 进制数据: 30 82 02
2)得到二进制数据: 00110000 10000010 00000010
3)每 6bit 分组: 001100 001000 001000 000010
4)得到数字: 12 8 8 2
5)根据查表得到结果 : M I I C
BASE64 填充:在不够的情况下在右边加 0。
有三种情况:
1) 输入数据比特数是 24 的整数倍(输入字节为 3 字节整数倍),则无填充;
2) 输入数据最后编码的是 1 个字节(输入数据字节数除 3 余 1),即 8 比特,则需要填充 2 个"==",因为要补齐 6 比特,需要加 2 个 00;
- 输入数据最后编码是 2 个字节(输入数据字节数除 3 余 2),则需要填充 1 个"=",因为补齐 6 比特,需要加一个 00。
举例如下:
对 0x30 编码:
1) 0x30 的二进制为:00110000
2) 分组为:001100 00
3) 填充 2 个 00:001100 000000
4) 得到数字:12 0
5) 查表得到的编码为 MA,另外填充两个==
所以最终编码为:MA==
base64 解码是其编码过程的逆过程。解码时,将 base64 编码根据表展开,根据有几个等号去掉结尾的几个 00,然后每 8 比特恢复即可。
在分析主要函数之前,我们需要先了解一个核心会用到的结构体EVP_ENCODE_CTX
:
/* ossl_typ.h */
typedef struct evp_Encode_Ctx_st EVP_ENCODE_CTX;
/* crypto/evp/evp_local.h */
struct evp_Encode_Ctx_st {
/* number saved in a partial encode/decode */
int num;
/*
* The length is either the output line length (in input bytes) or the
* shortest input line length that is ok. Once decoding begins, the
* length is adjusted up each time a longer line is decoded
*/
int length;
/* data to encode */
unsigned char enc_data[80];
/* number read on current line */
int line_num;
unsigned int flags;
};
各个字段的含义均有作者注释,其实现在不太理解也没关系,因为我们接下来要用到的地方并没有多么关注结构体的内部成员,况且高版本的openssl把结构体已经封装起来不允许访问内部成员,在调用时定义结构体指针使用即可 。
编码函数
EVP_EncodeInit()函数
void EVP_EncodeInit(EVP_ENCODE_CTX *ctx){
ctx->length = 48;
ctx->num = 0;
ctx->line_num = 0;
ctx->flags = 0;
}
这个函数主要是用来编码前进行初始化上下文,只传入了一个EVP_ENCODE_CTX
指针,很好理解。
EVP_EncodeUpdate()函数
int EVP_EncodeUpdate(EVP_ENCODE_CTX *ctx, unsigned char *out, int *outl, const unsigned char *in, int inl);
这个函数主要用来进行base64编码,可以多次调用,它编码in
中inl
个字节的数据。并将输出编码结果存储在out
中,输出长度为outl
,而ctx
即上一个函数初始化之后的对象,用来存储未被处理的剩余部分数据。
注意到这个函数有一个整型的返回值:返回0失败,返回1成功。
其中,函数的调用方需要保证输出缓冲区out
的大小足以容纳输出数据,这个函数只能立即处理和输出完整的数据块(48字节)。任何剩余部分都保存在 ctx
对象中,并通过随后对 EVP_EncodeUpdate()
或 EV _EncodeFinal()
的调用进行处理,这就是前面为什么说可以多次调用的原因。
而要计算所需的输出缓冲区大小,需要将 inl
值与 ctx
中保存的未处理数据量相加,然后将结果除以48(忽略任何余数)。这给出了将要处理的数据块的数量,确保输出缓冲区为每个块包含65个字节的存储,另外为 NULL
终止符包含一个额外的字节。可以重复调用EVP_EncodeUpdate()
来处理大量输入数据。如果出现错误,EVP_EncodeUpdate()
将把 *outl
设置为0并返回0。
EVP_EncodeFinal()函数
void EVP_EncodeFinal(EVP_ENCODE_CTX *ctx, unsigned char *out, int *outl)
必须在编码操作结束时调用 EVP_EncodeFinal()
。它将处理 ctx
对象中剩余的任何部分数据块,也就是说,明文不足48字节的部分才会到这里处理,上面EVP_EncodeUpdate()
处理不了。输出数据将被存储在输出中,所写数据的长度将被存储在 * outl
中。调用者有责任确保 out
的足够大容纳输出数据,而输出数据不会超过65字节,外加一个额外的 NULL
终止符(即总共66字节)。
示例
编写一个测试程序一试便知:
#include <string.h>
#include <openssl/evp.h>
typedef struct
{
int num;
int length;
unsigned char enc_data[80];
int line_num;
unsigned int flags;
} ENCODE_CTX; // 自定义一个结构体用来承载原始EVP_ENCOD_CTX
int main()
{
ENCODE_CTX ectx;
unsigned char in[500], out[800]; // 分别存储编码前后的数据
int inl, outl, i, total;
EVP_EncodeInit((EVP_ENCODE_CTX*)&ectx); // 初始化上下文
for (i = 0; i < 500; i++)
memset(&in[i], i, 1); // 填充待编码的数据
inl = 500;
total = 0;
EVP_EncodeUpdate((EVP_ENCODE_CTX*)&ectx, out, &outl, in, inl); // 只处理整块的65字节并累加长度
total += outl;
printf("%d\t%d\n", outl, total);
EVP_EncodeFinal((EVP_ENCODE_CTX*)&ectx, out + total, &outl); // 处理剩余不足65字节的数据
total += outl;
printf("%s%d\n%d\n", out, outl, total);
return 0;
}
观察一下运行结果:
kongds@kongds ~/Desktop/C$ ./base64
650 650
AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4v
MDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5f
YGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6P
kJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/
wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v
8PHy8/T19vf4+fr7/P3+/wABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f
ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU5P
UFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ent8fX5/
gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6v
sLGys7S1tre4ubq7vL2+v8DBwsPExcbHyMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f
4OHi4+Tl5ufo6err7O3u7/Dx8vM=
29
679
从输出结果来看,上面每行编码后的数据为64字节,但其实每行数据后面追加了一个终止符,在调用完EVP_EncodeUpdate
之后,可以观察到此时输出编码长度outl为650字节,total用来记录最终的编码长度,在调用完成EVP_EncodeFinal
之后,这次编码的长度为28,输出为29,同样是由于最后追加一个终止符。
这是格式化的编码,即每次编码最多只处理64个字节,其实上述最终编码长度只有668字节,但因为每次处理后面都会添加终止符,所以最终的输出长度为679。重点体会EVP_EncodeUpdate
、EVP_EncodeFinal
搭配使用的逻辑,如果需要,可能需要自己去除追加的终止符。
解码函数
EVP_DecodeInit()函数
void EVP_DecodeInit(EVP_ENCODE_CTX *ctx)
{
/* Only ctx->num and ctx->flags are used during decoding. */
ctx->num = 0;
ctx->length = 0;
ctx->line_num = 0;
ctx->flags = 0;
}
同编码前初始化上下文一样,这个操作也是在解码之前进行上下文初始化,很好理解。
EVP_DecodeUpdate()函数
int EVP_DecodeUpdate(EVP_ENCODE_CTX *ctx, unsigned char *out, int *outl, const unsigned char *in, int inl)
同样,EVP_DecodeUpdate
对in
中inl
个字节的编码数据进行解码,解码后的数据存放在out
中,长度存储在outl
。调用方需要确保 out 的缓冲足够大能够容纳输出数据。
这个函数将尝试解码尽可能多的4字节块数据,为什么是四字节,因为编码时的基本单位是三字节的明文被编码成4字节的base64码。任何空格、换行符或回车符都会被忽略。保留在末尾的任何未处理数据(1、2或3字节)的部分块将保存在 ctx 对象中,并通过随后对 EVP_DecodeUpdate
的调用进行处理。如果遇到任何非法的以64为基数的字符,或者如果在数据中间遇到以64为基数的填充字符“ =”,那么函数将返回 -1以指示错误。返回值0或1表示数据处理成功。返回值为0另外表示处理的最后一个输入数据字符包括以64为基数的填充字符“ =”,因此预计不会处理更多的非填充字符数据。对于每处理4个有效的基64字节(忽略空格、回车和换行符) ,将生成3个字节的二进制输出数据(或者在使用了填充字符“ =”的数据末尾生成更少的数据)。
EVP_DecodeFinal()函数
int EVP_DecodeFinal(EVP_ENCODE_CTX *ctx, unsigned char *out, int *outl)
EVP_DecodeFinal()
必须在解码操作结束时调用。如果 ctx 中还有任何未处理的数据,则输入数据不能是4的倍数,否则会因此发生错误。在这种情况下,该函数将返回 -1。否则函数成功返回1。
示例
关于这两个函数,看下面示例解释:
#include <string.h>
#include <openssl/evp.h>
typedef struct
{
int num;
int length;
unsigned char enc_data[80];
int line_num;
unsigned int flags;
} ENCODE_CTX;
int main()
{
ENCODE_CTX ectx, dctx;
unsigned char in[500], out[800], d[600]; // 分别存储编码前、后、解码后的数据
int inl, outl, total, ret, total2;
/* 编码操作 */
EVP_EncodeInit((EVP_ENCODE_CTX*)&ectx);
// 用ASCII的可显示部分填充明文
int index;
for (int i = 0; i < 500; i++){
index = 48 + i % 78;
memset(&in[i], index, 1);
}
printf("%s\n", in);
inl = 500;
total = 0;
EVP_EncodeUpdate((EVP_ENCODE_CTX*)&ectx, out, &outl, in, inl); // 只处理整块的65字节并累加长度
total += outl;
EVP_EncodeFinal((EVP_ENCODE_CTX*)&ectx, out + total, &outl);
total += outl;
printf("%s%d\n", out, outl);
/* 解码操作 */
EVP_DecodeInit((EVP_ENCODE_CTX*)&dctx);
outl = 0;
total2 = 0;
ret = EVP_DecodeUpdate((EVP_ENCODE_CTX*)&dctx, d, &outl, out, total);
printf("%d\n", outl);
total2 += outl;
ret = EVP_DecodeFinal((EVP_ENCODE_CTX*)&dctx, d, &outl);
printf("%d\n", outl);
total2 += outl;
printf("%s\n", d);
return 0;
}
相对上面的编码操作,增加了相应的解码操作,并更改了明文填充的数据内容(这里说明一下:上面编码操作因为最终所有的数据都会以base64编码显示,但是其填充的数据虽然也在ASCII范围内,但却有些数据不能显示出来,在使用%s
进行控制的时候,遇到这类控制字符会被截断,导致不能完整输出内容,所以这里采用了ASCII值在48~126这个可显示的范围内取值填充,这样最终也可以输出解码的明文查看对比)。
观察下结果:
kongds@kongds ~/Desktop/C$ gcc base64.c -o base64 -lssl -lcrypto
kongds@kongds ~/Desktop/C$ ./base64
0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNO
MDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5f
YGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9MDEyMzQ1Njc4OTo7PD0+P0BB
QkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3Bx
cnN0dXZ3eHl6e3x9MDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJT
VFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9MDEyMzQ1
Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2Rl
ZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9MDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZH
SElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3
eHl6e3x9MDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZ
WltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9MDEyMzQ1Njc4OTo7
PD0+P0BBQkNERUZHSElKS0xNTk8=
29
500
0
0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNO
观察结果可以看出:因为被解码的数据除去编码时追加的终止符,剩余的字节数为668,是4的整倍数,所以在调用EVP_DecodeUpdate()
之后,所有的数据就被处理完成了,返回的长度为500,即解码后的数据字节数,所以并没有剩余的数据再交给EVP_DecodeFinal()
处理,至此,可以完整输出解码后的数据,其实还可以加一句对比编解码前后的数据,是可以看出数据完全一致的。
memcmp(in, d, toatal2);
上面说道,上述格式化的编码操作,每一个数据块之后都会追加一个终止符,如果我想要完整的原始编码输出呢,是否需要自己去除终止符,其实没必要,因为还有以下两个函数可以进行整块数据的编解码:
其他处理函数
EVP_EncodeBlock()函数
int EVP_EncodeBlock(unsigned char *t, const unsigned char *f, int dlen);
EVP_EncodeBlock()
将一个完整的输入数据块 f,长度为 dlen,编码后将其存储为 t。每3个字节的输入将产生4个字节的输出数据。如果 dlen 不能被3整除,那么该块将被编码为最终的数据块,并且输出将被填充以使其始终可被4整除。此外,还将添加一个 NULL 终止符字符。例如,如果提供了16字节的输入数据,那么就创建了24字节的编码数据,加上 NUL 终止符的1字节(即总共25字节)。而函数的返回数据为不加终止符的编码数据的长度。
EVP_DecodeBlock()函数
int EVP_DecodeBlock(unsigned char *t, const unsigned char *f, int n);
EVP_DecodeBlock()
将对 f 中包含的以64为基数的数据的 n 个字符块进行解码,并将结果存储在 t 中。任何前导空格将被修剪,任何尾随的空格、换行符、回车符或 EOF 字符也将被修剪。在这样的修整之后,f 中数据的长度必须被4整除。每4个输入字节正好生成3个输出字节。如果需要,输出将用0位填充,以确保每4个输入字节的输出总是3个字节。这个函数将返回已解码数据的长度,如果出错则返回 -1,注意这个解码后的长度有填充的终止位,这是因为已编码的数据一般可以保证是4的倍数,但是原始的数据不能保证是3的倍数,所以会将其先解码为3的倍数的数据,可能会有终止符。
示例
#include <string.h>
#include <openssl/evp.h>
int main()
{
unsigned char in[500], out[800], d[500], *p;
int inl, len, pad;
inl = 500;
int index;
for (int i = 0; i < 500; i++){
index = 48 + i % 78;
memset(&in[i], index, 1);
}
len = EVP_EncodeBlock(out, in, inl);
printf("%s\n%d\n", out, len); // 输出编码后的数据和其长度
p = out + len - 1;
pad = 0;
for (int i = 0; i < 4; i++) // 处理填充位数
{
if (*p == '=')
pad++;
p--;
}
len = EVP_DecodeBlock(d, out, len);
printf("%s\n%d\n", d, len); // 输出解码后的数据和其长度
len -= pad; // 减去填充位数
if ((len != inl) || (memcmp(in, d, len))){ // 与编码前的数据进行对比
printf("err!\n");
return -1;
}
printf("test ok.\n");
return 0;
}
看下结果:
kongds@kongds ~/Desktop/C$ ./base64_2
MDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9MDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9MDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9MDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9MDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9MDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9MDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk8=
668
0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}0123456789:;<=>?@ABCDEFGHIJKLMNO
501
test ok.
可以看到原始数据编码后长度为668,与我们前面格式化处理计算的一致,而解码的数据长度确是501,为3的倍数,这是由于编解码前后数据位数的原因前面已经解释了。
Comments | 1 comment
Blogger Merrilee Jannsen
Your post is well-written. I enjoyed reading it. Thank you for posting.