使用 openssl 进行 RSA 加解密
目录
在我之前的一篇博客中 RSA 公钥加密原理 中, 对 RSA 非对称加密原理做了简单的阐述。这篇博客主要聊如何使用 OPENSSL 进行密钥对的生成,以及非对称加解密。
一. 生成密钥对
在 OPENSSL 中, RSA
是一个很重要的结构体。它的定义在 rsa_locl.h
中,面包含了在原理中提到的所有重要的变量 随机质数 p
, q
, 公钥指数 e
, 私钥指数 d
, 以及模数 n
1 2 3 4 5 6 7 8 9 |
struct rsa_st { // ... BIGNUM *n; BIGNUM *e; BIGNUM *d; BIGNUM *p; BIGNUM *q; // ... }; |
生成密钥函数:
1 |
int RSA_generate_key_ex(RSA *rsa, int bits, BIGNUM *e, BN_GENCB *cb); |
bits
密钥的规模(modulus
)。小于 1028 位的密钥是不安全的,小于 512 则会返回 0e
公开的指数。它应该是一个奇数(odd number), 一般是3, 17
或65537
cb
生成大随机数的回调函数。一般使用 NULL 即可, 默认为BN_GENCB_call()
生成公私钥并写入文件的代码示例:
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 |
#include <openssl/rsa.h> #include <openssl/evp.h> #include <openssl/bn.h> #include <openssl/pem.h> #include <openssl/applink.c> //important! void gen_rsa_key() { RSA *rsa = RSA_new(); BIGNUM *e = BN_new(); BN_set_word(e, 17); BN_GENCB* gencb = NULL; EVP_PKEY* pkey = EVP_PKEY_new(); int rst = RSA_generate_key_ex(rsa, 3072, e, gencb); rst = EVP_PKEY_set1_RSA(pkey, rsa); FILE* f_pri = fopen("pri.key", "wb"); FILE* f_pub = fopen("pub.pem", "wb"); rst = PEM_write_RSAPublicKey(f_pub, rsa); rst = PEM_write_RSAPrivateKey(f_pri, rsa, 0, 0, 0, 0, 0); fclose(f_pri); fclose(f_pub); f_pri = NULL; f_pub = NULL; RSA_free(rsa); BN_free(e); EVP_PKEY_free(pkey); BN_GENCB_free(gencb); } |
二. 公私钥加解密
公私钥加解密两个步骤, 一是载入密钥, 二是加解密。
载入密钥使用 API :
1 2 3 4 |
PEM_read_RSAPublicKey() PEM_read_RSAPrivateKey() PEM_read_bio_RSAPrivateKey() PEM_read_bio_RSAPublicKey() |
当载入失败时返回 NULL。
而加解密中两个重要的 API 是:
1 2 |
int RSA_public_encrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding); int RSA_private_decrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding); |
flen
是输入数据的长度,必须小于等于modulus = RSA_size(rsa)
个字节, 且和参数padding
相关。当 flen 不满足要求时,将会出现"data too small for key size"
或"data too large for key size"
错误。 flen 与 padding 的对应关系如下:RSA_PKCS1_PADDING
, flen <= modulus - 11RSA_SSLV23_PADDING
, flen <= modulus - 11RSA_NO_PADDING
, flen = modulusRSA_PKCS1_OAEP_PADDING
, flen <= modulus - 41 (2 倍的 SHA1 长度 + 1)
padding
是填充模式,当 flen 满足上述关系时,将会进行填充:RSA_PKCS1_PADDING
, 最常用的模式,使用 PKCS #1 v1.5 标准,前两字节填充0x00, 0x02
,接着的modulus - flen - 3
字节使用随机非 '\0'
值填充,接着填充一个字节的0x00
, 然后是from
数据RSA_SSLV23_PADDING
, 前两个字节填充0x00,0x02
, 接着的modulus - flen - 3 - 8
字节填充随机非 '\0'
值,然后填充 8 个0x03
, 接着填充一个字节的0x00
,然后是from
数据RSA_NO_PADDING
, 不需要填充RSA_PKCS1_OAEP_PADDING
, 前20字节使用 SHA1 填充, 接着填充至少 20 字节的0x00
, 最后填充0x01
,接着是from
数据
如图所示:
公私钥加解密示例:
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 |
#include <openssl/rsa.h> #include <openssl/evp.h> #include <openssl/bn.h> #include <openssl/pem.h> #include <openssl/applink.c> //important! const char* pri_key_path = "pri.key"; const char* pub_key_path = "pub.pem"; const char *plain_text = "The EVP interface supports the ability to perform authenticated encryption and decryption, as well as the option to attach unencrypted," " associated data to the message. Such Authenticated-Encryption with Associated-Data (AEAD) schemes provide confidentiality by encrypting the data, and also" " provide authenticity assurances by creating a MAC tag over the encrypted data. The MAC tag will ensure the data is not accidentally altered or maliciously" " tampered during transmission and storage."; bool pub_encrypt(const char* pub_key_path, const unsigned char* in, int in_len, unsigned char*& out, int& out_len) { bool ret = false; RSA* rsa = RSA_new(); FILE* f = fopen(pub_key_path, "r"); if (!f) goto ERR; rsa = PEM_read_RSAPublicKey(f, &rsa, 0, 0); if (!rsa) goto ERR; fclose(f); f = NULL; out_len = RSA_size(rsa); if (in_len > out_len) goto ERR; out = (unsigned char*)malloc(out_len); int padding = RSA_PKCS1_PADDING; int rst = RSA_public_encrypt(in_len, in, out, rsa, padding); if (rst <= 0) { free(out); out = NULL; printf("%s\n", ERR_reason_error_string(ERR_get_error())); goto ERR; } ret = true; ERR: if(rsa) RSA_free(rsa); return ret; } bool pri_decrypt(const char* pri_key_path, const unsigned char* in, int in_len, unsigned char*& out, int& out_len) { bool ret = false; RSA* rsa = RSA_new(); FILE* f = fopen(pri_key_path, "r"); if (!f) goto ERR; rsa = PEM_read_RSAPrivateKey(f, &rsa, 0, 0); if (!rsa) goto ERR; fclose(f); f = NULL; out_len = RSA_size(rsa); if (in_len > out_len) goto ERR; out = (unsigned char*)malloc(out_len); int padding = RSA_PKCS1_PADDING; int rst = RSA_private_decrypt(in_len, in, out, rsa, padding); if (rst < 0) { free(out); out = NULL; printf("%s", ERR_reason_error_string(ERR_get_error())); goto ERR; } out_len = rst; ret = true; ERR: if (rsa) RSA_free(rsa); return ret; } int main() { //init_openssl(); unsigned char* cipher_text = NULL, *re_plain_text = NULL; int enc_out_len = 0, dec_out_len; bool rst = pub_encrypt(pub_key_path, reinterpret_cast<const unsigned char*>(plain_text), strlen(plain_text), cipher_text, enc_out_len); assert(rst); rst = pri_decrypt(pri_key_path, cipher_text, enc_out_len, re_plain_text, dec_out_len); assert(rst); assert(strlen(plain_text) == dec_out_len); int cmp = memcmp(plain_text, re_plain_text, dec_out_len); assert(cmp == 0); free(cipher_text); free(re_plain_text); //clean_openssl(); return 0; } |
三. Tips
1. 在 Windows 下运行出现崩溃 "no OPENSSL_Applink"
多半是因为代码运行库不匹配导致的。在Windows 中,运行时库有这几种:
- Single Threaded
/ML
- MS VC++ often defaults to this for the release version of a new project. - Debug Single Threaded
/MLd
- MS VC++ often defaults to this for the debug version of a new project. - Multithreaded
/MT
- Debug Multithreaded
/MTd
- Multithreaded DLL
/MD
- OpenSSL defaults to this. - Debug Multithreaded DLL
/MDd
运行库不一致可能导致程序崩溃(通常是在进行IO操作时)。这种情况下可以重新编译 OPENSSL, 或者修改程序的运行库。更简单的办法,可以尝试添加一个文件引用:
1 |
#include <openssl/applink.c> |