エンコーダ・デコーダを作る
Rogueでは、外部へ出力しているデータを難読化していて、安易に解析できないようにしている。これはデータの改ざん対策でもあり、複数人で競って遊ぶためには大切だと思う。
もっともオープンソースになので、どのように難読化しているのかは、ソースコードを参照すればわかってしまうが、ロジックを読み解くスキルが求められる。
外部出力されているデータは、スコアとゲーム内容で、後者はゲームをレジュームする機能を担っている。1980年代のゲームにレジューム機能が実装されていたというのには驚いてしまう。
エンコード関数、デコード関数
データの難読化はエンコード関数encwriteで行われている。エンコード関数encwriteはソースコードsave.cに記述されている。処理はとても単純で、排他的論理和を利用して特定のビットを反転させることで難読化をかけている。
デコード関数encreadも同じソースコードに記述されている。排他的論理和を利用してエンコードしているので、同じ値を使ってビット反転をさせると元の値を復元することができる。つまり、同じビット操作を異なる2つの関数で行っている。
エンコード関数encwriteと、デコード関数enceradが分かれている理由は、エンコード、あるいはデコードしながらファイル操作を実施しているためで、それ以上の理由はない。
さらに重箱の隅をつつくなら、エンコード関数encwriteはFILE構造体を使った高級ファイル関数を使っていて、デコード関数encreadはファイル・ディスクリプタを使った低級ファイル関数を使っている。なぜ対称性がないのか、理由はわからない。
次にエンコード関数encwriteとデコード関数encreadを示す。
--- save.c ---
191: /*
192: * perform an encrypted write
193: */
194: encwrite(start, size, outf)
195: register char *start;
196: unsigned int size;
197: register FILE *outf;
198: {
199: register char *ep;
200:
201: ep = encstr;
202:
203: while (size--)
204: {
205: putc(*start++ ^ *ep++, outf);
206: if (*ep == '\0')
207: ep = encstr;
208: }
209: }
210:
211: /*
212: * perform an encrypted read
213: */
214: encread(start, size, inf)
215: register char *start;
216: unsigned int size;
217: register int inf;
218: {
219: register char *ep;
220: register int read_size;
221:
222: if ((read_size = read(inf, start, size)) == -1 || read_size == 0)
223: return read_size;
224:
225: ep = encstr;
226:
227: while (size--)
228: {
229: *start++ ^= *ep++;
230: if (*ep == '\0')
231: ep = encstr;
232: }
233: return read_size;
234: }
このエンコーディングをPythonへ移植するに当たり、ファイル操作関連を扱っているsave.cから切り出し、RogueEncodingクラスを作る。
Pythonへ移植する
Pythonへ移植する際にはリファクタリングをして、気になった点を改善しておきたい。
・エンコード、あるいはデコードと同時にファイル操作をしない
・エンコードとデコードのルーチンを共用する
操作するファイルはバイナリデータになるので、Pythonの場合はバイナリ文字列を利用すればいいようだ。
hoge = "test\0string"
print(hoge.encode())
>> b'test\x00string'
これを文字列で扱ってしまうと、ヌル文字がomitされてしまう。
hoge = "test\0string"
print(hoge)
>> 'teststring'
Python文字列を1文字ずつのリストへ変換するにはlist(string)とするが、バイナリ文字列も同様で、list(bstring)とすればいい。バイナリ文字列の場合は、文字コード値のリストに変換される。
hoge = b"test\0string"
for ch in list(hoge):
print(ch)
>> 116
>> 101
>> 115
>> 116
>> 0
>> 115
>> 116
>> 114
>> 105
>> 110
>> 103
文字コード値のリストを再びバイナリ文字列へ戻すには、bytes(list)とする。文字列のリスト化は検索するとすぐに情報を得られるが、バイナリ文字列については少ないようなので、ここにメモを残しておくことにした。
RogueEncoderクラスを作る
Rogueの難読化は排他的論理和演算をしながら文字列を走査していくので、汎用的なクラスBinaryStringXorEncoderを作成する。デコード関数はエンコード関数を呼び出すようにして、いずれかを2回呼び出せば元に戻るようにしておく。
class BinaryStringXorEncoder(object):
def __init__(self, xorBytes:bytes) -> None:
self.xorBytes:list = list(xorBytes)
def encode(self, bstring:bytes) -> bytes:
encoderIndex:int = 0
encodedBytes:list = []
for ch in list(bstring):
encodedBytes.append(ch ^ self.xorBytes[encoderIndex])
encoderIndex += 1
encoderIndex %= len(self.xorBytes)
return bytes(encodedBytes)
def decode(self, encodedBytes:bytes) -> bytes:
return self.encode(encodedBytes)
BinaryStringXorEncoderクラスができたら、これを包含したRogueEncoderクラスを作成する。RogueEncoderクラスにBinaryStringXorEncoderクラスを包含させるのは、RogueEncoderクラスをスタティックなクラスにしておきたいのと、クラスを生成する際に外部パッケージなどを参照しないようにするため。
Pythonのパッケージは循環参照に陥りやすいため、独立性を少しでも高くしておきたい。
class RogueEncoder(object):
encoder:BinaryStringXorEncoder = BinaryStringXorEncoder(b"\354\251\243\332A\201|\301\321p\210\251\327\"\257\365t\341%3\271^`~\203z{\341};\f\341\231\222e\234\351]\321")
@staticmethod
def encode(bstring:bytes) -> bytes:
return RogueEncoder.encoder.encode(bstring)
@staticmethod
def decode(encodedBytes:bytes) -> bytes:
return RogueEncoder.encoder.decode(encodedBytes)
テストコードを書いてみると、うまく動いていることを確認できた。
hoge = b"hoge\x00piyo\x01fuga"
print(hoge)
encoded = RogueEncoder.encode(hoge)
print(encoded)
decoded = RogueEncoder.decode(encoded)
print(decoded)
>> b'hoge\x00piyo\x01fuga'
>> b'\x84\xc6\xc4\xbfA\xf1\x15\xb8\xbeq\xee\xdc\xb0C'
>> b'hoge\x00piyo\x01fuga'