Unreal C++でAES暗号化しPHPで復号するときにハマったこと

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 )

``` C++
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`を追記します。

``` C#:YourModule.Build.cs
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`のみインクルードしています。

``` C++:Encryptor.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`というそれっぽいヘッダーファイルがあったので覗いてみたのですが全く分からなかったので手動で実装しました…

``` C++:Encryptor.cpp
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++で実装してる方はすみません、頑張って探してください。

``` C++:Encryptor.cpp#Encrypt()
AES_cbc_encrypt((unsigned char*)in, out,Size, &aesKey,iv, AES_ENCRYPT);
str = FBase64::Encode(out, Size);
str = FGenericPlatformHttp::UrlEncode(str);
return str;
```

# コードの全容

以上を踏まえて、今回書いたコードです。
モジュール名など一部脚色を加えています。
本当はこれにJSON関連の処理も入っているのですが省略しています。

``` C++:Encryptor.h
#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);
};

```

``` C++:Encryptor.cpp
#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 ""; //失敗したときは空の文字列を返す
}
```

![UE4上のレベルブループリント](https://qiita-image-store.s3.amazonaws.com/0/74419/6543bbad-42d6-c770-255a-c94db4b28d1b.png)


``` php:index.php
<?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
```