第13回:ファイルの暗号化



このゲームで使っている画像やシナリオのデータは、
dataフォルダにそのままの形式(bmp,txt)で置かれています。

じゃあ、ゲームを起動しなくても画像は見られるし、シナリオは読めるんじゃない?

その通りです。
ペイントやメモ帳で中身を読めてしまいますね。

「別に見られてもいいよ」という方には必要ありませんが、
おそらく多くの方にとって、ゲームを起動する前に
ゲームで使用している画像やシナリオを読まれることは避けたいでしょう。

読まれることを避けるにはどうすればよいか。
  1. データファイルの拡張子を変えてみる。
  2. データファイルを圧縮する。
  3. データファイルを暗号化する。
などが挙げられます。それぞれ検証してみましょう。

1の方法では、ファイルの拡張子を変えてもファイルの中身自体は変わりません。
よって、ちょっとした解析ソフトにかけるだけですぐに中身が分かってしまいます。

2の方法は中身も変わりますし、圧縮することでディスクの節約にもなります。
問題となるのは、圧縮アルゴリズムを実装しなければならないことでしょう。
ランレングスやらハフマン木など、ファイル圧縮を実装するアルゴリズムは沢山ありますが、
それらを説明すると本題から離れてしまうので、ここでは他の方法を使います。
興味のある方は、圧縮アルゴリズムを各自で調べてみてください。

3の暗号化するという方法は、圧縮はしないまでもファイルの中身を変えることで
ファイルを簡単には読まれないようにします。
圧縮するほどの難しいアルゴリズムも必要ありませんので、
この方法でファイルを読まれないようにしましょう。


さて、暗号化と一口に言っても、その実装方法は様々です。
ハッシュ関数を作ってみたり、生成多項式で割った余りを使ってみたり。
ファイルの中身が分からなくなればとりあえずOKなので、
ファイルを構成する各バイトに対して適当な数で排他的論理和を取ることにします。

排他的論理和で暗号化する方法を説明しましょう。
どんなファイルでも、バイト列から成り立っています。
例としてテキストファイルを一つ作成し、中に「あいうえお[改行]」と入力します。
そのファイルをバイナリエディタなどで読むと、以下のようなバイト列になっていることが分かります。

   82 A0 82 A2 82 A4 82 A6 82 A8 0D 0A
(16進数です。念のため。)

これらの数値に対して、特定の値で排他的論理和(XOR)を取ります。
特定の値は、00〜FF(255)の間の整数なら何でも構いませんが、
0で排他的論理和をとっても何も変わらないので、
ここではBB(187)で排他的論理和を取ります。
82とBBの排他的論理和は、以下のようになります。

 (16進数)  (2進数)
     82      1000 0010
     BB      1011 1011
-----------------------------
 XOR 39      0011 1001

出力された値に対して、排他的論理和を取った数と同じ数で排他的論理和を取ると、
元の数を取得できます。

 (16進数)  (2進数)
     39      0011 1001
     BB      1011 1011
-----------------------------
 XOR 82      1000 0010

この理論を使用して、暗号および復号を行います。


● ファイル暗号化ツールの作成

ファイルの暗号化はゲーム内で行うわけではありません。
暗号化を行うアプリケーションを別途作成します。

アプリケーションを起動して「暗号化」ボタンを押して暗号化する…といった形式ではなく、
エクスプローラ上でアプリケーションのアイコンに暗号化したいファイルを
ドラッグ&ドロップするとそのファイルを暗号化する、
といった流れを持つプログラムにしましょう。
ウィンドウ等のGUI部品を考えなくて済みます。

Delphiを起動し、[ファイル|新規作成]から「その他」を選択し、
表示されるダイアログから「コンソールアプリケーション」を選択します。
プログラムの雛形が表示されるので、[ファイル|名前を付けて保存]で
crypt.dprという名前をつけて保存します。

プログラムを以下のように記述します(リスト30)。
main関数に相当する手続きと、暗号化を行う手続きです。

(リスト30)
            
program crypt;
            
            
uses
   SysUtils, Classes;

