Qt和OpenSSL进行base64编解码开发

一、基本用法

1
2
3
4
5
6
7
// 发送端对加密后的数据使用base64编码
Base64 base64;
QByteArray encryptData = base64.enCode(reinterpret_cast<char*>(out), outLen);

// 接收端先对数据进行base64解码
Base64 base64;
data = base64.deCode(data);

必要头文件

1
2
3
#include <openssl/bio.h>
#include <openssl/buffer.h>
#include <openssl/evp.h>

二、Base64编码

2.1、BIO_new

创建 BIO 对象

1
BIO *BIO_new(const BIO_METHOD *method);

参数解释:

  • method :BIO_METHOD 结构体指针,表示要使用的 BIO 方法,要获取一个 BIO_METHOD* 指针,你可以使用 OpenSSL 库中一组已经定义好的 BIO_METHOD 对象。这些 BIO_METHOD 对象通常以 BIO_s_ 开头,表示特定类型的 BIO 操作方法

    以下是几个常用的 BIO_METHOD 对象及其对应的函数:

    • BIO_f_base64():用于base64编码

    • BIO_s_file():用于文件 I/O 操作。

    • BIO_s_mem():用于内存缓冲区的读写操作。

    • BIO_s_socket():用于套接字 I/O 操作。

返回值:返回一个BIO对象指针

1
2
3
// 创建bio对象
BIO* base64 = BIO_new(BIO_f_base64());
BIO* mem = BIO_new(BIO_s_mem());

2.2、BIO_push

用于将两个 BIO 对象连接起来的函数,该函数将 next 对象串联在 bio 对象之后,形成一个 BIO 对象链。

1
BIO *BIO_push(BIO *bio, BIO *next);

参数解释:

  • bio:当前使用的 BIO 对象
  • next:要连接的下一个 BIO 对象

需要注意的是,当连接多个 BIO 对象时,应将它们以正确的顺序连接起来,确保数据可以按照正确的顺序流经整个 BIO 对象链。例如,如果需要在内存缓冲区和文件之间进行数据传输,应该先将内存缓冲区 BIO 对象链接到文件 BIO 对象之后,这样在写入数据时,数据会先写入内存缓冲区,然后再写入文件。

1
2
// 组织bio链
BIO_push(base64, mem);

2.3、BIO_write

用于在 BIO 对象中写入数据的函数

1
int BIO_write(BIO *bio, const void *buf, int len);

参数解释:

  • 要写入数据的 BIO 对象指针 bio
  • 要写入的数据缓冲区指针 buf
  • 要写入的数据长度 len

返回值:函数返回一个整数,表示实际写入的数据长度。

1
2
3
// 数据编码
BIO_write(base64, data, length);
BIO_flush(base64);

2.4、BIO_flush

将缓冲区中的数据写入磁盘

1
int BIO_flush(BIO *bio);

参数解释:

  • bio:指向要刷新的 BIO 对象的指针。

返回值:

  • 成功:返回 1。
  • 失败:返回 0。

2.5、BIO_get_mem_ptr

用于获取内存 BIO 对象中的数据指针和数据长度的函数。该函数会返回一个 BIO_MEM_PTR 结构体,包含了指向数据的指针和数据的长度。需要注意的是,获取到的数据指针 ptr 指向的数据是内存 BIO 对象中的数据,因此在使用数据时需要保证内存 BIO 对象的有效性,避免发生悬空指针的问题。

1
int BIO_get_mem_ptr(BIO *bio, BUF_MEM **pp);

参数解释:

  • bio:之前写入数据的BIO对象
  • pp:BUF_MEM对象,BIO_get_mem_ptr函数会将编码后的数据和数据的长度写入到pp

返回值:

  • 1:成功
  • 0:失败
1
2
3
4
// 把编码后的数据读出来
BUF_MEM* ptr;
BIO_get_mem_ptr(base64, &ptr);
QByteArray str(ptr->data, ptr->length);

2.6、BIO_free_all

用于释放 BIO 对象以及其关联资源的函数。该函数会递归地释放与 BIO 对象相关联的所有资源,包括底层的文件描述符、内存缓冲区等。

1
void BIO_free_all(BIO *bio);

参数解释:

  • bio:前面创建的base64编码的BIO对象
1
BIO_free_all(base64);

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
QByteArray Base64::enCode(const char* data, int length)
{
// 创建bio对象
BIO* base64 = BIO_new(BIO_f_base64());
BIO* mem = BIO_new(BIO_s_mem());
// 组织bio链
BIO_push(base64, mem);
// 数据编码
BIO_write(base64, data, length);
BIO_flush(base64);
// 把编码后的数据读出来
BUF_MEM* ptr;
BIO_get_mem_ptr(base64, &ptr);
QByteArray str(ptr->data, ptr->length);
BIO_free_all(base64);
return str;
}

三、Base64解码

3.1、BIO_new

创建 BIO 对象

1
BIO *BIO_new(const BIO_METHOD *method);

参数解释:

  • method :BIO_METHOD 结构体指针,表示要使用的 BIO 方法,要获取一个 BIO_METHOD* 指针,你可以使用 OpenSSL 库中一组已经定义好的 BIO_METHOD 对象。这些 BIO_METHOD 对象通常以 BIO_s_ 开头,表示特定类型的 BIO 操作方法

    以下是几个常用的 BIO_METHOD 对象及其对应的函数:

    • BIO_f_base64():用于base64编码

    • BIO_s_file():用于文件 I/O 操作。

    • BIO_s_mem():用于内存缓冲区的读写操作。

    • BIO_s_socket():用于套接字 I/O 操作。

返回值:返回一个BIO对象指针

1
2
3
// 创建bio对象
BIO* base64 = BIO_new(BIO_f_base64());
BIO* mem = BIO_new(BIO_s_mem());

3.2、BIO_push

用于将两个 BIO 对象连接起来的函数,该函数将 next 对象串联在 bio 对象之后,形成一个 BIO 对象链。

1
BIO *BIO_push(BIO *bio, BIO *next);

参数解释:

  • bio:当前使用的 BIO 对象
  • next:要连接的下一个 BIO 对象

需要注意的是,当连接多个 BIO 对象时,应将它们以正确的顺序连接起来,确保数据可以按照正确的顺序流经整个 BIO 对象链。例如,如果需要在内存缓冲区和文件之间进行数据传输,应该先将内存缓冲区 BIO 对象链接到文件 BIO 对象之后,这样在写入数据时,数据会先写入内存缓冲区,然后再写入文件。

1
2
// 组织bio链
BIO_push(base64, mem);

3.3、BIO_write

用于在 BIO 对象中写入数据的函数

1
int BIO_write(BIO *bio, const void *buf, int len);

参数解释:

  • 要写入数据的 BIO 对象指针 bio
  • 要写入的数据缓冲区指针 buf
  • 要写入的数据长度 len

返回值:函数返回一个整数,表示实际写入的数据长度。

1
2
// 将待解码的数据写入mem节点
BIO_write(mem, data, length);

2.5、BIO_read

用于从 BIO 对象中读取数据的函数

1
int BIO_read(BIO *bio, void *buf, int len);

参数解释:

  • 要读取数据的 BIO 对象指针 bio
  • 用于存储读取数据的缓冲区指针 buf
  • 要读取的最大数据长度 len

返回值:实际读取的数据长度,需要注意的是,BIO_read 函数可能会返回 0,表示已经没有更多数据可供读取。此外,如果返回的读取数据长度小于预期的长度 len,应当根据实际需求进行处理。

1
2
3
4
// 解码,数据存入buf
char* buf = new char[length];
int result = BIO_read(base64, buf, length);
QByteArray out(buf, result);

2.6、BIO_free_all

用于释放 BIO 对象以及其关联资源的函数。该函数会递归地释放与 BIO 对象相关联的所有资源,包括底层的文件描述符、内存缓冲区等。

1
void BIO_free_all(BIO *bio);

参数解释:

  • bio:前面创建的base64编码的BIO对象
1
BIO_free_all(base64);

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
QByteArray Base64::deCode(const char* data, int length)
{
// 创建bio对象
BIO* base64 = BIO_new(BIO_f_base64());
BIO* mem = BIO_new(BIO_s_mem());
// 组织bio链
BIO_push(base64, mem);
// 将待解码的数据写入mem节点
BIO_write(mem, data, length);
// 解码,数据存入buf
char* buf = new char[length];
int result = BIO_read(base64, buf, length);
QByteArray out(buf, result);
BIO_free_all(base64);
delete[] buf;
return out;
}

Qt和OpenSSL进行RSA非对称加解密开发(3)--数据加解密

4.1使用公钥加密

4.1.1、EVP_PKEY_CTX_new

用于创建与给定密钥对象(EVP_PKEY)相关联的密钥上下文(EVP_PKEY_CTX)。

1
EVP_PKEY_CTX *EVP_PKEY_CTX_new(EVP_PKEY *pkey, ENGINE *e);

参数解释:

  • pkey:与上下文关联的密钥对象。这可以是一个公钥、私钥或对称密钥对象,具体取决于使用场景。
  • e:可选参数,与上下文关联的引擎(Engine)。如果不需要使用特定引擎,可以传入 NULL

返回值

  • EVP_PKEY_CTX 类型的指针,即新创建的密钥上下文对象
1
2
3
// 创建加密数据的上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(m_publicKey, NULL);
assert(ctx != NULL);

需要注意的是,使用完密钥上下文后应该调用 EVP_PKEY_CTX_free 函数来释放相应的资源,以避免内存泄漏。

4.1.2、EVP_PKEY_encrypt_init

用于初始化使用非对称密钥进行加密操作

1
int EVP_PKEY_encrypt_init(EVP_PKEY_CTX *);

参数解释:

  • ctx:要进行加密操作的密钥上下文

返回值

  • 1:成功
  • 0:失败
1
2
int ret = EVP_PKEY_encrypt_init(ctx);
assert(ret == 1);
4.1.3、EVP_PKEY_CTX_set_rsa_padding

用于设置 RSA 加密或解密操作的填充方式

1
int EVP_PKEY_CTX_set_rsa_padding(EVP_PKEY_CTX *ctx, int pad);

参数解释:

  • ctx:RSA 加密或解密操作的上下文(EVP_PKEY_CTX)。
  • pad:要设置的加密填充(padding)方式,可以是以下值之一:
    • RSA_PKCS1_PADDING:PKCS#1 填充方式,是最常见的 RSA 填充方式。
    • RSA_PKCS1_OAEP_PADDING:PKCS#1 OAEP 填充方式,带有随机性质的填充方式,安全性更高。
    • RSA_NO_PADDING:不进行填充操作,仅加密或解密数据。

设置签名时,不能使用RSA_PKCS1_OAPE_PADDING这种填充方式,因为OAEP 是一种概率性加密填充,每次加密同一明文会产生不同的密文,但签名需要确定性,这样才能保证相同数据的签名结果可以被验证

返回值

  • 1:成功
  • 0:失败
1
2
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING);
assert(ret == 1);
4.1.4、EVP_PKEY_encrypt

使用非对称密钥进行加密操作

1
2
3
int EVP_PKEY_encrypt(EVP_PKEY_CTX *ctx,
unsigned char *out, size_t *outlen,
const unsigned char *in, size_t inlen);

参数解释:

  • ctx:指向 EVP_PKEY_CTX 对象的指针,用于进行加密操作的上下文。
  • out:指向输出缓冲区的指针,用于存储加密后的数据。
  • outlen:指向保存输出数据长度的变量的指针,同时也作为输入来指定输出缓冲区的大小。
  • in:指向输入缓冲区的指针,包含需要加密的数据。
  • inlen:输入数据的长度。

返回值

  • 1:成功
  • 0:失败
1
2
3
4
5
6
7
8
size_t outLen = 0;
ret = EVP_PKEY_encrypt(
ctx, NULL, &outLen, reinterpret_cast<const unsigned char*>(data.data()), data.size());
assert(ret == 1);
unsigned char* out = new unsigned char[outLen];
EVP_PKEY_encrypt(
ctx, out, &outLen, reinterpret_cast<const unsigned char*>(data.data()), data.size());
assert(ret == 1);

使用公钥加密数据,由于不知道加密数据的大小,所以第一次调用EVP_PKEY_encrypt的目的是获取outLen,这是因为outLen记录着加密后数据的长度,通过这个长度就能创建出合适的内存大小

4.1.5、EVP_PKEY_CTX_free

释放密钥对上下文对象

1
void EVP_PKEY_CTX_free(EVP_PKEY_CTX *ctx);

参数解释:

  • ctx:指向要释放内存的密钥对上下文对象的指针。
1
2
3
// 释放资源
delete[] out;
EVP_PKEY_CTX_free(ctx);
完整示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
QByteArray RSACrypto::publicEncrypt(const QByteArray& data)
{
// 创建加密数据的上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(m_publicKey, NULL);
assert(ctx != NULL);

// 设置加密和填充模式
int ret = EVP_PKEY_encrypt_init(ctx);
assert(ret == 1);
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING);
assert(ret == 1);

// 使用公钥加密数据,由于不知道加密数据的大小,所以第一次调用EVP_PKEY_encrypt的目的是获取outLen,
// 这是因为outLen记录着加密后数据的长度,通过这个长度就能创建出合适的内存大小
size_t outLen = 0;
ret = EVP_PKEY_encrypt(
ctx, NULL, &outLen, reinterpret_cast<const unsigned char*>(data.data()), data.size());
assert(ret == 1);
unsigned char* out = new unsigned char[outLen];
EVP_PKEY_encrypt(
ctx, out, &outLen, reinterpret_cast<const unsigned char*>(data.data()), data.size());
assert(ret == 1);

