データをセーブする(2)(Unityメモ)

通信とセーブデータとでは暗号化の前提条件が違っていた

なぜ暗号化したいのか

前回の記事でも書きましたが改めて。
制作中のゲームについて、特に他人と関わる要素をつける予定は現状ありません。一人遊びが一人で閉じている分には、チートをしても自己満足で閉じるだけです。それでもセーブデータの暗号化について考えているのは、技術的な興味という趣味の領域です。逆にゲームが他人に何らかの関わりを持つのなら、社会からチート対策が要求されることになります。
またチート対策は運営からの対策がメインになります。データの暗号化は末端レベルの対策です。

暗号化に関して考える項目

暗号化について多少調べたことをまとめます。
まず、戦略面でシステム構成がオンラインかどうかの違いが大きいです。よくあるブロック暗号・ストリーム暗号の区分はデータ形式の話であり戦術レベルです。

平文・鍵・暗号化手法・送信者・受信者

種類の前に、暗号化の基本的な構成要素を挙げておきます。
平文:暗号化される前の文(データ列)。
:暗号化・復号(後述)するために用いるデータ。
暗号化手法:暗号化を行う手法(アルゴリズム)。
送信者・受信者:暗号を送信する者と受信する者のペア。人間だけとは限らない

また、用語も挙げておきます。
暗号文:鍵が分からない限り復号ができないような文(データ列)
暗号化:平文から鍵を用いて暗号文を生成する。特に受信者以外には平文を知られないような文を作る。
復号:暗号文から鍵を用いて平文を生成する。平文に「戻す」作業。復号化ではなく復号。
解読:鍵を用いずに暗号文から平文を生成する。暗号化を「破る」作業。
不正な復号:本来の受信者以外の者が、鍵を不正に入手して暗号文を復号する。解読とは異なり、鍵を使っている。
改ざん:文を送信時点から改変する作業。平文と暗号文の両方について改ざんの概念がある。また受信者・送信者による改ざんもありうる。

平文を構成要素として挙げているのは、平文の構造が特徴的な場合、暗号文手法によっては暗号化していても鍵を使わず復号(解読)される場合があるためです。また、平文は人間が読める文だけではなく、受け取るシステムが正常に読み込めるデータ列も指します。
また送信者・受信者についても暗号システム全体の構成要素に含まれます。これは暗号化においては、送信者・受信者が信頼できる場合とできない場合の両方を考慮する必要があるためです。
鍵については、一般的なイメージは「共通鍵」です。暗号化と復号に同じ鍵を使います。共通鍵は送信者・受信者を除いて公開しません。
一方、暗号化と復号に異なる鍵のペアを使う手法もあります。この手法のうち、片方を公開する暗号化手法が「公開鍵暗号」で、公開される鍵を「公開鍵」と呼びます。

暗号化の構成要素をセーブデータの処理に対応付けると、ゲームの進捗情報が平文にあたります。バイナリデータでも平文と呼びます。送信者・受信者についてはシステムとユーザが該当しますが、単純に切り分けられないので後述します。そしてセーブデータにおいては改ざんの概念と対策が最も重要になります。

セキュリティの要素

セーブデータに関しては、特に機密性、完全性、真正性、信頼性が重要になります。
完全性と信頼性の違いが分かりづらいのですが、以下の違いだと思いました。
    完全性:情報が侵襲されていないかどうか。(integrity:尊厳、健全さ)
    信頼性:情報がその機能を果たすか。情報が正確かどうか。
組み合わせで考えるともう少し違いがはっきりします。
    信頼性がなく完全性がある:不正確だが改ざんされていない
    信頼性があるが完全性がない:正確だが改ざんされている

暗号化の安全性の担保

暗号化に関しては以下のような条件で安全性(解読や改ざんの防止)が担保されます。
・秘密情報を第三者が再現できない
・秘密情報をシステムの公開部分から隔離する
・平文のデータ構造やパターンが推測されない

オンライン構成とスタンドアロン(オフライン)構成

