银行安全传输平台(七)秘钥协商服务器和客户端实现-2(bug解决)

前言

这篇笔记我们主要解决在调试密钥协商服务器和客户端的时候遇到的一些问题,第一个是在签名时遇到的段错误,第二个是在密钥传输过程中遇到的段错误导致传输不稳定的问题.

一、签名时遇到的段错误

1.1 错误原因

在我们调试过程中会遇到服务器端会遇到这样的报错(客户端也可能会发生这种问题,概率问题):

在这里插入图片描述
这段报错是说在RSA公钥部分时遇到了段错误。分析一下,客户端使用私钥进行签名,然后把数据发送给服务器,然后服务器进行校验,然后用公钥进行加密,所以涉及到的只有这两个地方,到底时校验的时候出错了还是加密的时候出错了,我们不清楚,现在我们加入大量日志发现签名校验成功/失败根本没有输出,说明签名校验这一步压根就没有进入。
然后我们分析为什么签名校验失败,rsa加密有这么一个注意事项,rsa加解密操作数据不能太长,不能超过密钥长度,现在我们清楚了,原先签名的数据(reqMsg->data())数据太长,比密钥长(因为这个数据是进行base64编码之后写道磁盘里面的,不是原始公钥):

	// 读公钥文件
	ifstream ifs("public.pem");
	stringstream str;
	str << ifs.rdbuf();

所以要么签名的时候会出现段错误,要么校验的时候会出现段错误。

1.2 解决办法

要想把公钥的数据变短,很自然的办法就是将公钥数据(经过base64编码之后的)进行一次哈希运算,然后将哈希值进行签名,因为签名本身是为了认证,所以对哈希值进行签名并不影响签名本身的功能。
然后在服务器那边,将获得的公钥进行相同的哈希运算然后进行校验,就不会出现这种段错误了。
客户端:

	// 创建哈希对象
	Hash sha1(T_SHA1);
	sha1.addData(str.str());//str就是取出的公钥
	reqInfo.sign = rsa.rsaSign(sha1.result());	// 公钥的的哈希值签名
	cout << "签名完成..." << endl;

服务器:

	// 将收到的公钥数据写入本地磁盘
	ofstream ofs("public.pem");
	ofs << reqMsg->data();
	//把所有待写数据强制刷到磁盘,并关闭这个文件”,保证后面用 OpenSSL 读入时,文件里的公钥内容是完整且可用的
	ofs.close();//强制写数据
	// 创建非对称加密对象
	RespondInfo info;
	RsaCrypto rsa("public.pem", false);

	// 创建哈希对象
	Hash sha(T_SHA1);
	sha.addData(reqMsg->data());
	cout << "1111111111111111" << endl;
	bool bl = rsa.rsaVerify(sha.result(), reqMsg->sign());
	cout << "00000000000000000000" << endl;

1.3 解决一个疑问

有人会觉得,既然在写入磁盘时经过了base64编码,那么我在取出公钥时并没有进行对等的解码,为什么可以正常加解密和签名校验呢。
其实是这样的,你看到的 PEM 格式(Base64 编码)并不是直接交给 RSA_public_encrypt() 使用的原始密钥内容,而是被 OpenSSL 的 PEM_read_RSA_PUBKEY() 或 PEM_read_RSAPublicKey() 这样的函数处理并解码还原成 RSA* 对象,这个 RSA* 就能直接用于加密函数,如 RSA_public_encrypt()。如果我们手动从 JSON 或网络接收的是裸的 Base64 字符串(不是 PEM 文件),那确实需要你手动 Base64 解码。但本项目是通过 PEM 文件加载,因此自动完成了解码过程。
详细的可以自行了解openssl里面的BIO接口。

二、密钥传输过程中遇到的段错误

2.1 错误原因

在我们调试密钥协商过程的时候偶尔会出现这个错误:
在这里插入图片描述

调试过程,我们已经解决了签名和校验签名的段错误,所以这里应该不是签名的问题,所以我们先加入日志,看看是否是生成密钥方面的问题:

		string key = getRandKey(Len16);
		cout << "生成的随机秘钥: " << key << endl;
		// 2. 通过公钥加密
		cout << "aaaaaaaaaaaaaaaa" << endl;
		string seckey = rsa.rsaPubKeyEncrypt(key);
		cout << "加密之后的秘钥: " << seckey << endl;