const
   KEY : Byte = 187; //0〜255の間の適当な値で

//引数FileNameで指定されたファイルを暗号化する
procedure Encrypt(FileName : String);
var
   inMem, outMem : TFileStream;
   buffer : array[0..65535] of Byte;
   i, index : Integer;
begin
   //入力ファイルのストリームを作成
   inMem := TFileStream.Create(FileName, fmOpenRead);  -------------------------(48)
   //出力ファイルのストリームを作成
   outMem := TFileStream.Create(ChangeFileExt(FileName, '.cpt'), fmCreate);  ---(49)
   try
      while True do
      begin
         //入力ファイルのストリームから最大65536バイトを読み出す
         index := inMem.Read(buffer, SizeOf(buffer));  -------------------------(50)
         //1バイトも読み込めなかったら
         //ファイルの終端なのでループを抜ける
         if index = 0 then Break;

         //読み出した各バイトに対して187とのXORを取る
         for i := 0 to index-1 do
            buffer[i] := buffer[i] xor KEY;  -----------------------------------(51)

         //出力ファイルのストリームに出力する
         outMem.Write(buffer, index);  -----------------------------------------(52)
      end;
   finally
      //ストリームを解放する
      inMem.Free;
      outMem.Free;
   end;

end;

//いわゆるmain関数
var
   i : Integer;
            
            
begin
            
            
   //ドラッグ&ドロップされた全てのファイルに対して暗号化を施す
   for i := 1 to ParamCount do  ------------------------------------------------(53)
   begin
      Encrypt(ParamStr(i));  ---------------------------------------------------(54)
   end;
            
            
end.
            

先にmainに相当する箇所の説明をします。
(53)のParamCountはC言語のargcに相当し、自分自身を含めた引数の個数を持っています。
例えば、このアプリケーションにファイルを三つドラッグ&ドロップして起動した場合、
ParamCount=4となります。

(54)のParamStrはC言語のargvに相当し、ドラッグ&ドロップされたファイルの
ファイル名をフルパスで持った配列です。
ParamStr(0)は自分自身のファイル名であり、ドラッグ&ドロップしたファイルが一つなら、
ParamStr(1)にそのファイルのファイル名がフルパスで入っています。

暗号化したいファイルのファイル名を手続きEncryptに渡します。
手続きEncryptでは、(48)で暗号化するファイルのストリームを作成、
(49)で暗号化されたファイルのストリームを作成します。

while Trueで無限ループに入ります。
(50)で入力ストリームからバッファに最大65536バイトを読み出します。
このとき、バッファに読み出したバイト数が変数countに代入されます。
countの値が0だったら、ファイルの最後まで読み出したことになるので
ループから抜け出します。

(51)で、バッファに読み出した各バイトに対してKEY(=187)で排他的論理和を取ります。
排他的論理和を取ったバッファを(52)で出力ストリームに書き出します。

ファイルの暗号化の方法は以上です。早速試してみましょう。
コンパイルして出力されたcrypt.exeにエクスプローラ上で
先程出てきたテキストファイルmoto.txt(中身は「あいうえお[改行]」)を
ドラッグ&ドロップすると(図13-1)、暗号化されてmoto.cptが出力されます。

図13-1:暗号化アプリケーションの使用方法


暗号化前と暗号化後のファイルの中身を比較してみると、図13-2、図13-3のようになりました。
暗号化後のファイルからは、元のファイルが何だったか全く分かりませんね。

図13-2:暗号化前 図13-3:暗号化後

これにて暗号化は完了です。
ゲームで使う画像やシナリオファイルを暗号化しておきましょう。


● ファイル復号ルーチンの組み込み

暗号化したファイルは、ゲーム内でそのファイルが持つデータを必要とした時に
随時復号して読み出すようにします。

これまでファイルからデータを読み出す時には
TBitmap.LoadFromFileやTStringList.LoadFromFileの手続きを使用していました。
暗号化されたファイルを直接TBitmap.LoadFromFileで読み出すことはできないので、
どこかで復号してからTBitmap等に読み出す必要があります。
ファイルを読み出す直前に、暗号化されたファイルを復号ルーチンに通して
bmpなどの元のファイルに戻してから、TBitmapに読み出すというのが妥当に見えるかもしれません。
しかし、その方法では
 復号ファイルの出力→復号ファイルの読み出し→復号ファイルの削除