QByteArray encryptData(reinterpret_cast<char*>(out), outLen);

// 释放资源
delete[] out;
EVP_PKEY_CTX_free(ctx);
return encryptData;
}

4.2 使用私钥解密

4.2.1、EVP_PKEY_CTX_new

用于创建与给定密钥对象(EVP_PKEY)相关联的密钥上下文(EVP_PKEY_CTX)。

1
EVP_PKEY_CTX *EVP_PKEY_CTX_new(EVP_PKEY *pkey, ENGINE *e);

参数解释:

  • pkey:与上下文关联的密钥对象。这可以是一个公钥、私钥或对称密钥对象,具体取决于使用场景。
  • e:可选参数,与上下文关联的引擎(Engine)。如果不需要使用特定引擎,可以传入 NULL

返回值

  • EVP_PKEY_CTX 类型的指针,即新创建的密钥上下文对象
1
2
3
// 创建加密数据的上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(m_privateKey, NULL);
assert(ctx != NULL);

需要注意的是,使用完密钥上下文后应该调用 EVP_PKEY_CTX_free 函数来释放相应的资源,以避免内存泄漏。

4.2.2、EVP_PKEY_decrypt_init

用于初始化使用非对称密钥进行解密操作

1
int EVP_PKEY_decrypt_init(EVP_PKEY_CTX *);

参数解释:

  • ctx:要进行加密操作的密钥上下文

返回值

  • 1:成功
  • 0:失败
1
2
int ret = EVP_PKEY_decrypt_init(ctx);
assert(ret == 1);
4.2.3、EVP_PKEY_CTX_set_rsa_padding

用于设置 RSA 加密或解密操作的填充方式

1
int EVP_PKEY_CTX_set_rsa_padding(EVP_PKEY_CTX *ctx, int pad);

参数解释:

  • ctx:RSA 加密或解密操作的上下文(EVP_PKEY_CTX)。
  • pad:要设置的加密填充(padding)方式,可以是以下值之一:
    • RSA_PKCS1_PADDING:PKCS#1 填充方式,是最常见的 RSA 填充方式。
    • RSA_PKCS1_OAEP_PADDING:PKCS#1 OAEP 填充方式,带有随机性质的填充方式,安全性更高。
    • RSA_NO_PADDING:不进行填充操作,仅加密或解密数据。

设置签名时,不能使用RSA_PKCS1_OAPE_PADDING这种填充方式,因为OAEP 是一种概率性加密填充,每次加密同一明文会产生不同的密文,但签名需要确定性,这样才能保证相同数据的签名结果可以被验证

返回值

  • 1:成功
  • 0:失败
1
2
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING);
assert(ret == 1);
4.2.4、EVP_PKEY_decrypt

使用非对称密钥进行加密操作

1
2
3
int EVP_PKEY_decrypt(EVP_PKEY_CTX *ctx,
unsigned char *out, size_t *outlen,
const unsigned char *in, size_t inlen);

参数解释:

  • ctx:指向 EVP_PKEY_CTX 对象的指针,用于进行解密操作的上下文。
  • out:指向输出缓冲区的指针,用于存储解密后的数据。
  • outlen:指向保存输出数据长度的变量的指针,同时也作为输入来指定输出缓冲区的大小。
  • in:指向输入缓冲区的指针,包含需要解密的数据。
  • inlen:输入数据的长度。

返回值

  • 1:成功
  • 0:失败
1
2
3
4
5
6
7
8
size_t outLen = 0;
ret = EVP_PKEY_decrypt(
ctx, NULL, &outLen, reinterpret_cast<const unsigned char*>(data.data()), data.size());
assert(ret == 1);
unsigned char* out = new unsigned char[outLen];
EVP_PKEY_decrypt(
ctx, out, &outLen, reinterpret_cast<const unsigned char*>(data.data()), data.size());
assert(ret == 1);

使用私钥解密数据,由于不知道解密数据的大小,所以第一次调用EVP_PKEY_decrypt的目的是获取outLen,这是因为outLen记录着解密后数据的长度,通过这个长度就能创建出合适的内存大小

4.2.5、EVP_PKEY_CTX_free

释放密钥对上下文对象

1
void EVP_PKEY_CTX_free(EVP_PKEY_CTX *ctx);

参数解释:

  • ctx:指向要释放内存的密钥对上下文对象的指针。
1
2
3
// 释放资源
delete[] out;
EVP_PKEY_CTX_free(ctx);
完整示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
QByteArray RSACrypto::privateDecrypt(const QByteArray& data)
{
// 创建解密数据的上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(m_privateKey, NULL);
assert(ctx != NULL);

// 设置加密和填充模式
int ret = EVP_PKEY_decrypt_init(ctx);
assert(ret == 1);
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING);
assert(ret == 1);

// 使用私钥解密
size_t outLen = 0;
ret = EVP_PKEY_decrypt(
ctx, NULL, &outLen, reinterpret_cast<const unsigned char*>(data.data()), data.size());
assert(ret == 1);
unsigned char* out = new unsigned char[outLen];
ret = EVP_PKEY_decrypt(
ctx, out, &outLen, reinterpret_cast<const unsigned char*>(data.data()), data.size());
assert(ret == 1);

QByteArray decryptData(reinterpret_cast<char*>(out), outLen);

// 释放资源
delete[] out;
EVP_PKEY_CTX_free(ctx);
return decryptData;
}

Qt和OpenSSL进行RSA非对称加解密开发(4)--数字签名和校验

数字签名和校验的流程:计算数据的哈希值,然后对哈希值进行数据签名,数据校验时,也是先计算接受到的数据的哈希值,然后对哈希值进行校验

5.1、数据签名

5.1.1、哈希值计算

Qt的QCryptographicHash提供了一系列的加密算法实现,其中就包括哈希值计算

1
2
3
4
5
6
7
8
QCryptographicHash::Algorithm hashType;
hashType = QCryptographicHash::Sha256;

// 计算哈希值
QCryptographicHash hashCode(hashType);
hashCode.addData(data);
// 目前md存储的是二进制格式数据
QByteArray md = hashCode.result();
5.1.2、EVP_PKEY_CTX_new

用于创建与给定密钥对象(EVP_PKEY)相关联的密钥上下文(EVP_PKEY_CTX)。

1
EVP_PKEY_CTX *EVP_PKEY_CTX_new(EVP_PKEY *pkey, ENGINE *e);

参数解释:

  • pkey:与上下文关联的密钥对象。这可以是一个公钥、私钥或对称密钥对象,具体取决于使用场景。
  • e:可选参数,与上下文关联的引擎(Engine)。如果不需要使用特定引擎,可以传入 NULL

返回值

  • EVP_PKEY_CTX 类型的指针,即新创建的密钥上下文对象
1
2
3
// 创建加密数据的上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(m_privateKey, NULL);
assert(ctx != NULL);

需要注意的是,使用完密钥上下文后应该调用 EVP_PKEY_CTX_free 函数来释放相应的资源,以避免内存泄漏。

5.1.3、EVP_PKEY_sign_init

用于初始化使用非对称密钥进行签名操作的函数

1
int EVP_PKEY_sign_init(EVP_PKEY_CTX *);

参数解释:

  • ctx:要进行数字签名操作的密钥上下文

返回值

  • 1:成功
  • 0:失败
1
2
int ret = EVP_PKEY_sign_init(ctx);
assert(ret == 1);
5.1.4、EVP_PKEY_CTX_set_rsa_padding

用于设置 RSA 加密或解密操作的填充方式

1
int EVP_PKEY_CTX_set_rsa_padding(EVP_PKEY_CTX *ctx, int pad);

参数解释:

  • ctx:RSA 加密或解密操作的上下文(EVP_PKEY_CTX)。
  • pad:要设置的加密填充(padding)方式,可以是以下值之一:
    • RSA_PKCS1_PADDING:PKCS#1 填充方式,是最常见的 RSA 填充方式。
    • RSA_PKCS1_OAEP_PADDING:PKCS#1 OAEP 填充方式,带有随机性质的填充方式,安全性更高。
    • RSA_NO_PADDING:不进行填充操作,仅加密或解密数据。

设置签名时,不能使用RSA_PKCS1_OAPE_PADDING这种填充方式,因为OAEP 是一种概率性加密填充,每次加密同一明文会产生不同的密文,但签名需要确定性,这样才能保证相同数据的签名结果可以被验证

返回值

  • 1:成功
  • 0:失败
1
2
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING);
assert(ret == 1);
5.1.5、EVP_PKEY_CTX_set_signature_md

用于设置签名算法

1
int EVP_PKEY_CTX_set_signature_md(EVP_PKEY_CTX *ctx, const EVP_MD *md);

参数解释:

  • ctx:指向 EVP_PKEY_CTX 上下文结构的指针,用于设置签名算法。
  • md:指向 EVP_MD 结构的指针,表示要使用的签名算法。

签名算法由 OpenSSL 中的 EVP_MD 结构表示,其中包含有关算法的信息,如名称、摘要长度等

返回值

  • 1:成功
  • 0:失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const QMap<QCryptographicHash::Algorithm, hashFunc> hashMethods = {
{QCryptographicHash::Md5, EVP_md5},
{QCryptographicHash::Sha1, EVP_sha1},
{QCryptographicHash::Sha224, EVP_sha224},
{QCryptographicHash::Sha256, EVP_sha256},
{QCryptographicHash::Sha384, EVP_sha384},
{QCryptographicHash::Sha512, EVP_sha512},
{QCryptographicHash::Sha3_224, EVP_sha3_224},
{QCryptographicHash::Sha3_256, EVP_sha3_256},
{QCryptographicHash::Sha3_384, EVP_sha3_384},
{QCryptographicHash::Sha3_512, EVP_sha3_512},
};
// 设置签名使用的哈希算法
ret = EVP_PKEY_CTX_set_signature_md(ctx, hashMethods.value(hashType)());
assert(ret == 1);
5.1.6、EVP_PKEY_sign

使用非对称密钥进行签名操作的函数

1
2
3
int EVP_PKEY_sign(EVP_PKEY_CTX *ctx, 
unsigned char *sig, size_t *siglen,
const unsigned char *tbs, size_t tbslen);

参数解释:

  • ctx:指向 EVP_PKEY_CTX 上下文结构的指针,表示签名操作的上下文。
  • sig:指向缓冲区的指针,用于存储签名结果。
  • siglen:指向 sig 缓冲区长度的指针,表示输入时表示 sig 缓冲区的长度,输出时表示实际写入 sig 的字节数。
  • tbs:指向要签名的数据的指针。
  • tbslen:要签名的数据的长度。

返回值:

  • 1:成功
  • 0:失败
1
2
3
4
5
6
7
8
9
// 数据签名
size_t outLen = 0;
ret = EVP_PKEY_sign(
ctx, NULL, &outLen, reinterpret_cast<const unsigned char*>(md.data()), md.size());
assert(ret == 1);
unsigned char* out = new unsigned char[outLen];
ret = EVP_PKEY_sign(
ctx, out, &outLen, reinterpret_cast<const unsigned char*>(md.data()), md.size());
assert(ret == 1);
5.1.7、EVP_PKEY_CTX_free

释放密钥对上下文对象

1
void EVP_PKEY_CTX_free(EVP_PKEY_CTX *ctx);

参数解释:

  • ctx:指向要释放内存的密钥对上下文对象的指针。
1
2
// 释放上下文
EVP_PKEY_CTX_free(ctx);

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
QByteArray RSACrypto::sign(const QByteArray& data, QCryptographicHash::Algorithm hashType)
{
// 计算哈希值
QCryptographicHash hashCode(hashType);
hashCode.addData(data);
// 目前md存储的是二进制格式数据
QByteArray md = hashCode.result();

// 创建解密数据的上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(m_privateKey, NULL);
assert(ctx != NULL);

// 设置加密和填充模式
int ret = EVP_PKEY_sign_init(ctx);
assert(ret == 1);

// NOTE 设置签名时,不能使用RSA_PKCS1_OAPE_PADDING这种填充方式
// OAEP 是一种概率性加密填充,每次加密同一明文会产生不同的密文
// 但签名需要确定性,这样才能保证相同数据的签名结果可以被验证
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING);
assert(ret == 1);

// 设置签名使用的哈希算法
ret = EVP_PKEY_CTX_set_signature_md(ctx, hashMethods.value(hashType)());
assert(ret == 1);

// 数据签名
size_t outLen = 0;
ret = EVP_PKEY_sign(
ctx, NULL, &outLen, reinterpret_cast<const unsigned char*>(md.data()), md.size());
assert(ret == 1);
unsigned char* out = new unsigned char[outLen];
ret = EVP_PKEY_sign(
ctx, out, &outLen, reinterpret_cast<const unsigned char*>(md.data()), md.size());
assert(ret == 1);

QByteArray signData(reinterpret_cast<char*>(out), outLen);

// 释放资源
delete[] out;
EVP_PKEY_CTX_free(ctx);
return signData;
}

5.2、数据校验

5.2.1、哈希值计算

Qt的QCryptographicHash提供了一系列的加密算法实现,其中就包括哈希值计算

1
2
3
4
5
6
7
8
QCryptographicHash::Algorithm hashType;
hashType = QCryptographicHash::Sha256;