暗号化に関わるシステム構成の違いです。運営体制が関わってきます。
オンライン:ゲームシステムが外部と情報をやり取りできる。鍵またはその元となる秘密情報をゲームシステムの外部に配置し、特に不正ユーザから秘匿できる。
スタンドアロン:オフライン。ゲームシステムが外部と情報をやり取りできない。秘密情報を本質的に不正ユーザから秘匿できない。

暗号化の観点で見たゲームシステム構成

スタンドアロン構成はカセットやCD-ROM等で売る昔ながらのゲームのほか、オンライン機能のないダウンロードゲームや、ブラウザゲームでもオンライン構成化せず投稿サイトに公開するようなものも含みます。

通常のセキュリティに関しては、オンラインシステムの方がスタンドアロンよりも不正アクセスされる機会が多いです。しかしゲームの暗号化においては、ゲームシステムと鍵を隔離できるかどうかがセキュリティの本質的な危険度となります。
オンラインの場合、鍵を別のサーバに置くことで、ゲームシステムと鍵を隔離できます。復号時だけ鍵をサーバから入手し、その時以外は鍵を捨てることで、不正なユーザがゲームシステム単体を探っても鍵は見つかりません。
しかしスタンドアロンの場合、ゲームシステムの内部に鍵を置かないと復号ができません(外部にアクセスできるのならそれはオンライン構成)。そのため、ゲームシステムを探られるといずれ鍵を見つけられてしまいます。
従ってゲームの暗号化に関しては、オンライン構成の方がスタンドアロンよりも安全と言えます。
とはいえオンライン構成でも実行中のメモリを直接攻撃して鍵を入手される危険性はあるのですが、それは別の話とします。

暗号利用モード

実用的な暗号化処理では、鍵よりも長いデータ列を暗号化します。その際のデータ列の扱い方によって暗号化処理は2つに大別されます。
ブロック暗号:平文を複数のブロックに分割し、ブロックごとに暗号化したものを連結して全体の暗号文とする。暗号化の強度が高い。
ストリーム暗号:1バイトや1ビットなど、データ列の基本単位ごとに暗号化し、出力する。処理速度が高い。
特にブロック暗号について、様々なブロックの扱い方があります。この扱い方「暗号利用モード」と言います。
暗号利用モードもいくつかの種類に分けられますが、大まかな特徴は次の通りです。
・同じ平文のパターンに対して同じ暗号文のパターンが出ないような対策がされている
・上記の対策としてパターンを撹拌する。その際に初期化ベクトルを使う。
・何らかの暗号化データと未処理のデータとのXORをとる(後述)

具体的な暗号利用モードでよく知られているのは以下の通りです。
ECB:最も基本的。単純に1ブロックずつ暗号化する。同じ平文パターンへの対策がされていない。
CBC, CFB:直前の暗号化ブロックを用いて次のブロックを撹拌する。
OFB:初期化ベクトルを暗号化してゆき、平文とXORする。
CTR:初期化ベクトルをブロックごとにカウントアップしたものを暗号化し、平文とXORする。ストリーム暗号に近い。

CTRは初期化ベクトルを再利用しない分には安全で高速な手法です。そのため通信においては、CTRとメッセージ認証を組み合わせたAES-GCMという暗号利用モードがよく使われます。

鍵を使わない平文の改ざん

セーブデータ処理では処理速度は重要ではないので、強度のあるブロック暗号を用いることにします。しかしセーブデータ処理に関しては、状況によっては暗号鍵を使うことなく平文を改ざんできる、という問題がどの暗号利用モードにもあることに気づきました。
ECB:同じ平文パターンへの対策がされていない
それ以外:XORをとる、という演算の性質を悪用する。特に初期化ベクトルに関する脆弱性がある

初期化ベクトルの再利用により、暗号文を復号せずに平文の改ざんができてしまうことは以下の記事で知りました。

鍵を使わない平文の改ざんは、特にストリーム暗号で顕著な問題です。OFB, CTRといったストリーム暗号とみなせるブロック暗号化手法でも同様の問題があります。(CFBはストリーム暗号というよりはCBCに近いです)

また、初期化ベクトルを再利用してしまうと、認証付き暗号(暗号利用モード)でもメッセージ認証符号ごと改ざんできてしまいます。

