0. 背景
最近,需要新做一个游戏demo,类似《部落冲突·皇室战争》的推塔玩法。客户端使用Unity,编程语言为C#,服务端使用C++。由于从零开始,需要建立基础部件,其中网络模块是最重要的模块之一。网络模块协议遵从如下图步骤时序图进行通信。
图1.通信步骤时序图
由上图可以看出网络通信主要分为两个步骤,第一步:客户端使用RSA加解密请求通信服务端,获取RC4秘钥;第二步:客户端与服务端使用RC4加解密通信。本文主要介绍第一步中在Server端(C++)的RSA加解密模块。
注:本文不叙述网络模块中的框架设计,只关注在客户端/服务端获得加密消息后,进行解密的技术环节。
1. RSA加密与解密
RSA为非对称加密方式,在C/C++中是基于使用OpenSSL库来实现。通过以下4个步骤来简单讲述。
a). 使用OpenSSL产生RSA秘钥
RSA的私钥包含了公钥的信息,所以首先通过以下命令产生长度为2048的RSA私钥。
openssl genrsa -out private.pem 2048
这样private.pem就会生成在当前目录下,使用以下命令就可以从private.pem提取出public.pem。
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
如此,就产生两个秘钥,不难猜出:
public.pem是使用PEM格式的公钥,而private.pem是对应的私钥。
b). 公钥加密与私钥解密
RSA是一种非对称的加解密方式。基于它的特性(wiki),它可以以公钥加密,然后私钥解密;也可以以私钥加密,公钥解密。本小节罗列下OpenSSL对于RSA提供的接口:
int RSA_public_encrypt(int flen, unsigned char *from,
unsigned char *to, RSA *rsa, int padding);
int RSA_private_decrypt(int flen, unsigned char *from,
unsigned char *to, RSA *rsa, int padding);
从函数名可以清晰地看出接口的功能。以RSA_public_encrypt
为例叙述参数,其他的接口可参考linux Doc:
- from: 待加密的字符串;
- flen: 待加密字符串长度;
- to: 加密完成后的字符串,其长度必须是RSA_size(rsa);
- rsa: 公钥;
- padding: 填充模式,有一下模式可供选择:
1)RSA_PKCS1_PADDING
:最常用的模式,待加密字符串长度flen < RSA_size(rsa)-11
;
2)RSA_PKCS1_OAEP_PADDING
:这种模式对于新应用也是推荐的,待加密字符串长度flen < RSA_size(rsa)-41
;
3)RSA_SSLV23_PADDING
:不常用模式;
4)RSA_NO_PADDING
:原RSA加密。这种模式只用于实现应用程序中的加密声音的填充模式。直接对用户数据RSA加密是不安全的,待加密字符串长度flen < RSA_size(rsa)
。
RSA_private_decrypt
接口真是从长度为flen的from字符串中使用私钥解密,并将解密后的字符存储在to中,而to字符串是指向一个足够存储解密字符串的buffer中,使用的填充模式padding是采取加密时的填充模式。
RSA_public_encrypt
接口返回的是加密后字符串的长度,而RSA_private_decrypt
接口返回的是解密后字符串的长度。如果加密/解密出错,返回-1。
我们可以通过以下函数封装,来使用公钥加密数据和私钥解密:
int padding = RSA_PKCS1_PADDING; //最常用的填充模式
char* pub_fp = $path_to_public_pem$;
char* pri_fp = $path_to_private_pem$;
// 公钥加密封装
int public_encrypt(unsigned char * data,int data_len,unsigned char * key, unsigned char *encrypted)
{
RSA * rsa = createRSAWithFilename(pub_fp,1);
int result = RSA_public_encrypt(data_len,data,encrypted,rsa,padding);
return result;
}
// 私钥解密封装
int private_decrypt(unsigned char * enc_data,int data_len,unsigned char * key, unsigned char *decrypted)
{
RSA * rsa = createRSAWithFilename(pri_fp,0);
int result = RSA_private_decrypt(data_len,enc_data,decrypted,rsa,padding);
return result;
}
注:公钥加密是可以使用所以的填充模式(padding mode).
其中createRSAWithFilename(char* filename, int nPublic)
接口是加载 a). 使用OpenSSL产生RSA秘钥的,filename
为公私钥的文件路径,nPublic
表示是否是公钥,具体实现如下:
RSA * createRSAWithFilename(char * filename,int nPublic)
{
FILE * fp = fopen(filename,"rb");
if(fp == NULL)
{
printf("Unable to open file %s \n",filename);
return NULL;
}
RSA *rsa= RSA_new() ;
if(nPublic)
{
rsa = PEM_read_RSA_PUBKEY(fp, &rsa,NULL, NULL);
}
else
{
rsa = PEM_read_RSAPrivateKey(fp, &rsa,NULL, NULL);
}
return rsa;
}
c). 私钥加密与公钥解密
下面是OpenSSL中私钥加密与公钥解密的接口:
int RSA_public_encrypt(int flen, unsigned char *from,
unsigned char *to, RSA *rsa, int padding);
int RSA_private_decrypt(int flen, unsigned char *from,
unsigned char *to, RSA *rsa, int padding);
接口详述参考 b).公钥加密与私钥解密,或是参考linux Doc。
d). 举个栗子
栗子代码如下:
//rsa_example.cpp
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/rsa.h>
#include <openssl/evp.h>
#include <openssl/bio.h>
#include <openssl/err.h>
#include <stdio.h>
int padding = RSA_PKCS1_PADDING;
RSA* createRSAWithFilename(const char*, int);
int public_encrypt(unsigned char * data,int data_len, const char* pub_fp, unsigned char *encrypted)
{
RSA * rsa = createRSAWithFilename(pub_fp,1);
int result = RSA_public_encrypt(data_len,data,encrypted,rsa,padding);
return result;
}
int private_decrypt(unsigned char * enc_data,int data_len, const char* pri_fp, unsigned char *decrypted)
{
RSA * rsa = createRSAWithFilename(pri_fp,0);
int result = RSA_private_decrypt(data_len,enc_data,decrypted,rsa,padding);
return result;
}
int private_encrypt(unsigned char * data,int data_len, const char* pri_fp, unsigned char *encrypted)
{
RSA * rsa = createRSAWithFilename(pri_fp,0);
int result = RSA_private_encrypt(data_len,data,encrypted,rsa,padding);
return result;
}
int public_decrypt(unsigned char * enc_data,int data_len, const char *pub_fp, unsigned char *decrypted)
{
RSA * rsa = createRSAWithFilename(pub_fp,1);
int result = RSA_public_decrypt(data_len,enc_data,decrypted,rsa,padding);
return result;
}
void printLastError(char *msg)
{
char * err = (char*)malloc(130);
ERR_load_crypto_strings();
ERR_error_string(ERR_get_error(), err);
printf("%s ERROR: %s\n",msg, err);
free(err);
}
RSA * createRSAWithFilename(const char * filename,int nPublic)
{
FILE * fp = fopen(filename,"rb");
if(fp == NULL)
{
printf("Unable to open file %s \n",filename);
return NULL;
}
RSA *rsa= RSA_new() ;
if(nPublic)
{
rsa = PEM_read_RSA_PUBKEY(fp, &rsa,NULL, NULL);
}
else
{
rsa = PEM_read_RSAPrivateKey(fp, &rsa,NULL, NULL);
}
return rsa;
}
int main(){
unsigned char plainText[2048/8] = "Hello this is tab_space";
unsigned char encrypted[4098]={};
unsigned char decrypted[4098]={};
const char* pub_fp = "/home/wsn/crypt/res/public.pem";
const char* pri_fp = "/home/wsn/crypt/res/private.pem";
int encrypted_length= public_encrypt(plainText,strlen((const char*)plainText),pub_fp,encrypted);
if(encrypted_length == -1)
{
printLastError((char*)"Public Encrypt failed ");
exit(0);
}
printf("Encrypted length =%d\n",encrypted_length);
int decrypted_length = private_decrypt(encrypted,encrypted_length, pri_fp, decrypted);
if(decrypted_length == -1)
{
printLastError((char*)"Private Decrypt failed ");
exit(0);
}
printf("Decrypted Text =%s\n",decrypted);
printf("Decrypted Length =%d\n",decrypted_length);
encrypted_length= private_encrypt(plainText,strlen((const char*) plainText),pri_fp,encrypted);
if(encrypted_length == -1)
{
printLastError((char*)"Private Encrypt failed");
exit(0);
}
printf("Encrypted length =%d\n",encrypted_length);
decrypted_length = public_decrypt(encrypted,encrypted_length,pub_fp, decrypted);
if(decrypted_length == -1)
{
printLastError((char*)"Public Decrypt failed");
exit(0);
}
printf("Decrypted Text =%s\n",decrypted);
printf("Decrypted Length =%d\n",decrypted_length);
}
编译代码:
[root@xxx crypt]# g++ -o bin/example -I/usr/include/openssl/ -lcrypto src/example.cpp
打印的结果:
[root@xxx crypt]# bin/example
Encrypted length =256
Decrypted Text =Hello this is tab_space
Decrypted Length =23
Encrypted length =256
Decrypted Text =Hello this is tab_space
Decrypted Length =23
2. 总结
本文讲述了在服务端(Linux/C++)使用OpenSSL库对通信消息进行RSA非对称加解密的技术过程,下一篇会讲述客户端(Unity/C#)使用OpenSSL库对通信消息进行RSA加解密的技术过程。栗子的代码上传至csdz的github中。