// 计算哈希值
QCryptographicHash hashCode(hashType);
hashCode.addData(data);
// 目前md存储的是二进制格式数据
QByteArray md = hashCode.result();
5.2.2、EVP_PKEY_CTX_new

用于创建与给定密钥对象(EVP_PKEY)相关联的密钥上下文(EVP_PKEY_CTX)。

1
EVP_PKEY_CTX *EVP_PKEY_CTX_new(EVP_PKEY *pkey, ENGINE *e);

参数解释:

  • pkey:与上下文关联的密钥对象。这可以是一个公钥、私钥或对称密钥对象,具体取决于使用场景。
  • e:可选参数,与上下文关联的引擎(Engine)。如果不需要使用特定引擎,可以传入 NULL

返回值

  • EVP_PKEY_CTX 类型的指针,即新创建的密钥上下文对象
1
2
3
// 创建加密数据的上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(m_privateKey, NULL);
assert(ctx != NULL);

需要注意的是,使用完密钥上下文后应该调用 EVP_PKEY_CTX_free 函数来释放相应的资源,以避免内存泄漏。

5.2.3、EVP_PKEY_verify_init

用于使用非对称密钥进行验签操作的函数

1
int EVP_PKEY_verify_init(EVP_PKEY_CTX *);

参数解释:

  • ctx:要进行数据校验操作的密钥上下文

返回值

  • 1:成功
  • 0:失败
1
2
int ret = EVP_PKEY_sign_init(ctx);
assert(ret == 1);
5.2.4、EVP_PKEY_CTX_set_rsa_padding

用于设置 RSA 加密或解密操作的填充方式

1
int EVP_PKEY_CTX_set_rsa_padding(EVP_PKEY_CTX *ctx, int pad);

参数解释:

  • ctx:RSA 加密或解密操作的上下文(EVP_PKEY_CTX)。
  • pad:要设置的加密填充(padding)方式,可以是以下值之一:
    • RSA_PKCS1_PADDING:PKCS#1 填充方式,是最常见的 RSA 填充方式。
    • RSA_PKCS1_OAEP_PADDING:PKCS#1 OAEP 填充方式,带有随机性质的填充方式,安全性更高。
    • RSA_NO_PADDING:不进行填充操作,仅加密或解密数据。

设置签名时,不能使用RSA_PKCS1_OAPE_PADDING这种填充方式,因为OAEP 是一种概率性加密填充,每次加密同一明文会产生不同的密文,但签名需要确定性,这样才能保证相同数据的签名结果可以被验证

返回值

  • 1:成功
  • 0:失败
1
2
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING);
assert(ret == 1);
5.2.5、EVP_PKEY_CTX_set_signature_md

用于设置签名算法

1
int EVP_PKEY_CTX_set_signature_md(EVP_PKEY_CTX *ctx, const EVP_MD *md);

参数解释:

  • ctx:指向 EVP_PKEY_CTX 上下文结构的指针,用于设置签名算法。
  • md:指向 EVP_MD 结构的指针,表示要使用的签名算法。

签名算法由 OpenSSL 中的 EVP_MD 结构表示,其中包含有关算法的信息,如名称、摘要长度等

返回值

  • 1:成功
  • 0:失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const QMap<QCryptographicHash::Algorithm, hashFunc> hashMethods = {
{QCryptographicHash::Md5, EVP_md5},
{QCryptographicHash::Sha1, EVP_sha1},
{QCryptographicHash::Sha224, EVP_sha224},
{QCryptographicHash::Sha256, EVP_sha256},
{QCryptographicHash::Sha384, EVP_sha384},
{QCryptographicHash::Sha512, EVP_sha512},
{QCryptographicHash::Sha3_224, EVP_sha3_224},
{QCryptographicHash::Sha3_256, EVP_sha3_256},
{QCryptographicHash::Sha3_384, EVP_sha3_384},
{QCryptographicHash::Sha3_512, EVP_sha3_512},
};
// 设置校验使用的哈希算法,与签名一致
ret = EVP_PKEY_CTX_set_signature_md(ctx, hashMethods.value(hashType)());
assert(ret == 1);
5.2.6、EVP_PKEY_verify

使用非对称密钥进行签名操作的函数

1
2
3
int EVP_PKEY_sign(EVP_PKEY_CTX *ctx, 
unsigned char *sig, size_t *siglen,
const unsigned char *tbs, size_t tbslen);

参数解释:

  • ctx:指向 EVP_PKEY_CTX 上下文结构的指针,表示签名操作的上下文。
  • sig:指向缓冲区的指针,用于存储签名结果。
  • siglen:指向 sig 缓冲区长度的指针,表示输入时表示 sig 缓冲区的长度,输出时表示实际写入 sig 的字节数。
  • tbs:指向要签名的数据的指针。
  • tbslen:要签名的数据的长度。

返回值:

  • 1:成功
  • 0:失败
1
2
3
4
5
6
7
// 签名校验
size_t outLen = 0;
ret = EVP_PKEY_verify(ctx,
reinterpret_cast<const unsigned char*>(sign.data()),
sign.size(),
reinterpret_cast<const unsigned char*>(md.data()),
md.size());
5.2.7、EVP_PKEY_CTX_free

释放密钥对上下文对象

1
void EVP_PKEY_CTX_free(EVP_PKEY_CTX *ctx);

参数解释:

  • ctx:指向要释放内存的密钥对上下文对象的指针。
1
2
// 释放上下文
EVP_PKEY_CTX_free(ctx);
完整示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
bool RSACrypto::varify(const QByteArray& sign,
const QByteArray& data,
QCryptographicHash::Algorithm hashType)
{
// 计算哈希值
QCryptographicHash hashCode(hashType);
hashCode.addData(data);
// 目前md存储的是二进制格式数据
QByteArray md = hashCode.result();

// 创建解密数据的上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(m_publicKey, NULL);
assert(ctx != NULL);

// 设置加密和填充模式
int ret = EVP_PKEY_verify_init(ctx);
assert(ret == 1);
// 填充要与签名的一致
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING);
assert(ret == 1);

// 设置签名使用的哈希算法
ret = EVP_PKEY_CTX_set_signature_md(ctx, hashMethods.value(hashType)());
assert(ret == 1);

// 签名校验
size_t outLen = 0;
ret = EVP_PKEY_verify(ctx,
reinterpret_cast<const unsigned char*>(sign.data()),
sign.size(),
reinterpret_cast<const unsigned char*>(md.data()),
md.size());
EVP_PKEY_CTX_free(ctx);
if (ret == 1)
return true;
return false;
}

Qt和OpenSSL进行RSA非对称加解密开发(2)--读取或释放密钥对

3.1、从文件中读取密钥对

3.1.1、BIO_new_file

用于创建适用于文件 I/O 的 BIO 对象,它允许将文件与 BIO 抽象接口结合使用,以便进行读取或写入文件的操作

1
BIO *BIO_new_file(const char *filename, const char *mode);

参数解释:

  • filename:要进行读取或写入的文件的名称或路径。
  • mode:表示文件操作模式的字符串。常见的模式包括:
    • "r":只读模式。
    • "w":写入模式,如果文件不存在则创建,如果文件存在则截断。
    • "a":追加模式,在文件末尾写入数据。
    • "rb":以二进制格式打开文件进行只读操作。
    • "wb":以二进制格式打开文件进行写入操作。
    • "ab":以二进制格式打开文件进行追加操作。

返回值:

  • 成功:指向新创建的文件 BIO 对象的指针。
  • 失败:返回 NULL
1
2
BIO* bio = BIO_new_file(fileName.data(), "rb");
assert(bio != NULL);
3.1.2、PEM_read_bio_PUBKEY

从一个 BIO 对象(在内存中的数据流)中读取 PEM 编码的公钥,PEM_read_bio_PUBKEY 函数读取 PEM 编码的公钥,并将结果存储在 EVP_PKEY 结构体中。

1
2
3
#include <openssl/pem.h>

EVP_PKEY *PEM_read_bio_PUBKEY(BIO *bp, EVP_PKEY **x, pem_password_cb *cb, void *u);

该函数接受以下参数:

  • bp:指向 BIO 对象的指针,可用于读取 PEM 文件中的数据或从其他数据源读取 PEM 格式的数据。
  • x:指向 EVP_PKEY 指针的指针,用于接收读取的公钥。
  • cb:一个回调函数,用于处理密码(如果 PEM 文件有密码保护)。
  • u:用户自定义数据,在回调函数中可以使用。

返回值:

  • 成功,返回 EVP_PKEY 指针
  • 失败,返回 NULL。
1
2
3
4
if (keyType == PUBLICKEY)
{
PEM_read_bio_PUBKEY(bio, &m_publicKey, NULL, NULL);
}
3.1.3、PEM_read_bio_PrivateKey

用于从一个 BIO 对象(在内存中的数据流)中读取 PEM 编码的私钥,PEM_read_bio_PrivateKey 函数读取 PEM 编码的公钥,并将结果存储在 EVP_PKEY 结构体中。

1
2
3
#include <openssl/pem.h>

EVP_PKEY *PEM_read_bio_PrivateKey(BIO *bp, EVP_PKEY **x, pem_password_cb *cb, void *u);

该函数接受以下参数:

  • bp:指向 BIO 对象的指针,可用于读取 PEM 文件中的数据或从其他数据源读取 PEM 格式的数据。
  • x:指向 EVP_PKEY 指针的指针,用于接收读取的私钥。
  • cb:一个回调函数,用于处理密码(如果 PEM 文件有密码保护)。
  • u:用户自定义数据,在回调函数中可以使用。

返回值:

  • 成功,返回 EVP_PKEY 指针
  • 失败,返回 NULL。
1
2
3
4
else
{
PEM_read_bio_PrivateKey(bio, &m_privateKey, NULL, NULL);
}

记得调用BIO_free(bio);函数释放BIO对象

完整示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
RSACrypto::RSACrypto(const QByteArray& fileName, KeyType keyType, QObject* parent)
{
BIO* bio = BIO_new_file(fileName.data(), "rb");
assert(bio != NULL);
if (keyType == PUBLICKEY)
{
PEM_read_bio_PUBKEY(bio, &m_publicKey, NULL, NULL);
}
else
{
PEM_read_bio_PrivateKey(bio, &m_privateKey, NULL, NULL);
}
BIO_free(bio);
}

3.2 释放密钥对对象

3.2.1、EVP_PKEY_free
1
void EVP_PKEY_free(EVP_PKEY *pkey);

参数解释:

  • pkey: 要释放的私钥或公钥的EVP_PKEY对象
1
2
3
4
5
6
7
8
9
10
11
RSACrypto::~RSACrypto()
{
if (m_privateKey)
{
EVP_PKEY_free(m_privateKey);
}
if (m_publicKey)
{
EVP_PKEY_free(m_publicKey);
}
}

Qt和OpenSSL进行RSA非对称加解密开发(1)--生成密钥对

一、基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 生成密钥对
RSACrypto rsa;
rsa.generateRSAKeyPair(RSACrypto::BITS_2K);

// 公钥加密
RSACrypto rsa1("public.pem", RSACrypto::PUBLICKEY);
QByteArray cipher = rsa1.publicEncrypt("测试数据");

// 私钥解密
RSACrypto rsa2("private.pem", RSACrypto::PRIVATEKEY);
QByteArray plain = rsa2.privateDecrypt(cipher);

// 数字签名
QByteArray signature = rsa2.sign(plain);

// 签名验证
bool isValid = rsa1.verify(signature, plain);

必要头文件

1
2
3
4
5
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/evp.h>
#include <openssl/rsa.h>
#include <openssl/bio.h>

二、生成密钥对

2.1、EVP_PKEY_CTX_new_id

用于创建密钥上下文的函数,基于给定的公钥算法标识符创建一个新的密钥上下文对象

1
EVP_PKEY_CTX *EVP_PKEY_CTX_new_id(int id, ENGINE *e);

参数解释:

  • id : 公钥算法的标识符,用于指定要使用的加密算法,可以使用 EVP_PKEY_XXX 常量(例如 EVP_PKEY_RSAEVP_PKEY_EC)指定所需的算法。
  • e:可选参数,指定要使用的加密引擎。如果为 NULL,则使用默认的加密引擎

返回值:

  • 成功:返回指向新创建的 EVP_PKEY_CTX 对象的指针。
  • 失败:返回 NULL。
1
2
3
// 创建密钥上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
assert(ctx != NULL);

2.2、EVP_PKEY_keygen_init

用于初始化密钥对生成操作的函数,用于初始化设置密钥生成操作的参数和上下文ctx

1
int EVP_PKEY_keygen_init(EVP_PKEY_CTX *ctx);

参数解释:

  • ctx:指向要初始化的密钥对上下文的指针。

返回值:

  • 1:成功初始化密钥对生成操作。
  • 0 或者 负数:初始化失败。
1
2
3
// 初始化ctx
int ret = EVP_PKEY_keygen_init(ctx);
assert(ret == 1);

2.3、EVP_PKEY_CTX_set_rsa_keygen_bits

用于设置生成的 RSA 密钥对的位数

1
int EVP_PKEY_CTX_set_rsa_keygen_bits(EVP_PKEY_CTX *ctx, int bits);

参数解释:

  • ctx:指向要设置 RSA 密钥对位数的密钥对上下文的指针。
  • bits:要生成的 RSA 密钥对的位数。