といった流れとなり、ディスクの入出力が増えてしまいます。
ゲーム中にはできるだけディスク入出力は避けた方が、
パフォーマンスの低下を防ぐためにもよいでしょう。

そして、現在のプログラムをあまり書き換えなくても
(LoadFromFileの前にいちいち復号ルーチンを書かなくても)済むようにするためには…?

継承しましょう。

TBitmapやTStringListをそれぞれ継承したクラスを作成し、
その派生クラスでファイルを復号して読み込む手続きを作成すれば、
メインプログラムをあまり書き換える必要はありません。

TBitmapを継承したクラスTBitmapExを作成し、
ファイルを復号して読み出す手続きLoadFromCryptFileを実装します。
[ファイル|新規作成]から「ユニット」を選択し、bitmapex.pasで保存してください。
中身を以下のように記述します(リスト31)。

(リスト31)
            
unit bitmapex;

interface
            
            
uses
  SysUtils, Classes, Graphics;

type
   TBitmapEx = class(TBitmap)  -------------------------------------------------(55)
   public
      procedure LoadFromCryptFile(const FileName: string);  --------------------(56)
   end;

const
   KEY : Byte = 187;
            
            
implementation
            
            
//暗号ファイルを復号して自分自身に読み込む
procedure TBitmapEx.LoadFromCryptFile(const FileName: string);
var
   inMem : TFileStream;
   outMem : TMemoryStream;
   buffer : array[0..65535] of Byte;
   i, index : Integer;
begin
   //入力ファイルのストリームを作成
   inMem := TFileStream.Create(FileName, fmOpenRead);
   //復号ファイルのストリームを作成
   outMem := TMemoryStream.Create;
   try
      while True do
      begin
         //入力ファイルのストリームから最大65536バイトを読み出す
         index := inMem.Read(buffer, SizeOf(buffer));
         //1バイトも読み込めなかったら
         //ファイルの終端なのでループを抜ける
         if index = 0 then Break;

         //読み出した各バイトに対して187とのXORを取る
         for i := 0 to index-1 do
            buffer[i] := buffer[i] xor KEY;

         //復号ファイルのストリームに出力する
         outMem.Write(buffer, index);
      end;
      //ストリームの現在位置を最初に戻す
      outMem.Seek(0, soFromBeginning);  ----------------------------------------(57)
      //復号ストリームからTBitmapに読み込む
      LoadFromStream(outMem);  -------------------------------------------------(58)
   finally
      //ストリームを解放する
      inMem.Free;
      outMem.Free;
   end;
end;
            
            
end.
            

あるクラスを継承して新しいクラスを作成する場合には、(55)のように
(派生クラス名) = class (親クラス名)
と記述し、追加する手続きや関数の宣言を(56)のように宣言します。

追加した手続きの実装をimplementation節以下に記述します。
ここではファイルを復号して自分(TBitmapEx)自身に読み込む手続きLoadFromCryptFileを
実装しています。
入力ファイルの各バイトで排他的論理和を取るだけなので、
この手続きは暗号化のアルゴリズムと基本的には同じです。
異なるのは、自分自身に読み込ませる(58)のTBitmap.LoadFromStreamの呼び出しと
読み出すストリームの位置を開始位置に戻す(57)のSeekの呼び出しが追加されていることです。
復号されたバッファはTMemoryStreamのoutMemに格納されているので
TBitmapの手続きLoadFromStreamで(継承しているので当然TBitmapExの手続きでもあります)
outMemから直接(ファイル入出力を行わずに)読み出すことができます。

TBitmapExの作成が終わったら、同様にTStringListも継承してTStringListExを作成し、
TBitmapExと同じ要領でLoadFromCryptFileを作成してください。
このファイルはstringlistex.pasとします。


二つのクラスを継承したら、メインプログラムからそれらを呼び出して
暗号化されたファイルからデータを読み出せるようにします。

