20.6 OpenSSL 套接字分发RSA公钥

通过上一节的学习读者应该能够更好的理解RSA加密算法在套接字传输中的使用技巧,但上述代码其实并不算完美的,因为我们的公钥和私钥都必须存储在本地文本中且公钥与私钥是固定的无法做到更好的保护效果,而一旦公钥与私钥泄密则整个传输流程都将会变得不安全,最好的保护效果是RSA密钥在每次通信时都进行变换,依次来实现随机密钥对的功能。

20.6.1 RSA算法封装

要实现这个效果我们就需要封装一套可以在内存中生成密钥对的函数,当需要传输数据时动态的生成密钥对,并将公钥部分通过套接字传输给对应的客户端,当客户端收到公钥后则可以使用该公钥进行通信,此时公钥与私钥全程不会存储为文件,这能极大的提升RSA算法的安全性。

要实现内存传输则首先需要封装实现RSA内存生成密钥对函数GenerateMemoryRSAKeys,以及rsa_encrypt加密函数,rsa_decrypt解密函数,读者可自行理解并使用如下代码片段。

#include <iostream>
#include <Windows.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/crypto.h>

extern "C"
{
#include <openssl/applink.c>
}

#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib,"libssl_static.lib")
#pragma comment(lib,"libcrypto.lib")

// 生成RSA需要的公钥和私钥
BOOL GenerateMemoryRSAKeys(char** private_key, char** public_key, int key_length)
{
// 生成Key函数
RSA* keypair = RSA_generate_key(key_length, 3, NULL, NULL);

BIO* pri = BIO_new(BIO_s_mem());
BIO* pub = BIO_new(BIO_s_mem());

// 生成写出私钥
if (!PEM_write_bio_RSAPrivateKey(pri, keypair, NULL, NULL, 0, NULL, NULL))
{
return FALSE;
}

// 生成写出公钥
if (!PEM_write_bio_RSAPublicKey(pub, keypair))
{
return FALSE;
}

size_t pri_len = BIO_pending(pri);
size_t pub_len = BIO_pending(pub);

// 分配内存存储公钥与私钥
char* prikey = (char*)malloc(pri_len + 1);
char* pubkey = (char*)malloc(pub_len + 1);

if (prikey == NULL && pubkey == NULL)
{
return FALSE;
}

// 将公钥与私钥读入到堆中
BIO_read(pri, prikey, pri_len);
BIO_read(pub, pubkey, pub_len);

*private_key = prikey;
*public_key = pubkey;

RSA_free(keypair);
BIO_free_all(pri);
BIO_free_all(pub);
return TRUE;
}

// RSA 加密函数
// type=public 使用公钥加密 type=private 使用私钥加密
BOOL rsa_encrypt(char* pub_key, char* msg, char** encrypt, int* encrypt_len, char *type)
{
RSA* rsa = NULL;
BIO* keybio = BIO_new_mem_buf((void*)pub_key, -1);
char* err = (char*)malloc(130);

if (keybio == NULL)
{
return FALSE;
}

// 如果是public则使用公钥加密/如果是private则使用私钥
if (strcmp(type, "public") == 0)
{
// PEM_read_bio_RSA_PUBKEY(keybio, NULL, NULL, NULL)
if (!(rsa = PEM_read_bio_RSAPublicKey(keybio, NULL, NULL, NULL)))
{
return FALSE;
}
}
else if (strcmp(type, "private") == 0)
{
// 读取私钥文件
if (!(rsa = PEM_read_bio_RSAPrivateKey(keybio, NULL, NULL, NULL)))
{
return FALSE;
}
}

*encrypt_len = RSA_size(rsa);
*encrypt = (char*)malloc(4096);

if (strcmp(type, "public") == 0)
{
// 使用公钥加密
if ((RSA_public_encrypt(strlen(msg) + 1, (unsigned char*)msg, (unsigned char*)*encrypt, rsa, RSA_PKCS1_PADDING)) == -1)
{
return FALSE;
}
}
else if (strcmp(type, "private") == 0)
{
// 使用私钥加密
if ((RSA_private_encrypt(strlen(msg) + 1, (unsigned char*)msg, (unsigned char*)*encrypt, rsa, RSA_PKCS1_PADDING)) == -1)
{
return FALSE;
}
}

RSA_free(rsa);
free(err);
BIO_free_all(keybio);
return TRUE;
}