返回值:

  • 1:成功设置 RSA 密钥生成位数。
  • 0: 或者 负数:设置失败。
1
2
3
4
5
6
7
8
9
10
11
12
// 密钥长度,单位为比特
enum KeyLength
{
BITS_1K = 1024,
BITS_2K = 2048,
BITS_3K = 3072,
BITS_4K = 4096
};

// 指定密钥对长度,bits是KeyLength类型
ret = EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits);
assert(ret == 1);

2.4、EVP_PKEY_generate

根据提供的密钥对上下文对象,生成一个新的密钥对,生成的密钥对保存在ppkey中

1
int EVP_PKEY_generate(EVP_PKEY_CTX *ctx, EVP_PKEY **ppkey);

参数解释:

  • ctx:指向已初始化的密钥对上下文的指针。
  • ppkey:指向接收生成的密钥对的指针,生成的密钥对保存在ppkey中。

返回值:

  • 1:成功生成密钥对。
  • 0 或者 负数:生成失败。
1
2
3
4
EVP_PKEY* m_privateKey = NULL;
// 生成密钥对
ret = EVP_PKEY_generate(ctx, &m_privateKey);
assert(ret == 1);

注意:ppkey可以使用私钥,这是因为私钥包含公钥,而公钥只包含公钥。同时,如果在函数中使用new创建一个EVP_PKEY*对象,没有回收会导致内存泄露。

2.5、EVP_PKEY_CTX_free

释放密钥对上下文对象

1
void EVP_PKEY_CTX_free(EVP_PKEY_CTX *ctx);

参数解释:

  • ctx:指向要释放内存的密钥对上下文对象的指针。
1
2
// 释放上下文
EVP_PKEY_CTX_free(ctx);

2.6、BIO_new_file

用于创建适用于文件 I/O 的 BIO 对象,它允许将文件与 BIO 抽象接口结合使用,以便进行读取或写入文件的操作

1
BIO *BIO_new_file(const char *filename, const char *mode);

参数解释:

  • filename:要进行读取或写入的文件的名称或路径。
  • mode:表示文件操作模式的字符串。常见的模式包括:
    • "r":只读模式。
    • "w":写入模式,如果文件不存在则创建,如果文件存在则截断。
    • "a":追加模式,在文件末尾写入数据。
    • "rb":以二进制格式打开文件进行只读操作。
    • "wb":以二进制格式打开文件进行写入操作。
    • "ab":以二进制格式打开文件进行追加操作。

返回值:

  • 成功:指向新创建的文件 BIO 对象的指针。
  • 失败:返回 NULL
1
2
3
4
5
6
7
8
9
10
11
12
13
// 将私钥写入文件(私钥里面包含公钥),pri是私钥文件名
BIO* bio = BIO_new_file(pri.data(), "wb");
ret = PEM_write_bio_PrivateKey(bio, m_privateKey, NULL, NULL, 0, NULL, NULL);
assert(ret == 1);
BIO_flush(bio);
BIO_free(bio);

// 将公钥写入文件,pub是公钥文件名
bio = BIO_new_file(pub.data(), "wb");
ret = PEM_write_bio_PUBKEY(bio, m_privateKey);
assert(ret == 1);
BIO_flush(bio);
BIO_free(bio);

2.7、BIO_flush

刷新 BIO 对象,将内存中的数据写入磁盘中,跟fflush函数类似

1
int BIO_flush(BIO *bio);

参数解释:

  • bio:指向要刷新的 BIO 对象的指针。

返回值:

  • 成功:返回 1。
  • 失败:返回 0。

2.8、BIO_free

释放 BIO 对象

1
void BIO_free(BIO *bio);

参数解释:

  • bio:指向要释放的 BIO 对象的指针。

完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void RSACrypto::generateRSAKeyPair(KeyLength bits, const QByteArray& pub, const QByteArray& pri)
{
// 创建密钥上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
assert(ctx != NULL);

// 初始化ctx
int ret = EVP_PKEY_keygen_init(ctx);
assert(ret == 1);

// 指定密钥对长度
ret = EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits);
assert(ret == 1);

// 生成密钥对
ret = EVP_PKEY_generate(ctx, &m_privateKey);
assert(ret == 1);

// 释放上下文
EVP_PKEY_CTX_free(ctx);

// 将私钥写入文件(私钥里面包含公钥)
BIO* bio = BIO_new_file(pri.data(), "wb");
ret = PEM_write_bio_PrivateKey(bio, m_privateKey, NULL, NULL, 0, NULL, NULL);
assert(ret == 1);
BIO_flush(bio);
BIO_free(bio);

// 将公钥写入文件
bio = BIO_new_file(pub.data(), "wb");
ret = PEM_write_bio_PUBKEY(bio, m_privateKey);
assert(ret == 1);
BIO_flush(bio);
BIO_free(bio);
}

Qt和OpenSSL进行RSA非对称加解密开发(总)

文章有很多相同函数的解释,建议直接跳到需要学习的目录

一、基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 生成密钥对
RSACrypto rsa;
rsa.generateRSAKeyPair(RSACrypto::BITS_2K);

// 公钥加密
RSACrypto rsa1("public.pem", RSACrypto::PUBLICKEY);
QByteArray cipher = rsa1.publicEncrypt("测试数据");

// 私钥解密
RSACrypto rsa2("private.pem", RSACrypto::PRIVATEKEY);
QByteArray plain = rsa2.privateDecrypt(cipher);

// 数字签名
QByteArray signature = rsa2.sign(plain);

// 签名验证
bool isValid = rsa1.verify(signature, plain);

必要头文件

1
2
3
4
5
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/evp.h>
#include <openssl/rsa.h>
#include <openssl/bio.h>

二、生成密钥对

2.1、EVP_PKEY_CTX_new_id

用于创建密钥上下文的函数,基于给定的公钥算法标识符创建一个新的密钥上下文对象

1
EVP_PKEY_CTX *EVP_PKEY_CTX_new_id(int id, ENGINE *e);

参数解释:

  • id : 公钥算法的标识符,用于指定要使用的加密算法,可以使用 EVP_PKEY_XXX 常量(例如 EVP_PKEY_RSAEVP_PKEY_EC)指定所需的算法。
  • e:可选参数,指定要使用的加密引擎。如果为 NULL,则使用默认的加密引擎

返回值:

  • 成功:返回指向新创建的 EVP_PKEY_CTX 对象的指针。
  • 失败:返回 NULL。
1
2
3
// 创建密钥上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
assert(ctx != NULL);

2.2、EVP_PKEY_keygen_init

用于初始化密钥对生成操作的函数,用于初始化设置密钥生成操作的参数和上下文ctx

1
int EVP_PKEY_keygen_init(EVP_PKEY_CTX *ctx);

参数解释:

  • ctx:指向要初始化的密钥对上下文的指针。

返回值:

  • 1:成功初始化密钥对生成操作。
  • 0 或者 负数:初始化失败。
1
2
3
// 初始化ctx
int ret = EVP_PKEY_keygen_init(ctx);
assert(ret == 1);

2.3、EVP_PKEY_CTX_set_rsa_keygen_bits

用于设置生成的 RSA 密钥对的位数

1
int EVP_PKEY_CTX_set_rsa_keygen_bits(EVP_PKEY_CTX *ctx, int bits);

参数解释:

  • ctx:指向要设置 RSA 密钥对位数的密钥对上下文的指针。
  • bits:要生成的 RSA 密钥对的位数。

返回值:

  • 1:成功设置 RSA 密钥生成位数。
  • 0: 或者 负数:设置失败。
1
2
3
4
5
6
7
8
9
10
11
12
// 密钥长度,单位为比特
enum KeyLength
{
BITS_1K = 1024,
BITS_2K = 2048,
BITS_3K = 3072,
BITS_4K = 4096
};

// 指定密钥对长度,bits是KeyLength类型
ret = EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits);
assert(ret == 1);

2.4、EVP_PKEY_generate

根据提供的密钥对上下文对象,生成一个新的密钥对,生成的密钥对保存在ppkey中

1
int EVP_PKEY_generate(EVP_PKEY_CTX *ctx, EVP_PKEY **ppkey);

参数解释:

  • ctx:指向已初始化的密钥对上下文的指针。
  • ppkey:指向接收生成的密钥对的指针,生成的密钥对保存在ppkey中。

返回值:

  • 1:成功生成密钥对。
  • 0 或者 负数:生成失败。
1
2
3
4
EVP_PKEY* m_privateKey = NULL;
// 生成密钥对
ret = EVP_PKEY_generate(ctx, &m_privateKey);
assert(ret == 1);

注意:ppkey可以使用私钥,这是因为私钥包含公钥,而公钥只包含公钥。同时,如果在函数中使用new创建一个EVP_PKEY*对象,没有回收会导致内存泄露。

2.5、EVP_PKEY_CTX_free

释放密钥对上下文对象

1
void EVP_PKEY_CTX_free(EVP_PKEY_CTX *ctx);

参数解释:

  • ctx:指向要释放内存的密钥对上下文对象的指针。
1
2
// 释放上下文
EVP_PKEY_CTX_free(ctx);

2.6、BIO_new_file

用于创建适用于文件 I/O 的 BIO 对象,它允许将文件与 BIO 抽象接口结合使用,以便进行读取或写入文件的操作

1
BIO *BIO_new_file(const char *filename, const char *mode);

参数解释:

  • filename:要进行读取或写入的文件的名称或路径。
  • mode:表示文件操作模式的字符串。常见的模式包括:
    • "r":只读模式。
    • "w":写入模式,如果文件不存在则创建,如果文件存在则截断。
    • "a":追加模式,在文件末尾写入数据。
    • "rb":以二进制格式打开文件进行只读操作。
    • "wb":以二进制格式打开文件进行写入操作。
    • "ab":以二进制格式打开文件进行追加操作。

返回值:

  • 成功:指向新创建的文件 BIO 对象的指针。
  • 失败:返回 NULL
1
2
3
4
5
6
7
8
9
10
11
12
13
// 将私钥写入文件(私钥里面包含公钥),pri是私钥文件名
BIO* bio = BIO_new_file(pri.data(), "wb");
ret = PEM_write_bio_PrivateKey(bio, m_privateKey, NULL, NULL, 0, NULL, NULL);
assert(ret == 1);
BIO_flush(bio);
BIO_free(bio);

// 将公钥写入文件,pub是公钥文件名
bio = BIO_new_file(pub.data(), "wb");
ret = PEM_write_bio_PUBKEY(bio, m_privateKey);
assert(ret == 1);
BIO_flush(bio);
BIO_free(bio);

2.7、BIO_flush

刷新 BIO 对象,将内存中的数据写入磁盘中,跟fflush函数类似

1
int BIO_flush(BIO *bio);

参数解释:

  • bio:指向要刷新的 BIO 对象的指针。

返回值:

  • 成功:返回 1。
  • 失败:返回 0。

2.8、BIO_free

释放 BIO 对象

1
void BIO_free(BIO *bio);

参数解释:

  • bio:指向要释放的 BIO 对象的指针。

完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void RSACrypto::generateRSAKeyPair(KeyLength bits, const QByteArray& pub, const QByteArray& pri)
{
// 创建密钥上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
assert(ctx != NULL);

// 初始化ctx
int ret = EVP_PKEY_keygen_init(ctx);
assert(ret == 1);

// 指定密钥对长度
ret = EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits);
assert(ret == 1);

// 生成密钥对
ret = EVP_PKEY_generate(ctx, &m_privateKey);
assert(ret == 1);

// 释放上下文
EVP_PKEY_CTX_free(ctx);

// 将私钥写入文件(私钥里面包含公钥)
BIO* bio = BIO_new_file(pri.data(), "wb");
ret = PEM_write_bio_PrivateKey(bio, m_privateKey, NULL, NULL, 0, NULL, NULL);
assert(ret == 1);
BIO_flush(bio);
BIO_free(bio);

// 将公钥写入文件
bio = BIO_new_file(pub.data(), "wb");
ret = PEM_write_bio_PUBKEY(bio, m_privateKey);
assert(ret == 1);
BIO_flush(bio);
BIO_free(bio);
}

三、读取或释放密钥对

3.1、从文件中读取密钥对

3.1.1、BIO_new_file

用于创建适用于文件 I/O 的 BIO 对象,它允许将文件与 BIO 抽象接口结合使用,以便进行读取或写入文件的操作

1
BIO *BIO_new_file(const char *filename, const char *mode);

参数解释:

  • filename:要进行读取或写入的文件的名称或路径。
  • mode:表示文件操作模式的字符串。常见的模式包括:
    • "r":只读模式。
    • "w":写入模式,如果文件不存在则创建,如果文件存在则截断。
    • "a":追加模式,在文件末尾写入数据。
    • "rb":以二进制格式打开文件进行只读操作。
    • "wb":以二进制格式打开文件进行写入操作。
    • "ab":以二进制格式打开文件进行追加操作。

返回值:

  • 成功:指向新创建的文件 BIO 对象的指针。
  • 失败:返回 NULL
1
2
BIO* bio = BIO_new_file(fileName.data(), "rb");
assert(bio != NULL);
3.1.2、PEM_read_bio_PUBKEY

从一个 BIO 对象(在内存中的数据流)中读取 PEM 编码的公钥,PEM_read_bio_PUBKEY 函数读取 PEM 编码的公钥,并将结果存储在 EVP_PKEY 结构体中。

1
2
3
#include <openssl/pem.h>

