見出し画像

暗号化と復号のテクニックを解明しようパート4(Deciphering the Technique of Encryption and Decryption Part IV)


"データを持つ前に理論を立てるのは致命的な誤りです。無意識に、人は事実を理論に合わせようとし、理論を事実に合わせる代わりに事実をねじ曲げ始めます。"_シャーロック・ホームズ(Sherlock Holmes)

本日はRSA暗号を紹介致します。
"RSA"という名前は、RSA暗号システムの三人の発明者のイニシャルに由来します。これらの発明者は、Ron Rivest(ロン・リヴェスト)、Adi Shamir(アディ・シャミール)、およびLeonard Adleman(レナード・アドルマン)です。彼らは1977年にRSAアルゴリズムを導入し、その際に彼らの姓の頭文字を使って"RSA"という頭字語を作成し、これを暗号システムの名前としました。
RSA(Rivest-Shamir-Adleman)暗号アルゴリズムは非対称暗号アルゴリズムで、非対称とは、公開鍵と秘密鍵の2つの異なる鍵で動作することを意味します。その名前が示すように、公開鍵は誰にでも提供され、秘密鍵は秘密に保たれます。
RSAのアイデアは、大きな整数を因数分解することが難しいという事実に基づいています。公開鍵は、2つの大きな素数の積である2つの数から成ります。秘密鍵も同じ2つの素数から導出されます。したがって、誰かがその大きな数を因数分解できれば、秘密鍵が危険にさらされます。そのため、暗号の強度は鍵のサイズに完全に依存し、鍵のサイズを2倍または3倍にすると、暗号の強度が指数関数的に向上します。RSAキーは通常、1024ビットまたは2048ビットの長さですが、専門家は1024ビットのキーは近い将来に破られる可能性があると考えています。しかし、現時点では実現不可能なタスクのようです。

以下は、ASCII値を持つ平文「Hello」を使用したRSAの動作の簡略化された説明です:

  1. 鍵の生成

    • RSAには、公開鍵と秘密鍵の2つの鍵が関与します。これらの鍵は次のように生成されます:

      • 2つの大きな素数、pq を選択します。

      • それらの積 n = p * q を計算します。この n は公開鍵と秘密鍵の一部として使用されます。

      • オイラーのトーシェント関数 φ(n) = (p-1)(q-1) を計算します。これは秘密鍵を決定するために重要です。

      • 公開指数として e を選択しますが、通常は65537(2^16 + 1)のような小さな素数です。これは φ(n) と互いに素(1以外の共通の因数を持たない)である必要があります。

      • 秘密指数 d を計算し、 (d * e) % φ(n) = 1 となるようにします。これはモジュラ演算を使用して行います。

      • 公開鍵は (n, e) であり、秘密鍵は (n, d) です。

  2. 暗号化

    • 平文「Hello」を暗号化するには、まずASCII値のシーケンスとして表現します:「72 101 108 108 111」。

    • 公開鍵 (n, e) が暗号化に使用されます。

    • 各ASCII値は個別に暗号化されます。最初の値である72を取り上げてみましょう。

    • 暗号化の式は ciphertext = (plaintext ^ e) % n で、 ^ はべき乗を表します。

    • H (72) に対して、ciphertext = (72 ^ 65537) % n となります。これを各文字に繰り返します。

  3. 復号

    • 復号には秘密鍵 (n, d) が使用され、プロセスを逆転させます。

    • 復号の式は plaintext = (ciphertext ^ d) % n です。

    • したがって、各文字に対して ciphertext = (ciphertext ^ d) % n を適用します。

    • 各文字に対してASCII値が戻り、それを組み合わせることで元の「Hello」が得られます。

  4. RSAが動作する理由

    • RSAのセキュリティは、大きな数 n をその素因数 pq に因数分解する難しさに基づいています。 ne が与えられた場合、素因数を知らない限り d を計算することは計算的に困難です。

    • RSAのセキュリティは、大きな半素数を因数分解する難しさに依存しており、特に n が十分に大きく選ばれた場合、この作業は非常に時間がかかります。