※上記は初期化ベクトルの運用、つまりサーバ運用や実装ライブラリの話です。言うまでもなく、正しく使えばAES-GCMのアルゴリズムは信頼性が高いものです。

セーブデータの暗号化の目的はまさに平文にあたるゲーム進捗を改ざんされたくないことだったので、これは大きな問題です。

スタンドアロン構成での暗号化

鍵をゲームシステム内に置かざるをえない

上記にもありますが、スタンドアロン構成の場合、ゲームシステムの内部に鍵を置かないと復号ができません。そのため、理論上は無限の時間をかけると鍵を見つけられてしまいます。鍵そのものではなく、鍵を作るための秘密情報であっても同様ですし、秘密情報を分割しても同様です。
一方で、いずれ鍵を見つけられるとしても、その時間や労力を実用上不可能なほどに引き上げることでクラッキングを諦めさせる、という人間に対する戦略もあります。これを実現するのが「難読化」です。難読化は鍵を使わない(鍵に頼れない)ため、暗号化とはまた別の概念です。

リプレイ攻撃を試行されやすい

スタンドアロンの場合、セーブデータを一旦ユーザ側の領域に保存し、後日システムがそれを読み込む、という動作でセーブ機能を実現します。そのため、セーブデータを扱う処理は根本的にリプレイ攻撃されやすい状況にあります。

初期化ベクトルを相互監視できない

OFBやCTR系が想定する初期化ベクトルの使い方。安全性は初期化ベクトルの非再現性に依存
スタンドアロン構成における初期化ベクトルの保存先の比較。どちらにも脆弱性がある

ECB以外のブロック暗号では初期化ベクトルを使いますが、スタンドアロンの場合はこの扱いが難しいです。
システムに初期化ベクトルを埋め込む場合は変更ができず、常に初期化ベクトルを再利用することになります。初期化ベクトルを変動させたい時はセーブデータに保存するしかありませんが、この場合でも暗号化時点(セーブ時)と復号時点(ロード時)とで有効な初期化ベクトルは変わらないため、実質初期化ベクトルを再利用しているのと変わりません。
どちらの場合でも、根本的には初期化ベクトルをシステムかユーザの片方が持っていて、相互監視できる状況になっていないのが問題です。
そもそも初期化ベクトルは安全性を直接確保するためのものではなく、初期化ベクトルを公開しても安全性が変わらないような暗号化の手法が望ましいです。しかしブロック暗号の手法は初期化ベクトルに関連した改ざんに弱いです。特にOFB, CTR系が顕著なため、スタンドアロン構成でセーブデータの暗号化にAES-GCMを用いると、危険性がかえって高まるように感じます。

平文の情報源が既知

平文そのものではなくその元となる情報が入手できる場合でも、暗号文に対する攻撃の難易度は下がります。
ゲームの場合、ステータス情報などゲーム上で表示される情報をある程度自由に操作できます。そのため、攻撃者が指定した情報に対応するセーブデータを作成できます。任意の平文の情報源と暗号文のペアを入手できる、という状況です。
これに加え、セーブデータの構造が既知の場合は、任意の平文そのものと暗号文のペアが入手可能、要するに選択平文攻撃が可能になります。

セーブデータの構造が既知(ソースコード公開時)

これに関しては特殊な条件で、特にGPLライセンスのソースを使う場合に考慮すべき事項となります。制作中のゲームでもソースを公開するかどうかは検討中です。
セーブデータの構造、特にプレイ進捗情報のデータ仕様が既知の場合、暗号化前の平文データを自由に作ることができます。そのため不正な暗号文を作る際のゴールとなる改ざんしたい平文も自由に作れるようになります。
通信の場合、データ仕様が公開されていることが前提で対策をとるのですが、スタンドアロン構成の場合は問題です。前述のように、通信の場合は秘密情報を第三者が再現できないことと、秘密情報を復号システムと隔離することで暗号化の安全性を確保できます。しかしスタンドアロンの場合はそのどちらも実現できないので、セーブデータの構造を隠すことが暗号化の安全性につながるためです。