EVP_PKEY *PEM_read_bio_PUBKEY(BIO *bp, EVP_PKEY **x, pem_password_cb *cb, void *u);

该函数接受以下参数:

  • bp:指向 BIO 对象的指针,可用于读取 PEM 文件中的数据或从其他数据源读取 PEM 格式的数据。
  • x:指向 EVP_PKEY 指针的指针,用于接收读取的公钥。
  • cb:一个回调函数,用于处理密码(如果 PEM 文件有密码保护)。
  • u:用户自定义数据,在回调函数中可以使用。

返回值:

  • 成功,返回 EVP_PKEY 指针
  • 失败,返回 NULL。
1
2
3
4
if (keyType == PUBLICKEY)
{
PEM_read_bio_PUBKEY(bio, &m_publicKey, NULL, NULL);
}
3.1.3、PEM_read_bio_PrivateKey

用于从一个 BIO 对象(在内存中的数据流)中读取 PEM 编码的私钥,PEM_read_bio_PrivateKey 函数读取 PEM 编码的公钥,并将结果存储在 EVP_PKEY 结构体中。

1
2
3
#include <openssl/pem.h>

EVP_PKEY *PEM_read_bio_PrivateKey(BIO *bp, EVP_PKEY **x, pem_password_cb *cb, void *u);

该函数接受以下参数:

  • bp:指向 BIO 对象的指针,可用于读取 PEM 文件中的数据或从其他数据源读取 PEM 格式的数据。
  • x:指向 EVP_PKEY 指针的指针,用于接收读取的私钥。
  • cb:一个回调函数,用于处理密码(如果 PEM 文件有密码保护)。
  • u:用户自定义数据,在回调函数中可以使用。

返回值:

  • 成功,返回 EVP_PKEY 指针
  • 失败,返回 NULL。
1
2
3
4
else
{
PEM_read_bio_PrivateKey(bio, &m_privateKey, NULL, NULL);
}

记得调用BIO_free(bio);函数释放BIO对象

完整示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
RSACrypto::RSACrypto(const QByteArray& fileName, KeyType keyType, QObject* parent)
{
BIO* bio = BIO_new_file(fileName.data(), "rb");
assert(bio != NULL);
if (keyType == PUBLICKEY)
{
PEM_read_bio_PUBKEY(bio, &m_publicKey, NULL, NULL);
}
else
{
PEM_read_bio_PrivateKey(bio, &m_privateKey, NULL, NULL);
}
BIO_free(bio);
}

3.2 释放密钥对对象

3.2.1、EVP_PKEY_free
1
void EVP_PKEY_free(EVP_PKEY *pkey);

参数解释:

  • pkey: 要释放的私钥或公钥的EVP_PKEY对象
1
2
3
4
5
6
7
8
9
10
11
RSACrypto::~RSACrypto()
{
if (m_privateKey)
{
EVP_PKEY_free(m_privateKey);
}
if (m_publicKey)
{
EVP_PKEY_free(m_publicKey);
}
}

四、数据加解密

4.1使用公钥加密

4.1.1、EVP_PKEY_CTX_new

用于创建与给定密钥对象(EVP_PKEY)相关联的密钥上下文(EVP_PKEY_CTX)。

1
EVP_PKEY_CTX *EVP_PKEY_CTX_new(EVP_PKEY *pkey, ENGINE *e);

参数解释:

  • pkey:与上下文关联的密钥对象。这可以是一个公钥、私钥或对称密钥对象,具体取决于使用场景。
  • e:可选参数,与上下文关联的引擎(Engine)。如果不需要使用特定引擎,可以传入 NULL

返回值

  • EVP_PKEY_CTX 类型的指针,即新创建的密钥上下文对象
1
2
3
// 创建加密数据的上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(m_publicKey, NULL);
assert(ctx != NULL);

需要注意的是,使用完密钥上下文后应该调用 EVP_PKEY_CTX_free 函数来释放相应的资源,以避免内存泄漏。

4.1.2、EVP_PKEY_encrypt_init

用于初始化使用非对称密钥进行加密操作

1
int EVP_PKEY_encrypt_init(EVP_PKEY_CTX *);

参数解释:

  • ctx:要进行加密操作的密钥上下文

返回值

  • 1:成功
  • 0:失败
1
2
int ret = EVP_PKEY_encrypt_init(ctx);
assert(ret == 1);
4.1.3、EVP_PKEY_CTX_set_rsa_padding

用于设置 RSA 加密或解密操作的填充方式

1
int EVP_PKEY_CTX_set_rsa_padding(EVP_PKEY_CTX *ctx, int pad);

参数解释:

  • ctx:RSA 加密或解密操作的上下文(EVP_PKEY_CTX)。
  • pad:要设置的加密填充(padding)方式,可以是以下值之一:
    • RSA_PKCS1_PADDING:PKCS#1 填充方式,是最常见的 RSA 填充方式。
    • RSA_PKCS1_OAEP_PADDING:PKCS#1 OAEP 填充方式,带有随机性质的填充方式,安全性更高。
    • RSA_NO_PADDING:不进行填充操作,仅加密或解密数据。

设置签名时,不能使用RSA_PKCS1_OAPE_PADDING这种填充方式,因为OAEP 是一种概率性加密填充,每次加密同一明文会产生不同的密文,但签名需要确定性,这样才能保证相同数据的签名结果可以被验证

返回值

  • 1:成功
  • 0:失败
1
2
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING);
assert(ret == 1);
4.1.4、EVP_PKEY_encrypt

使用非对称密钥进行加密操作

1
2
3
int EVP_PKEY_encrypt(EVP_PKEY_CTX *ctx,
unsigned char *out, size_t *outlen,
const unsigned char *in, size_t inlen);

参数解释:

  • ctx:指向 EVP_PKEY_CTX 对象的指针,用于进行加密操作的上下文。
  • out:指向输出缓冲区的指针,用于存储加密后的数据。
  • outlen:指向保存输出数据长度的变量的指针,同时也作为输入来指定输出缓冲区的大小。
  • in:指向输入缓冲区的指针,包含需要加密的数据。
  • inlen:输入数据的长度。

返回值

  • 1:成功
  • 0:失败
1
2
3
4
5
6
7
8
size_t outLen = 0;
ret = EVP_PKEY_encrypt(
ctx, NULL, &outLen, reinterpret_cast<const unsigned char*>(data.data()), data.size());
assert(ret == 1);
unsigned char* out = new unsigned char[outLen];
EVP_PKEY_encrypt(
ctx, out, &outLen, reinterpret_cast<const unsigned char*>(data.data()), data.size());
assert(ret == 1);

使用公钥加密数据,由于不知道加密数据的大小,所以第一次调用EVP_PKEY_encrypt的目的是获取outLen,这是因为outLen记录着加密后数据的长度,通过这个长度就能创建出合适的内存大小

4.1.5、EVP_PKEY_CTX_free

释放密钥对上下文对象

1
void EVP_PKEY_CTX_free(EVP_PKEY_CTX *ctx);

参数解释:

  • ctx:指向要释放内存的密钥对上下文对象的指针。
1
2
3
// 释放资源
delete[] out;
EVP_PKEY_CTX_free(ctx);
完整示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
QByteArray RSACrypto::publicEncrypt(const QByteArray& data)
{
// 创建加密数据的上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(m_publicKey, NULL);
assert(ctx != NULL);

// 设置加密和填充模式
int ret = EVP_PKEY_encrypt_init(ctx);
assert(ret == 1);
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING);
assert(ret == 1);

// 使用公钥加密数据,由于不知道加密数据的大小,所以第一次调用EVP_PKEY_encrypt的目的是获取outLen,
// 这是因为outLen记录着加密后数据的长度,通过这个长度就能创建出合适的内存大小
size_t outLen = 0;
ret = EVP_PKEY_encrypt(
ctx, NULL, &outLen, reinterpret_cast<const unsigned char*>(data.data()), data.size());
assert(ret == 1);
unsigned char* out = new unsigned char[outLen];
EVP_PKEY_encrypt(
ctx, out, &outLen, reinterpret_cast<const unsigned char*>(data.data()), data.size());
assert(ret == 1);

QByteArray encryptData(reinterpret_cast<char*>(out), outLen);

// 释放资源
delete[] out;
EVP_PKEY_CTX_free(ctx);
return encryptData;
}

4.2 使用私钥解密

4.2.1、EVP_PKEY_CTX_new

用于创建与给定密钥对象(EVP_PKEY)相关联的密钥上下文(EVP_PKEY_CTX)。

1
EVP_PKEY_CTX *EVP_PKEY_CTX_new(EVP_PKEY *pkey, ENGINE *e);

参数解释:

  • pkey:与上下文关联的密钥对象。这可以是一个公钥、私钥或对称密钥对象,具体取决于使用场景。
  • e:可选参数,与上下文关联的引擎(Engine)。如果不需要使用特定引擎,可以传入 NULL

返回值

  • EVP_PKEY_CTX 类型的指针,即新创建的密钥上下文对象
1
2
3
// 创建加密数据的上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(m_privateKey, NULL);
assert(ctx != NULL);

需要注意的是,使用完密钥上下文后应该调用 EVP_PKEY_CTX_free 函数来释放相应的资源,以避免内存泄漏。

4.2.2、EVP_PKEY_decrypt_init

用于初始化使用非对称密钥进行解密操作

1
int EVP_PKEY_decrypt_init(EVP_PKEY_CTX *);

参数解释:

  • ctx:要进行加密操作的密钥上下文

返回值

  • 1:成功
  • 0:失败
1
2
int ret = EVP_PKEY_decrypt_init(ctx);
assert(ret == 1);
4.2.3、EVP_PKEY_CTX_set_rsa_padding

用于设置 RSA 加密或解密操作的填充方式

1
int EVP_PKEY_CTX_set_rsa_padding(EVP_PKEY_CTX *ctx, int pad);

参数解释:

  • ctx:RSA 加密或解密操作的上下文(EVP_PKEY_CTX)。
  • pad:要设置的加密填充(padding)方式,可以是以下值之一:
    • RSA_PKCS1_PADDING:PKCS#1 填充方式,是最常见的 RSA 填充方式。
    • RSA_PKCS1_OAEP_PADDING:PKCS#1 OAEP 填充方式,带有随机性质的填充方式,安全性更高。
    • RSA_NO_PADDING:不进行填充操作,仅加密或解密数据。

设置签名时,不能使用RSA_PKCS1_OAPE_PADDING这种填充方式,因为OAEP 是一种概率性加密填充,每次加密同一明文会产生不同的密文,但签名需要确定性,这样才能保证相同数据的签名结果可以被验证

返回值

  • 1:成功
  • 0:失败
1
2
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING);
assert(ret == 1);
4.2.4、EVP_PKEY_decrypt

使用非对称密钥进行加密操作

1
2
3
int EVP_PKEY_decrypt(EVP_PKEY_CTX *ctx,
unsigned char *out, size_t *outlen,
const unsigned char *in, size_t inlen);

参数解释:

  • ctx:指向 EVP_PKEY_CTX 对象的指针,用于进行解密操作的上下文。
  • out:指向输出缓冲区的指针,用于存储解密后的数据。
  • outlen:指向保存输出数据长度的变量的指针,同时也作为输入来指定输出缓冲区的大小。
  • in:指向输入缓冲区的指针,包含需要解密的数据。
  • inlen:输入数据的长度。

返回值

  • 1:成功
  • 0:失败
1
2
3
4
5
6
7
8
size_t outLen = 0;
ret = EVP_PKEY_decrypt(
ctx, NULL, &outLen, reinterpret_cast<const unsigned char*>(data.data()), data.size());
assert(ret == 1);
unsigned char* out = new unsigned char[outLen];
EVP_PKEY_decrypt(
ctx, out, &outLen, reinterpret_cast<const unsigned char*>(data.data()), data.size());
assert(ret == 1);

使用私钥解密数据,由于不知道解密数据的大小,所以第一次调用EVP_PKEY_decrypt的目的是获取outLen,这是因为outLen记录着解密后数据的长度,通过这个长度就能创建出合适的内存大小

4.2.5、EVP_PKEY_CTX_free

释放密钥对上下文对象

1
void EVP_PKEY_CTX_free(EVP_PKEY_CTX *ctx);

参数解释:

  • ctx:指向要释放内存的密钥对上下文对象的指针。
1
2
3
// 释放资源
delete[] out;
EVP_PKEY_CTX_free(ctx);
完整示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
QByteArray RSACrypto::privateDecrypt(const QByteArray& data)
{
// 创建解密数据的上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(m_privateKey, NULL);
assert(ctx != NULL);

// 设置加密和填充模式
int ret = EVP_PKEY_decrypt_init(ctx);
assert(ret == 1);
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING);
assert(ret == 1);

// 使用私钥解密
size_t outLen = 0;
ret = EVP_PKEY_decrypt(
ctx, NULL, &outLen, reinterpret_cast<const unsigned char*>(data.data()), data.size());
assert(ret == 1);
unsigned char* out = new unsigned char[outLen];
ret = EVP_PKEY_decrypt(
ctx, out, &outLen, reinterpret_cast<const unsigned char*>(data.data()), data.size());
assert(ret == 1);

QByteArray decryptData(reinterpret_cast<char*>(out), outLen);

// 释放资源
delete[] out;
EVP_PKEY_CTX_free(ctx);
return decryptData;
}

五、数字签名和校验