実際には、「Hello」はバイトに分割され、PKCS#1などのパディング方式が使用されてデータの整合性とセキュリティが確保されます。さらに、RSAは通常、メッセージを直接暗号化するのではなく、メッセージ自体を効率的に暗号化するために使用される対称鍵(共通鍵)を暗号化するために使用されます。

それではRSA暗号を理解したと思います。RSA暗号を使用してOpenSSLライブラリを参考する簡単なC++プログラムを作成して見ましょう。

#include <iostream>
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <cstring>
#include <string>
#include <memory>

using namespace std;

#pragma GCC diagnostic ignored "-Wdeprecated-declarations"

int keyGeneration(const string& privateKeyFile, const string& publicKeyFile) 
{
    // RSAキーの生成
    EVP_PKEY* pkey = EVP_PKEY_new();
    RSA* rsa = RSA_new();

    BIGNUM* bne = BN_new();
    if (!BN_set_word(bne, RSA_F4)) 
    {
        cerr << "RSA指数の設定に失敗しました" << endl;
        return 1;
    }

    if (!RSA_generate_key_ex(rsa, 2048, bne, NULL)) 
    {
        cerr << "RSAキーペアの生成に失敗しました" << endl;
        return 1;
    }

    if (!EVP_PKEY_set1_RSA(pkey, rsa)) 
    {
        cerr << "EVP_PKEYへのRSAキーの設定に失敗しました" << endl;
        return 1;
    }

    // 公開鍵と秘密鍵の保存
    BIO* bio_private = BIO_new_file(privateKeyFile.c_str(), "w");
    if (!PEM_write_bio_PrivateKey(bio_private, pkey, NULL, NULL, 0, NULL, NULL)) 
    {
        cerr << "秘密鍵の書き込みに失敗しました" << endl;
        return 1;
    }

    BIO* bio_public = BIO_new_file(publicKeyFile.c_str(), "w");
    if (!PEM_write_bio_PUBKEY(bio_public, pkey)) 
    {
        cerr << "公開鍵の書き込みに失敗しました" << endl;
        return 1;
    }

    // クリーンアップ
    RSA_free(rsa);
    EVP_PKEY_free(pkey);
    BN_free(bne);

    cout << "RSAキーペアが生成され、" << privateKeyFile << " および " << publicKeyFile << " に保存されました" << endl;

    // データの暗号化と復号化(ここにデータの暗号化および復号化ロジックを追加できます)

    ERR_free_strings();
    EVP_cleanup();

    return 0;
}

class CustomException : public exception 
{
public:
    CustomException(const string& msg) : message(msg) {}
    const char* what() const noexcept override { return message.c_str(); }

private:
    string message;
};

EVP_PKEY* loadPrivateKey(const string& privateKeyFile) 
{
    EVP_PKEY* pvkey = EVP_PKEY_new();
    if (!pvkey) 
    {
        throw CustomException("EVP_PKEYの作成に失敗しました");
    }

    unique_ptr<BIO, decltype(&BIO_free)> bio_private(BIO_new_file(privateKeyFile.c_str(), "r"), &BIO_free);
    if (!bio_private) 
    {
        throw CustomException("秘密鍵用のBIOの作成に失敗しました");
    }

    if (!PEM_read_bio_PrivateKey(bio_private.get(), &pvkey, nullptr, nullptr)) 
    {
        throw CustomException("秘密鍵の読み込みに失敗しました");
    }

    return pvkey;
}

