文章目录
前言
为了实现密钥的交换,我们需要将协商好的对称加密密钥使用RSA加密传输,这里我们就要用到OpenSSL库,因为博主本身在校期间已经花了很大一部分时间在学密码,所以本篇笔记就不再详细讲述RSA或者哈希函数背后的数论知识,只简单介绍接口,封装好我们所要用的模块即可。
一、OpenSSL
OpenSSL 是一个安全套接字层密码库,囊括主要的密码算法、常用的密钥和证书封装管理功能及SSL协议,并提供丰富的应用程序供测试或其它目的使用。
1.1 配置OpenSSL
因为这个项目主要实在Linux上运行,所以我主要记录Linux下如何配置,Windows稍微麻烦一点,感兴趣的可以自己了解一下。
安装包下载地址:https://www.openssl.org/source/
这里我以openssl-3.0.13为例
首先解压缩:
$ tar zxvf openssl-3.0.13.tar.gzip
# 加压完成得到目录: openssl-3.0.13
然后进入解压目录openssl-3.0.13,安装:
$ ./config
$ make
$ make test
$ make install (使用管理员权限)
然后验证是否安装成功:
openssl version -a
如果安装成功应该会输出版本号
二、密码学基础
这里我们简单记录一下密码学的一些基础概念,以理解这些东西在整个加密通信过程中做了些什么。
加密三要素、对称加密非对称加密这些概念不再介绍,主要理解对称加密效率高非对称加密效率低和几种常用的加密方式,我们在这个项目中,用rsa来进行密钥交换,用aes进行加密通信即可。
接下来我们主要介绍秘钥交换、数字前面、消息认证码、哈希算法等知识。
2.1 秘钥交换
假设我们有两个要通信的双方客户端c和服务端s
非对称加密秘钥分发方便, 但是效率低 -> 改进: 需要使用对称加密;
使用对称加密 -> 秘钥分发困难 -> 改进: 使用非对称加密进行秘钥分发。
- 分发是对称加密的秘钥, 本质就是一个字符串
- 在服务器端生成一个非对称加密的密钥对: 公钥, 私钥
- 服务器将公钥发送给客户端, 客户端有了公钥
- 在客户端生成一个随机字符串 -> 这就是对称加密需要使用的秘钥
- 在客户端使用公钥对生成的对称加密的秘钥进行加密 -> 密文
- 将加密的密文发送给服务器
- 服务器端使用私钥解密 -> 对称加密的秘钥
- 双方使用同一秘钥进行对称加密通信
2.2 哈希算法
简单说,不管原始数据多长,经过哈希算法计算,得到固定长度的数据,一个安全的哈希算法应该是抗碰撞的(很难找到两个数据对应到同一个数据),所以可以用来将进行勾走消息认证码HMAC。
一定要注意的是哈希算法不是加密算法。
2.3 消息认证码
主要作用是用来检验数据是否被篡改
(原始数据 + 秘钥) * 哈希函数 = 消息认证码
发送方将原始数据和消息认证码加密之后发送出去,接收方解密之后用同样的算法对原始数据进行哈希看看是否匹配,匹配则说明没有被篡改,否则说明被篡改了。
2.4 数字签名
用私钥签名公钥验证,一个安全的签名算法应该还要加上消息认证码,主要作用是验证发送者的身份,证明发送者是发送者。
三、封装密码学接口
关于openssl的api这里不再介绍,我们直接来看如何封装我们所要用到的接口。
首先还是要说明的是这里面有一个base64编码的用法我们留到下一篇笔记解释
3.1 RsaCrypto.h
#pragma once
#include <string>
#include <openssl/rsa.h>
#include <openssl/pem.h>
using namespace std;
enum SignLevel
{
Level1 = NID_md5,
Level2 = NID_sha1,
Level3 = NID_sha224,
Level4 = NID_sha256,
Level5 = NID_sha384,
Level6 = NID_sha512
};
class RsaCrypto
{
public:
RsaCrypto();
RsaCrypto(string fileName, bool isPrivate = true);
~RsaCrypto();
// 通过解析字符串得到秘钥
void parseKeyString(string keystr, bool pubKey = true);
// 生成RSA密钥对
void generateRsakey(int bits, string pub = "public.pem", string pri = "private.pem");
// 公钥加密
string rsaPubKeyEncrypt(string data);
// 私钥解密
string rsaPriKeyDecrypt(string encData);
// 使用RSA签名
string rsaSign(string data, SignLevel level = Level3);
// 使用RSA验证签名
bool rsaVerify(string data, string signData, SignLevel level = Level3);
// base64编码
private:
string toBase64(const char* str, int len);
// base64解码
char* fromBase64(string str);
// 得到公钥
bool initPublicKey(string pubfile);
// 得到私钥
bool initPrivateKey(string prifile);
private:
RSA* m_publicKey; // 私钥
RSA* m_privateKey; // 公钥
};
由于c++中不建议使用宏,所以我们使用常量/枚举/内联->空间换时间,这里我们默认用sha224来进行签名和认证。
3.2 RsaCrypto.cpp
#include "RsaCrypto.h"
#include <openssl/bio.h>
#include <openssl/err.h>
#include <iostream>
#include <openssl/buffer.h>
#include <string.h>
RsaCrypto::RsaCrypto()
{
m_publicKey = RSA_new();
m_privateKey = RSA_new();
}
RsaCrypto::RsaCrypto(string fileName, bool isPrivate)
{
m_publicKey = RSA_new();
m_privateKey = RSA_new();
if (isPrivate)
{
initPrivateKey(fileName);
}
else
{
initPublicKey(fileName);
}
}
RsaCrypto::~RsaCrypto()
{
RSA_free(m_publicKey);
RSA_free(m_privateKey);
}
// 将公钥/私钥字符串数据解析到 RSA 对象中
void RsaCrypto::parseKeyString(string keystr, bool pubKey)
{
// 字符串数据 -> BIO对象中
BIO* bio = BIO_new_mem_buf(keystr.data(), keystr.size());
// 公钥字符串
if (pubKey)
{
PEM_read_bio_RSAPublicKey(bio, &m_publicKey, NULL, NULL);
}
else
{
// 私钥字符串
PEM_read_bio_RSAPrivateKey(bio, &m_privateKey, NULL, NULL);
}
BIO_free(bio);
}
void RsaCrypto::generateRsakey(int bits, string pub, string pri)
{
RSA* r = RSA_new();
// 生成RSA密钥对
// 创建bignum对象
BIGNUM* e = BN_new();
// 初始化bignum对象
BN_set_word(e, 456787);
RSA_generate_key_ex(r, bits, e, NULL);
// 创建bio文件对象
BIO* pubIO = BIO_new_file(pub.data(), "w");
// 公钥以pem格式写入到文件中
PEM_write_bio_RSAPublicKey(pubIO, r);
// 缓存中的数据刷到文件中
BIO_flush(pubIO);
BIO_free(pubIO);
// 创建bio对象
BIO* priBio = BIO_new_file(pri.data(), "w");
// 私钥以pem格式写入文件中
PEM_write_bio_RSAPrivateKey(priBio, r, NULL, NULL, 0, NULL, NULL);
BIO_flush(priBio);
BIO_free(priBio);
// 得到公钥和私钥
m_privateKey = RSAPrivateKey_dup(r);
m_publicKey = RSAPublicKey_dup(r);
// 释放资源
BN_free(e);
RSA_free(r);
}
bool RsaCrypto::initPublicKey(string pubfile)
{
// 通过BIO读文件
BIO* pubBio = BIO_new_file(pubfile.data(), "r");
// 将bio中的pem数据读出
if (PEM_read_bio_RSAPublicKey(pubBio, &m_publicKey, NULL, NULL) == NULL)
{
ERR_print_errors_fp(stdout);
return false;
}
BIO_free(pubBio);
return true;
}
bool RsaCrypto::initPrivateKey(string prifile)
{
// 通过bio读文件
BIO* priBio = BIO_new_file(prifile.data(), "r");
// 将bio中的pem数据读出
if (PEM_read_bio_RSAPrivateKey(priBio, &m_privateKey, NULL, NULL) == NULL)
{
ERR_print_errors_fp(stdout);
return false;
}
BIO_free(priBio);
return true;
}
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];
// 使用公钥加密
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;
}
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;
}
string RsaCrypto::rsaSign(string data, SignLevel level)
{
unsigned int len;
char* signBuf = new char[1024];
memset(signBuf, 0, 1024);
int ret = RSA_sign(level, (const unsigned char*)data.data(), data.size(), (unsigned char*)signBuf,
&len, m_privateKey);
if (ret == -1)
{
ERR_print_errors_fp(stdout);
}
cout << "sign len: " << len << ", ret: " << ret << endl;
string retStr = toBase64(signBuf, len);
delete[]signBuf;
return retStr;
}
bool RsaCrypto::rsaVerify(string data, string signData, SignLevel level)
{
// 验证签名
int keyLen = RSA_size(m_publicKey);
char* sign = fromBase64(signData);
int ret = RSA_verify(level, (const unsigned char*)data.data(), data.size(),
(const unsigned char*)sign, keyLen, m_publicKey);
delete[]sign;
if (ret == -1)
{
ERR_print_errors_fp(stdout);
}
if (ret != 1)
{
return false;
}
return true;
}
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;
}
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;
}
要想解释清楚这个模块,还是要介绍一下里面一些比较陌生的api,如下:
#include <openssl/rsa.h>
// 申请一块内存, 存储了公钥和私钥
// 如果想得到RSA类型变量必须使用 RSA_new();
RSA *RSA_new(void);
void RSA_free(RSA *);
BIGNUM* BN_new(void);
void BN_free(BIGNUM*);
// 生成密钥对, 密钥对存储在内存中
int RSA_generate_key_ex(RSA *rsa, int bits, BIGNUM *e, BN_GENCB *cb);
参数:
- rsa: 通过RSA_new()获得
- bits: 秘钥长度, 单位: bit, 常用的长度 1024*n (n正整数)
- e: 比较大的数(5位以内)
- 通过 BN_new 得到对应的变量
- 初始化: BN_set_word(e, 12345);
- cb: 回调函数, 用不到, 直接写NULL
// rsa公钥私钥类型是一样的: RSA类型
// 将参数rsa中的公钥提取出来
RSA *RSAPublicKey_dup(RSA *rsa);
- rsa参数: 秘钥信息
- 返回值: rsa公钥
// 将参数rsa中的私钥提取出来
RSA *RSAPrivateKey_dup(RSA *rsa);
- rsa参数: 秘钥信息
- 返回值: rsa私钥
// 创建bio对象
// 密钥对写磁盘文件的时候, 需要编码 -> base64
// 封装了fopen
BIO *BIO_new_file(const char *filename, const char *mode);
参数:
- filename: 文件名
- mode: 文件打开方式和fopen打开方式的指定相同
int PEM_write_bio_RSAPublicKey(BIO* bp, const RSA* r);
int PEM_write_bio_RSAPrivateKey(BIO* bp, const RSA* r, const EVP_CIPHER* enc,
unsigned char* kstr, int klen, pem_password_cb *cb, void* u);
RSA* PEM_read_bio_RSAPublicKey(BIO* bp, RSA** r, pem_password_cb *cb, void* u);
RSA* PEM_read_bio_RSAPrivateKey(BIO* bp, RSA** r, pem_password_cb *cb, void* u);
参数:
- bp: 通过BIO_new_file();函数得到该对象
- r: 传递一个RSA* rsa指针的地址, 传出参数-> 公钥/私钥
- cb: 回调函数, 用不到, 指定为NULL
- u: 给回调传参, 用不到, 指定为NULL
//
//
RSA* PEM_read_RSAPublicKey(FILE* fp, RSA** r, pem_password_cb *cb, void* u);
RSA* PEM_read_RSAPrivateKey(FILE* fp, RSA** r, pem_password_cb *cb, void* u);
// 写入文件中的公钥私钥数据不是原始数据, 写入的编码之后的数据
// 是一种pem的文件格式, 数据使用base64进行编码
int PEM_write_RSAPublicKey(FILE* fp, const RSA* r);
int PEM_write_RSAPrivateKey(FILE* fp, const RSA* r, const EVP_CIPHER* enc,
unsigned char* kstr, int klen, pem_password_cb *cb, void* u);
参数:
- fp: 需要打开一个磁盘文件, 并且指定写权限
- r: 存储了密钥对
- 私钥独有的参数
- enc: 指定的加密算法 -> 对称加密 -> NULL
- kstr: 对称加密的秘钥 -> NULL
- klen: 秘钥长度 -> 0
- cb: 回调函数, 用不到, NULL
- u: 给回调传参, 用不到, NULL
所以整个代码块看下来主要完成了这几个任务:生成密钥对、公钥加密、私钥解密、数据签名、验证签名。至于toBase64和fromBase64就是为了解决我们的加密后的密钥在传输过程中解决的一些问题,这都是后话了,所以整体来看,这段代码可以抽象至如下表示:
class MyRSA
{
public:
MyRSA();
~MyRSA;
// 生成密钥对
// 公钥加密
// 私钥解密
// 数据签名
// 验证签名
private:
RSA* pubkey;
RSA* pirKey;
}
四、关于hsah模块的说明
我们在这个项目中其实还封装了一个hash模块来调用,不过那是到最后封装外联接口支持网点之间直接用aes加解密的情况了,所以我们也是放到之后在做记录。