// RSA 解密函数
// type=public 使用公钥解密 type=private 使用私钥解密
BOOL rsa_decrypt(char* pri_key, char* msg, char** decrypt, int encrypt_len, char *type)
{
RSA* rsa = NULL;
BIO* keybio = BIO_new_mem_buf(pri_key, -1);
if (keybio == NULL)
{
return FALSE;
}

// 如果是public则使用公钥解密/如果是private则使用私钥
if (strcmp(type, "public") == 0)
{
// 读入公钥文件
if (!(rsa = PEM_read_bio_RSAPublicKey(keybio, NULL, NULL, NULL)))
{
return FALSE;
}
}
else if (strcmp(type, "private") == 0)
{
// PEM_read_bio_RSA_PRIVATE(keybio, NULL, NULL, NULL)
if (!(rsa = PEM_read_bio_RSAPrivateKey(keybio, NULL, NULL, NULL)))
{
return FALSE;
}
}

char* err = (char*)malloc(130);
*decrypt = (char*)malloc(encrypt_len);

if (strcmp(type, "public") == 0)
{
// 使用公钥解密
if (RSA_public_decrypt(encrypt_len, (unsigned char*)msg, (unsigned char*)*decrypt, rsa, RSA_PKCS1_PADDING) == -1)
{
return FALSE;
}
}
else if (strcmp(type, "private") == 0)
{
// 私用私钥解密
if (RSA_private_decrypt(encrypt_len, (unsigned char*)msg, (unsigned char*)*decrypt, rsa, RSA_PKCS1_PADDING) == -1)
{
return FALSE;
}
}

RSA_free(rsa);
free(err);
BIO_free_all(keybio);
return TRUE;
}

int main(int argc, char *argv)
{
// 生成内存RSA密钥对
char *private_key, *public_key;

if (GenerateMemoryRSAKeys(&private_key, &public_key, 2048))
{
std::cout << "生成私钥: " << private_key << std::endl;
std::cout << "生成公钥: " << public_key << std::endl;
}

char *encrypt, *decrypt;
int encrypt_length;
BOOL flag;

// 公钥加密
flag = rsa_encrypt(public_key, (char*)"hello lyshark", &encrypt, &encrypt_length, (char *)"public");
if (flag == TRUE)
{
std::cout << "[公钥加密] 公钥加密字节: " << strlen(encrypt) << std::endl;
}

// 私钥解密
flag = rsa_decrypt(private_key, encrypt, &decrypt, encrypt_length, (char *)"private");
if (flag == TRUE)
{
std::cout << "[私钥解密] 私钥解密字节: " << decrypt << std::endl;
}

// 私钥加密
flag = rsa_encrypt(private_key, (char*)"hello lyshark", &encrypt, &encrypt_length, (char*)"private");
if (flag == TRUE)
{
std::cout << "[私钥加密] 私钥加密字节: " << strlen(encrypt) << std::endl;
}

// 公钥解密
flag = rsa_decrypt(public_key, encrypt, &decrypt, encrypt_length, (char*)"public");
if (flag == TRUE)
{
std::cout << "[公钥解密] 公钥解密字节: " << decrypt << std::endl;
}

system("pause");
return 0;
}

读者可自行编译上述代码并运行,此时该代码将通过GenerateMemoryRSAKeys函数生成内存密钥对,并调用rsa_encryptrsa_decrypt两个函数实现对特定字符串的加解密功能,输出效果图如下;

20.6.2 公钥动态配对

有了上述内存生成RSA密钥对的方法,那么实现密钥对远程分发将变得很容易实现,首先我们来看客户端的实现方式,当客户端成功连接到了服务端则首先接收服务端传来的公钥,当收到服务器传来的公钥后通过使用rsa_encrypt函数并用公钥对待发送字符串进行加密,加密后调用send将加密数据发送给服务端,解密动作与加密保持一致,同样使用公钥进行解密,这段客户端代码如下所示;