uses節にbitmapexとstringlistexを追加します。

uses
  effect, fontselect, bitmapex, stringlistex;

グローバル宣言部で宣言しているTBitmapとTStringListのインスタンス変数の中で
LoadFromFileを使うことが分かっているものをそれぞれTBitmapEx、TStringListExに置き換えます。

var
  BackBmp : TBitmap;
  CurBack : Integer;
  CharaBmp : TBitmap;
  CurChara : Integer;
  SetChara : Boolean;
  MesWinBmp : TBitmap;
  MesWinTempBmp : TBitmap;
  TextBmp : TBitmap;
  AllText : TStringList;
  TextPoint : Integer;
  MTo : Integer;
  CurTrack : Integer;
  phase : TPhase;
  SkipTo : array[0..3] of Integer;
  ChoiceNum : Integer;
  SkipLoop : Boolean;
var
  BackBmp : TBitmapEx;
  CurBack : Integer;
  CharaBmp : TBitmapEx;
  CurChara : Integer;
  SetChara : Boolean;
  MesWinBmp : TBitmapEx;
  MesWinTempBmp : TBitmap;
  TextBmp : TBitmap;
  AllText : TStringListEx;
  TextPoint : Integer;
  MTo : Integer;
  CurTrack : Integer;
  phase : TPhase;
  SkipTo : array[0..3] of Integer;
  ChoiceNum : Integer;
  SkipLoop : Boolean;

FormCreateにてこれらのインスタンスの実体を作成している箇所(Create手続きの呼び出し)で
インスタンスを作成するクラスをそれぞれTBitmapEx、TStringListExに書き換えます。

   BackBmp := TBitmap.Create;
    :
   CharaBmp := TBitmap.Create;
    :
   MesWinBmp := TBitmap.Create;
    :
   AllText := TStringList.Create;
   BackBmp := TBitmapEx.Create;
    :
   CharaBmp := TBitmapEx.Create;
    :
   MesWinBmp := TBitmapEx.Create;
    :
   AllText := TStringListEx.Create;

すべてのLoadFromFileをLoadFromCryptFileに書き換え、
引数に記述されているファイル名の拡張子も、bmpやtxtからcptに置き換えます。

   [FormCreate]
   LoadFromFile(Format('..\\..\\data\\bg%.2d.bmp',[CurBack]));

   LoadFromFile(Format('..\\..\\data\\chara%.2d.bmp',[CurChara]));

   LoadFromFile('..\\..\\data\\meswin.bmp');

   AllText.LoadFromFile('..\\..\\data\\scenario.txt');

   [OneStep]
   CharaBmp.LoadFromFile(Format('..\\..\\data\\chara%.2d.bmp',
                                [StrToInt(tempstr1)]));

   [ChangeBackGround]
   BackBmp.LoadFromFile(Format('..\\..\\data\\bg%.2d.bmp',[CurBack]));
   [FormCreate]
   LoadFromCryptFile(Format('..\\..\\data\\bg%.2d.cpt',[CurBack]));

   LoadFromCryptFile(Format('..\\..\\data\\chara%.2d.cpt',[CurChara]));

   LoadFromCryptFile('..\\..\\data\\meswin.cpt');

   AllText.LoadFromCryptFile('..\\..\\data\\scenario.cpt');

   [OneStep]
   CharaBmp.LoadFromCryptFile(Format('..\\..\\data\\chara%.2d.cpt',
                                     [StrToInt(tempstr1)]));

   [ChangeBackGround]
   BackBmp.LoadFromCryptFile(Format('..\\..\\data\\bg%.2d.cpt',[CurBack]));

動作を確認するために、dataフォルダには元のデータを暗号化したcptファイルのみ設置して
元のファイルは他所に移動させるか削除してしまいましょう。

実行して、前回と変わらないように動作していることが分かればOKです。
見た目前回と同じでも、ゲームで使うデータは暗号化されたものを
逐次復号して読み込んでいるのです。


アドベンチャーゲーム設計論トップへ戻る
開発分室へ戻る
トップページへ戻る