暗号化済みセーブ機能を作った話(仮)

またまた、備忘録的な文章です。そのはずなのに解説口調なのは…まあ、察してください。あと、安定の雑、嘘、間違い成分があるので…そこら辺は見逃してください…。

C#で暗号化をしてみたい

ゲーム作るときに必要になってくるセーブ機能。セーブ機能をつくるなら、やっぱりデータは暗号化して(ある程度)安全に保存したいですよね。

ってことでネットを漁り、多くの人々のブログやら記事を見たり、マイクロソフトのリファレンス見たりして、ようやく安定した暗号化ができそうなのでここにまとめるのです。

暗号化のスクリプト

安定のまず、スクリプトドーン

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Security.Cryptography;
using System.Text;
using System.IO;

public class ango : MonoBehaviour {
	RijndaelManaged rijndael;
	
	int bitLength=128;
	/*
	128ビット=16バイト=半角16文字
	192ビット=24バイト=半角24文字
	256ビット=32バイト=半角32文字
	*/

	string password="passwardpassword"; //キー
	string iv="iviviviviviviviv"; //初期化ベクトル
	
	string dataPath=Application.streamingAssetsPath+"/data.txt";
	
	public void Encode(string characters){ //暗号化
		Setting(); //初期設定
		byte[] byteMozi=Encoding.UTF8.GetBytes(characters);//文をバイト配列化
		byte[] changedByteMozi; //暗号化したものを格納
		
		using(ICryptoTransform encryptor=rijndael.CreateEncryptor()){
			changedByteMozi=encryptor.TransformFinalBlock(byteMozi,0,byteMozi.Length);//暗号化
		}
		
		using(FileStream fileStream=new FileStream(dataPath,FileMode.OpenOrCreate,FileAccess.Write)){
			fileStream.Write(changedByteMozi,0,changedByteMozi.Length);//changedByteMoziをchangedByteMoziの長さだけ書き込み
		}
	}
	
	public string Decode(){ //復号
		Setting();
		byte[] kariData;
		byte[] banira;
		string data;
		try{
			using(FileStream fileStream=new FileStream(dataPath,FileMode.Open,FileAccess.Read)){
				kariData=new byte[fileStream.Length];
				fileStream.Read(kariData,0,kariData.Length);//fileStreamで開いたファイルの中身を入れる
			}
			try{
				using(ICryptoTransform decryptor=rijndael.CreateDecryptor()){
					banira=decryptor.TransformFinalBlock(kariData,0,kariData.Length);//解読
				}
				data=Encoding.GetEncoding("UTF-8").GetString(banira);//バイト配列から文字へ
			}catch(CryptographicException ex){
				return "File cannot decoded";
			}
		}catch(FileNotFoundException ex){
			return "File not found";
		}
		
		return data;
	}
	
	void Setting(){
		rijndael=new RijndaelManaged();
		rijndael.KeySize=bitLength;
		rijndael.BlockSize=bitLength;
		rijndael.Mode=CipherMode.CBC;
		rijndael.Padding=PaddingMode.PKCS7;

		rijndael.Key=Encoding.UTF8.GetBytes(password);
		rijndael.IV=Encoding.UTF8.GetBytes(iv);
	}
	
	void Start(){
		Encode("こんにちは、");
		Debug.Log(Decode());
	}
}

クラス名は「ango」ですが、気にしないでください。英語は苦手なんです。そのほかにも英語と日本語混ざっているところありますが、気にしないでくださいな()。

関数の説明ですが、Encodeで暗号化、Decodeで複合、Settingで色々と必要な設定をするといった形です。

Encodeは引数に暗号化したい文字列を、Decodeには返り値に復号した文字列が設定されています。

ここから下は雑説明っぽいんで、読む必要はあんまりないです…。

間違ったことかいてあるかもなので…、あまり真剣に読まなくていいかも。

そういえばRijndaelManagedって何。

Rijndaelはラインダールって読むらしいです。

まあだいたい上に書いてあるURLのここを見てもらえればいいんですが、読むのが面倒な私のために私が書き残します。そうでもしなければまともにわかろうとしなさそうなので…。

まず、上のURL先(翻訳ツールは使いましたが)に書いてあるように

pic_1 - コピー (8)

たしかに、RijndaelManagedを使うだけでそこそこ簡単に暗号化に必要なものができました。でも、RijndaelやらAESって何ですかね?ということで、

まず、AESとは「Advanced Encryption Standard」のことで、訳は「高度な暗号化の標準規格」…的な?だいたいAESの中のRijndaelって感じですかね…?おそらくAESは総称っぽい感じです。

詳しく知りたい方はWikipedia見たほうが早いと思います…。

なんとなくこれらの関係をまとめると、

「RijndaelはAESの一つで、Rijndaelはキーサイズとブロックサイズは128~256ビットのうち32ビットの倍数から選べたそうですが、AESとされているのはブロックサイズは128ビット固定でキーサイズは128、192、256ビットの3種類から選べる」、みたいな感じですかね。

急に出てきた「ブロックサイズ」やら「キーサイズ」ですが、

暗号化は一文字一文字やっているわけではなく何文字かまとめてやっているそうで、ブロックサイズとはまとめて暗号化するビット量のことです。このとき文字数がブロックサイズに満たない場合に備えて、足りない部分に余計なデータを追加する「パディング」というものもあります。キーサイズは暗号化に必要な鍵のビットサイズです。

さて、RijndaelManagedを使うためにまず色々と必要なことがあるそうで、でもとりあえず今は使えるようにします。きっと理解はあとからついてくるでしょう。(適当)

画像2