int main(int argc, char* argv[])
{
char buf[256] = "The National Aeronautics and Space Administration";

WSADATA WSAData;

// 初始化套接字库
if (WSAStartup(MAKEWORD(2, 0), &WSAData))
{
return 0;
}

// 创建套接字
SOCKET client_socket;
client_socket = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in ClientAddr;
ClientAddr.sin_family = AF_INET;
ClientAddr.sin_port = htons(9999);
ClientAddr.sin_addr.s_addr = inet_addr("127.0.0.1");

// 链接远程服务器
if (connect(client_socket, (LPSOCKADDR)&ClientAddr, sizeof(ClientAddr)) != SOCKET_ERROR)
{
// 接收公钥
char public_key[1024] = { 0 };
int recv_key_flag = recv(client_socket, public_key, 1024, 0);
if (recv_key_flag > 0)
{
std::cout << "接收公钥字节: " << public_key << std::endl;
}

// 公钥加密并发送数据
char* encrypt = nullptr;
int encrypt_length = 0;

rsa_encrypt(public_key, buf, &encrypt, &encrypt_length, (char*)"public");
std::cout << "[服务端发送] 公钥加密字节: " << strlen(encrypt) << std::endl;
send(client_socket, encrypt, encrypt_length, 0);

// 公钥接收并解密数据
char* decrypt = nullptr;

memset(buf, 0, 256);
recv(client_socket, buf, 256, 0);
rsa_decrypt(public_key, buf, &decrypt, 256, (char *)"public");
std::cout << "[服务端返回] 原始数据包: " << decrypt << std::endl;

closesocket(client_socket);
WSACleanup();
}

system("pause");
return 0;
}

与客户端相比,服务端在执行时只是多出来了执行GenerateMemoryRSAKeys函数的功能,通过执行该函数我们可以得到一个动态的内存加密密钥对,有了密钥对则我们就可以使用私钥对数据进行加密与解密操作,如下是服务端核心实现代码;

int main(int argc, char* argv[])
{
WSADATA WSAData;

// 初始化套接字库
if (WSAStartup(MAKEWORD(2, 0), &WSAData))
{
return 0;
}

// 创建套接字
SOCKET server_socket;
server_socket = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in ServerAddr;
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_port = htons(9999);
ServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1");

// 绑定并侦听套接字
bind(server_socket, (LPSOCKADDR)&ServerAddr, sizeof(ServerAddr));
listen(server_socket, 10);

// 生成RSA密钥对
char* private_key, *public_key;
BOOL gen_flag = GenerateMemoryRSAKeys(&private_key, &public_key, 2048);
if (gen_flag == TRUE)
{
// std::cout << "生成私钥: " << private_key << std::endl;
// std::cout << "生成公钥: " << public_key << std::endl;
std::cout << "[+] 已生成RSA密钥对" << std::endl;
}

// 接收请求
SOCKET message_socket;
if ((message_socket = accept(server_socket, (LPSOCKADDR)0, (int*)0)) != INVALID_SOCKET)
{
// 发送公钥给客户端
int put_key_flag = send(message_socket, public_key, strlen(public_key), 0);
if (put_key_flag > 0)
{
std::cout << "本地私钥字节: " << private_key << std::endl;
std::cout << "发送公钥字节: " << public_key << std::endl;
}

// 私钥解密: 接收并解密
char recv_message[256] = { 0 };
recv(message_socket, recv_message, 256, 0);

char* decrypt = nullptr;
rsa_decrypt(private_key, recv_message, &decrypt, 256, (char*)"private");
std::cout << "[客户端返回] 原始数据包: " << decrypt << std::endl;

// 私钥加密: 加密并发送
char send_message[256] = "hello lyshark";
char* encrypt = nullptr;
int encrypt_length = 0;

rsa_encrypt(private_key, send_message, &encrypt, &encrypt_length, (char*)"private");
send(message_socket, encrypt, encrypt_length, 0);
}
closesocket(server_socket);
WSACleanup();

system("pause");
return 0;
}

读者可自行编译并运行上述代码,首先运行服务端接着运行客户端,读者则可看到如下图所示的输出信息;