1. hibara

    Posted

    hibara
Changes in title
+Visual Studio C#でファイルを暗号化してみる
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,388 @@
+<a href="http://hibara.org/blog/2011/02/20/c-sharp-net-framework-aes/" target="_blank">自分のブログ</a>でも書いたので、Qiitaでの再掲ともなりますが、そのときよりも少しだけ発展させ、かつ詳説を加えてみたいと思います。
+
+まず「暗号化」するための常識、というか、ある程度の定石について解説します。
+
+次に実際にソースコードを交えてファイルを暗号化してみます。
+
+.NET Framework 4.0 には、標準で暗号化に必要なクラスがそろっていますので、今回は、それらを駆使して、自分のコードは必要最低限で済ませてみましょう。
+
+
+## 暗号化には定石がある
+
+意外とこのポイントを知らずに、ただ暗号化クラスを使っている方も多いと思います。
+
+「定石」とは、暗号アルゴリズムは何を使うか?といったレベルではなく、もっと上のレイヤー、「暗号化とは?」に近い、基礎的な手法を指します。
+
+それを「暗号化モード(Block cipher modes of operation)」と言います。
+
+
+## 暗号化モードとは?
+
+基本的に、どの暗号アルゴリズムを使うかに限らず、それが「ブロック暗号方式」であるのなら、まず「暗号化モード」は、何を使うのか?を検討しましょう。
+
+よくわからず、暗号化でやってしまいがちなのは、「ECBモード」でしょうか。
+
+![ecb_mode.png](https://qiita-image-store.s3.amazonaws.com/0/30945/4bcd15b9-c247-430a-79b8-6b2bd0c3e874.png)
+
+これの何がダメなのでしょうか?
+
+このモードでは、同じデータ、同じパスワードだと、毎回同じ暗号化データが生成されてしまうことが問題になります。
+
+「毎回同じデータが生成される」ということは、別のデータと比較することで、悪意ある攻撃者に、少なからず「ヒント」を与えることになります。
+
+たとえば、必ず同じブロックが生成されるなら、それが生成されるまでパスワードの総当り攻撃がしやすくなります。つまり、ファイル全体ではなく、小さなブロックデータなら、解析が容易になるということです。
+
+ただ、毎回同じパスワードと同じデータなど、そんなにあるわけない、と思われるかもしれません。
+
+しかし、日付など同じヘッダ情報が付加されるデータや、同じく終端データなどはどうでしょうか。
+
+ユーザーや開発者が意図しなくとも、知らずに同じデータが入ってしまうこともあるのです。
+
+
+## CBCモードを使うのがベター
+
+逆に言えば、同じデータ、同じパスワードでも、毎回暗号化するたびに中身が変われば、攻撃者にヒントを与えにくくするということになります。
+
+そこで出てくるのが、別の暗号化モードです。
+
+実は、暗号化モードには、いろいろな種類があり、.NET Framework にも、ほぼ主要なものはそろっています。
+
+<strong>CipherMode 列挙体</strong>
+http://msdn.microsoft.com/ja-jp/library/system.security.cryptography.ciphermode(v=vs.110).aspx
+
+とはいえ、どのモードにも、メリット、デメリットがあります。
+
+その中で、暗号強度、コストパフォーマンスのバランスから、<strong>CBCモード</strong>を選択するのがベターでしょう。
+
+暗号化の前に、初期化ベクトル(IV)を与えることで、縄のように、データを交互にねじり合せていくイメージです。
+
+![cbc_mode.png](https://qiita-image-store.s3.amazonaws.com/0/30945/38b2af20-0e91-789f-52a6-623455330387.png)
+
+暗号化する度にランダムのデータ列が与えられることで、ファイル全体が、毎回ちがう値になることが保証されます。
+
+このモードは、最初に与えるIVの質さえ考慮していれば良く、安全に暗号化を行うことができます。
+
+ちなみに、暗号の大家である<a href="https://www.schneier.com/" target="_blank">ブルース・シュナイアー氏</a>がその著書『暗号技術大全』(日本語訳版は絶版・・・)の中でも、
+
+> ファイルを暗号化するのであれば、CBCモードがベストだろう。このモードを使えば、セキュリティは大きく向上するし、保存したデータに多少エラーが発生しても、同期エラーが発生することはまずない。アプリケーションが(ハードウェアではなく)ソフトウェアベースであれば、CBCがほぼ確実にいちばんいい。
+
+と書いています。
+
+
+## ブロック暗号における「パディング」とは?
+
+これも、ブロック暗号を行うときに避けては通れないポイントです。
+
+AESでは、128bit(16バイト)と決められたデータサイズで暗号化されます。
+
+ですので、たとえば16バイトで割り切れないデータサイズのファイルを暗号化するときには、「余り」ができてしまうことがあります(もちろんピッタリの場合もありますが)。
+
+暗号化するときには、特に大きな問題は起きませんが、実は、復号するときに問題となります。
+
+なぜなら元のデータと、暗号化データの境界線がわからなくなるからです(元ファイルのサイズがわからなくなる、とも言えます)。
+
+そこで、ブロック暗号には、「暗号化モード」とは別に、「パディングモード」というものも指定する必要があります。
+
+このパディングモードも、.NET Frameworkでは、いろいろ用意されおり、自前で実装する必要がありません。
+
+<strong>PaddingMode 列挙体</strong>
+http://msdn.microsoft.com/ja-jp/library/vstudio/system.security.cryptography.paddingmode(v=vs.100).aspx
+
+今回は、PaddingMode.PKCS7 を使うことにしましょう。
+
+たとえば、以下の例ですと、データ長が8バイトで、実際のデータ列が9バイトあれば、残りの7バイトは、以下のように埋められます。
+
+![padding_mode.png](https://qiita-image-store.s3.amazonaws.com/0/30945/2c842534-306f-5617-f4b3-0e2320d76eb6.png)
+
+つまり「余り」に埋められた合計サイズが、数値として埋められるというわけです。
+
+これにより、復号時に、データ境界線をプログラムで判別できるようになります。
+
+
+## AESとは?
+
+さて、本題の「AES」についても少し説明をしておきましょう。
+
+<strong>A</strong>dvanced <strong>E</strong>ncryption <strong>S</strong>tandard の略です。
+
+2000年、アメリカ国立標準技術研究所(<a title="NIST" href="http://www.nist.gov/index.html" target="_blank">NIST</a>)による、厳しい選定審査によって選ばれた、オープンな暗号アルゴリズムです。
+
+正式名は、Rijndael(ラインダール)と言いますが、≒ AESです。完全にイコールでない理由は、後述します。
+
+米国産とはいえ、言葉どおり、世界のスタンダードになりつつあります。今のところ、大きな脆弱性もなく、世界各国、いろいろな場所での採用が進んでいます。
+
+共通鍵暗号方式を採用するときは、ほぼ「AES」の一択でしょう。
+
+どうして他のアルゴリズムじゃダメなのか? あるいは、独自暗号じゃダメなのか?という議論もあるでしょう。しかし、原則的には推奨できません。
+
+AESは、アルゴリズムが完全にオープンにされ、多くの数学者や暗号研究家たちの検証に晒された上で耐え、最終選定されています。
+
+それは数学的にも、アルゴリズム的にも、脆弱性が「今のところない」と、公に証明されていることになります。保証されている、とも言い換えられます。
+
+独自アルゴリズムだと、それがないため、未知の脆弱性が多く潜んでいる可能性を否定できません。
+
+また、たとえ万が一、AESに脆弱性が発見されても(そんな事態になったら世界的な危機ですが・・・)、知名度の高さから、ニュースとしてすぐに報じられ、対応策の情報も即座に得やすいでしょう。
+
+と、前置きが長くなりましたが、実際にC#のコードにしてみましょう。
+
+```csharp
+
+AesManaged aes = new AesManaged();
+aes.BlockSize = 128; // BlockSize = 16bytes
+aes.KeySize = 128; // KeySize = 16bytes
+aes.Mode = CipherMode.CBC; // CBC mode
+aes.Padding = PaddingMode.PKCS7; // Padding mode is "PKCS7".
+
+// パスワード文字列が大きい場合は、切り詰め、16バイトに満たない場合は0で埋めます
+byte[] bufferKey = new byte[16];
+byte[] bufferPassword = Encoding.UTF8.GetBytes(Password);
+for (i = 0; i < bufferKey.Length; i++)
+{
+ if (i < bufferPassword.Length)
+ {
+ bufferKey[i] = bufferPassword[i];
+ }
+ else
+ {
+ bufferKey[i] = 0;
+ }
+}
+aes.Key = bufferKey;
+
+//初期化ベクトル
+byte[] iv = new byte[16];
+RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
+rng.GetNonZeroBytes(iv);
+aes.IV = iv;
+
+//Encryption interface.
+ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
+
+```
+
+初期化ベクトルをつくるときに、さらっと「RNGCryptoServiceProvider」を使っていますが、これは、乱数でも、質の高いものを生成するためのクラスです。
+
+RNG(Random Number Generator)、すなわち「暗号乱数ジェネレータ」を使うことで、IVが攻撃者によって推測され、同じものを発生させにくくしています。
+
+これも本来なら、自前で実装する必要がありますが、.NET Frameworkですでに用意されています。便利。
+
+これで暗号化の準備は整いました。
+
+先ほど、チラリと言及しましたが、NISTでは、Rijndaelアルゴリズムで、ブロックサイズ、キー長ともに、<strong>128bit</strong>が「AES」と定義しています。
+
+ですので、キーとブロックサイズに、それ以上の値を設定すると、AESではないので、このクラスではエラーを吐きます。
+
+どうしても強度面で、128bit以上、たとえば256bitで運用したいという場合は、
+
+System.Security.Cryptography.RijndaelManaged クラスを使いましょう。
+
+そのときは、"AesManaged"の部分を置換するだけです。
+
+```csharp
+
+RijndaelManaged aes = new RijndaelManaged();
+aes.BlockSize = 256;
+aes.KeySize = 256;
+
+```
+
+ただ、この場合、厳密には「AESを使っている」とは言えなくなります。「Rijndaelを使っている」となります。
+> ※これは定義的な問題で、中身に問題があるわけではありません。たとえば開発依頼された顧客に対して、暗号化内容を説明するときに注意が必要ということです。
+
+
+## 暗号化前に圧縮をかけるのも定石
+
+実は、暗号化前に圧縮をかけるというも、定石とされています。
+
+データ量を減らせることができれば、処理の高速化が期待できるためです。ただし、圧縮アルゴリズムのパフォーマンスが極端に悪いと、そうとも言い切れませんが、たいていは、暗号アルゴリズムの処理負荷を下げます。
+
+前回、僕のサイトのブログの記事を書いたときは、圧縮までやっていませんでしたが、これもついでにやってしまいましょう。
+
+ここでは、.NET Framework に標準である System.IO.Compression.DeflateStream クラスを使います。
+
+すべてStream派生クラスですので、親和性が高く、合わせて使えば、ほとんどプログラマーはするべきことがありません。ホント、.NET Framework様々ですね(笑)。
+
+```csharp
+
+using (CryptoStream cse = new CryptoStream(outfs, encryptor, CryptoStreamMode.Write))
+{ // IV
+ outfs.Write(iv, 0, 16); //ファイル先頭に埋め込む
+ using (DeflateStream ds = new DeflateStream(cse, CompressionMode.Compress)) //圧縮
+ {
+ using (FileStream fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read))
+ {
+ while ((len = fs.Read(buffer, 0, 4096)) > 0)
+ {
+ ds.Write(buffer, 0, len);
+ }
+ }
+ }
+}
+
+```
+
+## サンプルコード
+
+一応、そのまま貼り付けて使えるように、以下に、関数化したサンプルソースコードも載せておきましょう。
+
+まずは、暗号化から。↓
+
+```csharp
+
+private bool FileEncrypt(string FilePath, string Password)
+{
+ int i, len;
+ byte[] buffer = new byte[4096];
+
+ //出力ファイル(入力ファイルのディレクトリで、拡張子が".enc"とする
+ string OutFilePath = Path.Combine(Path.GetDirectoryName(FilePath), Path.GetFileNameWithoutExtension(FilePath)) + ".enc";
+
+ using (FileStream outfs = new FileStream(OutFilePath, FileMode.Create, FileAccess.Write))
+ {
+ using (AesManaged aes = new AesManaged())
+ {
+ aes.BlockSize = 128; // BlockSize = 16bytes
+ aes.KeySize = 128; // KeySize = 16bytes
+ aes.Mode = CipherMode.CBC; // CBC mode
+ aes.Padding = PaddingMode.PKCS7; // Padding mode is "PKCS7".
+ byte[] bufferKey = new byte[16];
+ byte[] bufferPassword = Encoding.UTF8.GetBytes(Password);
+ //パスワードは128bitなので、16バイトまで切り詰めるか、あるいはそのサイズまで埋める処理
+ for (i = 0; i < bufferKey.Length; i++)
+ {
+ if (i < bufferPassword.Length)
+ {
+ //Cut down to 16bytes characters.
+ bufferKey[i] = bufferPassword[i];
+ }
+ else
+ {
+ bufferKey[i] = 0;
+ }
+ }
+ aes.Key = bufferKey;
+ // 初期化ベクトルの生成(IV)
+ byte[] iv = new byte[16];
+ RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
+ rng.GetNonZeroBytes(iv);
+ aes.IV = iv;
+
+ //Encryption interface.
+ ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
+
+ using (CryptoStream cse = new CryptoStream(outfs, encryptor, CryptoStreamMode.Write))
+ {
+ outfs.Write(iv, 0, 16); //IDファイル先頭に埋め込む
+ using (DeflateStream ds = new DeflateStream(cse, CompressionMode.Compress)) //圧縮
+ {
+ using (FileStream fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read))
+ {
+ while ((len = fs.Read(buffer, 0, 4096)) > 0)
+ {
+ ds.Write(buffer, 0, len);
+ }
+ }
+ }
+ }
+ }
+ }
+ //Encryption succeed.
+ return (true);
+}
+
+```
+
+次に復号する場合ですが、暗号化とまったく逆のことをすれば良いだけです。
+
+暗号化では、<strong>先に</strong>圧縮していたので、<strong>復号後</strong>に、解凍処理という順番となっています。
+
+
+```csharp
+
+private bool FileDecrypt(string FilePath, string Password)
+{
+ int i, len;
+ byte[] buffer = new byte[4096];
+
+ if (String.Compare(Path.GetExtension(FilePath), ".enc", true) != 0)
+ {
+ //The file are not encrypted file! Decryption failed
+ return (false); ;
+ }
+
+ //Output file path.
+ string OutFilePath = Path.Combine(Path.GetDirectoryName(FilePath), Path.GetFileNameWithoutExtension(FilePath)) + ".txt";
+
+ using (FileStream outfs = new FileStream(OutFilePath, FileMode.Create, FileAccess.Write))
+ {
+ using (FileStream fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read))
+ {
+ using (AesManaged aes = new AesManaged())
+ {
+ aes.BlockSize = 128; // BlockSize = 16bytes
+ aes.KeySize = 128; // KeySize = 16bytes
+ aes.Mode = CipherMode.CBC; // CBC mode
+ aes.Padding = PaddingMode.PKCS7; // Padding mode is "PKCS7".
+ byte[] bufferKey = new byte[16];
+ byte[] bufferPassword = Encoding.UTF8.GetBytes(Password);
+ //パスワードは128bitなので、16バイトまで切り詰めるか、あるいはそのサイズまで埋める処理
+ for (i = 0; i < bufferKey.Length; i++)
+ {
+ if (i < bufferPassword.Length)
+ {
+ //Cut down to 16bytes characters.
+ bufferKey[i] = bufferPassword[i];
+ }
+ else
+ {
+ bufferKey[i] = 0;
+ }
+ }
+ aes.Key = bufferKey;
+ // 初期化ベクトル
+ byte[] iv = new byte[16];
+ fs.Read(iv, 0, 16); //ファイル先頭から取得する
+ aes.IV = iv;
+
+ //Decryption interface.
+ ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
+
+ using (CryptoStream cse = new CryptoStream(fs, decryptor, CryptoStreamMode.Read))
+ {
+ using (DeflateStream ds = new DeflateStream(cse, CompressionMode.Decompress)) //解凍
+ {
+ while ((len = ds.Read(buffer, 0, 4096)) > 0)
+ {
+ outfs.Write(buffer, 0, len);
+ }
+ }
+ }
+ }
+ }
+ }
+ //Decryption succeed.
+ return (true);
+}
+
+```
+
+ここで注意していただきたいのは、一度暗号化してしまうと、ファイルを復号しても、ファイル情報が失われてしまうことです。
+
+ファイル情報とは、ファイル名、属性、タイムスタンプなどです。
+
+もしそうした情報も正常に戻せるようにするには、暗号化ファイルにそのデータも含めるなど、少々複雑な処理が必要になってくるでしょう。
+
+また、パスワードが合っているのか、間違っているのかの判定もしていません。
+
+このソースコードでは、誤ったパスワードで復号すると、さらに変なデータ配列に混ぜられてしまいます(おそらく元のデータに戻せなくなります)。
+
+これについても、復号が成功したか、ヘッダに何かしらの印を埋め込んでおいて、それでパスワードの成否を判定する必要があります。
+
+今回は、「ファイルを暗号化する」という部分に焦点を合わせて書きましたので、そういった「完全版」は、また別の機会に取り上げたいと思います。
+
+一応、GitHubにも今回のプロジェクトファイルごと上げました。↓
+https://github.com/hibara/FileEncryptSample
+
+Visual Studio C# 2010 Express、または、VS Express 2013 for Desktopでの動作、ビルドを確認済みです。
+
+ちなみに、MITライセンスです。ご自由にどうぞ。