こんにちは!ITO です。
最近マジで暑いです。。。1年ほど前に「暑さをパスワードロック」とか意味不明なことを言って、エクセルマクロでパスワード画面を作成するまとめもを書きました。
なので今年も暑さをロックです。今年は暗号化してロックしてしまいましょう!
以前とあるアプリケーションを作成しているときに、アプリケーション上で扱う数値とかテキストとかのデータを PC 内にそのまま保存していてはダメだなーと思いました。
そこで暗号化して保存しておけばひとまずセキュリティアップということで、テキストを暗号化する方法と、その暗号文を復号する方法のメモです。
アプリケーションを C# (シーシャープ) で開発していたので暗号化、復号ともに C# での方法を載せています。
暗号化には AES
暗号化する方法はたくさんありますがその中でも特に強力と言われている AES (Advanced Encryption Standard) を使います。AES はその英語の通り標準です。中身というか採用されているアルゴリズムとしては、アメリカの NIST というところが公募して決まった Rijndael (ラインダール) という暗号化の方法です。
ただし純粋なラインダールと AES では、ブロックサイズが AES では 128bit なのに対して、純粋ラインダールでは 128bit から 256bit の間の32の倍数などなどなどの微妙な違いがあるようです。ブロックサイズについては後でもうちょっと詳しく書きます。
ここでは AES とラインダールは同じ意味として話を進めます!
そもそも暗号化とは
暗号化と聞けば言葉の響きからなんとなく意味が分かるかもしれませんが、データを他の人に分からないように変換することです。
例えば、単純なやり方でアルファベットを辞書順に2つ後ろにずらすという暗号を考えます。
「ITO room」という文字列なら「KVQ tqqo」となります。
少し分かりにくくはなりましたが、頑張れば元の文字列を突き止めることはできなくもない気もしないではありません(笑)
ちなみにこのアルファベットをずらす暗号化の方法をシーザー暗号というらしいです。
ラインダールで暗号化
では、先ほどの「ITO room」ラインダール (AES) で暗号化すると、、、
「QVR7yKPF6W5tSQZn7I8ONw==」
かなり分かりにくくなりました。分かりにくいどころか元の文字列がなんなのかまったく想像出来ません。なんか長さまで変わってます!(長さが変わっているのは暗号文をテキストにするときに Base64 化したからでもあります)
シーザー暗号では単純にずらすだけでしたが、ラインダールではずらしたり、置き換えたり、行列の計算をしたりなどの処理を何度も何度も繰り返すことで上記のような暗号文が生まれるらしい。すごい!
C#で実装 (暗号化)
AES での暗号化を C# で実装します。実装といってもラインダールのアルゴリズムを見て一から全部コードを書く必要はなく、C# には AES に関する便利な標準ライブラリが用意されています。
今回は標準ライブラリを利用して「暗号化したい平文」「初期化ベクトル」「暗号化キー」の3つの引数を渡せば暗号化された文字列が返ってくる MyEncrypt 関数を作ります。
利用する名前空間
今回 AES 暗号化をするにあたって暗号化、復号ともに以下の名前空間をユージングしています。
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Text;
using System.Security.Cryptography;
MyEncrypt 関数のソースコード
// 暗号化する関数
// 引数は平文、暗号化キー、初期化ベクトル
static public string MyEncrypt(string plain_text, byte[] key, byte[] iv)
{
// 暗号化した文字列格納用
string encrypted_str;
// Aesオブジェクトを作成
using (Aes aes = Aes.Create())
{
// Encryptorを作成
using (ICryptoTransform encryptor = aes.CreateEncryptor(key, iv))
{
// 出力ストリームを作成
using (MemoryStream out_stream = new MemoryStream())
{
// 暗号化して書き出す
using (CryptoStream cs = new CryptoStream(out_stream, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter sw = new StreamWriter(cs))
{
// 出力ストリームに書き出し
sw.Write(plain_text);
}
}
// Base64文字列にする
byte[] result = out_stream.ToArray();
encrypted_str = Convert.ToBase64String(result);
}
}
}
return encrypted_str;
}
Aes オブジェクトを作成し、その Aes オブジェクトから暗号化器を作って、出力用のストリームを作って、、、としているのでちょっとごにょごにょしています。
この関数で受け取る平文はテキスト (string) 、暗号化キーと初期化ベクトルはともにバイト型配列 (byte []) です。暗号化キーと初期化ベクトルについては後で詳しく述べます。
Base64 にしてますが・・・
最後に Base64 文字列にしているのですが、これは文字列として見やすく (扱いやすく) するためのもので暗号化プロセスにおいて必須ではないです。
暗号化されたデータはバイト型のデータとして出力ストリームに入ってるので、そのままバイト型データとして保存しておいても OK です。
ちなみに、さきほどラインダールで「ITO room」を暗号化すると「QVR7yKPF6W5tSQZn7I8ONw==」となったと紹介しました。これも最後に Base64 文字列に変換したことでこうなっています。
Base64 化する前のバイトデータは「41-54-7B-C8-A3-C5-E9-6E-6D-49-06-67-EC-8F-0E-37」となっており単純に文字列にすると印字できない文字もあります。
テキストとして扱うにはちょっと不便なので Base64 文字列に変換してます。
C#で実装 (復号)
さて暗号化が完了したので次は復号です。
さっきの暗号化と似ています。引数として、「復号したい暗号化された文字列 (Base64)」「暗号化キー」「初期化ベクトル」を渡すと元の平文を返す MyDecrypt 関数を作ります。
ここで注意が。暗号化キーと初期化ベクトルは暗号するときに使った値と同じ値を与えてください!でないと正しく復号出来ません。
暗号化キーと呼ばれているくらいなので、ロックするときと違う鍵で解除できたらダメですからね。
MyDecrypt 関数のソースコード
// 復号する関数
// 引数は暗号化されたテキスト(Base64)、暗号化キー、初期化ベクトル
static public string MyDecrypt(string base64_text, byte[] key, byte[] iv)
{
string plain_text;
// Base64文字列をバイト型配列に変換
byte[] cipher = Convert.FromBase64String(base64_text);
// AESオブジェクトを作成
using (Aes aes = Aes.Create())
{
// 復号器を作成
using (ICryptoTransform decryptor = aes.CreateDecryptor(key, iv))
{
// 入力ストリームを作成
using (MemoryStream in_stream = new MemoryStream(cipher))
{
// 一気に復号
using (CryptoStream cs = new CryptoStream(in_stream, decryptor, CryptoStreamMode.Read))
{
using (StreamReader sr = new StreamReader(cs))
{
plain_text = sr.ReadToEnd();
}
}
}
}
}
return plain_text;
}
まず、第一引数で受け取った暗号文は Base64 文字列なので、これをバイト型配列に変換しています。
byte[] cipher = Convert.FromBase64String(base64_text);
この部分です。
あとは暗号化のときと流れはだいたい同じです。Aes オブジェクトを作って、復号器を作って、復号用のストリームを作って、、、としてます。
実際に使ってみる
作った暗号化する関数 (MyEncrypt 関数) と復号を行う関数 (MyDecrypt 関数) を実際に使って正しく暗号化&復号が行われているのか確かめたいと思います!
ソースコード全体
まずはソースコード全体。上で作った2つの関数も含まれています。
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Text;
using System.Security.Cryptography;
namespace ITO_room
{
class MyAes
{
static void Main()
{
// 暗号化key生成するための文字列 (今回は256bitキーなので32文字)
const string key_text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
// 初期化ベクトルを生成するための文字列 (128bit = 16文字)
const string iv_text = "bbbbbbbbbbbbbbbb";
// 暗号化キーと初期化ベクトルをそれぞれバイト型配列に変換
byte[] key = Encoding.UTF8.GetBytes(key_text);
byte[] iv = Encoding.UTF8.GetBytes(iv_text);
// 暗号化したい文字列
const string text = "ITO room";
string encrypted_text = MyEncrypt(text, key, iv);
string round_trip_text = MyDecrypt(encrypted_text, key, iv);
Console.WriteLine($"元の文字列: {text}");
Console.WriteLine($"暗号化された文字列(base64): {encrypted_text}");
Console.WriteLine($"復号した文字列: {round_trip_text}");
}
// 暗号化する関数
// 引数は平文、暗号化キー、初期化ベクトル
static public string MyEncrypt(string plain_text, byte[] key, byte[] iv)
{
// 暗号化した文字列格納用
string encrypted_str;
// Aesオブジェクトを作成
using (Aes aes = Aes.Create())
{
// Encryptorを作成
using (ICryptoTransform encryptor = aes.CreateEncryptor(key, iv))
{
// 出力ストリームを作成
using (MemoryStream out_stream = new MemoryStream())
{
// 暗号化して書き出す
using (CryptoStream cs = new CryptoStream(out_stream, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter sw = new StreamWriter(cs))
{
// 出力ストリームに書き出し
sw.Write(plain_text);
}
}
// Base64文字列にする
byte[] result = out_stream.ToArray();
encrypted_str = Convert.ToBase64String(result);
}
}
}
return encrypted_str;
}
// 復号する関数
// 引数は暗号化されたテキスト(Base64)、暗号化キー、初期化ベクトル
static public string MyDecrypt(string base64_text, byte[] key, byte[] iv)
{
string plain_text;
// Base64文字列をバイト型配列に変換
byte[] cipher = Convert.FromBase64String(base64_text);
// AESオブジェクトを作成
using (Aes aes = Aes.Create())
{
// 復号器を作成
using (ICryptoTransform decryptor = aes.CreateDecryptor(key, iv))
{
// 復号用ストリームを作成
using (MemoryStream in_stream = new MemoryStream(cipher))
{
// 一気に復号
using (CryptoStream cs = new CryptoStream(in_stream, decryptor, CryptoStreamMode.Read))
{
using (StreamReader sr = new StreamReader(cs))
{
plain_text = sr.ReadToEnd();
}
}
}
}
}
return plain_text;
}
}
}
これを実行した結果が以下になります。コマンドプロンプトに出力されたものをそのまま表示してます。
元の文字列: ITO room
暗号化された文字列(base64): QVR7yKPF6W5tSQZn7I8ONw==
復号した文字列: ITO room
暗号化キーと初期化ベクトル
やーーっと、暗号化キーと初期化ベクトルのお話です。
暗号化キー
暗号化キーはその名の通り暗号化するための鍵です。パスワード的なものです。
AES ではこの暗号化キーの長さを 128bit 、192bit 、256bit から選べます。長い方 (256bit) が安全らしい。
同じ平文であってもこの暗号化キーが変われば暗号文も変わります。
当然ながら暗号化キーが間違っていれば正しく復号することはできません。
C# で暗号化器作成時に渡す暗号化キーは byte 型配列で、今回は 256bit のキーを使うので 32byte のデータがいります。
上記のソースコードでは人間が管理しやすいようにまずは、32文字 (32バイト=256ビット) の文字列を用意して、GetBytes 関数でバイト型配列に変換しています。
直でバイト型配列の値を指定して、32バイトの配列を初期化しても OK です。
上記ソースコードでは a を32個並べた文字列を使ったので、
const string key_text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
byte[] key = Encoding.UTF8.GetBytes(key_text);
ではなくて、
byte[] key = {0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61,
0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61,
0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61,
0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61};
としてもまったく問題ありません。同じ結果が得られます。
ソースコード内に暗号化キーを書いておくならこっちの方が少しは安全な気はします。
初期化ベクトル
初期化ベクトルも暗号化キーと同様、暗号化器に与える際はバイト型配列である必要があるので GetBytes 関数で文字列をバイト型配列に変換しています。
そして初期化ベクトルに関してはちょっぴり小難しい話があります。
AES 暗号はブロック暗号の一種で、ブロック暗号とはその名の通り平文を一定のサイズ毎に分けて処理していく暗号化の方法です。
このとき分けられた1つの塊のことをブロックと呼び、ブロック暗号にはいくつかのモードがあります。一般的によく利用されているのが CBC (Cipher Block Chaining) モードというもの。
今回の C# 実装でもデフォルトでこの CBC モードになっています。
CBC モードでは、あるブロックを処理するのにそのブロックの1つ前のブロックの処理結果を利用します。例えば、5番目のブロックを処理するためには4番目のブロックの処理結果が必要です。
昔高校の授業でやった漸化式みたいな感じです。
そうすると1番目のブロックの処理に必要な前ブロックがない!!ってなります。そのブロックの代わりとなるのが初期化ベクトルです。
初期化ベクトルのサイズが 128bit なのは AES のブロックサイズが 128bit だからです。
初期化ベクトルが必要なのは最初の1ブロック目の処理のみで、2ブロック目以降の処理に初期化ベクトルを直接使うことは基本ありません。
これは、最悪初期化ベクトルが分からなくても暗号化キーが分かっていれば2ブロック目以降 (17バイト目以降) は復号できるということ。
実際にやってみました。暗号化のときと全く違う初期化ベクトルで復号すると、、、
元の文字列は「そんなオカルトありえませんっ!!」
そして復号した結果は「?????????????????トありえませんっ!!」
しっかりと17バイト目以降は復号できております。日本語はだいたい1文字3バイトと考えると前半6文字 (18バイト分) が正しく復号できてないのにも納得です。
トワイエ!ちょっと例として分かりにくいのでもっと分かりやすい例で試してみましょう(笑)
今度の元の文字列は「abcdefghigklmnopqrstuvwxyz」
そして復号した結果は「3016745:;59>?<=”qrstuvwxyz」
ちょうど前半16文字が正しく復号できず、17文字目以降は復号できています。
初期化ベクトルは不要なのか?
17バイト目以降のデータが初期化ベクトル不明でも復号可能ということは CBC モードでは初期化ベクトルはいらないのではないかと考えてしまいますが、決してそんなことはなく、しっかりと役割もあります。
そもそも16バイト以下のデータを暗号化するときには初期化ベクトルがないと復号出来ません。何より初期化ベクトルが変わると暗号文が変わります。
最初の1ブロック (16byte) だけでなく、暗号文全体が変わります。
なので暗号化の度に初期化ベクトルをランダム生成することで同じ平文、同じ暗号化キーだったとしても異なる暗号文が得られます!毎回違う暗号文でちょっぴりセキュリティアップです!
実際に利用するときは、暗号化時は平文の前に16バイトのダミーデータをくっつけてやり、テキトーな初期化ベクトルで暗号化して、復号するときも好きな初期化ベクトルで復号し、復号できたデータの前半16バイトは無視!って運用にすれば暗号化キーのみを管理すればいいのでラクです。
ちなみに、Aes オブジェクトを作成するとランダムな暗号化キーと初期化ベクトルが自動で生成されています。なんでそれを使うのが手っ取り早いでしょう。
using (Aes aes = Aes.Create())
{
Console.WriteLine($"暗号化キー: {BitConverter.ToString(aes.Key)}");
Console.WriteLine($"初期化ベクトル: {BitConverter.ToString(aes.IV)}");
}
これの出力結果が以下となります。
暗号化キー: 08-70-7B-74-F8-13-45-4E-B4-FE-26-97-69-2B-FC-B5-81-98-61-2E-B7-C5-70-2C-08-3B-97-F8-0F-DA-36-C0
初期化ベクトル: AB-18-00-90-20-0F-F3-66-77-E7-B6-B7-D0-54-A4-BD
次はファイルの暗号化だ
かなり長くなってしまいました(笑) 暗号系のお話はボリューミーになりますね。
C# での実装方法より初期化ベクトルの話が多いような気もしますが、、、、
今回は文字列を暗号化することをメインにしておりました。このままファイルの暗号化の方法までメモしようと思ったのですが、かなり分量が増えそうなので別の記事にします!
(2022年7月14日追記) C# でファイルを暗号化する方法の記事を新たに書きました。
今回のまとめもを書くのにマイクロソフトのドキュメントにかなりお世話になりました。
Aes オブジェクトのその他のメソッドやプロパティも詳しく載っています!
コメント