Unreal Engine内のゲームのリザルト情報を暗号化し、サーバー上で復号する処理を書いてみました。
C++でのAES暗号化に関してはとにかく情報が少なく、まず暗号化の方式から勉強するハメになりました。
結論としては、AES-256-CBC方式で、OpenSSLを使って暗号化を行っています。
具体的には、Unreal C++でFString
をunsigned char*
に変換、AES_cbc_encrypt()
を用いて暗号化しFBase64::Encode()
でBase64エンコード、それをPHP側でopenssl_decrypt()
しています。
以下、Unreal Engine特有の記述も含まれます。
純粋にC++でOpenSSLを使ってAESで暗号化したい方は適宜読み飛ばしてください。
コードの全容は末尾に載せています。
ざっくり
- Unreal Engine内蔵のAES暗号化はセキュリティの観点から微妙なのでOpenSSLを使うべき
- PHPの
openssl_encrypt()
は中でBase64エンコードをしている - AES-256の中にもいくつかモードがある
- パディングは0で埋めればいいというわけではない
AES-256と「暗号利用モード」
AESとは暗号と復号に同じ鍵を用いる共通鍵方式の暗号化アルゴリズムです。
鍵の長さによりAES-128, AES-192, AES-256に分けられます。今回はAES-256を用います。
AESが一度に暗号化できるデータの長さは16バイトと決まっています。
AES-256で16バイト以上のデータを暗号化したい場合は暗号化処理を繰り返し行う必要があります。
その暗号化処理の繰り返しをどうやって行っていくかを「暗号利用モード」と言います。
詳細はこちらの記事が分かりやすいです。
【暗号化】ブロック暗号のモードまとめ (比較表付き) - Qiita
https://qiita.com/shoichi0599/items/6082b765c1257b71985b
よく登場するのは2つ。
- 左から右に素直に暗号化していくECB (セキュリティ微妙)
- ひとつ前のブロックとのXORをとりながら暗号化するCBC (セキュリティ良い)
ECBを除く多くのモードでは初期化ベクトル、略してIVを必要とします。
Unreal内蔵のAES暗号化はおそらくECB
以上を踏まえて、Unreal Engineに初めから内蔵されているAES暗号ライブラリ"FAES"を使ってみましょう。
(参考: https://answers.unrealengine.com/questions/289228/ue4-fstring-encryption-example-using-faes.html )
FString UResultEncryptor::Encrypt(FString str)
{
if (str.IsEmpty()) return str;
//暗号化するデータを16バイト単位にする
uint32 Size = str.Len();
Size = Size + (FAES::AESBlockSize - (Size % FAES::AESBlockSize));
// キーを生成
FString Key = "hogehogefooofooohogehogefooofooo";
TCHAR *KeyTChar = Key.GetCharArray().GetData();
ANSICHAR *KeyAnsi = (ANSICHAR*)TCHAR_TO_ANSI(KeyTChar);
uint8* Blob = new uint8[Size];//実際に暗号化するバイト列
if (FString::ToBlob(str, Blob, str.Len())) {
FAES::EncryptData(Blob, Size, KeyAnsi);//暗号化
str = FBase64::Encode(Blob, Size);
delete Blob;
return str;
}
delete Blob;
return "";
}
前の章で述べたIVを指定する箇所がないことから、暗号利用モードはECBかと推測されます。
というわけで今回は、AES-256-CBCで暗号化ができるOpenSSLをインクルードして使ってみることにしました。
Unreal C++でOpenSSLを使う方法
Build.csにコンポーネントOpenSSL
を追記します。
using UnrealBuildTool;
public class YourModule : ModuleRules
{
public YourModule(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "OpenSSL" }); //OpenSSLを追記
}
}
暗号化を行いたいファイルに以下を記述します。
今回はAESしか使わないのでaes.h
のみインクルードしています。
#define UI UI_ST
THIRD_PARTY_INCLUDES_START
#if PLATFORM_WINDOWS
#include "ThirdParty/OpenSSL/1.0.2g/include/Win64/VS2015/openssl/aes.h"
#elif PLATFORM_MAC
#include "ThirdParty/OpenSSL/1.0.2g/include/Mac/openssl/aes.h"
#endif
THIRD_PARTY_INCLUDES_END
#undef UI
正しいパディングの埋め方"PKCS#7"
前述の通りAES-256は16バイト単位で暗号化を行うのですが、暗号化したいデータが16バイトの倍数であるとは限りません。
そんなときはデータが16バイトの倍数になるようにデータを水増しします。その水増し部分をパディングと言います。
さて、そのパディングを何で埋めますか?
「そりゃ0でしょ」とお思いのあなた、違います。
PHPのopenssl_encrypt()
およびopenssl_decrypt()
ではPKCS#7と呼ばれる方式でパディングを埋めてるとのことです。
これは「0ではなくパディングのバイト長で埋める」という考え方です。
例えば暗号化したいデータを16バイト単位で区切っていった結果末尾が以下で終わったとします。
0A CB EB 47 3E FC FF 00 00 2D
この場合パディング長、つまり16バイトになるまでに足りないバイト数は6バイトです。
よって、以下のようにパディングを埋めます。
0A CB EB 47 3E FC FF 00 00 2D 06 06 06 06 06 06
パディングが0バイト、つまり元のデータがちょうど16バイトの倍数の場合は、パディングが16バイトとして解釈するようです。
データの末尾が01
で終わった場合に、それが1バイトのパディングを付与した結果なのか元データが01
で終わったのかが区別できないからでしょう。
OpenSSLにもpkcs7.h
というそれっぽいヘッダーファイルがあったので覗いてみたのですが全く分からなかったので手動で実装しました…
FString UResultEncryptor::Encrypt(FString str)
{
//...
uint32 Size = str.Len();
Size = Size + (AES_BLOCK_SIZE - (Size % AES_BLOCK_SIZE));
//strをもとに暗号化するバイト列 in を用意
TCHAR *c = str.GetCharArray().GetData();
unsigned char *in = new unsigned char[Size];
strcpy((char*)in, (char*)TCHAR_TO_UTF8(c));
//PKCS#7パディングを付加
int padSize = Size - str.Len();
memset(in + str.Len(), padSize, padSize);
//...
Base64エンコードを忘れずに
AES-256は暗号化する内容がテキストかバイナリかなんて考えてくれないので、暗号化した後のデータをそのままUTF-8の文字列として解釈できる可能性は限りなく低いです。
PHPのopenssl_encrypt()
およびopenssl_decrypt()
は、オプションを指定しない限りは暗号化した後はBase64エンコード、復号化する前はBase64デコードを行います。
よって、C++側でも暗号化の後にはBase64エンコードしてあげましょう。
今回はUnreal Engine内蔵のFBase64
を用いました。
純粋なC++で実装してる方はすみません、頑張って探してください。
AES_cbc_encrypt((unsigned char*)in, out,Size, &aesKey,iv, AES_ENCRYPT);
str = FBase64::Encode(out, Size);
str = FGenericPlatformHttp::UrlEncode(str);
return str;
コードの全容
以上を踏まえて、今回書いたコードです。
モジュール名など一部脚色を加えています。
本当はこれにJSON関連の処理も入っているのですが省略しています。
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Json.h"
#include "Base64.h"
#include "GenericPlatformHttp.h"
#define UI UI_ST
THIRD_PARTY_INCLUDES_START
#if PLATFORM_WINDOWS
#include "ThirdParty/OpenSSL/1.0.2g/include/Win64/VS2015/openssl/aes.h"
#elif PLATFORM_MAC
#include "ThirdParty/OpenSSL/1.0.2g/include/Mac/openssl/aes.h"
#endif
THIRD_PARTY_INCLUDES_END
#undef UI
#define AES_KEYLENGTH 256
#include "Encryptor.generated.h"
UCLASS()
class MYPROJECT_API UEncryptor : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Encrypt")
static FString Encrypt(FString str);
};
#include "Encryptor.h"
FString UEncryptor::Encrypt(FString str)
{
if (str.IsEmpty()) return str;
//Size設定 ブロックサイズ単位になるように調整
uint32 Size;
Size = str.Len();
Size = Size + (AES_BLOCK_SIZE - (Size % AES_BLOCK_SIZE));
// キー KeyChar を生成
FString Key = "hogehogefooofooohogehogefooofooo";
TCHAR *KeyTChar = Key.GetCharArray().GetData();
unsigned char *KeyChar = new unsigned char[AES_KEYLENGTH];
strcpy((char*)KeyChar, (char*)TCHAR_TO_UTF8(KeyTChar));
//strをもとに暗号化するバイト列 in を用意
TCHAR *c = str.GetCharArray().GetData();
unsigned char *in = new unsigned char[Size];
strcpy((char*)in, (char*)TCHAR_TO_UTF8(c));
//PKCS#7パディングを付加
int padSize = Size - str.Len();
memset(in + str.Len(), padSize, padSize);
//暗号化されたバイト列が入る out
unsigned char* out = new unsigned char[Size];
//初期化ベクター
//ホントは0で埋めずにちゃんと作ってあげた方がいいです
unsigned char iv[AES_BLOCK_SIZE];
memset(iv, 0x00, AES_BLOCK_SIZE);
//暗号化
if (in != nullptr) {
AES_KEY aesKey;
AES_set_encrypt_key((unsigned char*)KeyChar, AES_KEYLENGTH, &aesKey);
AES_cbc_encrypt((unsigned char*)in, out,Size, &aesKey,iv, AES_ENCRYPT);
str = FBase64::Encode(out, Size);
str = FGenericPlatformHttp::UrlEncode(str);
return str;
}
return ""; //失敗したときは空の文字列を返す
}
<?php
function decrypt($str) {
//暗号化パラメータ
define('PASSWORD','hogehogefooofooohogehogefooofooo');
$password = PASSWORD;
$method = 'aes-256-cbc';
if(!empty($str))
{
$str = openssl_decrypt($str, $method, $password);
}
return $str;
}
echo decrypt($_GET['s']);
?>
hoge