BIO是OpenSSL的一个重要的结构和概念,是对数据IO与传递处理的一种类型抽象和功能封装,这里所说的数据IO与传递的“介质”包括:内存、文件、日志、标准设备、网络socket等,“处理”包括简单、加/解密、摘要、ssl协议下的读/写等的数据变换。
本文主要介绍了BIO的结构和使用方法,并以示例方式给出了一种对BIO的封装,可为下一步与socket的相关操作结合,编写自定义的“加密/解密流”。
本文示例适用于openssl3.0+。
1.概念
BIO是针对数据传递而设计的逻辑结构,因此它基本的概念模型是“管道”:数据由BIO一端进入,另一端流出,中间也可能进行数据变换。这样的“管道”结构可以依据是否有“缓存”,出入口等条件进行划分。
OpenSSL中将BIO分为两种类型:
- Filter BIO:就是纯管道型BIO,数据不能保存在其中。
- source/sink BIO:自带有容器的BIO,数据进行缓存,如果进一步细分,source BIO就是只出不进BIO,sink BIO就是只进不出BIO。
两种类型的BIO的概念示意图如上图所示。可以想象,如果将BIO首尾连接起来,就会构成BIO链,也如上图所示。BIO链在使用上,虽然仅对首尾BIO进行了读写操作,但是这种操作是会依次传递给下一个BIO的,因而BIO链逻辑上看作是一个复杂的BIO。
2.类型
OpenSSL中已经预定义了若干种BIO,直接可以使用它们。
主要Source/Sink类型BIO
- BIO_s_file/ BIO_s_fd:文件BIO,BIO_s_file对应FILE*,而BIO_s_fd对应POSIX文件描述符,它们可用于写入和读取文件。
- BIO_s_socket:网络socketBIO,用于通过网络进行通信。
- BIO_s_null: 空BIO,类似/dev/null,只能写入,读取数据会导致EOF。
- BIO_s_mem:内存BIO,用于写入和读取内存。
- BIO_s_bio:一种特别的BIO,被称为BIO pair,后文单独说明。
主要Filter类型BIO
- BIO_f_base64:base64 BIO,通过此BIO的BIO_write将数据编码为base64格式, BIO_read通过此BIO解码base64格式的数据。
- BIO_f_cipher:密码BIO,通过它的数据会被加/解密,密码算法可以设置。
- BIO_f_md:摘要计算BIO,它不会修改通过它的数据,而仅计算流经其中的数据摘要,摘要算法可以设置,使用特殊功能检索计算出的摘要。
- BIO_f_buffer:缓冲BIO,它也不会更改通过它的数据。写入此BIO的数据被缓冲,因此并非每次对该BIO的写入操作都会导致将数据写入下一个BIO。至于阅读,情况类似。这样可以减少位于缓冲IO后面的BIO上的IO操作数。
- BIO_f_ssl :SSL/TLS 协议BIO,通过它的数据会按照协议规则进行加解密
3.基本使用函数
创建/释放函数为:
BIO *BIO_new(const BIO_METHOD *)
BIO_free_all(BIO *)
设置/控制基本函数为:
BIO_ctrl(BIO *,int,long ,void *)
以此函数为基础,定义了一些方便使用的宏:BIO_reset,BIO_tell,BIO_eof,BIO_flush等
读写操作的基本函数为:
int BIO_read_ex(BIO *, void *, size_t, size_t *)
int BIO_write_ex(BIO *, const void *, size_t, size_t *)
对于BIO链,bio_st结构中有变量next_bio,prev_bio,可以指向其前后的BIO,这也是BIO链式操作的基础。BIO链的基本操作函数为:
BIO * BIO_push(BIO *a,BIO *b);
BIO * BIO_pop(BIO *b);
前者将b链接到a之后,返回b,后者将b从链条上摘除,返回b,原来的链条依然完整。
BIO还有一些辅助函数,例如处理错误,获取状态等函数。
4.BIO Pair
BIO对是一种比较特别的BIO,它由两个BIO组成,但从它的实现代码来看,它似乎是与BIO平行的一种实现方式,因而不能单纯的用BIO链来说明,它的逻辑结构如下图所示。
BIO pair连接两个外部端A,B,从外部来看,A端写,则可从B端读出;B端写,则可从A端读出。从内部来看,有两个内存型BIOA和BIOB,分别与A端和B端相连,它们有各自的缓存,A端存入和B端读取的数据,利用的是BIOA缓存,B端写入A端读取则利用的是BIOB的缓存。
因此BIO pair类似于“双向有缓存管道”,从任一端写入,另一端读出,由于内存缓存的利用,使得一端的读/写操作都是“即刻”完成的,它不用关心另一端什么时候做写/读操作,这就是典型的异步操作。目前很多网络通信库采用的都是异步读写,因而BIO pair这种应用模型是OpenSSL适配这些网络库的一个重要方法。
创建BIO pair的简洁方法是BIO_new_bio_pair,它实际上是 BIO_new, BIO_make_bio_pair, BIO_set_write_buf_size的组合。
5.构造自定义BIO类型
OpenSSL已定义了若干BIO,当然也可以自定义一个。下面的示例构造了一个简单的CMYBIO,以此来说明BIO的工作原理。
class CMYBIO
{
private:
static int write_ex(BIO* h, const char* buf, size_t num, size_t * len)
{
printf("CMYBIO::write_ex\n");
for (int i = 0; i < num; ++i) printf("%c", buf[i]);
printf("\n");
*len = num;
return 1;
}
static int read_ex(BIO* h, char* buf, size_t size, size_t* len)
{
printf("CMYBIO::read_ex\n");
unsigned int* opt = (unsigned int*)BIO_get_data(h);
if (*opt == 0)
{
size = size > 6 ? 6 : size;
memcpy(buf, "openss", size);
*len = size;
// BIO_clear_retry_flags(h);
// BIO_copy_next_retry(h);
return 1;
}
if (*opt == 1)
{
size = 9;
memcpy(buf, "MTIzNDU2\n", size); //base64("123456")
*len = size;
BIO_clear_retry_flags(h);
// BIO_copy_next_retry(h);
return 1;
}
return 0;
}
static long ctrl(BIO* h, int cmd, long arg1, void* arg2)
{
printf("CMYBIO::ctrl[%d]\n", cmd);
if (cmd == 0xff)
{
unsigned int* opt = (unsigned int*)BIO_get_data(h);
*opt = arg1;
}
return 1;
}
static int create(BIO* bio)
{
printf("CMYBIO::create\n");
BIO_set_init(bio, 1); //”init” must be set explicitly,otherwise “read”/”write” methods will be invoked. Although it seems odd, the “framework” would not do It for you by the returned value.
unsigned int* opt =(unsigned int*) malloc(sizeof(int));
*opt = 0;
BIO_set_data(bio,opt);
return 1;
}
static int destory(BIO* bio)
{
printf("CMYBIO::destory\n");
unsigned int* opt=(unsigned int*)BIO_get_data(bio);
free(opt);
return 1;
}
static BIO_METHOD* method;
public:
static BIO_METHOD* BIO_s_my()
{
return CMYBIO::method;
}
static void UnInit()
{
BIO_meth_free(CMYBIO::method);
}
static void Init(void)
{
CMYBIO::method = BIO_meth_new((100 | BIO_TYPE_SOURCE_SINK), "My BIO");
int r;
r = BIO_meth_set_create(CMYBIO::method, CMYBIO::create);
r = BIO_meth_set_destroy(CMYBIO::method, CMYBIO::destory);
r = BIO_meth_set_write_ex(CMYBIO::method, CMYBIO::write_ex);
r=BIO_meth_set_read_ex(CMYBIO::method, CMYBIO::read_ex);
r=BIO_meth_set_puts(CMYBIO::method, nullptr);
r=BIO_meth_set_gets(CMYBIO::method, nullptr);
r=BIO_meth_set_ctrl(CMYBIO::method, CMYBIO::ctrl);
r=BIO_meth_set_callback_ctrl(CMYBIO::method, nullptr);
}
};
BIO_METHOD* CMYBIO::method=nullptr;
下面的代码使用了上面构造的CMYBIO,其中第一段是单独使用CMYBIO,第二段是与BIO_f_base64组成链式应用。CMYBIO作为单独应用,构造的功能已经够用,但组成链式应用,由于链式调用需要在每个具体的实现方法内来完成,因此上面的代码还不够,为简化,这里仅把CMYBIO作为链式应用的最后一级来使用。
long bio_cb(BIO* b, int oper, const char* argp, size_t len, int argi, long argl, int ret, size_t* processed)
{
printf("bio callback:%d, %u\n", oper,(unsigned int)len);
return 1;
}
void test_bio()
{
BIO* bmy = NULL;
size_t s = 0;
int len = 0;
char* out = NULL;
char cc[24];
memset(cc, 0, sizeof(cc));
CMYBIO::Init();
bmy = BIO_new(CMYBIO::BIO_s_my());
BIO_set_callback_ex(bmy, bio_cb);
len = BIO_write_ex(bmy, "openssl", 7, &s);
printf("BIO_write_ex return [%d, %u]\n\n", len, (unsigned int)s);
len = 7;
out = (char*)OPENSSL_malloc(len);
memset(out, 0, len);
len = BIO_read_ex(bmy, out, len-1, &s);
printf("BIO_read_ex return [%d,%u]%s\n", len, (unsigned int)s, out);
OPENSSL_free(out);
BIO_free(bmy);
printf("--------------------------------\n");
BIO* b64 = BIO_new(BIO_f_base64());
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
bmy = BIO_new(CMYBIO::BIO_s_my());
BIO_push(b64, bmy);
len=BIO_write_ex(b64, "123456", 6, &s);
printf("BIO_write_ex return [%d, %u]\n\n", len, (unsigned int)s);
BIO_flush(b64); //important!!!
BIO_ctrl(bmy, 0xff, 1, nullptr);
len = BIO_read_ex(b64, cc, sizeof(cc)- 1, &s);
printf("BIO_read_ex return [%d,%u]%s\n\n", len, (unsigned int)s, cc);
//OPENSSL_free(out);
BIO_free_all(b64);
CMYBIO::UnInit();
}
整个过程输出如下图所示。前一个示例主要展示CMYBIO中各方法的调用过程,其中还设置了“钩子”函数,从输出可以清晰看出BIO的工作过程。后一个示例稍有些麻烦,其中增加了CMYBIO内部的设置,从而可以实现不同的效果。
需要注意的是,BIO_f_base64有其特殊之处,对于“write”,需要在完成后调用flush才能正确工作,对于“read”,编码串结束的标志是“\n”,但还是要设置BIO_set_flags(b, BIO_FLAGS_BASE64_NO_NL),否则可能会有错误。