グッドモーニング!ITOです。
つい先日、C# で AES をつかって文字列を暗号化する方法を書きました。
それのファイルバージョンです。文字列ではなくファイルを暗号化します。このときのファイルは画像でも、PDF でもエクセルファイルでも何でーもオッケーです。
復号もやります!
さっそく実装
やることは文字列のときとほとんど同じです。ただ、文字列のときと細かいところで違っている部分やファイルだからこそのポイントもあります。
実装の流れとポイント
今回も暗号化する関数と復号を行う関数を作ります。
暗号化、復号ともに以下の流れで行います。
- Aes オブジェクトを作成
- 暗号化器 or 復号器を作成
- 入力ファイルを開く
- 出力ファイルを開く
- 一定サイズずつ暗号化 or 復号
ここでポイント!今回は両関数ともに初期化ベクトルは引数にはしません。初期化ベクトルは管理せずに暗号化のたびにランダムに生成します。
なぜそれでいけるのかは文字列の暗号化の記事の下の方にある初期化ベクトルについての記述を見てください!
暗号化
まず暗号化を行う MyEncryptFile 関数をつくります。引数は「暗号化対象のファイルのパス」「暗号化キー」の2つ。
この関数を呼び出すと暗号化されたファイルが暗号化対象のファイルと同じ場所に保存されます。
MyEncryptFile 関数ソースコード
ソースコードです。
static public int MyEncryptFile(string filepath, byte[] key)
{
// Aesオブジェクトを作成
using (Aes aes = Aes.Create())
{
// Encryptorを用意
using (ICryptoTransform encryptor = aes.CreateEncryptor(key, aes.IV))
{
// 入力ファイルストリーム
using (FileStream in_stream = new FileStream(filepath, FileMode.Open, FileAccess.Read))
{
// 暗号化したデータを書き出すための出力ファイルストリーム
string out_filepath = filepath + "_ciphered.bin";
using (FileStream out_fs = new FileStream(out_filepath, FileMode.Create, FileAccess.Write))
{
// 一定サイズずつ暗号化して出力ファイルストリームに書き出す
using (CryptoStream cs = new CryptoStream(out_fs, encryptor, CryptoStreamMode.Write))
{
// 先頭16バイトは適当な値(いまはゼロ)で埋める
byte[] dummy = new byte[16];
cs.Write(dummy, 0, 16);
// 一定量ずつ暗号化して書き込み
byte[] buffer = new byte[8192];
int len = 0;
while ((len = in_stream.Read(buffer, 0, 8192)) > 0)
{
cs.Write(buffer, 0, len);
}
}
}
}
}
}
return 0;
}
MyEncryptFile 関数について
Aes オブジェクトを作って、その Aes オブジェクトをもとに暗号化器 (Encryptor) を作成。
このときに用いる暗号化キーは引数で受け取ったものですが、初期化ベクトルは Aes オブジェクトが自動生成したランダムな値を使います。
そして引数として受け取ったファイルパスのファイルを開きます。
どのような形式 (拡張子) のファイルであってもバイナリのファイルとして扱います。
暗号化したいデータが詰まっている入力ファイルストリームから一定サイズずつ読み出して暗号化していきます。
初期化ベクトルを管理しないので、まず初めに適当な16バイトのデータを暗号化して書き出しておきます。
byte[] dummy = new byte[16];
cs.Write(dummy, 0, 16);
これで「先頭16バイトにダミーデータを付与」を実現できます。
あとは一定サイズずつ入力ファイルストリームから読み出して処理していきます。このときの一定サイズは AES のブロックサイズの倍数 (16byte の倍数) にしましょう!
// 一定量ずつ暗号化して書き込み
byte[] buffer = new byte[8192];
int len = 0;
while ((len = in_stream.Read(buffer, 0, 8192)) > 0)
{
cs.Write(buffer, 0, len);
}
今回は 8192 バイトずつやっています。
暗号化されたデータを分かりやすくするために、保存するときのファイル名は元のファイル名に _ciphered.bin を付けたものにしています。
であれば
となります。
あと、関数の機能としては対象ファイルを暗号化して保存なので戻り値はなくてもオッケーです。私は癖で void ではなく、int にして return 0 にしてしまっています。
復号
さてさて続きまして復号。
おなじように復号を行う MyDecryptFile 関数をつくります。引数は「復号対象のファイルのパス」「暗号化キー」の2つ。
この関数を呼び出すと復号されたファイルが、復号対象のファイルと同じ場所に保存されます。
MyDecryptFile 関数ソースコード
まずはソースコード。
static public int MyDecryptFile(string filepath, byte[] key)
{
// Aesオブジェクトを作成
using (Aes aes = Aes.Create())
{
// 復号器を用意
using (var decryptor = aes.CreateDecryptor(key, aes.IV))
{
// 入力ファイルストリーム
using (FileStream in_fs = new FileStream(filepath, FileMode.Open, FileAccess.Read))
{
// 復号したデータを書き出すための出力ファイルストリーム
string out_filepath = filepath.Replace("_ciphered.bin", "");
using (FileStream out_fs = new FileStream(out_filepath, FileMode.Create, FileAccess.Write))
{
// 復号して一定サイズずつ読み出し、出力ファイルストリームに書き出す
using (CryptoStream cs = new CryptoStream(in_fs, decryptor, CryptoStreamMode.Read))
{
// 先頭16バイトは不要なのでまず復号して破棄
byte[] dummy = new byte[16];
cs.Read(dummy, 0, 16);
// 一定量ずつ処理していく
byte[] buffer = new byte[8192];
int len = 0;
while ((len = cs.Read(buffer, 0, 8192)) > 0)
{
out_fs.Write(buffer, 0, len);
}
}
}
}
}
}
return 0;
}
MyDecryptFile 関数について
暗号化と同じような流れです。Aes オブジェクトを作成からの復号器を作成!復号器を作るときに用いる暗号化キーは引数で受け取ったもの。もちろんですが、暗号化のときと同じものでないと正しく復号出来ません。
初期化ベクトルについても暗号化の時と一緒でランダムに生成されたものを利用。こちらに関しては暗号化のときに生成されたものと同じでなくても OK !
次に、復号したいファイルを開きます。入力ファイルストリームを用意です!
続いて、出力ファイルストリームを用意。このストリームには復号が完了したデータが入ってファイルとして書き出されます。
そして復号実行です。復号も一定バイトずつ復号して読み出して、出力ファイルストリームにどんどん追加していってます。
このとき先頭16バイトは不要なので、まず初めに16バイトだけ復号して放置 (破棄) します。
byte[] dummy = new byte[16];
cs.Read(dummy, 0, 16);
実際に使ってみます
先ほど作った二つの関数を使って正しくファイル暗号化アンド復号ができているのかを確認していきます。
せっかくなので簡単な UI (ユーザーインターフェース) も作ってみました。
ソースコード全体
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Security.Cryptography;
namespace ItoRoomForm
{
internal class MyAesFiles
{
static public int Run(string mode)
{
string filepath = null;
// ファイル選択ダイアログ関連
OpenFileDialog ofd = new OpenFileDialog();
ofd.Title = "ファイルを選択";
// 復号のときはバイナリファイルのみ選択可能に
if (mode == "decrypt")
{
ofd.Filter = "復号したいバイナリファイル(*_ciphered.bin)|*_ciphered.bin";
}
if (ofd.ShowDialog() == DialogResult.OK)
{
filepath = ofd.FileName;
}
else
{
return 0;
}
// 暗号化key生成するための文字列 (今回は256bitキーなので32文字)
const string key_text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
// 暗号化キーと初期化ベクトルをそれぞれバイト型配列に変換
byte[] key = Encoding.UTF8.GetBytes(key_text);
// 引数で暗号化するのか復号するのかを判断
if (mode == "encrypt")
{
MyEncryptFile(filepath, key);
}
else if (mode == "decrypt")
{
MyDecryptFile(filepath, key);
}
else
{
return -1;
}
return 0;
}
static public int MyEncryptFile(string filepath, byte[] key)
{
// Aesオブジェクトを作成
using (Aes aes = Aes.Create())
{
// Encryptorを用意
using (ICryptoTransform encryptor = aes.CreateEncryptor(key, aes.IV))
{
// 入力ファイルストリーム
using (FileStream in_stream = new FileStream(filepath, FileMode.Open, FileAccess.Read))
{
// 暗号化したデータを書き出すための出力ファイルストリーム
string out_filepath = filepath + "_ciphered.bin";
using (FileStream out_fs = new FileStream(out_filepath, FileMode.Create, FileAccess.Write))
{
// 復号して一定サイズずつ読み出し、出力ファイルストリームに書き出す
using (CryptoStream cs = new CryptoStream(out_fs, encryptor, CryptoStreamMode.Write))
{
// 先頭16バイトは適当な値(いまはゼロ)で埋める
byte[] dummy = new byte[16];
cs.Write(dummy, 0, 16);
// 一定量ずつ暗号化して書き込み
byte[] buffer = new byte[8192];
int len = 0;
while((len = in_stream.Read(buffer, 0, 8192)) > 0)
{
cs.Write(buffer, 0, len);
}
}
}
}
}
}
return 0;
}
static public int MyDecryptFile(string filepath, byte[] key)
{
// Aesオブジェクトを作成
using (Aes aes = Aes.Create())
{
// 復号器を用意
using (var decryptor = aes.CreateDecryptor(key, aes.IV))
{
// 入力ファイルストリーム
using (FileStream in_fs = new FileStream(filepath, FileMode.Open, FileAccess.Read))
{
// 復号したデータを書き出すための出力ファイルストリーム
string out_filepath = filepath.Replace("_ciphered.bin", "");
using (FileStream out_fs = new FileStream(out_filepath, FileMode.Create, FileAccess.Write))
{
// 復号して一定サイズずつ読み出し、出力ファイルストリームに書き出す
using (CryptoStream cs = new CryptoStream(in_fs, decryptor, CryptoStreamMode.Read))
{
// 先頭16バイトは不要なのでまず復号して破棄
byte[] dummy = new byte[16];
cs.Read(dummy, 0, 16);
// 一定量ずつ処理していく
byte[] buffer = new byte[8192];
int len = 0;
while ((len = cs.Read(buffer, 0, 8192)) > 0)
{
out_fs.Write(buffer, 0, len);
}
}
}
}
}
}
return 0;
}
}
}
ソースコードです。上で作った2つの関数も含めています。
Run 関数に暗号化するのか復号するのかを判断する引数を渡して呼び出すと、さっき作った MyEncrypt 関数や MyDecrypt 関数を使ってファイルを暗号化します。
UI を作ったので各ボタンから呼び出すコードもいります!
private void buttonEncrypt_Click(object sender, EventArgs e)
{
ItoRoomForm.MyAesFiles.Run("encrypt");
}
private void buttonDecrypt_Click(object sender, EventArgs e)
{
ItoRoomForm.MyAesFiles.Run("decrypt");
}
それそれボタンが押されたら、したいことに合わせて Run 関数に引数を与えて呼び出しています。
暗号化、復号ともにファイル選択も UI でやりました。その時のプチポイントとして、復号するファイルを選択するダイアログではファイル名が「_ciphered.bin」で終わっているものしか選択できないようにしています。
ofd.Filter = "復号したいバイナリファイル(*_ciphered.bin)|*_ciphered.bin";
この部分です。
暗号化と復号をやってみる
ちゃんとできてるか確認するためにテキストファイルと画像ファイルを試します。
その1 テキストファイル
元のテキストファイルはこれ↓です。英語、ひらがな、カタカナ、漢字と数多の文字が散りばめられたファイルです。ファイル名は test.txt にしてます。
this is a pen.
abcdefghijklmnopqrstuvwxyz
嶺上開花
今日はコレ和了ってもいいですよね
そんなオカルトありませんっ!!
カン、もいっこカン、もいっこカン!!
立直一発門前清自摸和平和純全帯ヤオ九三色同順一盃口ドラ三
実行して暗号化ボタンを押すとちゃんと暗号化済みファイル (test.txt_ciphered.bin) が生成されております!
無理やりメモ帳で開いてみると、、、
ちゃんと暗号化できてるっぽいです(笑) 文字化けして意味不明な記号の羅列になってます。
そして緊張の復号。結果は。。。
問題なく復号できている!
その2 画像ファイル
続いて画像ファイルも暗号化&復号していきます。
まずはこちらの画像ファイルを暗号化します。
ここに載せてる画像は本サイトにアップロードするために画像サイズを小さくしたものです。
実際は4メガバイトぐらいあります。
暗号化するとフォトアプリでは開けません。拡張子が「.bin」になっていますし、「.jpg」 に変えても開けませんのでちゃんと暗号化出来てるっぽい!
以下暗号化前後でのプロパティの比較です。
わずかにファイルサイズが増えています。ほんの微量です。
そして復号した結果は、、、
無事に復号できてフォトアプリでも開くことができました。プロパティを見てファイルサイズを確認すると暗号化前の元ファイルと完全に一致してます。
さいごに
とりあえずファイルの暗号化はできそうです!
今回は単純にファイル暗号化するところにのみ注目してまとメモを書きました。
実際に製品に使ったりする場合は、さらに安全性を高めるために文字列を直で暗号化キーにするのではなく、人間が決めた文字列をもとにランダムに生成した値から暗号化キーを作成。とか、暗号化前にファイル圧縮する。とかいろいろ考えることはありそうです。
また、ファイルを開いたりしてるのでファイルの存在確認とかもやらないといけないかなと思います。
暗号化前の元データを暗号化と同時に消す運用にする場合は、正しく復号できているのかをテストデータでしっかり検証しないと、最悪ファイルを元に戻せなくなるかもしれないので注意が必要です!
ともあれ、stream がいろいろ便利すぎました!!
コメント