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;

  1. 输入数据最后编码是 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编码,可以多次调用,它编码ininl个字节的数据。并将输出编码结果存储在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_EncodeUpdateEVP_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_DecodeUpdateininl个字节的编码数据进行解码,解码后的数据存放在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的倍数,这是由于编解码前后数据位数的原因前面已经解释了。

该文章内容由作者“樱花狗子”提供,并非商业用途,转载请申明 私自转载将依法追究其法律责任

lionの金库