然后通过运行我们发现了,生成随机密钥的过程并没有任何问题,每次都可以成功生成,但是这个密钥协商是否成功并不一定,有时候成功,有时候不成功,有时候会出现段错误。
由于有了第一步的经验,所以我们考虑二进制在传输的过程中会自动截断的问题。
调试思路: 加入大量日志,详细打印,把二进制转成十六进制,全部进行输出,发现,有些数据在服务器端生成了密文(密钥的密文)然后发送给客户端,发现会少一部分,然后数字符,发现遇到\0会截断。
我们生成的随机密钥(这里是aes16字节)都是ASCII可见的(对我们来说),但是我们加密之后(通过rsa加密)数据就变成了二进制数据,二进制里可能包含 \0(NUL)、控制字符、甚至非 UTF-8 序列,打印到终端或当作 C 字符串看就像“乱码”或突然截断。
那么现在我们总结一下:

  • 如果加密之后\0出现在中间位置,直接截断,只能收到一部分,求的字符串的长度的部分有问题,所以加密的时候会出现段错误。
  • 比较幸运,没有\0,那么程序正常执行。

2.2 解决方案

不能进行哈希,因为本身不是长度的问题,跟哈希八竿子打不着。
最简单的处理方式就是用base64对二进制数据进行编码,不管可见不可见都把他变为可见字符,在计算二进制字符串长度是就不会有\0出现。
base64编码过程:

string RsaCrypto::toBase64(const char* str, int len)
{
	BIO* mem = BIO_new(BIO_s_mem());
	BIO* bs64 = BIO_new(BIO_f_base64());
	// mem添加到bs64中
	bs64 = BIO_push(bs64, mem);
	// 写数据
	BIO_write(bs64, str, len);
	BIO_flush(bs64);
	// 得到内存对象指针
	BUF_MEM *memPtr;
	BIO_get_mem_ptr(bs64, &memPtr);
	string retStr = string(memPtr->data, memPtr->length - 1);
	BIO_free_all(bs64);
	return retStr;
}

base64解码过程:

char* RsaCrypto::fromBase64(string str)
{
	int length = str.size();
	BIO* bs64 = BIO_new(BIO_f_base64());
	BIO* mem = BIO_new_mem_buf(str.data(), length);
	BIO_push(bs64, mem);
	char* buffer = new char[length];
	memset(buffer, 0, length);
	BIO_read(bs64, buffer, length);
	BIO_free_all(bs64);

	return buffer;
}

找到二进制数据生成的那个位置(服务器端),公钥加密后立即进行base64编码:

string RsaCrypto::rsaPubKeyEncrypt(string data)
{
	cout << "加密数据长度: " << data.size() << endl;
	// 计算公钥长度
	int keyLen = RSA_size(m_publicKey);
	cout << "pubKey len: " << keyLen << endl;
	// 申请内存空间
	char* encode = new char[keyLen + 1];
	// 使用公钥加密
	//encode 缓冲区里存的是RSA算法生成的任意字节,其中零值字节会让字符串截断。
	int ret = RSA_public_encrypt(data.size(), (const unsigned char*)data.data(),
		(unsigned char*)encode, m_publicKey, RSA_PKCS1_PADDING);
	string retStr = string();
	if (ret >= 0)
	{
		// 加密成功
		cout << "ret: " << ret << ", keyLen: " << keyLen << endl;
		retStr = toBase64(encode, ret);
	}
	else
	{
		ERR_print_errors_fp(stdout);
	}
	// 释放资源
	delete[]encode;
	return retStr;
	//retStr里是ASCII范围内的Base64文本,无论里面有没有原始的\0,都不会被截断,可以安全地当普 C++字符串。
}

找到base64编码后的数据解码成二进制原始数据,然后正常进行解密等到aes密钥(客户端):


//新建Base64解码BIO和一个内存BIO(把Base64文本塞进去)
//读出解码后的原始字节流,长度与 RSA 密钥长度一致。
//得到的text缓冲区就是完整的二进制密文,无丢失,然后交给 OpenSSL做私钥解密,恢复出原始密钥
string RsaCrypto::rsaPriKeyDecrypt(string encData)
{
	// text指向的内存需要释放
	char* text = fromBase64(encData);
	// 计算私钥长度
	//cout << "解密数据长度: " << text.size() << endl;
	int keyLen = RSA_size(m_privateKey);
	// 使用私钥解密
	char* decode = new char[keyLen + 1];
	// 数据加密完成之后, 密文长度 == 秘钥长度
	int ret = RSA_private_decrypt(keyLen, (const unsigned char*)text,
		(unsigned char*)decode, m_privateKey, RSA_PKCS1_PADDING);
	string retStr = string();
	if (ret >= 0)
	{
		retStr = string(decode, ret);
	}
	else
	{
		cout << "私钥解密失败..." << endl;
		ERR_print_errors_fp(stdout);
	}
	delete[]decode;
	delete[]text;
	return retStr;
}

三、测试结果

在这里插入图片描述
可以看到密钥协商的结果现在没有问题了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值