Cocos2d-xでOpenSSLを使った256bit AES暗号化・復号化(3.17.1対応版)

以前にCocos2d-xに組み込まれたOpenSSLを使ってハッシュ計算をしましたが今回は、AES暗号化・復号化をしたいと思います。

前回の記事はこちら。

takachan.hatenablog.com

確認環境

確認環境は以下の通り。

  • VisualStudio2017
  • Cosoc2d-x 3.17.1
  • OpenSSL 1.1
    • AES暗号化・復号化に利用
    • キーの長さは256ビット
    • CBC方式
    • パック方式はPKCS7(既定値
    • IVは16bit(既定値

モバイルアプリへのクラック行為が横行してるので少しでも強度を稼ぐためにキー長さを256ビットでCBC方式を使用します。

EBCも指定できますが、zipにパスワードをかける感じの軽い保護なので現在は非推奨。CBCはIV(初期ベクトル)というシード値のようなものを起点として暗号化する方式でEBCより強度が高いのでそちらを採用します。

あと、少し前にOpenSSLがバージョン1.1になって少し記述方法が変わったのでそれも対応したいと思います。

暗号化クラス(Aes256Encryption.hpp)

一度作成すればそうそう変更するものでもないので全部ヘッダーに処理を記載しちゃいます。

もう少し柔軟かつ安全に記述できそうですが、まぁ気になったら各々直してください。

Cocos2d-xに元々同梱されているOpenSSLを使用していきます。

// Aes256Encryption.hpp

#pragma once
#pragma execution_character_set("utf-8")

#include "JsonHelper.hpp"

#include "cocos2d.h"

// OpenSSLの実装は各プラットフォームで異なる
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include "external/openssl/include/android/openssl/evp.h"
#elif(CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
#include "external/openssl/include/ios/openssl/evp.h"
#elif(CC_TARGET_PLATFORM == CC_PLATFORM_WIN32)
#include "external/openssl/include/win32/openssl/evp.h"
#endif

/**
 * AES-256形式の暗号化に関係する処理を定義します。
 */
class Aes256Encryption
{
    static const int KEY_LENGTH = 32; // 256bit-key
    static const int IV_LENGTH = 16;
    const EVP_CIPHER* algorithm = EVP_aes_256_cbc();
    unsigned char key[KEY_LENGTH]{};
    unsigned char iv[IV_LENGTH]{};

public:

    // must be 16byte
    void setKey(const unsigned char* key)
    {
        std::memcpy(&this->key[0], key, KEY_LENGTH);
    }

    // 【非推奨】文字列リテラルからキーを生成すると強度が低下する
    void setKey(const std::string& key)
    {
        this->setKey(reinterpret_cast<const unsigned char*>(key.c_str()));
    }

    // must be 16byte
    void setIV(const unsigned char* iv)
    {
        std::memcpy(&this->iv[0], iv, IV_LENGTH);
    }

    // 【非推奨】文字列リテラルから初期ベクトルを生成すると強度が低下する
    void setIV(const std::string& iv)
    {
        this->setIV(reinterpret_cast<const unsigned char*>(iv.c_str()));
    }

    void serializeToFile(const std::string& savePath, const std::string& body)
    {
        using namespace std;
        using namespace cocos2d;

        // 初期化
        EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); // OpenSSL 1.1から生成方式が変更
        EVP_EncryptInit_ex(ctx, this->algorithm, nullptr, &this->key[0], &this->iv[0]);

        // 入力パラメータの準備
        vector<unsigned char> buffer(body.length() + EVP_MAX_BLOCK_LENGTH);
        auto instr = reinterpret_cast<const unsigned char*>(body.c_str());

        // ブロックサイズ分の切れのいいところまでを処理
        int out_len_1 = 0;
        EVP_EncryptUpdate(ctx, buffer.data(), &out_len_1, instr, body.length());

        // ブロック長に満たないデータの処理
        int out_len_2 = 0;
        EVP_EncryptFinal_ex(ctx, buffer.data() + out_len_1, &out_len_2);

        // bufferに書き込まれた暗号化データの長さ
        int enc_length = out_len_1 + out_len_2;

        // 暗号化文字列をファイルに書き出す(cocos2d-x固有)
        Data encrypted;
        encrypted.copy(buffer.data(), enc_length);
        FileUtils::getInstance()->writeDataToFile(encrypted, savePath);
    }

    std::string deserializeFromFile(const std::string& loadPath)
    {
        using namespace std;
        using namespace cocos2d;

        // 暗号化データをファイルから読みだす(cocos2d-x固有)
        Data enc_bin = FileUtils::getInstance()->getDataFromFile(loadPath);

        // 初期化
        EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); // OpenSSL 1.1から生成方式が変更
        EVP_DecryptInit_ex(ctx, this->algorithm, nullptr, &this->key[0], &this->iv[0]);

        // 入力パラメータの準備
        vector<unsigned char> buffer(enc_bin.getSize() + EVP_MAX_BLOCK_LENGTH);
        auto instr = reinterpret_cast<const unsigned char*>(enc_bin.getBytes());

        // ブロックサイズ分の切れのいいところまでを処理
        int out_len_1 = 0;
        EVP_DecryptUpdate(ctx, buffer.data(), &out_len_1, instr, enc_bin.getSize());

        // ブロック長に満たないデータの処理
        int out_len_2 = 0;
        EVP_DecryptFinal_ex(ctx, buffer.data() + out_len_1, &out_len_2);

        // bufferに書き込まれた暗号化データの長さ
        int dec_length = out_len_1 + out_len_2;

        string decrypted = "";
        decrypted.append(reinterpret_cast<char*>(buffer.data()), static_cast<size_t>(dec_length));
        return decrypted;
    }
};

コピペで行けるハズですが処理中の「EVP_*」から始まる関数の詳細は以下を参照してください。

sehermitage.web.fc2.com

OpenSSLで暗号化したバイナリデータをCocos2d-xと連携して端末内の保存可能な位置に保存または読み取りを行います。

使い方

上記ライブラリの使い方です。

#include "Aes256Encryption.hpp"

void foo()
{
    // 初期条件の設定
    Aes256Encryption aes;
    aes.setKey("12345678901234561234567890123456");
    aes.setIV("1234567890123456");

    // ファイルに保存
    string filePath = FileUtils::getInstance()->getWritablePath() + "savedata.bin";
    aes.serializeToFile(filePath, "01234567890123456789012345678901234567890123456789");
    
    // savedata.bin
    // テキストエディタで開くと以下のような感じで保存されている
    //
    // > ハホcョ?ワC乘?p\/ カw?覲ッ?セ?#?"p湮? h*z(?)@?サ」・b+U洲:e゙涇?屓?Vd
    
    // 保存した内容の復元
    string dec = aes.deserializeFromFile(filePath);
    
    // decの中には元の文字列が入っている
    // > 01234567890123456789012345678901234567890123456789
}

基本的にstringとしてデータを扱っています。引数で指定したファイルパスに内容を暗号化してデータを保存したり、復元したりします。

リテラルからKeyやIVを直接生成した場合、バイナリを覗くと値が即座にバレるのでうまく隠すように工夫してください。XOR取るとか。

ちなみにVisualStudioでしたまだ動作確認していないのでAndroidで変更必要であればおいおい訂正したいと思います。

参照

sehermitage.web.fc2.com

gatito.hateblo.jp

www.openssl.org

brbranch.jp

以上です。