AESアルゴリズムの.NET実装であるAesManagedクラスを利用して、文字列(やファイル)を対象に暗号化/復号を行う方法を取り上げる。
現在では、アプリの設定ファイルに保存するための文字列を暗号化するといった目的には、AES方式を使うのが一般的だ。本稿では、その方法と、どの種類の暗号を選べばよいかの基準などを解説する。
なお、AES方式は.NET Framework 3.5(Visual Studio 2008)から利用できるが、本稿のサンプルコードにはそれより新しい内容も含んでいる。サンプルコードをそのまま試すには、Visual Studio 2015以降が必要である。
AESアルゴリズムについては、次の記事が詳しい(本稿ではAESアルゴリズムの詳細については取り上げない)。
マスターIT/暗号技術:第3回 AES暗号化
まず、AesManagedクラス(System.Security.Cryptography名前空間)にKeyとIV(Initialization Vector)をバイト配列で渡して、Encryptor(暗号化器)またはDecryptor(復号器)オブジェクトを得る。次に、そのEncryptor/Decryptorオブジェクトと入出力用のストリームを渡してCryptoStreamクラス(System.Security.Cryptography名前空間)のインスタンスを作る。CryptoStreamオブジェクトを通してバイトデータの書き込み/読み込みを行えば、暗号化/復号される。AesManagedクラスを使った暗号化/復号の大まかな流れを以下に示す。
ただし、以上の手順では入出力がバイト配列であるので、前後に文字列との変換処理が必要だ。それを含めて、暗号化/復号とそれを利用するメソッドの例を次からのコードに示す。まず、暗号化/復号に必要な名前空間を示す。
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
Imports System.Console
Imports System.IO
Imports System.Security.Cryptography
Imports System.Text
AesManagedクラスを使って暗号化を行うコードは次のようになる。その大きな流れはこれまでに見たものと同じだが、文字列とバイト配列の変換、入出力の設定、戻り値の作成などがあるので、実際のコードは少々複雑なものになっている。
// 入力文字列をAES暗号化してBase64形式で返すメソッド
static async Task<string> EncryptToBase64(string plainText, byte[] key, byte[] iv)
{
// 入力文字列をバイト型配列に変換
byte[] src = Encoding.Unicode.GetBytes(plainText);
WriteLine($"平文のバイト型配列の長さ={src.Length}");
// 出力例:平文のバイト型配列の長さ=60
// Encryptor(暗号化器)を用意する
using (var am = new AesManaged())
using (var encryptor = am.CreateEncryptor(key, iv))
// ファイルを入力とするなら、ここでファイルを開く
// using (FileStream inStream = new FileStream(FilePath, ……省略……
// 出力ストリームを用意する
using (var outStream = new MemoryStream())
{
// 暗号化して書き出す
using (var cs = new CryptoStream(outStream, encryptor, CryptoStreamMode.Write))
{
await cs.WriteAsync(src, 0, src.Length);
// 入力がファイルなら、inStreamから一定量ずつバイトバッファーに読み込んで
// cse.Writeで書き込む処理を繰り返す(復号のサンプルコードを参照)
}
// 出力がファイルなら、以上で完了
// Base64文字列に変換して返す
byte[] result = outStream.ToArray();
WriteLine($"暗号のバイト型配列の長さ={result.Length}");
// 出力例:暗号のバイト型配列の長さ=64
// 出力サイズはBlockSize(既定値16バイト)の倍数になる
return Convert.ToBase64String(result);
}
}
' 入力文字列をAES暗号化してBase64形式で返すメソッド
Async Function EncryptToBase64(plainText As String, key As Byte(), iv As Byte()) As Task(Of String)
' 入力文字列をバイト型配列に変換
Dim src() As Byte = Encoding.Unicode.GetBytes(plainText)
WriteLine($"平文のバイト型配列の長さ={src.Length}")
' 出力例:平文のバイト型配列の長さ=60
' Encryptor(暗号化器)を用意する
Using am = New AesManaged()
Using encryptor = am.CreateEncryptor(key, iv)
' ファイルを入力とするなら、ここでファイルを開く
'Using inStream = New FileStream(FilePath, ……省略……
' 出力ストリームを用意する
Using outStream = New MemoryStream()
' 暗号化して書き出す
Using cs = New CryptoStream(outStream, encryptor, CryptoStreamMode.Write)
Await cs.WriteAsync(src, 0, src.Length)
' 入力がファイルなら、inStreamから一定量ずつバイトバッファーに読み込んで
' cse.Writeで書き込む処理を繰り返す(復号のサンプルコードを参照)
End Using
' 出力がファイルなら、以上で完了
' Base64文字列に変換して返す
Dim result() As Byte = outStream.ToArray()
WriteLine($"暗号のバイト型配列の長さ={result.Length}")
' 出力例:暗号のバイト型配列の長さ=64
' 出力サイズはBlockSize(既定値16バイト)の倍数になる
Return Convert.ToBase64String(result)
End Using
End Using
End Using
End Function
AesManagedクラスを使って復号を行うコードは次のようになる。暗号化を行うコードと同様、大きな流れは既に説明した通りだが、実際のコードは複雑なものになっている。
// 暗号化されたBase64形式の入力文字列をAES復号して平文の文字列を返すメソッド
static async Task<string> DecryptFromBase64(string base64Text, byte[] key, byte[] iv)
{
// Base64文字列をバイト型配列に変換
byte[] src = Convert.FromBase64String(base64Text);
// Decryptor(復号器)を用意する
using (var am = new AesManaged())
using (var decryptor = am.CreateDecryptor(key, iv))
// 入力ストリームを開く
using(var inStream = new MemoryStream(src, false))
// 出力ストリームを用意する
using (var outStream = new MemoryStream())
{
// 復号して一定量ずつ読み出し、それを出力ストリームに書き出す
using (var cs = new CryptoStream(inStream, decryptor, CryptoStreamMode.Read))
{
byte[] buffer = new byte[4096]; // バッファーサイズはBlockSizeの倍数にする
int len = 0;
while ((len = await cs.ReadAsync(buffer, 0, 4096)) > 0)
outStream.Write(buffer, 0, len);
}
// 出力がファイルなら、以上で完了
// 文字列に変換して返す
byte[] result = outStream.ToArray();
return Encoding.Unicode.GetString(result);
}
}
' 暗号化されたBase64形式の入力文字列をAES復号して平文の文字列を返すメソッド
Async Function DecryptFromBase64(base64Text As String, key As Byte(), iv As Byte()) As Task(Of String)
' Base64文字列をバイト型配列に変換
Dim src() As Byte = Convert.FromBase64String(base64Text)
' Decryptor(復号器)を用意する
Using am = New AesManaged()
Using decryptor = am.CreateDecryptor(key, iv)
' 入力ストリームを開く
Using inStream = New MemoryStream(src, False)
' 出力ストリームを用意する
Using outStream = New MemoryStream()
' 復号して一定量ずつ読み出し、それを出力ストリームに書き出す
Using cs = New CryptoStream(inStream, decryptor, CryptoStreamMode.Read)
Dim buffer(4095) As Byte
Dim len As Integer = Await cs.ReadAsync(buffer, 0, 4096)
While (len > 0)
outStream.Write(buffer, 0, len)
len = Await cs.ReadAsync(buffer, 0, 4096)
End While
End Using
' 出力がファイルなら、以上で完了
' 文字列に変換して返す
Dim result() As Byte = outStream.ToArray()
Return Encoding.Unicode.GetString(result)
End Using
End Using
End Using
End Using
End Function
最後に、上に示した暗号化/復号を行うメソッドの利用例を示す。
// 上記の暗号化/復号メソッドを使う例
static async void CryptoSample()
{
// KeyとIV(一例)
byte[] key
= {
0xED, 0x0B, 0x56, 0xAF, 0x61, 0xA2, 0x71, 0x39,
0xE0, 0x4B, 0xDC, 0xC9, 0x23, 0x69, 0x8C, 0xBD,
0xB9, 0x86, 0x98, 0x28, 0xC8, 0x3E, 0x62, 0xA7,
0xFA, 0x17, 0xC1, 0x33, 0x64, 0xBF, 0x96, 0x24
};
byte[] iv
= {
0x6F, 0xDF, 0x98, 0x00, 0x67, 0x36, 0x7D, 0x3B,
0xFF, 0xC9, 0x3B, 0x79, 0x4D, 0xD4, 0x81, 0x72
};
// 暗号化したい平文
string plainText = "業務アプリInsiderは業務アプリ開発者のためのサイトです";
// 暗号化
string encrypted = await EncryptToBase64(plainText, key, iv);
WriteLine($"'{encrypted}'");
// 出力:'CUiUxGqvU1az7THf81lpEmijZrIn……省略……9Vr38ouENvfcaV0en5uw=='
// 復号
string decrypted = await DecryptFromBase64(encrypted, key, iv);
WriteLine($"'{decrypted}'");
// 出力:'業務アプリInsiderは業務アプリ開発者のためのサイトです'
}
' 上記の暗号化/復号メソッドを使う例
Async Sub CryptoSample()
' KeyとIV(一例)
Dim key() As Byte _
= {
&HED, &HB, &H56, &HAF, &H61, &HA2, &H71, &H39,
&HE0, &H4B, &HDC, &HC9, &H23, &H69, &H8C, &HBD,
&HB9, &H86, &H98, &H28, &HC8, &H3E, &H62, &HA7,
&HFA, &H17, &HC1, &H33, &H64, &HBF, &H96, &H24
}
Dim iv() As Byte _
= {
&H6F, &HDF, &H98, &H0, &H67, &H36, &H7D, &H3B,
&HFF, &HC9, &H3B, &H79, &H4D, &HD4, &H81, &H72
}
' 暗号化したい平文
Dim plainText As String = "業務アプリInsiderは業務アプリ開発者のためのサイトです"
' 暗号化
Dim encrypted As String = Await EncryptToBase64(plainText, key, iv)
WriteLine($"'{encrypted}'")
' 出力:'CUiUxGqvU1az7THf81lpEmijZrIn……省略……9Vr38ouENvfcaV0en5uw=='
' 復号
Dim decrypted As String = Await DecryptFromBase64(encrypted, key, iv)
WriteLine($"'{decrypted}'")
' 出力:'業務アプリInsiderは業務アプリ開発者のためのサイトです'
End Sub
暗号化/復号を実装しようと考えたとき、まず悩むのはどの暗号化アルゴリズムを採用するかであろう。以下に、目的ごとに推奨される暗号化方式をMSDNから引用しておこう。なお、表形式に整えて、暗号化の種類を追記した。
目的 | アルゴリズム | 種類 |
---|---|---|
データのプライバシー | AES | 共通鍵暗号アルゴリズム (対称アルゴリズム) |
データの整合性 | HMACSHA256 HMACSHA512 |
ハッシュアルゴリズム |
デジタル署名 | ECDSA RSA |
公開鍵暗号アルゴリズム (非対称アルゴリズム) |
キー交換 | ECDiffieHellman(ECDH) RSA |
公開鍵暗号アルゴリズム (非対称アルゴリズム) |
乱数生成 | RNGCryptoServiceProvider | (下記説明参照) |
パスワードからのキー生成 | Rfc2898DeriveBytes | (下記説明参照) |
なお、.NET Frameworkには、共通鍵暗号アルゴリズム(対称アルゴリズム)の実装として、AESの他に、DESアルゴリズムのDESCryptoServiceProviderクラスやトリプルDESアルゴリズムのTripleDESCryptoServiceProviderクラス、RC2アルゴリズムのRC2CryptoServiceProviderクラスなどもあるが、暗号強度の点から現在では使うことはないだろう。
.NET Frameworkには、Rijndael(ラインダール)アルゴリズムのRijndaelManagedクラスもある。実は、AESはRijndaelのサブセットである。BlockSizeとしてAES(16バイト)よりも大きな値を使いたい場合などで、Rijndaelを使うことになるだろう。
また、.NET FrameworkのAESの実装は2種類ある。本稿のサンプルコードで使っているAesManagedクラスは、マネージコードで書かれている。以前からあるAesCryptoServiceProviderクラスは、AESのWindows暗号化API(CAPI)実装のラッパーだが、すでに開発は止まっているのでよほどの理由がない限りは使わない方がよいだろう。
共通鍵暗号アルゴリズム(対称アルゴリズム)を使うには、KeyとIV(Initialization Vector:初期化ベクター)が必要だ。Keyは秘密にしておく。IVは公開してしまっても大丈夫だが、暗号の強度を保つためには暗号化するごとに変更すべきものである(MSDN「暗号化と復号化のためのキーの生成」を参照)。
IVは知られても構わないのでその保存や伝達は、暗号化後のデータの先頭に付加したり、アプリの設定ファイルなどではIVを保持しておく項目を作ったりすればよい。
Keyはどのようにして秘匿すればよいだろうか? クライアント=サーバ型のシステムでは、サーバ側でKeyを生成して、公開鍵暗号アルゴリズム(非対称アルゴリズム)のRSAやECDHを使ってクライアントに渡す(公開鍵暗号アルゴリズムでデータの暗号化もしてしまえばよいのではと思うかもしれないが、速度の点からそれは避けるべきである)。スタンドアロンの場合は、せいぜいプログラム自体を難読化するくらいであろう。
Keyはどのようにして生成すればよいだろうか? ユーザーが入力したパスワードを基にして、Rfc2898DeriveBytesクラスに生成させる方法が1つある。もう1つは、Key/IVともにAesManagedクラスに自動生成してもらう方法だ。
AesManagedクラスのインスタンスを作るたびに新しいKey/IVが生成される(次のコード)。また、同じインスタンスの下でも、GenerateKeyメソッド/GenerateIVメソッドを呼び出すことで新しいKey/IVを生成できる。併せて、AesManagedクラスのその他の既定値も確認するコードも示した。
// 冒頭のサンプルコードに示したusingが必要
// バイト配列を表示するメソッド
static string ByteArrayToHex(byte[] array)
=> $"{array.Length} bytes, {string.Join(" ", array.Select(b => b.ToString("X2")).ToArray())}";
……省略……
using (var am = new AesManaged())
{
// 既定値の確認
WriteLine($"BlockSize = {am.BlockSize / 8} bytes");
// 出力:BlockSize = 16 bytes
WriteLine($"KeySize = {am.KeySize / 8} bytes");
// 出力:KeySize = 32 bytes
WriteLine($"Mode = {am.Mode.GetType().Name}.{am.Mode}");
// 出力:Mode = CipherMode.CBC
WriteLine($"Padding = {am.Padding.GetType().Name}.{am.Padding}");
// 出力:Padding = PaddingMode.PKCS7
// 自動生成されたKeyとIV(Initialization Vector)
WriteLine($"Key: {ByteArrayToHex(am.Key)}");
// 出力例:Key: 32 bytes, ED 0B 56 AF 61 A2 71 39 E0 4B DC C9 23 69 8C BD B9 86 98 28 C8 3E 62 A7 FA 17 C1 33 64 BF 96 24
WriteLine($"IV: {ByteArrayToHex(am.IV)}");
// 出力例:IV: 16 bytes, 6F DF 98 00 67 36 7D 3B FF C9 3B 79 4D D4 81 72
}
' 冒頭のサンプルコードに示したusingが必要
' バイト配列を表示するメソッド
Function ByteArrayToHex(array As Byte()) As String
Return $"{array.Length} bytes, {String.Join(" ", array.Select(Function(b) b.ToString("X2")).ToArray())}"
End Function
……省略……
Using am = New AesManaged()
' 既定値の確認
WriteLine($"BlockSize = {am.BlockSize / 8} bytes")
' 出力:BlockSize = 16 bytes
WriteLine($"KeySize = {am.KeySize / 8} bytes")
' 出力:KeySize = 32 bytes
WriteLine($"Mode = {am.Mode.GetType().Name}.{am.Mode}")
' 出力:Mode = CipherMode.CBC
WriteLine($"Padding = {am.Padding.GetType().Name}.{am.Padding}")
' 出力:Padding = PaddingMode.PKCS7
' 自動生成されたKeyとIV(Initialization Vector)
WriteLine($"Key: {ByteArrayToHex(am.Key)}")
' 出力例:Key: 32 bytes, ED 0B 56 AF 61 A2 71 39 E0 4B DC C9 23 69 8C BD B9 86 98 28 C8 3E 62 A7 FA 17 C1 33 64 BF 96 24
WriteLine($"IV: {ByteArrayToHex(am.IV)}")
' 出力例:IV: 16 bytes, 6F DF 98 00 67 36 7D 3B FF C9 3B 79 4D D4 81 72
End Using
PCL(ポータブルクラスライブラリ)ではSystem.Security.Cryptography名前空間が使えないため、Xamarin.Formsの開発などで暗号化に困ってしまうことがある。そんなときはオープンソースソフトウェア(OSS)を探してみてほしい。筆者のお勧めはPCLCryptoだ。
文字列などのデータの暗号化/復号は、現在ではAesManagedを使うと覚えておけばよいだろう。どの共通鍵暗号方式を使うにせよKey/IVの生成と管理は面倒なものだが、そこは割り切って簡易的に(暗号強度は犠牲にして)ハードコーディングしてしまうこともある。
利用可能バージョン:.NET Framework 3.5以降(Visual Studio 2008以降)
カテゴリ:クラスライブラリ 処理対象:暗号
使用ライブラリ: AesManagedクラス(System.Security.Cryptography名前空間)
使用ライブラリ: CryptoStreamクラス(System.Security.Cryptography名前空間)
関連TIPS:進行状況を表示しながらハッシュ値を計算するには?[C#、VB]
関連TIPS:文字列をBase64でエンコード/デコードするには?[C#、VB
関連TIPS:バイト列を文字列に変換するには?
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]
関連TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?
Copyright© Digital Advantage Corp. All Rights Reserved.