数字签名和校验的流程:计算数据的哈希值,然后对哈希值进行数据签名,数据校验时,也是先计算接受到的数据的哈希值,然后对哈希值进行校验

5.1、数据签名

5.1.1、哈希值计算

Qt的QCryptographicHash提供了一系列的加密算法实现,其中就包括哈希值计算

1
2
3
4
5
6
7
8
QCryptographicHash::Algorithm hashType;
hashType = QCryptographicHash::Sha256;

// 计算哈希值
QCryptographicHash hashCode(hashType);
hashCode.addData(data);
// 目前md存储的是二进制格式数据
QByteArray md = hashCode.result();
5.1.2、EVP_PKEY_CTX_new

用于创建与给定密钥对象(EVP_PKEY)相关联的密钥上下文(EVP_PKEY_CTX)。

1
EVP_PKEY_CTX *EVP_PKEY_CTX_new(EVP_PKEY *pkey, ENGINE *e);

参数解释:

  • pkey:与上下文关联的密钥对象。这可以是一个公钥、私钥或对称密钥对象,具体取决于使用场景。
  • e:可选参数,与上下文关联的引擎(Engine)。如果不需要使用特定引擎,可以传入 NULL

返回值

  • EVP_PKEY_CTX 类型的指针,即新创建的密钥上下文对象
1
2
3
// 创建加密数据的上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(m_privateKey, NULL);
assert(ctx != NULL);

需要注意的是,使用完密钥上下文后应该调用 EVP_PKEY_CTX_free 函数来释放相应的资源,以避免内存泄漏。

5.1.3、EVP_PKEY_sign_init

用于初始化使用非对称密钥进行签名操作的函数

1
int EVP_PKEY_sign_init(EVP_PKEY_CTX *);

参数解释:

  • ctx:要进行数字签名操作的密钥上下文

返回值

  • 1:成功
  • 0:失败
1
2
int ret = EVP_PKEY_sign_init(ctx);
assert(ret == 1);
5.1.4、EVP_PKEY_CTX_set_rsa_padding

用于设置 RSA 加密或解密操作的填充方式

1
int EVP_PKEY_CTX_set_rsa_padding(EVP_PKEY_CTX *ctx, int pad);

参数解释:

  • ctx:RSA 加密或解密操作的上下文(EVP_PKEY_CTX)。
  • pad:要设置的加密填充(padding)方式,可以是以下值之一:
    • RSA_PKCS1_PADDING:PKCS#1 填充方式,是最常见的 RSA 填充方式。
    • RSA_PKCS1_OAEP_PADDING:PKCS#1 OAEP 填充方式,带有随机性质的填充方式,安全性更高。
    • RSA_NO_PADDING:不进行填充操作,仅加密或解密数据。

设置签名时,不能使用RSA_PKCS1_OAPE_PADDING这种填充方式,因为OAEP 是一种概率性加密填充,每次加密同一明文会产生不同的密文,但签名需要确定性,这样才能保证相同数据的签名结果可以被验证

返回值

  • 1:成功
  • 0:失败
1
2
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING);
assert(ret == 1);
5.1.5、EVP_PKEY_CTX_set_signature_md

用于设置签名算法

1
int EVP_PKEY_CTX_set_signature_md(EVP_PKEY_CTX *ctx, const EVP_MD *md);

参数解释:

  • ctx:指向 EVP_PKEY_CTX 上下文结构的指针,用于设置签名算法。
  • md:指向 EVP_MD 结构的指针,表示要使用的签名算法。

签名算法由 OpenSSL 中的 EVP_MD 结构表示,其中包含有关算法的信息,如名称、摘要长度等

返回值

  • 1:成功
  • 0:失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const QMap<QCryptographicHash::Algorithm, hashFunc> hashMethods = {
{QCryptographicHash::Md5, EVP_md5},
{QCryptographicHash::Sha1, EVP_sha1},
{QCryptographicHash::Sha224, EVP_sha224},
{QCryptographicHash::Sha256, EVP_sha256},
{QCryptographicHash::Sha384, EVP_sha384},
{QCryptographicHash::Sha512, EVP_sha512},
{QCryptographicHash::Sha3_224, EVP_sha3_224},
{QCryptographicHash::Sha3_256, EVP_sha3_256},
{QCryptographicHash::Sha3_384, EVP_sha3_384},
{QCryptographicHash::Sha3_512, EVP_sha3_512},
};
// 设置签名使用的哈希算法
ret = EVP_PKEY_CTX_set_signature_md(ctx, hashMethods.value(hashType)());
assert(ret == 1);
5.1.6、EVP_PKEY_sign

使用非对称密钥进行签名操作的函数

1
2
3
int EVP_PKEY_sign(EVP_PKEY_CTX *ctx, 
unsigned char *sig, size_t *siglen,
const unsigned char *tbs, size_t tbslen);

参数解释:

  • ctx:指向 EVP_PKEY_CTX 上下文结构的指针,表示签名操作的上下文。
  • sig:指向缓冲区的指针,用于存储签名结果。
  • siglen:指向 sig 缓冲区长度的指针,表示输入时表示 sig 缓冲区的长度,输出时表示实际写入 sig 的字节数。
  • tbs:指向要签名的数据的指针。
  • tbslen:要签名的数据的长度。

返回值:

  • 1:成功
  • 0:失败
1
2
3
4
5
6
7
8
9
// 数据签名
size_t outLen = 0;
ret = EVP_PKEY_sign(
ctx, NULL, &outLen, reinterpret_cast<const unsigned char*>(md.data()), md.size());
assert(ret == 1);
unsigned char* out = new unsigned char[outLen];
ret = EVP_PKEY_sign(
ctx, out, &outLen, reinterpret_cast<const unsigned char*>(md.data()), md.size());
assert(ret == 1);
5.1.7、EVP_PKEY_CTX_free

释放密钥对上下文对象

1
void EVP_PKEY_CTX_free(EVP_PKEY_CTX *ctx);

参数解释:

  • ctx:指向要释放内存的密钥对上下文对象的指针。
1
2
// 释放上下文
EVP_PKEY_CTX_free(ctx);

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
QByteArray RSACrypto::sign(const QByteArray& data, QCryptographicHash::Algorithm hashType)
{
// 计算哈希值
QCryptographicHash hashCode(hashType);
hashCode.addData(data);
// 目前md存储的是二进制格式数据
QByteArray md = hashCode.result();

// 创建解密数据的上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(m_privateKey, NULL);
assert(ctx != NULL);

// 设置加密和填充模式
int ret = EVP_PKEY_sign_init(ctx);
assert(ret == 1);

// NOTE 设置签名时,不能使用RSA_PKCS1_OAPE_PADDING这种填充方式
// OAEP 是一种概率性加密填充,每次加密同一明文会产生不同的密文
// 但签名需要确定性,这样才能保证相同数据的签名结果可以被验证
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING);
assert(ret == 1);

// 设置签名使用的哈希算法
ret = EVP_PKEY_CTX_set_signature_md(ctx, hashMethods.value(hashType)());
assert(ret == 1);

// 数据签名
size_t outLen = 0;
ret = EVP_PKEY_sign(
ctx, NULL, &outLen, reinterpret_cast<const unsigned char*>(md.data()), md.size());
assert(ret == 1);
unsigned char* out = new unsigned char[outLen];
ret = EVP_PKEY_sign(
ctx, out, &outLen, reinterpret_cast<const unsigned char*>(md.data()), md.size());
assert(ret == 1);

QByteArray signData(reinterpret_cast<char*>(out), outLen);

// 释放资源
delete[] out;
EVP_PKEY_CTX_free(ctx);
return signData;
}

5.2、数据校验

5.2.1、哈希值计算

Qt的QCryptographicHash提供了一系列的加密算法实现,其中就包括哈希值计算

1
2
3
4
5
6
7
8
QCryptographicHash::Algorithm hashType;
hashType = QCryptographicHash::Sha256;

// 计算哈希值
QCryptographicHash hashCode(hashType);
hashCode.addData(data);
// 目前md存储的是二进制格式数据
QByteArray md = hashCode.result();
5.2.2、EVP_PKEY_CTX_new

用于创建与给定密钥对象(EVP_PKEY)相关联的密钥上下文(EVP_PKEY_CTX)。

1
EVP_PKEY_CTX *EVP_PKEY_CTX_new(EVP_PKEY *pkey, ENGINE *e);

参数解释:

  • pkey:与上下文关联的密钥对象。这可以是一个公钥、私钥或对称密钥对象,具体取决于使用场景。
  • e:可选参数,与上下文关联的引擎(Engine)。如果不需要使用特定引擎,可以传入 NULL

返回值

  • EVP_PKEY_CTX 类型的指针,即新创建的密钥上下文对象
1
2
3
// 创建加密数据的上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(m_privateKey, NULL);
assert(ctx != NULL);

需要注意的是,使用完密钥上下文后应该调用 EVP_PKEY_CTX_free 函数来释放相应的资源,以避免内存泄漏。

5.2.3、EVP_PKEY_verify_init

用于使用非对称密钥进行验签操作的函数

1
int EVP_PKEY_verify_init(EVP_PKEY_CTX *);

参数解释:

  • ctx:要进行数据校验操作的密钥上下文

返回值

  • 1:成功
  • 0:失败
1
2
int ret = EVP_PKEY_sign_init(ctx);
assert(ret == 1);
5.2.4、EVP_PKEY_CTX_set_rsa_padding

用于设置 RSA 加密或解密操作的填充方式

1
int EVP_PKEY_CTX_set_rsa_padding(EVP_PKEY_CTX *ctx, int pad);

参数解释:

  • ctx:RSA 加密或解密操作的上下文(EVP_PKEY_CTX)。
  • pad:要设置的加密填充(padding)方式,可以是以下值之一:
    • RSA_PKCS1_PADDING:PKCS#1 填充方式,是最常见的 RSA 填充方式。
    • RSA_PKCS1_OAEP_PADDING:PKCS#1 OAEP 填充方式,带有随机性质的填充方式,安全性更高。
    • RSA_NO_PADDING:不进行填充操作,仅加密或解密数据。

设置签名时,不能使用RSA_PKCS1_OAPE_PADDING这种填充方式,因为OAEP 是一种概率性加密填充,每次加密同一明文会产生不同的密文,但签名需要确定性,这样才能保证相同数据的签名结果可以被验证

返回值

  • 1:成功
  • 0:失败
1
2
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING);
assert(ret == 1);
5.2.5、EVP_PKEY_CTX_set_signature_md

用于设置签名算法

1
int EVP_PKEY_CTX_set_signature_md(EVP_PKEY_CTX *ctx, const EVP_MD *md);

参数解释:

  • ctx:指向 EVP_PKEY_CTX 上下文结构的指针,用于设置签名算法。
  • md:指向 EVP_MD 结构的指针,表示要使用的签名算法。

签名算法由 OpenSSL 中的 EVP_MD 结构表示,其中包含有关算法的信息,如名称、摘要长度等

返回值

  • 1:成功
  • 0:失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const QMap<QCryptographicHash::Algorithm, hashFunc> hashMethods = {
{QCryptographicHash::Md5, EVP_md5},
{QCryptographicHash::Sha1, EVP_sha1},
{QCryptographicHash::Sha224, EVP_sha224},
{QCryptographicHash::Sha256, EVP_sha256},
{QCryptographicHash::Sha384, EVP_sha384},
{QCryptographicHash::Sha512, EVP_sha512},
{QCryptographicHash::Sha3_224, EVP_sha3_224},
{QCryptographicHash::Sha3_256, EVP_sha3_256},
{QCryptographicHash::Sha3_384, EVP_sha3_384},
{QCryptographicHash::Sha3_512, EVP_sha3_512},
};
// 设置校验使用的哈希算法,与签名一致
ret = EVP_PKEY_CTX_set_signature_md(ctx, hashMethods.value(hashType)());
assert(ret == 1);
5.2.6、EVP_PKEY_verify

使用非对称密钥进行签名操作的函数

1
2
3
int EVP_PKEY_sign(EVP_PKEY_CTX *ctx, 
unsigned char *sig, size_t *siglen,
const unsigned char *tbs, size_t tbslen);

参数解释:

  • ctx:指向 EVP_PKEY_CTX 上下文结构的指针,表示签名操作的上下文。
  • sig:指向缓冲区的指针,用于存储签名结果。
  • siglen:指向 sig 缓冲区长度的指针,表示输入时表示 sig 缓冲区的长度,输出时表示实际写入 sig 的字节数。
  • tbs:指向要签名的数据的指针。
  • tbslen:要签名的数据的长度。

返回值:

  • 1:成功
  • 0:失败
1
2
3
4
5
6
7
// 签名校验
size_t outLen = 0;
ret = EVP_PKEY_verify(ctx,
reinterpret_cast<const unsigned char*>(sign.data()),
sign.size(),
reinterpret_cast<const unsigned char*>(md.data()),
md.size());
5.2.7、EVP_PKEY_CTX_free

释放密钥对上下文对象

1
void EVP_PKEY_CTX_free(EVP_PKEY_CTX *ctx);

参数解释:

  • ctx:指向要释放内存的密钥对上下文对象的指针。
1
2
// 释放上下文
EVP_PKEY_CTX_free(ctx);
完整示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
bool RSACrypto::varify(const QByteArray& sign,
const QByteArray& data,
QCryptographicHash::Algorithm hashType)
{
// 计算哈希值
QCryptographicHash hashCode(hashType);
hashCode.addData(data);
// 目前md存储的是二进制格式数据
QByteArray md = hashCode.result();

// 创建解密数据的上下文对象
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(m_publicKey, NULL);
assert(ctx != NULL);

// 设置加密和填充模式
int ret = EVP_PKEY_verify_init(ctx);
assert(ret == 1);
// 填充要与签名的一致
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING);
assert(ret == 1);