先ほどのページの続きを読むと色々ありますが、つまりRijndaelManagedをAESとして使うためには、

ブロックサイズは128ビットを使用し、操作モードはCFBモード以外を使えば良い、と。先ほど書いたとおり、ブロックサイズは128ビットじゃないとAESではないみたいですね。

また、キーサイズも128、192、256ビットのどれかから選べばよいそうです。ビット数が大きくなれば計算速度も遅くなるでしょうし、ここでは128ビットでやっていきましょう。

さあ、設定をしよう

void Setting(){
	rijndael=new RijndaelManaged();
	rijndael.KeySize=bitLength;
	rijndael.BlockSize=bitLength;
	rijndael.Mode=CipherMode.CBC;
	rijndael.Padding=PaddingMode.PKCS7;

	rijndael.Key=Encoding.UTF8.GetBytes(password);
	rijndael.IV=Encoding.UTF8.GetBytes(iv);
}

上から順番に説明を…。

まず、キーサイズとブロックサイズを決めていきます。

キーサイズは既定値が256なので128にします、ここではすでに変数bitLengthを作っておいたのでそれを代入、ブロックサイズも同様に…ですがブロックサイズは既定値が128なうえ、サイズが128でないといけないなど、まあわざわざ代入しなくてもいいかもですね…。

次に暗号化モードですが、

上ページに書いてあるようにCBC、CFB、ECBなど結構色々ありますが、ここではCBC選択で行きます。ちなみにCBCも既定値なのでわざわざ書かなくてもいいかもしれません。

次にパディングモードの設定です、

また色々ありますね、違いが正直わからないので(おい)既定値のPKCS7で行きましょう。

さて、最後にキーと初期化ベクトルの設定です。事前に決めておいた文字列をバイト化して代入しましょう。キーは当然暗号化したり復号するときの鍵で、初期化ベクターは暗号にランダム性をもたせるためのバイト配列です。

先ほど書いたように、キーサイズは128ビット=半角16文字になります。

また、だいたいのケースで初期化ベクトルはブロックサイズやキーサイズと同じぐらいの長さらしいので、ここでもブロックサイズに合わせて128ビット、つまり半角16文字にしましょう。

string password="passwardpassword"; //キー 半角16文字
string iv="iviviviviviviviv"; //初期化ベクトル 半角16文字

またこれは、ずいぶん雑なキーと初期化ベクトルですね。

これでようやく暗号化や復号のための用意ができました!

暗号化、復号してみる

もうここまで来たらあとは簡単です。バイト配列を変換するICryptoTransformクラスとCreateEncryptorやCreateDecryptorの引数にキーを初期化ベクトルを入れ、暗号化したい文章はバイト配列化して、TransformFinalBlockで変換。復号ならファイルから読み取ったバイト配列をそのまま引数に…。といった感じでできると思います。

画像3

実行してみると、ちゃんと表示されることがわかります。

終わったあとから色々と…(修正版スクリプトあり)

書き終わってから気が付いたことが…

RijndaelをAESとして使うためにわざわざブロックサイズを128にしてやっているんだから、別にRijndaelManaged使わなくてもいいじゃん!

ってことに気が付きました。がまあいいです。

AesManaged使ってもRijndaelManaged使っても結局は同じこと書くので…

とりあえずそんなことは置いておいて、完成版スクリプト貼っておきます。

using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.IO;

public class CryptographyScript
{
	RijndaelManaged coding;
	Rfc2898DeriveBytes randomBytes;
	
	string password="passwardpassward"; //キー
	string iv="iviviviviviviviv"; //初期化ベクトル
	string salt="SaltNeedsEightBytesAtLeast"; //ソルト
	
	public byte[] Encode(string characters){
		byte[] byteMozi=Encoding.UTF8.GetBytes(characters);//文をバイト配列化
		byte[] changedBytes; //暗号化したものを格納
		Setting();
		
		using(ICryptoTransform encryptor=coding.CreateEncryptor()){
			changedBytes=encryptor.TransformFinalBlock(byteMozi,0,byteMozi.Length);//暗号化
		}
		
		return changedBytes;
	}
	
	public string Decode(byte[] bytes){
		byte[] changedData;
		string data;
		Setting();
		try{	
			using(ICryptoTransform decryptor=coding.CreateDecryptor()){
				changedData=decryptor.TransformFinalBlock(bytes,0,bytes.Length);//復号
			}
			data=Encoding.GetEncoding("UTF-8").GetString(changedData);//バイト配列から文字へ
		}catch(CryptographicException ex){
			return "File cannot decoded:"+ex.Message;
		}
		
		return data;
	}
	
	void Setting(){
		coding=new RijndaelManaged();
		coding.KeySize=128;
		coding.BlockSize=128;
		coding.Mode=CipherMode.CBC;
		coding.Padding=PaddingMode.PKCS7;
		
		randomBytes=new Rfc2898DeriveBytes(password,Encoding.UTF8.GetBytes(salt),1000); //パスワード、ソルト、反復回数
		coding.Key=randomBytes.GetBytes(coding.KeySize/8);
		
		randomBytes=new Rfc2898DeriveBytes(iv,Encoding.UTF8.GetBytes(salt),1000); //パスワード、ソルト、反復回数
		coding.IV=randomBytes.GetBytes(coding.BlockSize/8);
	}
}

​キーと初期化ベクトルを手で入力したものは良くない、ということで

Rfc2898DeriveBytesを利用してある程度結果のわかる乱数で設定していきます。

あとは元はセーブ機能用だったのでファイル書き込み機能を分離して、暗号化に特化してもらいました。

とりあえず終わりっ。

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