初めに
エンジニアであれば誰しもが一度は聞いたことがあるであろう暗号方式について、
Opensslのライブラリを使用してC++で手軽に実装する手順を書こうと思います。
参考にしたものは私の知識とChatGPT+ネットの情報だけなので間違っていたらごめんなさい🙇
コンパイラはG++でIDEはVScodeを使用しています。
ステップ1:Opensslを導入し、環境を整える
下記のリンクからOpensslをインストールしてください。
私はWin64 OpenSSL v3.3.0をダウンロードしました。
インストールが完全したらopenssl内のbinフォルダを環境変数のPathを通してください。
VScodeを使用している場合はc_cpp_properties.jsonのincludePathにopenssl内のincludeフォルダを
を追加して下さい
{
"configurations": [
{
"name": "Win32",
"includePath": [
"${workspaceFolder}/**",
"C:/Program Files/OpenSSL-Win64/include"//<--を追加
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
],
"windowsSdkVersion": "10.0.22621.0",
"cStandard": "c17",
"cppStandard": "c++17",
"intelliSenseMode": "gcc-x64",
"compilerPath": "C:/Program Files (x86)/mingw64/bin/g++.exe"
}
],
"version": 4
}
ステップ2:コーディング
ここから各暗号方式を簡単な説明込みで実装していきます。
※各コードの上部に私の環境でのコマンドラインオプションを記述しているので、それぞれの環境に合わせて変更してください。
【AES暗号方式】
AES暗号方式(Advanced Encryption Standard)は共通鍵暗号方式です。
暗号化と復号に同一の鍵を使用します。
鍵長は128,192,256bitから選択できますが今回は128bitを使用します。
今回の実装では初期化ベクトルを使用しています。
初期化ベクトルとはソルトのような、暗号化に使用するランダムなbit列のことです。
それでは実際に実装してみます。
Init_vec.cppは初期化ベクトル,共通鍵を生成するクラスです。
AES.cppで実行します。
#include <iostream>
#include <random>
#include "Init_vec.hpp"
unsigned long long InitClass::generateRandomNumber() {
// 乱数エンジンの生成
std::random_device rd;
std::mt19937_64 gen(rd());
// 16桁のランダムな整数を生成
std::uniform_int_distribution<unsigned long long> dist(1000000000000000ULL, 9999999999999999ULL);
return dist(gen);
}
std::string InitClass::generateCommonKey() {
// 乱数エンジンの生成
std::random_device rd;
std::mt19937 gen(rd());
// 16桁のランダムな文字列を生成
const std::string charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
std::uniform_int_distribution<std::size_t> dist(0, charset.size() - 1);
std::string result;
for (int i = 0; i < 16; ++i) {
result += charset[dist(gen)];
}
return result;
}
#ifndef InitClass_H
#define InitClass_H
class InitClass {
public:unsigned long long generateRandomNumber();
public:std::string generateCommonKey();
};
#endif
#include <iostream>
#include <random>
#include <string>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/err.h>
#include <openssl/aes.h>
#include "Init_vec.hpp"
#include "AES.hpp"
//コマンドラインオプション
//g++ -o AES.exe AES.cpp Init_vec.cpp -I"C:\Program Files\OpenSSL-Win64\include" -L"C:\Program Files\OpenSSL-Win64\lib\VC\x64\MTd" -lssl -lcrypto -Wno-deprecated-declarations
using namespace std;
// AES暗号化関数
string AESClass::aesEncrypt(string plaintext, string key,string iv) {
string ciphertext;
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
// 初期化
if (!EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, (const unsigned char*)key.c_str(), (const unsigned char*)iv.c_str())) {
cout << "暗号化の初期化に失敗しました" << endl;
return "";
}
// 暗号化する必要があるバッファのサイズを取得
int ciphertext_len = plaintext.length() + AES_BLOCK_SIZE;
unsigned char* encrypted = new unsigned char[ciphertext_len];
int len;
// 暗号化
if (!EVP_EncryptUpdate(ctx, encrypted, &len, (const unsigned char*)plaintext.c_str(), plaintext.length())) {
cout << "暗号化に失敗しました" << endl;
return "";
}
// ファイナライズ
int final_len;
if (!EVP_EncryptFinal_ex(ctx, encrypted + len, &final_len)) {
cout << "暗号化のファイナライズに失敗しました" << endl;
return "";
}
len += final_len;
// 暗号文を文字列に変換
ciphertext.assign((char*)encrypted, len);
delete[] encrypted;
EVP_CIPHER_CTX_free(ctx);
return ciphertext;
}
// AES復号化関数
string AESClass::aesDecrypt(string ciphertext, string key,string iv) {
string decryptedText;
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
// 初期化
if (!EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, (const unsigned char*)key.c_str(), (const unsigned char*)iv.c_str())) {
cout << "復号化の初期化に失敗しました" << endl;
return "";
}
// 復号化する必要があるバッファのサイズを取得
int decrypted_len = ciphertext.length() + AES_BLOCK_SIZE;
unsigned char* decrypted = new unsigned char[decrypted_len];
int len;
// 復号化
if (!EVP_DecryptUpdate(ctx, decrypted, &len, (const unsigned char*)ciphertext.c_str(), ciphertext.length())) {
cout << "復号に失敗しました" << endl;
return "";
}
int plaintext_len = len;
// ファイナライズ
int final_len;
if (!EVP_DecryptFinal_ex(ctx, decrypted + len, &final_len)) {
cout << "復号のファイナライズに失敗しました" << endl;
return "";
}
plaintext_len += final_len;
// 復号文を文字列に変換
decryptedText.assign((char*)decrypted, plaintext_len);
delete[] decrypted;
EVP_CIPHER_CTX_free(ctx);
return decryptedText;
}
int main() {
AESClass AES;
InitClass init;
// 暗号化したい文字列と鍵
std::string inputString;
string key = init.generateCommonKey();
string iv=std::to_string(init.generateRandomNumber());
// 文字列の入力
std::cout << "平文を入力してください: ";
std::getline(std::cin, inputString);
std::cout << "初期化ベクトル: " << iv << std::endl;
std::cout << "キー: " << key << std::endl;
// AES暗号化
string encryptedText = AES.aesEncrypt(inputString, key,iv);
if (encryptedText.empty()) {
cout << "AES暗号化に失敗しました" << endl;
return 1;
}
cout << "暗号化された文字列: " << encryptedText << endl;
// AES復号化
string decryptedText = AES.aesDecrypt(encryptedText, key,iv);
if (decryptedText.empty()) {
cout << "AES復号に失敗しました" << endl;
return 1;
}
cout << "復号された文字列: " << decryptedText << endl;
return 0;
}
#ifndef AESClass_H
#define AESClass_H
#include <string>
class AESClass {
public:
static std::string aesEncrypt(std::string plaintext, std::string key, std::string iv);
static std::string aesDecrypt(std::string plaintext, std::string key, std::string iv);
};
#endif
AES.cpp上部にコマンドラインオプションを記述しているのでターミナルで実行してみてください。
【実行画面】
上記のスクリーンショットのように暗号化,復号を行います。
【RSA暗号方式】
RSA暗号方式(Rivest Shamir Adleman)は公開鍵暗号方式です。
アルゴリズムに素因数分解を使用しています。
暗号化だけではなくディジタル署名などでも利用します。
暗号化の場合、送信者側は公開鍵で平文を暗号化します。受信者側は秘密鍵で復号します。
今回は2048bitの鍵長で生成します。
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <iostream>
#include <string>
//コンパイラオプション
//g++ -o RSA.exe RSA.cpp -I"C:\Program Files\OpenSSL-Win64\include" -L"C:\Program Files\OpenSSL-Win64\lib\VC\x64\MTd" -lssl -lcrypto -Wno-deprecated-declarations
// キーペアの生成
RSA* createRSAKeyPair() {
int keyLength = 2048;
unsigned long e = RSA_F4; // 公開指数(通常はRSA_F4)
RSA* rsa = RSA_generate_key(keyLength, e, NULL, NULL);
if (rsa == NULL) {
std::cerr << "鍵の生成に失敗しました" << std::endl;
return NULL;
}
return rsa;
}
// 公開鍵をPEM形式で取得
std::string getPublicKey(RSA* rsa) {
BIO* bio = BIO_new(BIO_s_mem());
PEM_write_bio_RSA_PUBKEY(bio, rsa);
size_t pubKeyLen = BIO_pending(bio);
char* pubKey = new char[pubKeyLen + 1];
BIO_read(bio, pubKey, pubKeyLen);
pubKey[pubKeyLen] = '\0';
std::string publicKey(pubKey);
delete[] pubKey;
BIO_free_all(bio);
return publicKey;
}
// 秘密鍵をPEM形式で取得
std::string getPrivateKey(RSA* rsa) {
BIO* bio = BIO_new(BIO_s_mem());
PEM_write_bio_RSAPrivateKey(bio, rsa, NULL, NULL, 0, NULL, NULL);
size_t privKeyLen = BIO_pending(bio);
char* privKey = new char[privKeyLen + 1];
BIO_read(bio, privKey, privKeyLen);
privKey[privKeyLen] = '\0';
std::string privateKey(privKey);
delete[] privKey;
BIO_free_all(bio);
return privateKey;
}
// メッセージの暗号化
std::string encryptMessage(RSA* rsa, const std::string& message) {
size_t rsaLen = RSA_size(rsa);
unsigned char* encryptedMessage = new unsigned char[rsaLen];
int result = RSA_public_encrypt(message.length(),
reinterpret_cast<const unsigned char*>(message.c_str()),
encryptedMessage,
rsa,
RSA_PKCS1_PADDING);
if (result == -1) {
char* err = new char[130];
ERR_load_crypto_strings();
ERR_error_string(ERR_get_error(), err);
std::cerr << "暗号化に失敗しました " << err << std::endl;
delete[] err;
return "";
}
std::string encryptedString(reinterpret_cast<char*>(encryptedMessage), result);
delete[] encryptedMessage;
return encryptedString;
}
// メッセージの復号
std::string decryptMessage(RSA* rsa, const std::string& encryptedMessage) {
size_t rsaLen = RSA_size(rsa);
unsigned char* decryptedMessage = new unsigned char[rsaLen];
int result = RSA_private_decrypt(encryptedMessage.length(),
reinterpret_cast<const unsigned char*>(encryptedMessage.c_str()),
decryptedMessage,
rsa,
RSA_PKCS1_PADDING);
if (result == -1) {
char* err = new char[130];
ERR_load_crypto_strings();
ERR_error_string(ERR_get_error(), err);
std::cerr << "復号に失敗しました" << err << std::endl;
delete[] err;
return "";
}
std::string decryptedString(reinterpret_cast<char*>(decryptedMessage), result);
delete[] decryptedMessage;
return decryptedString;
}
int main() {
//キーペアの生成
RSA* rsa = createRSAKeyPair();
if (rsa == NULL) {
return -1;
}
std::string message ;
// 文字列の入力
std::cout << "平文を入力してください: ";
std::getline(std::cin, message);
//公開鍵と秘密鍵の取得と表示
std::string publicKey = getPublicKey(rsa);
std::string privateKey = getPrivateKey(rsa);
std::cout << "\n公開鍵:\n" << publicKey << std::endl;
std::cout << "秘密鍵:\n" << privateKey << std::endl;
std::string encryptedMessage = encryptMessage(rsa, message);
std::cout << "暗号化された文字列:\n-----BEGIN ENCRYPTED STRING-----\n" << encryptedMessage <<"\n-----END ENCRYPTED STRING-----\n"<< std::endl;
//メッセージの復号
std::string decryptedMessage = decryptMessage(rsa, encryptedMessage);
std::cout << "復号された文字列: " << decryptedMessage << std::endl;
//RSAオブジェクトの解放
RSA_free(rsa);
return 0;
}
【実行画面】
しっかりと復号されています。
何故か秘密鍵の方が圧倒的に長くなって見えます。
知見のある方がいたら教えてほしいです🙇
【ハイブリッド暗号方式】
2つの実行結果からわかるように、
共通鍵暗号方式は鍵長が短く、暗号化と復号に共通の鍵を使用するので管理が楽で高速です。
公開鍵暗号方式は鍵長が長く、復号と暗号化に別の鍵を使用するので管理が面倒になり遅くなる反面、セキュリティ効果は期待できます。
これらの長所を兼ね備えたものがハイブリッド暗号方式です。
【手順(例)】
Aさん-->Bさんにメッセージを送信する場合
1. Aさんは共通鍵(セッション鍵)を生成してメッセージを共通鍵で暗号化します。
2. Aさんの共通鍵(セッション鍵)を公開鍵で暗号化して送信します。
3. Bさんは秘密鍵で共通鍵(セッション鍵)を復号します。
4. Bさんは複合した共通鍵(セッション鍵)でメッセージを復号します。
以上が主な手順となります。生成した共通鍵(セッション鍵)は一時利用なので後に廃棄されます。
それではこの手順を実装してみます。
※上記のAES.CPP,RSA.CPPをクラス定義して実装しています。
エントリーポイント(main関数)は削除しておいてください。
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <iostream>
#include <string>
#include "Init_vec.hpp"
#include "AES.hpp"
#include "RSA.hpp"
//コマンドラインオプション
//g++ -o Hybrid.exe Hybrid.cpp AES.cpp Init_vec.cpp RSA.cpp -I"C:\Program Files\OpenSSL-Win64\include" -L"C:\Program Files\OpenSSL-Win64\lib\VC\x64\MTd" -lssl -lcrypto -Wno-deprecated-declarations
using namespace std;
int main(){
/*********************************************/
/*******************ユーザーA側****************/
/*********************************************/
//鍵,初期化ベクトル生成クラス"Init_vec.hpp"
InitClass init;
//AES暗号メソッドクラス"AES.hpp"
AESClass AES;
//RSA暗号メソッドクラス"RSA.hpp"
RSAClass rsaclass;
// 文字列の入力
string message ;
cout << "\nメッセージを入力してください: ";
getline(cin, message);
//共通鍵を生成
string commonkey = init.generateCommonKey();
cout << "\n共通鍵: " << commonkey << endl;
//初期化ベクトルを生成
string iv=to_string(init.generateRandomNumber());
cout << "\n初期化ベクトル: " << iv << endl;
//共通鍵でメッセージを暗号化
string encryptedMessage = AES.aesEncrypt(message, commonkey,iv);
if (encryptedMessage.empty()) {
cout << "\nAES暗号化に失敗しました" << endl;
return 1;
}
cout << "\n暗号化されたメッセージ: " << encryptedMessage << endl;
//公開鍵,秘密鍵を生成
//キーペアの生成
RSA* rsa = rsaclass.createRSAKeyPair();
if (rsa == NULL) {
return -1;
}
string publicKey = rsaclass.getPublicKey(rsa);
string privateKey = rsaclass.getPrivateKey(rsa);
cout << "\n公開鍵:\n" << publicKey << endl;
cout << "秘密鍵:\n" << privateKey << endl;
//公開鍵で共通鍵,初期化ベクトルを暗号化
//共通鍵を暗号化
string encryptedcommonkey = rsaclass.encryptMessage(rsa, commonkey);
cout << "\n暗号化された共通鍵:\n-----BEGIN ENCRYPTED STRING-----\n" << encryptedcommonkey <<"\n-----END ENCRYPTED STRING-----\n"<< endl;
//初期化ベクトルを暗号化
string encryptediv = rsaclass.encryptMessage(rsa, iv);
cout << "\n暗号化された初期化ベクトル:\n-----BEGIN ENCRYPTED STRING-----\n" << encryptediv <<"\n-----END ENCRYPTED STRING-----\n"<< endl;
/*********************************************/
/*********************************************/
/*********************************************/
/*********************************************/
/*******************ユーザーB側****************/
/*********************************************/
/*受け取るもの:秘密鍵,暗号化された共通鍵,暗号化された初期化ベクトル,暗号化されたメッセージ*/
/*encryptedcommonkey,encryptediv,encryptedMessage*/
//秘密鍵で暗号化された共通鍵,初期化ベクトルを復号
string decryptedcommonkey = rsaclass.decryptMessage(rsa, encryptedcommonkey);
cout << "\n復号された共通鍵: " << decryptedcommonkey << endl;
string decryptediv = rsaclass.decryptMessage(rsa, encryptediv);
cout << "\n復号された初期化ベクトル: " << decryptediv << endl;
//復号した共通鍵で暗号化されたメッセージを復号
string decryptedMessage = AES.aesDecrypt(encryptedMessage, decryptedcommonkey,decryptediv);
if (decryptedMessage.empty()) {
cout << "\nAES復号に失敗しました" << endl;
return 1;
}
cout << "\n復号されたメッセージ: " << decryptedMessage << endl;
/*********************************************/
/*********************************************/
/*********************************************/
}
実行結果は長すぎてスクショできなかったので載せません。
ですが上記のような手順が表示されているのではないでしょうか?
最後に
私は秋期の情報処理安全確保支援士を受験するため、セキュリティ関連の勉強をしています。
その中で実際に実装してみたくなったので作成しました。
感想はOpensslという便利なライブラリがあることの驚きと
C++の難しさを痛感したことです。
もしこれらのような技術を実務で扱う方がいたら、実際にOpensslは使うのか?
アルゴリズムを1から作成するのか?
色々教えて頂けると幸いです。
今度は楕円曲線暗号方式を実装してみたいです。
最後まで読んでいただきありがとうございました。