// 设置签名使用的哈希算法
ret = EVP_PKEY_CTX_set_signature_md(ctx, hashMethods.value(hashType)());
assert(ret == 1);

// 签名校验
size_t outLen = 0;
ret = EVP_PKEY_verify(ctx,
reinterpret_cast<const unsigned char*>(sign.data()),
sign.size(),
reinterpret_cast<const unsigned char*>(md.data()),
md.size());
EVP_PKEY_CTX_free(ctx);
if (ret == 1)
return true;
return false;
}

Qt和OpenSSL进行AES对称加解密开发

一、对称加密

1、基本用法

1
2
3
4
5
6
7
8
9
10
11
12
// 创建密钥(大小必须与算法匹配)
QByteArray key(32, 'K'); // 32字节密钥用于256位加密

// 创建加密器
AESCrypto crypto(AESCrypto::Algorithm::AES_CBC_256, key);

// 加密数据
QByteArray plainText = "要加密的数据";
QByteArray encrypted = crypto.encrypt(plainText);

// 解密数据
QByteArray decrypted = crypto.decrypt(encrypted);

必要头文件

1
2
3
4
#include <QByteArray>
#include <QCryptographicHash>
#include <openssl/evp.h>
#include <openssl/aes.h>

2、密钥长度要求

  • AES_xxx_128: 16字节密钥
  • AES_xxx_192: 24字节密钥
  • AES_xxx_256: 32字节密钥

3、支持的加密模式

  • ECB: 最简单但最不安全,不需要IV (Electronic CodeBook)
  • CBC: 常用安全模式,需要IV (Cipher Block Chaining)
  • CFB: 流密码模式,需要IV (Cipher FeedBack)
  • OFB: 流密码模式,需要IV (Output FeedBack)
  • CTR: 计数器模式,需要IV (CounTeR)

4、基本5步骤

4.1、EVP_CIPHER_CTX_new

该函数用于创建一个新的对称加密算法上下文对象(EVP_CIPHER_CTX结构体),并返回指向该对象的指针。对称加密算法上下文对象包含了进行加密和解密所需的状态和数据结构

1
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();

4.2、 EVP_CipherInit_ex

用于初始化加密或解密操作的上下文

1
2
3
int EVP_CipherInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher,
ENGINE *impl, const unsigned char *key,
const unsigned char *iv, int enc);
  • cipher: 加密算法,这是一个函数指针,指向加密算法的实现,可以使用 EVP_aes_256_cbc() 或其他支持的算法
  • impl: 引擎,默认为nullptr
  • key: 对称加密算法所使用的密钥
  • iv:初始向量,用于加密算法,分组密码算法加密都需要初始向量来进行第一次加密(ECB模式不需要,但ECB不安全)
  • enc: 加密或解密, 1表示加密,0表示解密

返回值的含义如下:

  • 如果函数执行成功,则返回1
  • 如果发生错误,则返回0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
//定义函数指针
using algorithmFunc = const EVP_CIPHER* (*)();

//加密算法类型
enum class Algorithm
{
// 16字节
AES_ECB_128,
AES_CBC_128,
AES_CFB_128,
AES_OFB_128,
AES_CTR_128,
// 24字节
AES_ECB_192,
AES_CBC_192,
AES_CFB_192,
AES_OFB_192,
AES_CTR_192,
// 32字节
AES_ECB_256,
AES_CBC_256,
AES_CFB_256,
AES_OFB_256,
AES_CTR_256
};

// 加解密
enum class CryptoType
{
DECRYPTO,
ENCRYPTO
};

// 使用map将Algorithm中的加密算法和对应的函数指针进行关联
const QMap<Algorithm, algorithmFunc> m_algorithms =
{
// 16字节
{Algorithm::AES_ECB_128, EVP_aes_128_ecb},
{Algorithm::AES_CBC_128, EVP_aes_128_cbc},
{Algorithm::AES_CFB_128, EVP_aes_128_cfb128},
{Algorithm::AES_OFB_128, EVP_aes_128_ofb},
{Algorithm::AES_CTR_128, EVP_aes_128_ctr},
// 24字节
{Algorithm::AES_ECB_192, EVP_aes_192_ecb},
{Algorithm::AES_CBC_192, EVP_aes_192_cbc},
{Algorithm::AES_CFB_192, EVP_aes_192_cfb128},
{Algorithm::AES_OFB_192, EVP_aes_192_ofb},
{Algorithm::AES_CTR_192, EVP_aes_192_ctr},
// 32字节
{Algorithm::AES_ECB_256, EVP_aes_256_ecb},
{Algorithm::AES_CBC_256, EVP_aes_256_cbc},
{Algorithm::AES_CFB_256, EVP_aes_256_cfb128},
{Algorithm::AES_OFB_256, EVP_aes_256_ofb},
{Algorithm::AES_CTR_256, EVP_aes_256_ctr}
};

void AESCrypto::generateIvec(unsigned char* ivec)
{
// NOTE Qt通过QCryptographicHash类提供了对数据进行哈希计算的功能
// 创建一个QCryptographicHash对象,指定哈希算法为MD5
QCryptographicHash hash(QCryptographicHash::Md5);
// 对对称密钥进行哈希计算
hash.addData(m_key);
// result()返回一个QByteArray对象,包含了哈希计算的结果
std::string res = hash.result().toStdString();
// 将哈希值转换为初始向量
for (int i = 0; i < AES_BLOCK_SIZE; ++i)
{
ivec[i] = res.at(i);
}
}

//初始向量(IV)生成
unsigned char ivec[AES_BLOCK_SIZE];
generateIvec(ivec);

int ret = EVP_CipherInit_ex(ctx,
m_algorithms.value(m_algorithmType)(),
nullptr,
reinterpret_cast<unsigned char*>(m_key.data()),
ivec,
cryptoType == CryptoType::ENCRYPTO ? 1 : 0);

4.3、EVP_CipherUpdate

用于对数据进行分块处理,并在每个块的加密或解密过程中更新输出缓冲区

1
2
3
4
5
int EVP_CipherUpdate(EVP_CIPHER_CTX *ctx,
unsigned char *out,
int *outl,
const unsigned char *in,
int inl);
  • out: 输出缓冲区
  • outl: 整数指针,用于接收写入输出缓冲区的数据长度
  • in: 待处理的数据(用于加密的明文和用于解密的密文)
  • inl: 待处理的数据长度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 设置填充模式,准备存储数据的缓冲区
int length = data.size() + 1; // +1是为了存储结束符
if (length % AES_BLOCK_SIZE)
{
// 如果数据长度不是16的倍数,则需要进行填充
// length/AES_BLOCK_SIZE: 计算出完整的分组数,+1是多出来需要填充的分组
length = (length / AES_BLOCK_SIZE + 1) * AES_BLOCK_SIZE;
}
unsigned char* out = new unsigned char[length];
int outLength, totalLength = 0;

ret = EVP_CipherUpdate(
ctx, out, &outLength, reinterpret_cast<const unsigned char*>(data.data()), data.size());
totalLength += outLength;

4.4、EVP_CipherFinal_ex

该函数处理最后一个数据块,并输出加密或解密的最终结果

1
2
3
int EVP_CipherFinal_ex(EVP_CIPHER_CTX *ctx,
unsigned char *outm,
int *outl);
  • outm: 输出缓冲区
  • outl: 整数指针,用于接收写入输出缓冲区的数据长度
1
ret = EVP_CipherFinal_ex(ctx, out + outLength, &outLength);

4.5、 EVP_CIPHER_CTX_free

该函数释放由 EVP_CIPHER_CTX_new 函数创建的上下文

1
void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *ctx);

5、完整示例

  • AESCrypto.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#ifndef AESCRYPTO_H
#define AESCRYPTO_H

#include <QByteArray>
#include <QMap>
#include <QObject>
#include <openssl/evp.h>

// 数据加解密类
class AESCrypto : public QObject
{
Q_OBJECT
public:
// 加密算法类型
enum class Algorithm
{
// 16字节
AES_ECB_128,
AES_CBC_128,
AES_CFB_128,
AES_OFB_128,
AES_CTR_128,
// 24字节
AES_ECB_192,
AES_CBC_192,
AES_CFB_192,
AES_OFB_192,
AES_CTR_192,
// 32字节
AES_ECB_256,
AES_CBC_256,
AES_CFB_256,
AES_OFB_256,
AES_CTR_256
};

// 加解密
enum class CryptoType
{
DECRYPTO,
ENCRYPTO
};

// 定义函数指针,using 别名 = 返回类型 (*)(参数列表);
using algorithmFunc = const EVP_CIPHER* (*)();

AESCrypto(Algorithm algorithm, const QByteArray& key, QObject* parent = nullptr);
~AESCrypto();

// 加密-返回加密后的数据
QByteArray encrypt(const QByteArray& data);
// 解密-返回解密后的数据
QByteArray decrypt(const QByteArray& data);

private:
// 数据加解密处理
QByteArray processCrypto(const QByteArray& data, CryptoType cryptoType);
// 生成初始化向量,用于加密算法
void generateIvec(unsigned char* ivec);
// 使用map将Algorithm中的加密算法和对应的函数指针进行关联
const QMap<Algorithm, algorithmFunc> m_algorithms =
{
// 16字节
{Algorithm::AES_ECB_128, EVP_aes_128_ecb},
{Algorithm::AES_CBC_128, EVP_aes_128_cbc},
{Algorithm::AES_CFB_128, EVP_aes_128_cfb128},
{Algorithm::AES_OFB_128, EVP_aes_128_ofb},
{Algorithm::AES_CTR_128, EVP_aes_128_ctr},
// 24字节
{Algorithm::AES_ECB_192, EVP_aes_192_ecb},
{Algorithm::AES_CBC_192, EVP_aes_192_cbc},
{Algorithm::AES_CFB_192, EVP_aes_192_cfb128},
{Algorithm::AES_OFB_192, EVP_aes_192_ofb},
{Algorithm::AES_CTR_192, EVP_aes_192_ctr},
// 32字节
{Algorithm::AES_ECB_256, EVP_aes_256_ecb},
{Algorithm::AES_CBC_256, EVP_aes_256_cbc},
{Algorithm::AES_CFB_256, EVP_aes_256_cfb128},
{Algorithm::AES_OFB_256, EVP_aes_256_ofb},
{Algorithm::AES_CTR_256, EVP_aes_256_ctr}
};

private:
Algorithm m_algorithmType; // 加密算法类型
QByteArray m_key; // 对称密钥
};

#endif
  • AESCrypto.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include "AESCrypto.h"
#include "include/AESCrypto.h"
#include "openssl/evp.h"
#include <QCryptographicHash>
#include <cassert>
#include <openssl/aes.h>

AESCrypto::AESCrypto(Algorithm algorithm, const QByteArray& key, QObject* parent) : QObject(parent)
{
switch (algorithm)
{
case Algorithm::AES_CBC_128:
case Algorithm::AES_ECB_128:
case Algorithm::AES_OFB_128:
case Algorithm::AES_CFB_128:
case Algorithm::AES_CTR_128:
assert(key.size() == 16);
break;
case Algorithm::AES_CBC_192:
case Algorithm::AES_ECB_192:
case Algorithm::AES_OFB_192:
case Algorithm::AES_CFB_192:
case Algorithm::AES_CTR_192:
assert(key.size() == 24);
break;
case Algorithm::AES_CBC_256:
case Algorithm::AES_ECB_256:
case Algorithm::AES_OFB_256:
case Algorithm::AES_CFB_256:
case Algorithm::AES_CTR_256:
assert(key.size() == 32);
break;
}

m_algorithmType = algorithm;
m_key = key;
}

AESCrypto::~AESCrypto()
{
}

QByteArray AESCrypto::encrypt(const QByteArray& data)
{
return processCrypto(data, CryptoType::ENCRYPTO);
}

QByteArray AESCrypto::decrypt(const QByteArray& data)
{
return processCrypto(data, CryptoType::DECRYPTO);
}

QByteArray AESCrypto::processCrypto(const QByteArray& data, CryptoType cryptoType)
{
// 通过函数生成时需要确保加密和解密生成的密钥一致
// AES_BLOCK_SIZE: 为AES加密算法的分组长度,16字节
unsigned char ivec[AES_BLOCK_SIZE];
generateIvec(ivec);
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
assert(ctx);

int ret = EVP_CipherInit_ex(ctx,
m_algorithms.value(m_algorithmType)(),
nullptr,
reinterpret_cast<unsigned char*>(m_key.data()),
ivec,
cryptoType == CryptoType::ENCRYPTO ? 1 : 0);
assert(ret);

// 设置填充模式,准备存储数据的缓冲区
int length = data.size() + 1; // +1是为了存储结束符
if (length % AES_BLOCK_SIZE)
{
// 如果数据长度不是16的倍数,则需要进行填充
// length/AES_BLOCK_SIZE: 计算出完整的分组数,+1是多出来需要填充的分组
length = (length / AES_BLOCK_SIZE + 1) * AES_BLOCK_SIZE;
}
unsigned char* out = new unsigned char[length];
int outLength, totalLength = 0;

ret = EVP_CipherUpdate(
ctx, out, &outLength, reinterpret_cast<const unsigned char*>(data.data()), data.size());
totalLength += outLength;
assert(ret);

ret = EVP_CipherFinal_ex(ctx, out + outLength, &outLength);
totalLength += outLength;
assert(ret);

QByteArray outData(reinterpret_cast<char*>(out), totalLength);
delete[] out;
EVP_CIPHER_CTX_free(ctx); // 释放上下文
return outData;
}

