概要
ファイルを暗号化する場合、ファイル全体をメモリに読み込んでから処理すると、巨大なファイル(動画やバックアップデータなど)でメモリ不足になる危険があります。 CryptoStream と Stream.CopyTo を組み合わせることで、ファイルの中身を少しずつ読み出しながら暗号化し、別のファイルへ書き出す「ストリーム処理」を行います。これにより、ファイルサイズに関わらず一定のメモリ使用量で安全に処理できます。
仕様(入出力)
- 入力: 暗号化したい元ファイルのストリーム。
- 出力: 暗号化されたデータを書き込む先ファイルのストリーム。
- 動作: 入力ストリームから読み出したデータをAESで暗号化し、出力ストリームへ順次書き込む。
基本の使い方
出力用のストリーム(FileStreamなど)を CryptoStream でラップ(包む)し、そこへ入力ストリームの内容をコピーします。
// 出力先ファイル
using var fsOut = File.Create("output.dat");
// 暗号化フィルタを通すストリーム
using var cs = new CryptoStream(fsOut, encryptor, CryptoStreamMode.Write);
// 入力元ファイルを開いて、暗号化ストリームへ流し込む
using var fsIn = File.OpenRead("input.txt");
fsIn.CopyTo(cs);
コード全文
入力コードの誤字やロジックを修正し、汎用的に使える StreamEncrypter クラスとして実装しました。 ここでは「自分自身のソースコード(Program.cs)」を暗号化して「crypted.dat」というファイルを作成する例です。
using System;
using System.IO;
using System.Security.Cryptography;
class Program
{
static void Main()
{
// 1. 暗号化クラスの初期化(KeyとIVは自動生成)
var encryptor = new StreamEncrypter();
string sourceFile = "Program.cs";
string destFile = "crypted.dat";
// ファイルが存在しない場合のダミー作成(動作確認用)
if (!File.Exists(sourceFile))
{
File.WriteAllText(sourceFile, "This is a test file content.");
}
Console.WriteLine($"暗号化開始: {sourceFile} -> {destFile}");
try
{
// 2. ストリームを開いて暗号化を実行
using (var inStream = File.Open(sourceFile, FileMode.Open, FileAccess.Read))
using (var outStream = File.Open(destFile, FileMode.Create, FileAccess.Write))
{
encryptor.Encrypt(inStream, outStream);
}
Console.WriteLine("暗号化が完了しました。");
// 3. 生成された鍵情報の表示(復号に必要)
Console.WriteLine($"Key: {Convert.ToBase64String(encryptor.Key)}");
Console.WriteLine($"IV : {Convert.ToBase64String(encryptor.IV)}");
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}
}
}
public class StreamEncrypter
{
public byte[] Key { get; private set; }
public byte[] IV { get; private set; }
// コンストラクタ:キーがあれば使い、なければ生成する
public StreamEncrypter(byte[] key = null, byte[] iv = null)
{
using var aes = Aes.Create();
Key = key ?? aes.Key;
IV = iv ?? aes.IV;
}
/// <summary>
/// 入力ストリームを暗号化して出力ストリームに書き込みます
/// </summary>
public void Encrypt(Stream inStream, Stream outStream)
{
using var aes = Aes.Create();
aes.Key = Key;
aes.IV = IV;
// 暗号化器を作成
using var encryptor = aes.CreateEncryptor();
// 出力ストリームをCryptoStreamでラップする (Mode.Write)
// これにより、cryptoStreamに書き込まれたデータは暗号化されてoutStreamに届く
using var cryptoStream = new CryptoStream(outStream, encryptor, CryptoStreamMode.Write);
// 入力ストリームの内容をすべてCryptoStreamにコピーする
// CopyToはバッファリングを自動で行うため、巨大ファイルでもメモリを圧迫しない
inStream.CopyTo(cryptoStream);
// usingブロックを抜ける際、CryptoStreamが閉じられ、パディング(末尾調整)が書き込まれる
}
}
カスタムポイント
- 非同期処理: Webアプリケーションなどで使用する場合は、
CopyToの代わりにawait inStream.CopyToAsync(cryptoStream)を使用することで、スレッドをブロックせずに巨大なファイルを処理できます。 - 進捗表示:
CopyToは完了するまで戻ってきません。進捗バーを出したい場合は、ループでReadとWriteを行い、読み込んだバイト数をカウントする独自の実装が必要です。
注意点
- CryptoStreamのクローズ: ファイルへの書き込みを確定させるには、
CryptoStreamを正しく閉じる(Disposeする)ことが必須です。これが漏れると、ファイルの末尾データが欠落したり、パディング不正で復号できなくなります。 - Key/IVの保存: ファイルを暗号化しても、そのKeyとIVを紛失したら二度と復元できません。これらはファイルとは別の安全な場所に保存するか、パスワードから生成するロジックを組み込む必要があります。
まとめ
ファイルを暗号化する際は、データを一度にメモリへ読み込むのではなく、ストリームを使用してパイプライン処理を行うのが鉄則です。CryptoStream を書き込みモードで作成し、そこへ Stream.CopyTo メソッドを使用してデータを流し込むことで、メモリ効率よく安全に暗号化ファイルを生成することが可能です。この際、CryptoStream が正しくクローズされないとデータが破損するため、必ず using ステートメントを使用してリソース管理を行ってください。