EVP_PKEY* loadPublicKey(const string& publicKeyFile) 
{
    EVP_PKEY* ppkey = EVP_PKEY_new();
    if (!ppkey) 
    {
        throw CustomException("EVP_PKEYの作成に失敗しました");
    }

    unique_ptr<BIO, decltype(&BIO_free)> bio_public(BIO_new_file(publicKeyFile.c_str(), "r"), &BIO_free);
    if (!bio_public) 
    {
        throw CustomException("公開鍵用のBIOの作成に失敗しました");
    }

    if (!PEM_read_bio_PUBKEY(bio_public.get(), &ppkey, nullptr, nullptr)) 
    {
        throw CustomException("公開鍵の読み込みに失敗しました");
    }

    return ppkey;
}

int rsaEncrypt(const char* plaintext, size_t plaintext_len, unsigned char** encrypted, EVP_PKEY* ppkey) 
{
    RSA* rsa = EVP_PKEY_get1_RSA(ppkey);
    if (!rsa) 
    {
        throw CustomException("EVP_PKEYからRSAキーの取得に失敗しました");
    }

    int encrypted_len = RSA_size(rsa);
    *encrypted = new unsigned char[encrypted_len];
    if (RSA_public_encrypt(plaintext_len, reinterpret_cast<const unsigned char*>(plaintext), *encrypted, rsa, RSA_PKCS1_PADDING) == -1) 
    {
        throw CustomException("暗号化に失敗しました");
    }

    return encrypted_len;
}

int rsaDecrypt(const unsigned char* encrypted, int encrypted_len, unsigned char* decrypted, EVP_PKEY* pvkey) 
{
    RSA* rsa = EVP_PKEY_get1_RSA(pvkey);
    if (!rsa) 
    {
        throw CustomException("EVP_PKEYからRSAキーの取得に失敗しました");
    }

    return RSA_private_decrypt(encrypted_len, encrypted, decrypted, rsa, RSA_PKCS1_PADDING);
}

int main() 
{
    OpenSSL_add_all_algorithms();
    ERR_load_crypto_strings();

    string user1, user2, message;
    cout << "ユーザー名を入力してください: ";
    getline(cin, user1);
    cout << "2つ目のユーザー名を入力してください: ";
    getline(cin, user2);
    cout << "メッセージを入力してください: ";
    getline(cin, message);

    try 
    {
        int i = keyGeneration(user1 + "_private.pem", user1 + "_public.pem");
        int j = keyGeneration(user2 + "_private.pem", user2 + "_public.pem");

        EVP_PKEY* ppkey = loadPublicKey(user2 + "_public.pem");
        const char* plaintext = message.c_str();
        size_t plaintext_len = strlen(plaintext);

        unsigned char* encrypted = nullptr;
        int encrypted_len = rsaEncrypt(plaintext, plaintext_len, &encrypted, ppkey);

        EVP_PKEY* pvkey = loadPrivateKey(user2 + "_private.pem");

        unsigned char decrypted[2048]; // 復号されたデータを保持するのに十分な大きさ
        int decrypted_len = rsaDecrypt(encrypted, encrypted_len, decrypted, pvkey);

        cout << "元のテキスト: " << plaintext << endl;
        cout << "暗号化されたテキスト(HEX形式): ";
        for (int i = 0; i < encrypted_len; i++) 
        {
            printf("%02X", encrypted[i]);
        }
        cout << endl;

        decrypted[decrypted_len] = '\0'; // 復号された文字列にヌル終端を追加
        cout << "復号されたテキスト: " << decrypted << endl;

        delete[] encrypted;
    } 
    catch (const CustomException& ex) 
    {
        cerr << "エラー: " << ex.what() << endl;
    }

    EVP_cleanup();

    return 0;
}

 説明 :
1. int keyGeneration(const string& privateKeyFile, const string& publicKeyFile)

  • この関数は、RSA鍵ペアを生成し、それを秘密鍵と公開鍵のファイルに保存する役割を果たします。

  • privateKeyFile と publicKeyFile は、秘密鍵と公開鍵が保存されるファイル名です。

  • OpenSSL関数を使用してRSA鍵ペアを生成し、秘密鍵を作成して保存し、次に公開鍵を作成して保存します。

  • どのステップでエラーが発生しても、非ゼロの値を返してエラーを示します。