// 生成初始向量,对对称密钥进行哈希计算,将哈希值作为初始向量,
// 只要加密算法一致,加解密时使用的初始向量就会一致
void AESCrypto::generateIvec(unsigned char* ivec)
{
// NOTE Qt通过QCryptographicHash类提供了对数据进行哈希计算的功能
// 创建一个QCryptographicHash对象,指定哈希算法为MD5
QCryptographicHash hash(QCryptographicHash::Md5);
// 对对称密钥进行哈希计算
hash.addData(m_key);
// result()返回一个QByteArray对象,包含了哈希计算的结果
std::string res = hash.result().toStdString();
// 将哈希值转换为初始向量
for (int i = 0; i < AES_BLOCK_SIZE; ++i)
{
ivec[i] = res.at(i);
}
}

  • main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <QApplication>
#include <QDebug>
#include "login.h"
#include "AESCrypto.h"

void testAESCrypto() {
// 测试AES加密解密
QByteArray key = "1234567890123456";
AESCrypto aes(AESCrypto::Algorithm::AES_ECB_128, key);
QByteArray data = aes.encrypt("测试加密算法");
QByteArray text = aes.decrypt(data);
qDebug() << "解密后数据:" << text.data();
}

int main(int argc, char *argv[]) {
QApplication a(argc, argv);
Login login;
login.show();
testAESCrypto();

return QApplication::exec();
}

Qt自定义日志输出

一、注册自定义日志类

在main.cpp中注册日志类

1
qInstallMessageHandler(Logger::customMessageHandler);

二、创建自定义日志类

2.1 样式一:无颜色高亮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include "Logger.h"
#include <QDateTime>
#include <QFileInfo>
#include <cstdio>

namespace Logger
{

void customMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg)
{
QString timeText = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
QString file = QFileInfo(context.file ? context.file : "no-file").fileName();
QString function = context.function ? context.function : "no-function";

QString formattedMessage = QString("%1 [%2] (%3:%4, %5): %6")
.arg(timeText)
.arg(typeToString(type))
.arg(file)
.arg(context.line)
.arg(function)
.arg(msg);

fprintf(stderr, "%s\n", formattedMessage.toLocal8Bit().constData());
}

QString typeToString(QtMsgType type)
{
switch (type)
{
case QtDebugMsg:
return "Debug";
case QtInfoMsg:
return "Info";
case QtWarningMsg:
return "Warning";
case QtCriticalMsg:
return "Critical";
default:
return "Unknown";
}
}

} // namespace Logger

效果图:

image-20250310115157637

2.2 样式二:有颜色高亮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include "Logger.h"
#include <QDateTime>
#include <QFileInfo>
#include <Windows.h>
#include <cstdio>

namespace Logger
{

void customMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg)
{
QString timeText = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
QString file = QFileInfo(context.file ? context.file : "no-file").fileName();
QString function = context.function ? context.function : "no-function";

QString typeStr = typeToString(type);

HANDLE hConsole = GetStdHandle(STD_ERROR_HANDLE);

CONSOLE_SCREEN_BUFFER_INFO consoleInfo;
GetConsoleScreenBufferInfo(hConsole, &consoleInfo);
WORD originalAttrs = consoleInfo.wAttributes;

QString firstPart = QString("%1 ").arg(timeText);
fprintf(stderr, "%s", firstPart.toLocal8Bit().constData());

WORD colorAttrs = 0;
switch (type)
{
case QtDebugMsg:
colorAttrs = FOREGROUND_BLUE | FOREGROUND_INTENSITY; // Bright blue
break;
case QtInfoMsg:
colorAttrs = FOREGROUND_GREEN | FOREGROUND_INTENSITY; // Bright green
break;
case QtWarningMsg:
colorAttrs = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY; // Bright yellow
break;
case QtCriticalMsg:
colorAttrs = FOREGROUND_RED | FOREGROUND_INTENSITY; // Bright red
break;
default:
colorAttrs = originalAttrs;
}
SetConsoleTextAttribute(hConsole, colorAttrs);

QString coloredPart = QString("[%1]").arg(typeStr);
fprintf(stderr, "%s", coloredPart.toLocal8Bit().constData());

SetConsoleTextAttribute(hConsole, originalAttrs);

QString lastPart =
QString(" (%1:%2, %3): %4\n").arg(file).arg(context.line).arg(function).arg(msg);
fprintf(stderr, "%s", lastPart.toLocal8Bit().constData());

HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE);
if (hStdIn != INVALID_HANDLE_VALUE)
{
SetConsoleTextAttribute(hStdIn, originalAttrs);
}

HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (hStdOut != INVALID_HANDLE_VALUE)
{
SetConsoleTextAttribute(hStdOut, originalAttrs);
}
}

QString typeToString(QtMsgType type)
{
switch (type)
{
case QtDebugMsg:
return "Debug";
case QtInfoMsg:
return "Info";
case QtWarningMsg:
return "Warning";
case QtCriticalMsg:
return "Critical";
default:
return "Unknown";
}
}

} // namespace Logger

效果图

image-20250310115026790

vscode settings.json备份

{
“editor.fontFamily”: “‘JetBrains Mono’, Consolas, ‘Courier New’, monospace”,
“editor.fontSize”: 16,
“workbench.colorTheme”: “JetBrains Darcula Theme”,
“workbench.iconTheme”: “vscode-jetbrains-icon-theme”,
“remote.SSH.remotePlatform”: {
“13.250.32.58”: “linux”,
“43.163.110.165”: “linux”
},
“cmake.showOptionsMovedNotification”: false,
“cmake.pinnedCommands”: [
“workbench.action.tasks.configureTaskRunner”,
“workbench.action.tasks.runTask”
],
“files.autoSave”: “afterDelay”,
“github.copilot.enable”: {
“: false,
“plaintext”: false,
“markdown”: false,
“scminput”: false
},
“update.mode”: “start”,
“redhat.telemetry.enabled”: false,
“cmake.showConfigureWithDebuggerNotification”: false,
// 解决cmake输出中文乱码问题
“files.encoding”: “utf8”,
“cmake.outputLogEncoding”: “UTF-8”,
//解决终端乱码问题
// “terminal.integrated.profiles.windows”: {
// “PowerShell”: {
// “source”: “PowerShell”,
// “args”: [
// “-NoExit”,
// “-Command”,
// “chcp 65001”
// ]
// },
// “Command Prompt”: {
// “path”: “C:\Windows\System32\cmd.exe”,
// “args”: [
// “/K”,
// “chcp 65001”
// ]
// }
// },
“terminal.integrated.defaultProfile.windows”: “PowerShell”, //默认打开终端为PowerShell
“clangd.detectExtensionConflicts”: false,
“git.openRepositoryInParentFolders”: “always”,
“security.workspace.trust.enabled”: false,
“security.workspace.trust.emptyWindow”: false,
“[cpp]”: {
“editor.defaultFormatter”: “xaver.clang-format”
},
“clang-format.executable”: “D:/clang-format/clang-format.exe”,
“clang-format.fallbackStyle”: “None”,
“clang-format.language.apex.fallbackStyle”: “None”,
“clang-format.language.apex.style”: “None”,
“clangd.path”: “D:/clangd_18.1.3/bin/clangd.exe”,
“editor.quickSuggestionsDelay”: 0,
“workbench.startupEditor”: “none”,
“explorer.autoReveal”: false,
“editor.cursorStyle”: “block”,
“C_Cpp.intelliSenseEngine”: “disabled”,
“C_Cpp.codeAnalysis.runAutomatically”: true,
“C_Cpp.configurationWarnings”: “disabled”,
“C_Cpp.codeFolding”: “disabled”,
“C_Cpp.formatting”: “clangFormat”,
“C_Cpp.codeAnalysis.clangTidy.enabled”: true,
“C_Cpp.autocomplete”: “default”,
“C_Cpp.default.cppStandard”: “c++17”,
“C_Cpp.default.cStandard”: “c17”,
“C_Cpp.enhancedColorization”: “enabled”,
“C_Cpp.codeAnalysis.clangTidy.useBuildPath”: true,
“C_Cpp.default.compileCommands”: “${workspaceFolder}/build/compile_commands.json”,
“C_Cpp.codeAnalysis.clangTidy.config”: “${workspaceFolder}/.clang-tidy”,
//todo-tree
“todo-tree.highlights.defaultHighlight”: {
“icon”: “alert”,
“type”: “tag”,
“background”: “#e8cd37”, // 高亮背景颜色
“color”: “#000000”, // 字体颜色
“fontWeight”: “bold”
},
“todo-tree.regex.regex”: “(TODO|FIXME|NOTE)”, // 正则表达式,用于匹配关键字
“todo-tree.general.tags”: [
“TODO”,
“FIXME”,
“NOTE”
], // 自定义标签
“todo-tree.highlights.customHighlight”: {
“TODO”: {
“icon”: “check”,
“background”: “#e65a30”,
“color”: “#FFFFFF”,
“fontWeight”: “bold”
},
“FIXME”: {
“icon”: “bug”,
“background”: “#ed2e6e”,
“color”: “#FFFFFF”,
“fontWeight”: “bold”
},
“NOTE”: {
“icon”: “info”,
“background”: “#2596f2”,
“color”: “#FFFFFF”,
“fontWeight”: “bold”
}
},
“todo-tree.tree.autoRefresh”: true, // 自动刷新
“todo-tree.general.rootFolder”: “${workspaceFolder}”,
“todo-tree.filtering.includeGlobs”: [
“**/
“ // Include only files in the current workspace
],
“todo-tree.filtering.excludeGlobs”: [
/build/“,
/bin/“,
/.cache/“,
/.vscode/“,
/.git/“,
],
“todo-tree.tree.scanMode”: “workspace”,
“git.enableSmartCommit”: true,
“editor.unicodeHighlight.invisibleCharacters”: false,
“editor.unicodeHighlight.ambiguousCharacters”: false,
“makefile.configureOnOpen”: false,
“[c]”: {
“editor.defaultFormatter”: “llvm-vs-code-extensions.vscode-clangd”
},
“[javascript]”: {
“editor.defaultFormatter”: “vscode.typescript-language-features”
},
“[proto3]”: {
“editor.defaultFormatter”: “zxh404.vscode-proto3”
},
“workbench.editor.limit.enabled”: true,
“workbench.editor.limit.value”: 8,
“workbench.tree.renderIndentGuides”: “always”,
“workbench.tree.expandMode”: “doubleClick”,
“window.commandCenter”: false,
“java.configuration.maven.userSettings”: “D:/apache-maven-3.9.9/conf/settings.xml”,
“maven.executable.path”: “D:/apache-maven-3.9.9/bin/mvn”,// Maven可执行文件的完整路径
“maven.terminal.useJavaHome”: true, // 使用JAVA_HOME环境变量中的JDK
“maven.view”: “flat”, // 可选,设置Maven依赖视图样式
“java.maven.downloadSources”: true,// 自动下载源代码
“[java]”: {
“editor.defaultFormatter”: “redhat.java”
},
“github.copilot.selectedCompletionModel”: “gpt-4o-copilot”,
“cmake.automaticReconfigure”: false,
“cmake.configureOnEdit”: false
}

openssl安装和基本使用

一、windows安装

二、linux安装

源码地址:https://github.com/openssl/openssl

1
2
# 从 github 仓库下载
$ git clone https://github.com/openssl/openssl.git

安装 (安装过程可参考官方提供的文档 https://github.com/openssl/openssl/blob/master/NOTES-UNIX.md)

1
2
3
4
5
6
7
8
9
10
11
# 解压缩 (非git下载)
$ unzip openssl-master.zip
# 进入解压目录
$ cd openssl-master
# 构建并安装
# 检查安装环境, 生成 makefile
$ ./Configure --prefix=/usr/local/ssl \
--openssldir=/usr/local/ssl \
'-Wl,-rpath,$(LIBRPATH)'
$ make -j$(nproc)
$ sudo make install

安装完成之后,可执行程序被安装到了/usr/local/ssl/bin目录中:

1
2
$ ls /usr/local/ssl/bin/
c_rehash openssl

为了能够全局访问openssl,可以创建一个软连接(快捷方式):

1
$ sudo ln -s /usr/local/ssl/bin/openssl /usr/bin/openssl

测试

1
2
$ openssl version
OpenSSL 3.2.0-dev (Library: OpenSSL 3.2.0-dev )

如果openssl能够正常工作,我们就可以看到它的版本号了。

三、配置CMake

1、windows

1
2
3
4
set(OPENSSL_PATH "D:/OpenSSL-Win64")
include_directories(${OPENSSL_PATH}/include)
link_directories(${OPENSSL_PATH}/lib/VC/x64/MD)
target_link_libraries(${PROJECT_NAME} libcrypto)

2、linux

1
2
3
4
set(OPENSSL_PATH /usr/local/ssl)
include_directories(${OPENSSL_PATH}/include)
link_directories(${OPENSSL_PATH}/lib64)
target_link_libraries(${PROJECT_NAME} crypto)