cocos2d-xでプレイヤーのプレイデータや設定データなどを保存する際に、改竄されるとマズいデータを暗号化して保護したいというケースはよくあると思う。
一般的にcocos2d-xでユーザーデータを保存するならCCUserDefaultを使うと思うが、これにはデータを暗号化する機能はない。
なので自前で作っちゃおうというお話。
一番簡単なのは、CCUserDefaultに保存する値を暗号化する方法だろう。
ただし、CCUserDefaultはキーと値のペアでデータを保存するので、値だけ暗号化してもキーは丸見えになる。
暗号化してあるので値そのものを改竄することは容易ではないだろうが、値自体を削除してリセットすることは容易だろう。
内容を推測しにくいキーにするとか、削除されても困らないようなデータ設計にするとか、そうした工夫が必要になる。
今回は、そうした値だけを暗号化する方法ではなく、データ全体を暗号化する方法を試みた。
CCUserDefaultには当然データ全体を暗号化する術が無いので、ユーザーデータを保存するための独自のクラスを作成する必要が有る。
処理としては、
CCDictionary → 文字列化 → 暗号化 → ファイルに保存。
ファイルから文字列取得 → 復号化 → CCDictionaryに変換。
となる。
言わずもがな、このCCDictionaryがユーザーデータ。
CCDictionaryと文字列との変換には、JSONを使った。
以下のブログにそのものズバリなコードが載っているので、大いに参考にさせて頂きました。
ありがとうございます。
Cocos2d-xでJSONをCCDictionaryに変換する – 銀の人のメモ帳
cocos2d-x – CCObjectからJSONを作成する – Qiita [キータ]
暗号化については、AESなどの本格的なやつを使おうとも思ったのだけど、導入がなかなか面倒臭そうだったのでやめた。
要はデータの改竄を防止できれば良いので、それも100%完全に防止しなければならないほどのものでもないので、簡易的な暗号化でいいやということで、XORを使った暗号化で良しとした。
const std::string Encription::xorEncode(std::string text, std::string key) { std::string output = text; const unsigned int n = text.size(); const unsigned int keyN = key.size(); unsigned int keyIndex = 0; for (unsigned int i = 0; i < n; i++) { output[i] ^= key[keyIndex]; keyIndex = (keyIndex + 1) % keyN; } return output; }
こんな感じでいいのではないかと思う。
// 暗号化 std::string encrypted = xorEncode(jsonString, KEY); // 復号化 std::string decrypted = xorEncode(encrypted, KEY);
のように使う。
KEYは暗号鍵となる任意の文字列で。
残るはファイルへの読み書きだ。
以下のコードでは、ファイルの読み書き時にデータが破損するケースがあるようだ。
おそらく、暗号化によって文字データが特殊な値に変換された場合、それを”文字”として読み書きしようとして破損するのだろうと推測する。
元のコードの下に、修正したコードを記述するのでそちらを参考にどうぞ。
ファイルからの読み込みはCCFileUtilsを使って、こんな感じ。
// ファイル読み込み unsigned long nSize; const unsigned char* buffer = CCFileUtils::sharedFileUtils()->getFileData(filepath.c_str(), "rb", &nSize); // 成功したら復号化 if (buffer != NULL) { const std::string decrypted = xorEncode((const char*)buffer, KEY); }
ファイルへの保存はfopenを使って
// 暗号化 const std::string encrypted = xorEncode(jsonString, KEY); // ファイルに保存 FILE *outputfile = fopen(filepath.c_str(), "w"); if (outputfile != NULL) { fputs(encrypted.c_str(), outputfile); fclose(outputfile); }
以下、修正したコード。
ファイルの読み込み。
FILE *fp = fopen(filepath.c_str(), "rb"); if (fp != NULL) { std::string str = ""; do { const int c = getc(fp); if (c == EOF) { break; } else { str += (char)c; } } while (true); // 復号化 const std::string decrypted = xorEncode(str, KEY); }
ファイルへの保存。
// 暗号化 const std::string encrypted = xorEncode(jsonString, KEY); FILE *outputfile = fopen(filepath.c_str(), "wb"); if (outputfile != NULL) { const unsigned int n = encrypted.size(); for (unsigned int i = 0; i < n; i++) { putc(encrypted[i], outputfile); } fclose(outputfile); }
読み書きの際に、1文字ずつ明示的にバイトコードとして扱うように修正した。
これでたぶん大丈夫だと思うけど、どうかな。