2. class CustomException : public exception

  • これはプログラム内でエラーを処理するために使用されるカスタム例外クラスです。

  • これはC++の標準的な exception クラスを継承し、エラーメッセージを含むカスタム例外を投げたりキャッチしたりするために使用されます。

3. EVP_PKEY* loadPrivateKey(const string& privateKeyFile)

  • この関数はファイルから秘密鍵を読み込みます。

  • privateKeyFile は読み込まれる秘密鍵ファイルのファイル名です。

  • この関数は EVP_PKEY オブジェクトを作成し、ファイルから秘密鍵を読み込みます。

  • どのステップでエラーが発生しても、エラーメッセージを含むカスタム例外を投げます。

4. EVP_PKEY* loadPublicKey(const string& publicKeyFile)

  • この関数はファイルから公開鍵を読み込みます。

  • publicKeyFile は読み込まれる公開鍵ファイルのファイル名です。

  • この関数は EVP_PKEY オブジェクトを作成し、ファイルから公開鍵を読み込みます。

  • どのステップでエラーが発生しても、エラーメッセージを含むカスタム例外を投げます。

5. int rsaEncrypt(const char* plaintext, size_t plaintext_len, unsigned char** encrypted, EVP_PKEY* ppkey)

  • この関数はRSA公開鍵を使用して指定された平文を暗号化します。

  • plaintext は暗号化される入力テキスト、plaintext_len はその長さ、encrypted は暗号化されたデータの出力バッファ、ppkey は公開鍵です。

  • EVP_PKEY オブジェクトからRSA鍵を取得し、暗号化を実行し、結果を encrypted バッファに保存します。

  • どのステップでエラーが発生しても、エラーメッセージを含むカスタム例外を投げます。

6. int rsaDecrypt(const unsigned char* encrypted, int encrypted_len, unsigned char* decrypted, EVP_PKEY* pvkey)

  • この関数は指定された暗号化データをRSA秘密鍵を使用して復号します。

  • encrypted は入力暗号化データ、encrypted_len はその長さ、decrypted は復号されたデータの出力バッファ、pvkey は秘密鍵です。

  • EVP_PKEY オブジェクトからRSA鍵を取得し、復号を実行し、結果を decrypted バッファに保存します。

  • どのステップでエラーが発生しても、エラーメッセージを含むカスタム例外を投げます。

7. int main()

  • main 関数はプログラムの実行が開始される場所です。

  • OpenSSLライブラリを初期化し、ユーザーからユーザー名とメッセージの入力を受け付け、鍵生成、暗号化、および復号を実行します。

  • これらの関数の実行中にエラーが発生した場合、カスタム例外をキャッチして表示します。

プログラムを実行すると、ユーザー2名の名前とメッセージを入力すると、一人一人の公開鍵と秘密鍵がRSAアルゴリズムの通り作成され、送信側は受信側の公開鍵を使用してデータを暗号化し、受信側は自分の秘密鍵を使用してそれを復号できることがわかります。RSA暗号は複雑なアルゴリズムで、この記事も長くなってしまいました。お疲れ様でした。次の記事ではAES(Advanced Encryption Standard)を紹介しますので、ぜひお読みいただければ嬉しいです。
                                       エンジニアファーストの会社
株式会社CRE-CO
                         su_myat_phyu

参考
1. RaspberryPiFoundation: Introduction to Encryption and Cryptography
https://www.edx.org/learn/computer-programming/raspberry-pi-foundation-introduction-to-encryption-and-cryptography
2.
 https://www.geeksforgeeks.org/rsa-algorithm-cryptography/

OpenSSLインストール
https://wiki.openssl.org/index.php/Binaries

この記事が気に入ったらサポートをしてみませんか?