後半に続く

基礎知識の内容がかなり長くなったので、具体的なセーブデータ処理の内容は後半に続きます。


参考:GPLライセンスの取り扱い

GPLライセンスにおけるセーブ処理の開示の扱い

制作中のゲームのソースにはGPLライセンスの適用はしないつもりですが、念のためGPLライセンス下でのセーブ処理の扱いについて調べました。

GPLライセンスに従うプログラムを公開するとき、プログラムとソースを同時にWeb公開する以外にも方法をとれます。
物理メディアにプログラムとソースを同梱して配布しても構いません。またダウンロード手段を別に用意したうえで、プログラムだけを公開する、という方法でも構いません。(英語の原文も参考にしてください)

そしてGPL下での配布は有償でも構いません。ダウンロードに課金しても構わない、ということです。
https://www.gnu.org/licenses/gpl-faq.ja.html#DoesTheGPLAllowDownloadFee

https://www.gnu.org/licenses/gpl-faq.ja.html#CompanyGPLCostsMoney

一方でソースの頒布を拒否すること、及び請求者が制作物(バイナリとソース)の再配布することを禁止できません。特に有償で配布する場合でも、入手した人が無償で制作物を再配布することや、無償で公開された制作物を別の人が入手することを元の制作者はGPL下で禁止できません。そのため、制作物を販売するために有償にする意味はあまりありません。

https://www.gnu.org/licenses/gpl-faq.ja.html#DoesTheGPLRequireAvailabilityToPublic

GPLv3ではシステムの正常動作に必要な処理やデータはすべて開示対応が必要です。つまりセーブ機能がないと正常動作できない構成では、セーブ処理も含めて開示対応が必要です。特に秘密鍵がないと正常動作しない場合は、GPLv3では秘密鍵も含めた開示が必要です。セーブ機能に限らず、起動ロックの機能でも同様です。またオンライン構成の場合は、特にAGPLの場合に同様の判断が求められます。

https://www.gnu.org/licenses/gpl-faq.ja.html#GiveUpKeys

※「いいえ」の後の文言に該当。

一方で、どこまでを正常動作と解釈するのかは、GPLには記載がありません。つまり開発者の解釈(と裁判での判断)にゆだねられています。
そのため、セーブ処理、特に秘密鍵を公開したくない場合は、セーブ処理の部分を拡張プラグインとして実装し、セーブ処理なしでも正常動作するようにシステムを組むことが必要です。厳密には、開発者が正常動作であると主張して(裁判で)認められる程度には、セーブ処理なしでもシステムを使用できるようにする、ということです。
実際には、セーブ機能なしを無償の体験版として、購入によりセーブ機能を有効にできる、というのは一般的に妥当な対応だと思います。

GPLライセンスのシステムとアセットのライセンス

GPLの範囲はソースコードだけではなくシステムが使う画像等のアセットについても同様です。システムの正常動作のために必要ならGPLの条件によりアセットのソースの開示が必要です。追加プラグインとして扱えるのならGPLによる開示対応は不要、ただしアセット自体のライセンスが優先です。
https://www.gnu.org/licenses/gpl-faq.ja.html#WhatCaseIsOutputGPL

DLCは後者のプラグインとして扱えるので開示不要、と言って構わないと思います。ただしアセットのライセンスを優先で、特にクリエイティブコモンズの場合に注意です。
フォントやシステムのアイコンが微妙なラインですが、ただの装飾と扱えるならプラグイン相当・アセットのライセンスが優先、機能として必要ならGPLが適用されます。
https://www.gnu.org/distros/free-system-distribution-guidelines.html#non-functional-data

アイコンについては、ソース配布ができるアイコンを自前で作ってシステムに同梱し、別の配布ライセンス(パブリックドメイン含む)に従うアイコンアセットをオプションとして差し替えられるようにするのが良いのかもしれません。
フォントは文字表示というシステムの正常動作に欠かせません。つまりフォントは機能として必要です。そのため、GPLライセンスに従うシステムにはGPLライセンスに従うフォントしか同梱できません。注意しましょう。

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