連載
» 2017年09月06日 05時00分 公開

.NET TIPS:文字列を暗号化するには?[C#/VB]

AESアルゴリズムの.NET実装であるAesManagedクラスを利用して、文字列(やファイル)を対象に暗号化/復号を行う方法を取り上げる。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載目次

 現在では、アプリの設定ファイルに保存するための文字列を暗号化するといった目的には、AES方式を使うのが一般的だ。本稿では、その方法と、どの種類の暗号を選べばよいかの基準などを解説する。

 なお、AES方式は.NET Framework 3.5(Visual Studio 2008)から利用できるが、本稿のサンプルコードにはそれより新しい内容も含んでいる。サンプルコードをそのまま試すには、Visual Studio 2015以降が必要である。

AESアルゴリズムについては、次の記事が詳しい(本稿ではAESアルゴリズムの詳細については取り上げない)。
マスターIT/暗号技術:第3回 AES暗号化


AES方式で文字列を暗号化/復号するには?

 まず、AesManagedクラス(System.Security.Cryptography名前空間)にKeyとIV(Initialization Vector)をバイト配列で渡して、Encryptor(暗号化器)またはDecryptor(復号器)オブジェクトを得る。次に、そのEncryptor/Decryptorオブジェクトと入出力用のストリームを渡してCryptoStreamクラス(System.Security.Cryptography名前空間)のインスタンスを作る。CryptoStreamオブジェクトを通してバイトデータの書き込み/読み込みを行えば、暗号化/復号される。AesManagedクラスを使った暗号化/復号の大まかな流れを以下に示す。

AesManagedクラスを利用した暗号化の大まかな流れ
AesManagedクラスを利用した復号の大まかな流れ 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

以下のサンプルコードで使う名前空間の宣言(上:C#、下:VB)

 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

文字列をAES暗号化してBase64形式で返すメソッドの例(上:C#、下:VB)
処理の大まかな流れは、本文に書いた通りである。処理の前後に文字列とバイト配列の変換が入っているため、複雑なコードに見えるかもしれない。
ここではCryptoStreamクラスのWriteAsyncメソッド(非同期版)を使っているが、同期版のWriteメソッドもある。
なお、コメントに書いた出力例では入力の平文が60バイトで、出力の暗号データが64バイトになっている。出力データのサイズはBlockSize(後述)の倍数にそろえられるためだ。そのため、暗号化後のデータサイズから、暗号化前の平文の長さを(復号する前に)知ることはできない。

 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

Base64形式の暗号化された文字列をAES復号して返すメソッドの例(上:C#、下:VB)
暗号化とは逆の手順になる。先の暗号化のサンプルと同様に、ここではCryptoStreamクラスのReadAsyncメソッド(非同期版)を使っているが、同期版のReadメソッドもある。
ここではReadAsyncメソッドで読み取れるバイト数(の合計)が全く不明なものとしてwhile文を使った汎用的なコードにしている。
なお、本稿のこのサンプルコードでは、ファイルの暗号化にも応用が利くようにストリームを使った汎用的な書き方をしている。メモリ上のバイト配列の暗号化/復号だけならば、ストリームを使わず、Encrypter/DecrypterのTransformBlockメソッドとTransformFinalBlockメソッドを使うと少し簡潔なコードになる。

 最後に、上に示した暗号化/復号を行うメソッドの利用例を示す。

// 上記の暗号化/復号メソッドを使う例
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

上記の暗号化/復号メソッドを使う例(上:C#、下:VB)
KeyとIVという2つのバイト配列が暗号化/復号に必要である。その詳細は後述する。

暗号化方式を選ぶには?

 暗号化/復号を実装しようと考えたとき、まず悩むのはどの暗号化アルゴリズムを採用するかであろう。以下に、目的ごとに推奨される暗号化方式をMSDNから引用しておこう。なお、表形式に整えて、暗号化の種類を追記した。

目的 アルゴリズム 種類
データのプライバシー AES 共通鍵暗号アルゴリズム
(対称アルゴリズム)
データの整合性 HMACSHA256
HMACSHA512
ハッシュアルゴリズム
デジタル署名 ECDSA
RSA
公開鍵暗号アルゴリズム
(非対称アルゴリズム)
キー交換 ECDiffieHellman(ECDH)
RSA
公開鍵暗号アルゴリズム
(非対称アルゴリズム)
乱数生成 RNGCryptoServiceProvider (下記説明参照)
パスワードからのキー生成 Rfc2898DeriveBytes (下記説明参照)
目的ごとに推奨される暗号化アルゴリズム
MSDN「.NET Framework の暗号モデル」(最新版は「.NET Framework Cryptography Model」の記述に、暗号化の種類を追加した。
なお、最後の2項目は暗号化アルゴリズムではない。RNGCryptoServiceProviderクラスは、(Randomクラスよりも)厳密な乱数を生成させる。Rfc2898DeriveBytesクラスは、パスワード文字列からバイト配列のKeyを生成させるためのものだ。

 なお、.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とは?

 共通鍵暗号アルゴリズム(対称アルゴリズム)を使うには、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

AesManagedクラスの既定値と自動生成されたKey/IVの例(上:C#、下:VB)
既定値と異なる設定を使いたいときだけ(例えば暗号化モードにECBを使いたいとか、パディングはISO10126にしたいとか)、それらのプロパティを設定すればよい。通常は、前出のサンプルコードのように、ただインスタンス化して使えばよいだろう。
Key/IVは、AesManagedクラスが自動生成してくれたものを使うのが簡単である。簡易的な暗号化の場合は、前出のサンプルコードに示したように、Key/IVともハードコーディングしてしまうのも一案だ。

PCLで暗号化するには?

 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でコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?


「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。