- 投稿日:2020-11-14T23:58:29+09:00
C#でbase64エンコードしてBearerTokenを作成する
背景
TwitterAPIのaccessTokenを取得するためのBearerTokenをC#で作成する必要があり、
調べたときの備忘録です。コード
以下の流れで作成していきます。
1. API keyとAPI secret keyを用意する
2. API keyとAPI secret keyをコロンでつなぐ
3. byteの配列に変換する
4. base64に変換するusing System; // API keyとAPI secret keyを用意する string apiKey = "abcdefg"; string apiSecretKey = "kdsajfkllpwjioajfjdsa;fdksajf"; // API keyとAPI secret keyをコロンでつなぐ string strBearerTokenCredentials = apiKey + ":" + apiSecretKey; // byteの配列に変換する byte[] byteBearerTokenCredentials = Encoding.ASCII.GetBytes(strBearerTokenCredentials); Console.WriteLine(Encoding.ASCII.GetString(byteBearerTokenCredentials)); // stringのbase64に変換する string strBearerTokenCredentialsBase64 = Convert.ToBase64String(byteBearerTokenCredentials); Console.WriteLine(strBearerTokenCredentialsBase64);結果
// base64エンコードする前 abcdefg:kdsajfkllpwjioajfjdsa;fdksajf // base64エンコードした後 YWJjZGVmZzprZHNhamZrbGxwd2ppb2FqZmpkc2E7ZmRrc2FqZg==上記の流れで作成していきましたが、無駄な処理であったり、もっと良い作り方がある場合はご教示いただけると幸いです。
- 投稿日:2020-11-14T23:43:41+09:00
ハッシュ値を出力する湯婆婆をC#で実装してみる ーPBKDF2も対応(BCrypt、Argon2も少し)ー
はじめに
先日、.NET 5.0がリリースされました。業務でがっつりC#を使うわけではありませんが、でも、せっかくなので触ってみたいと思ったのと、加えて、私が情報処理安全確保支援士(SC)の学習をしているのもあり、このような記事になりました。
でも、他の記事と同じように、これもネタ記事です。
(勉強も兼ねていますので、指摘は歓迎です)なお、本記事では.NET 5.0(C# 9.0)で実装していますが、基本的なコードの部分に関しては、以下の記事を参考にさせて頂きました(ありがとうございます)。
更新履歴
2020/11/14 記事公開
2020/11/15 パスワード保存に適したハッシュ関数(PBKDF2)を追記
2020/11/16 BCrypt、Argon2を追記基本的なコード
最低限のコードは以下のような感じでしょうか。
C# 9.0から可能になったトップレベルステートメントで記述しています。
アルゴリズムにはSHA256を使用。using System; using System.Security.Cryptography; using System.Text; Console.WriteLine("契約書だよ。そこに名前を書きな。"); var name = Console.ReadLine(); Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ハッシュ値を算出 SHA256 Hash = SHA256.Create(); byte[] namebyte = Encoding.UTF8.GetBytes(name); byte[] hashbyte = Hash.ComputeHash(namebyte); string hashstring = BitConverter.ToString(hashbyte); Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!");実行結果は、こうなります。
ハッシュ値の基となる入力データは「千尋」です。契約書だよ。そこに名前を書きな。 千尋 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6Bだ。いいかい、05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6Bだよ。分かったら返事をするんだ、05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6B!!解説
以下はハッシュ関数のアルゴリズムとしてよく知られているものだと思いますが、.NETでも標準で使用可能です。
- SHA256
- SHA384
- SHA512
実は、SHA1とMD5も使用できますが、この2つは現在では安全ではないとされており、使用は推奨されていません。というのも、この2つは現在では現実的な時間で同一ハッシュ値(もしくは元データ)を得ることが可能であることが分かっているためです。ちなみに、元データが異なるのにハッシュ値が同一になることを「衝突」と呼びますが、ハッシュ関数には衝突への耐性が求められます(強衝突耐性)。
で、上の例ではSHA256を使用しています。「電子政府における調達のために参照すべき暗号のリスト(CRYPTREC暗号リスト)」にもあるもので、安全であると見なされています(SHA384、SHA512についても同様)。
ComputeHashに渡す値はbyte配列ですので、文字列から変換しています。
また、得られたハッシュ値もbyte配列なので、BitConverterで16進数の文字列に変換しています。「そもそも、ハッシュ値って何?」という方のために
まぁ、ここで説明するよりググって頂いたほうが詳しい説明が見つかりますが、念のため書きますと、パスワード認証の実装で使用したり、改ざんの検知にも使用されるもので、ある元となる値ないしデータ(上の例で言えば「千尋」)をある関数(ハッシュ関数)に通すことで得られる、不可逆な固定長の値のことを指します。SHAの後ろの数字は、それぞれ出力されるビットの長さを表します。
ちなみに、このようなハッシュ関数を暗号学的ハッシュ関数と呼び、ハッシュ値はメッセージダイジェストとも呼ばれることもあります。
パスワード認証の場面だと、DB内にパスワードをプレーンテキストで保管するのを避けるため、ハッシュ値に変換するという使い方をします。ですので、衝突の発見が容易だったり、ブルートフォースで元データが現実的な時間で特定可能だったりすると、認証突破の危険性を持つことになります。
また、改ざんの検知では、ダウンロードしたファイルが改ざんされていないかを検知するのに利用されます。サイトのダウンロードページなどにハッシュ値が掲載されていることがあるので、たまに見るかな…と。
あと、Webサイト(アプリ)制作の際、CDNリソースを参照する際にintegrity属性を付けることがありますが、ここで指定する値もハッシュ値です。SRI(サブリソース完全性)で検索すると出てくると思います。.NETだとパスワード認証を実装する必要がある時は、この機能は使用するかもしれませんね。
アルゴリズムを選べるようにしてみる
上の例だとSHA256でのみ出力するので、選べるようにしてみましょう。
(※追記:条件分岐の箇所はタプルとC# 8.0から追加されたswitch式で、実はもう少しすっきり書けます。具体的には、コメント欄をご覧ください)using System; using System.Security.Cryptography; using System.Text; Console.WriteLine("契約書だよ。そこに名前を書いてハッシュアルゴリズムを選びな。"); //名前(ハッシュコード計算の基となる値) Console.Write("名前:"); var name = Console.ReadLine(); //ハッシュアルゴリズム選択 Console.Write("アルゴリズム[MD5/SHA1/SHA256/SHA384/SHA512]:"); var algorism = Console.ReadLine(); algorism = algorism.ToUpper(); HashAlgorithm Hash; switch (algorism) { case "MD5": Hash = MD5.Create(); break; case "SHA1": Hash = SHA1.Create(); break; case "SHA256": Hash = SHA256.Create(); break; case "SHA384": Hash = SHA384.Create(); break; case "SHA512": Hash = SHA512.Create(); break; default: Console.WriteLine("アルゴリズムの選択間違ってるよ!"); Hash = SHA256.Create(); //dummy Environment.Exit(0); break; } Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ハッシュ値を算出 byte[] namebyte = Encoding.UTF8.GetBytes(name); byte[] hashbyte = Hash.ComputeHash(namebyte); string hashstring = BitConverter.ToString(hashbyte); Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!");行がちょっと長いですが、実行すると以下のようになります。
契約書だよ。そこに名前を書いてハッシュアルゴリズムを選びな。 名前:千尋 アルゴリズム[MD5/SHA1/SHA256/SHA384/SHA512]:SHA512 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2だ。いいかい、68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2だよ。分かったら返事をするんだ、68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2!!MD1、SHA1、SHA256、SHA384、SHA512は、いずれも基底クラスがHashAlgorithmです。どれも、Createメソッドでインスタンスを作成し、ComputeHashでハッシュ値を生成します。
アルゴリズムの指定を間違うと、怒られて終了します。
以上、C#で実装する「セキュアな湯婆婆」でした。
ソルトとストレッチングを実装して、より安全な湯婆婆にする(PBKDF2対応)
以下、2020/11/15追記分です。
ハッシュ値から元のパスワードを解析する手法もないわけではない
MD5とSHA1は現在では安全性が担保されていないことは上でも書きましたが、他にも、レインボーテーブルを使用した攻撃とか、攻撃対象のシステムにアカウントを作成して何らかの方法(SQLインジェクションとか)でハッシュ値を盗み出し、同一ハッシュ値を発見することでパスワードを特定するという方法があり得ます。この辺りは、↓の書籍が詳しいのでご興味がある方はどうぞ。
- 徳丸浩著 『体系的に学ぶ安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践』 SBクリエイティブ、2018
特に後者の方法をされてしまうと、上で説明した、単にSHA256でハッシュ値を求めただけの方法だと心許ない気がしますね。
で、ハッシュ解読を困難にする対策としては、以下のソルトを付加することと、ストレッチングがあります。
ソルト(salt)
ハッシュ値の元データ(パスワードなど)に付加する文字列です。レインボーテーブルを使用した攻撃の対策としてはある程度の長さのパスワードが求められるので、元の文字列にソルトを付けて長さを確保します。加えて、ソルトをユーザー毎に異なるものにすることで、別々のユーザーが作成したパスワードが仮に同一だった場合でも、同じハッシュ値を生成させないようにできます。
ストレッチング(stretching)
ハッシュ計算を繰り返し実行することで計算をより困難にさせる、という方法です。
これらは、わざわざ自分でゼロから実装する必要はありません。
OWASPのサイト(Password Storage Cheat Sheet - Custom Algorithms)で独自にアルゴリズムを組むことはやめるべきと書いてありますが(わざわざreally hardとかDo not do thisと太字で書いてある)、完成されたものがあるなら、わざわざ作るのもリスクになる気がします。ソルトやストレッチングの仕組みを備えたパスワード保管により適したハッシュ関数は既に考案されていて、BCrypt、Argon2、PBKDF2があります。.NETでは最初からPBKDF2 (Password-Based Key Derivation Function 2)が使用可能です。ちなみに、BCryptとArgon2はNugetから入手できます。
湯婆婆にもっと安全なハッシュ値を出力させてみる
前置きが長くなりましたが、PFKDF2を実装した湯婆婆が以下になります。
Rfc2898DeriveBytesというクラスを使用します。using System; using System.Security.Cryptography; Console.WriteLine("契約書だよ。そこに名前を書きな。"); var name = Console.ReadLine(); Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ソルトを作成(最低8byte必要) byte[] salt1 = new byte[8]; using (RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider()) { rngCsp.GetBytes(salt1); } //ハッシュ値を算出 string hashstring = string.Empty; //ストレッチングの反復処理回数1000、アルゴリズムにSHA256 using (Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(name, salt1, 1000, HashAlgorithmName.SHA256)) { byte[] hashbyte = pbkdf2.GetBytes(32); hashstring = BitConverter.ToString(hashbyte); } //出力 Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!"); Console.WriteLine($"そして、これがソルトだよ!:{BitConverter.ToString(salt1)}");ちなみに、ソルトをRNGCryptoServiceProviderクラスを使用して取得していますが、Rfc2898DeriveBytesのインスタンス作成時の第2引数(上の例で言えばsalt1)を整数にすることで、自動でソルトを作成させることも可能です。その場合、
pbkdf2.Salt
でソルトのbyte配列を取得可能です。文字列で取り出すなら、BitConverter.ToString(pbkdf2.Salt)
ですね。で、出力はこんな風です。
契約書だよ。そこに名前を書きな。 千尋 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6だ。いいかい、19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6だよ。分かったら返事をするんだ、19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6!! そして、これがソルトだよ!:AF-70-37-73-C6-E3-22-A9せっかくなので、ハッシュ値を突合させる処理も書いてみました。
色々とハードコーディングしていますが、例なので、ご容赦下さい。using System; using System.Security.Cryptography; string name = "千尋"; //前回決めたソルト string salt = "AF-70-37-73-C6-E3-22-A9"; //前回求めたハッシュ値 string prevhash = "19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6"; //ソルトをbyte配列に変換 string[] saltstrings = salt.Split("-"); byte[] saltbytes = new byte[saltstrings.Length]; for (int i=0; i < saltstrings.Length; i++) { saltbytes[i] = Convert.ToByte(saltstrings[i], 16); } //ハッシュ値を算出 string hashstring = string.Empty; using (Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(name, saltbytes, 1000, HashAlgorithmName.SHA256)) { byte[] hashbyte = pbkdf2.GetBytes(32); hashstring = BitConverter.ToString(hashbyte); } //結果判定を出力 if (prevhash.Equals(hashstring)) { Console.WriteLine("OK"); } else { Console.WriteLine("NG"); }以上です。
よりパスワードクラッキングされにくい、安全な湯婆婆ができあがりました。BCryptにも対応させてみる
以下、2020/11/16追記分です。
せっかくなので、パスワード保管に適しているとされる他のハッシュ関数も実装してみます。.NETに標準で使用可能なのは上に書いたPBKDF2ですが、BCryptについてはNugetから入手します。
いくつかあるようですが、今回は↓を使用します(Nugetからのインストールの仕方については…お手数ですが、ググってください)。
早速、コードです。
すっきりしていますね。
cost = 11
はストレッチングの回数ですが、例の場合、$2^{11}$回数分処理します。最大31まで指定可能ですが、あまり大きくすると負荷が高くなります(未指定の場合はデフォルトの11)。using System; using Bcrypt = BCrypt.Net.BCrypt; Console.WriteLine("契約書だよ。そこに名前を書きな。"); var name = Console.ReadLine(); Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ハッシュ値を算出 int cost = 11; //ストレッチングの回数 (2^11回) string hashstring = Bcrypt.EnhancedHashPassword(name, cost); //出力 Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!");出力結果は以下のようになります。
千尋 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は$2a$11$Z0h6Wc9xrSyRVntye6WecuIdurJzvNP7UvfY4yvZlKBBZPIjOA9W2だ。いいかい、$2a$11$Z0h6Wc9xrSyRVntye6WecuIdurJzvNP7UvfY4yvZlKBBZPIjOA9W2だよ。分かったら返事をするんだ、$2a$11$Z0h6Wc9xrSyRVntye6WecuIdurJzvNP7UvfY4yvZlKBBZPIjOA9W2!!詳しくはググって頂きたいですが、出力した文字列の中にcost、ソルト、ハッシュ値が全て含まれています(ソルトは自動生成)。DBへの格納が簡単ですみます。
ちなみに、検証は以下のようにします(trueが返されたら、OK)。
bool validate = Bcrypt.EnhancedVerify(name, hashstring); Console.WriteLine(validate);さらに、Argon2を実装してみる
なんでも、Password Hashing Competitionなる協議会で優勝したものなのだとか。
使用にはパラメータの操作が必要です(実行時間、使用メモリ、並列処理数)。上のOWASPサイトでは、パラメータ設定のやり方が分からない場合、Bcryptがよりよい方法かもしれないと説明されています。
参考までに、PHPのArgon2のデフォルトパラメータは以下のようになっています。
memory_cost = 1024 KiB time_cost = 2 threads = 2どのパラメータが最適かについては複雑そうなので、今は触れないことにします(また分かったら追記します)。
今回は、以下のライブラリを使用してみます。
パラメータは
Argon2Config
クラスをインスタンス化して値をセットし、Argon2のコンストラクタの引数として与えてやりますが、一応デフォルト値がセットされているようです(今回は何にしません)。using System; using Isopoh.Cryptography.Argon2; Console.WriteLine("契約書だよ。そこに名前を書きな。"); var name = Console.ReadLine(); Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ハッシュ値を算出 string hashstring = Argon2.Hash(name); //出力 Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!");結果は以下の通りです。
契約書だよ。そこに名前を書きな。 千尋 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は$argon2id$v=19$m=65536,t=3,p=1$h0XiPFdMvZzkWt5YePRd8Q$+GPj6ztOfXuaKsjbLED4dy0sYmOgzE1SCnDADzUe0Ooだ。いいかい、$argon2id$v=19$m=65536,t=3,p=1$h0XiPFdMvZzkWt5YePRd8Q$+GPj6ztOfXuaKsjbLED4dy0sYmOgzE1SCnDADzUe0Ooだよ。分かったら返事をするんだ、$argon2id$v=19$m=65536,t=3,p=1$h0XiPFdMvZzkWt5YePRd8Q$+GPj6ztOfXuaKsjbLED4dy0sYmOgzE1SCnDADzUe0Oo!!BCryptと同様、1つの文字列にパラメータ値やハッシュ値がまとめられているようです。
検証は、以下のようにします。
trueが返されたら、OK。bool validate = Argon2.Verify(hashstring, name); Console.WriteLine(validate);以上です。
調子に乗って、結構、長い記事になってしまいました。
- 投稿日:2020-11-14T23:43:41+09:00
ハッシュ値を出力する湯婆婆をC#で実装してみる(PBKDF2他対応)
はじめに
先日、.NET 5.0がリリースされました。業務でがっつりC#を使うわけではありませんが、でも、せっかくなので触ってみたいと思ったのと、加えて、私が情報処理安全確保支援士(SC)の学習をしているのもあり、このような記事になりました。
でも、他の記事と同じように、これもネタ記事です。
(勉強も兼ねていますので、指摘は歓迎です)なお、本記事では.NET 5.0(C# 9.0)で実装していますが、基本的なコードの部分に関しては、以下の記事を参考にさせて頂きました(ありがとうございます)。
更新履歴
2020/11/14 記事公開
2020/11/15 パスワード保存に適したハッシュ関数(PBKDF2)を追記
2020/11/16 BCrypt、Argon2を追記基本的なコード
最低限のコードは以下のような感じでしょうか。
C# 9.0から可能になったトップレベルステートメントで記述しています。
アルゴリズムにはSHA256を使用。using System; using System.Security.Cryptography; using System.Text; Console.WriteLine("契約書だよ。そこに名前を書きな。"); var name = Console.ReadLine(); Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ハッシュ値を算出 SHA256 Hash = SHA256.Create(); byte[] namebyte = Encoding.UTF8.GetBytes(name); byte[] hashbyte = Hash.ComputeHash(namebyte); string hashstring = BitConverter.ToString(hashbyte); Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!");実行結果は、こうなります。
ハッシュ値の基となる入力データは「千尋」です。契約書だよ。そこに名前を書きな。 千尋 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6Bだ。いいかい、05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6Bだよ。分かったら返事をするんだ、05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6B!!解説
以下はハッシュ関数のアルゴリズムとしてよく知られているものだと思いますが、.NETでも標準で使用可能です。
- SHA256
- SHA384
- SHA512
実は、SHA1とMD5も使用できますが、この2つは現在では安全ではないとされており、使用は推奨されていません。というのも、この2つは現在では現実的な時間で同一ハッシュ値(もしくは元データ)を得ることが可能であることが分かっているためです。ちなみに、元データが異なるのにハッシュ値が同一になることを「衝突」と呼びますが、ハッシュ関数には衝突への耐性が求められます(強衝突耐性)。
で、上の例ではSHA256を使用しています。「電子政府における調達のために参照すべき暗号のリスト(CRYPTREC暗号リスト)」にもあるもので、安全であると見なされています(SHA384、SHA512についても同様)。
ComputeHashに渡す値はbyte配列ですので、文字列から変換しています。
また、得られたハッシュ値もbyte配列なので、BitConverterで16進数の文字列に変換しています。「そもそも、ハッシュ値って何?」という方のために
まぁ、ここで説明するよりググって頂いたほうが詳しい説明が見つかりますが、念のため書きますと、パスワード認証の実装で使用したり、改ざんの検知にも使用されるもので、ある元となる値ないしデータ(上の例で言えば「千尋」)をある関数(ハッシュ関数)に通すことで得られる、不可逆な固定長の値のことを指します。SHAの後ろの数字は、それぞれ出力されるビットの長さを表します。
ちなみに、このようなハッシュ関数を暗号学的ハッシュ関数と呼び、ハッシュ値はメッセージダイジェストとも呼ばれることもあります。
パスワード認証の場面だと、DB内にパスワードをプレーンテキストで保管するのを避けるため、ハッシュ値に変換するという使い方をします。ですので、衝突の発見が容易だったり、ブルートフォースで元データが現実的な時間で特定可能だったりすると、認証突破の危険性を持つことになります。
また、改ざんの検知では、ダウンロードしたファイルが改ざんされていないかを検知するのに利用されます。サイトのダウンロードページなどにハッシュ値が掲載されていることがあるので、たまに見るかな…と。
あと、Webサイト(アプリ)制作の際、CDNリソースを参照する際にintegrity属性を付けることがありますが、ここで指定する値もハッシュ値です。SRI(サブリソース完全性)で検索すると出てくると思います。.NETだとパスワード認証を実装する必要がある時は、この機能は使用するかもしれませんね。
アルゴリズムを選べるようにしてみる
上の例だとSHA256でのみ出力するので、選べるようにしてみましょう。
(※追記:条件分岐の箇所はタプルとC# 8.0から追加されたswitch式で、実はもう少しすっきり書けます。具体的には、コメント欄をご覧ください)using System; using System.Security.Cryptography; using System.Text; Console.WriteLine("契約書だよ。そこに名前を書いてハッシュアルゴリズムを選びな。"); //名前(ハッシュコード計算の基となる値) Console.Write("名前:"); var name = Console.ReadLine(); //ハッシュアルゴリズム選択 Console.Write("アルゴリズム[MD5/SHA1/SHA256/SHA384/SHA512]:"); var algorism = Console.ReadLine(); algorism = algorism.ToUpper(); HashAlgorithm Hash; switch (algorism) { case "MD5": Hash = MD5.Create(); break; case "SHA1": Hash = SHA1.Create(); break; case "SHA256": Hash = SHA256.Create(); break; case "SHA384": Hash = SHA384.Create(); break; case "SHA512": Hash = SHA512.Create(); break; default: Console.WriteLine("アルゴリズムの選択間違ってるよ!"); Hash = SHA256.Create(); //dummy Environment.Exit(0); break; } Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ハッシュ値を算出 byte[] namebyte = Encoding.UTF8.GetBytes(name); byte[] hashbyte = Hash.ComputeHash(namebyte); string hashstring = BitConverter.ToString(hashbyte); Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!");行がちょっと長いですが、実行すると以下のようになります。
契約書だよ。そこに名前を書いてハッシュアルゴリズムを選びな。 名前:千尋 アルゴリズム[MD5/SHA1/SHA256/SHA384/SHA512]:SHA512 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2だ。いいかい、68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2だよ。分かったら返事をするんだ、68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2!!MD1、SHA1、SHA256、SHA384、SHA512は、いずれも基底クラスがHashAlgorithmです。どれも、Createメソッドでインスタンスを作成し、ComputeHashでハッシュ値を生成します。
アルゴリズムの指定を間違うと、怒られて終了します。
以上、C#で実装する「セキュアな湯婆婆」でした。
ソルトとストレッチングを実装して、より安全な湯婆婆にする(PBKDF2対応)
以下、2020/11/15追記分です。
ハッシュ値から元のパスワードを解析する手法もないわけではない
MD5とSHA1は現在では安全性が担保されていないことは上でも書きましたが、他にも、レインボーテーブルを使用した攻撃とか、攻撃対象のシステムにアカウントを作成して何らかの方法(SQLインジェクションとか)でハッシュ値を盗み出し、同一ハッシュ値を発見することでパスワードを特定するという方法があり得ます。この辺りは、↓の書籍が詳しいのでご興味がある方はどうぞ。
- 徳丸浩著 『体系的に学ぶ安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践』 SBクリエイティブ、2018
特に後者の方法をされてしまうと、上で説明した、単にSHA256でハッシュ値を求めただけの方法だと心許ない気がしますね。
で、ハッシュ解読を困難にする対策としては、以下のソルトを付加することと、ストレッチングがあります。
ソルト(salt)
ハッシュ値の元データ(パスワードなど)に付加する文字列です。レインボーテーブルを使用した攻撃の対策としてはある程度の長さのパスワードが求められるので、元の文字列にソルトを付けて長さを確保します。加えて、ソルトをユーザー毎に異なるものにすることで、別々のユーザーが作成したパスワードが仮に同一だった場合でも、同じハッシュ値を生成させないようにできます。
ストレッチング(stretching)
ハッシュ計算を繰り返し実行することで計算をより困難にさせる、という方法です。
これらは、わざわざ自分でゼロから実装する必要はありません。
OWASPのサイト(Password Storage Cheat Sheet - Custom Algorithms)で独自にアルゴリズムを組むことはやめるべきと書いてありますが(わざわざreally hardとかDo not do thisと太字で書いてある)、完成されたものがあるなら、わざわざ作るのもリスクになる気がします。ソルトやストレッチングの仕組みを備えたパスワード保管により適したハッシュ関数は既に考案されていて、BCrypt、Argon2、PBKDF2があります。.NETでは最初からPBKDF2 (Password-Based Key Derivation Function 2)が使用可能です。ちなみに、BCryptとArgon2はNugetから入手できます。
湯婆婆にもっと安全なハッシュ値を出力させてみる
前置きが長くなりましたが、PFKDF2を実装した湯婆婆が以下になります。
Rfc2898DeriveBytesというクラスを使用します。using System; using System.Security.Cryptography; Console.WriteLine("契約書だよ。そこに名前を書きな。"); var name = Console.ReadLine(); Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ソルトを作成(最低8byte必要) byte[] salt1 = new byte[8]; using (RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider()) { rngCsp.GetBytes(salt1); } //ハッシュ値を算出 string hashstring = string.Empty; //ストレッチングの反復処理回数1000、アルゴリズムにSHA256 using (Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(name, salt1, 1000, HashAlgorithmName.SHA256)) { byte[] hashbyte = pbkdf2.GetBytes(32); hashstring = BitConverter.ToString(hashbyte); } //出力 Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!"); Console.WriteLine($"そして、これがソルトだよ!:{BitConverter.ToString(salt1)}");ちなみに、ソルトをRNGCryptoServiceProviderクラスを使用して取得していますが、Rfc2898DeriveBytesのインスタンス作成時の第2引数(上の例で言えばsalt1)を整数にすることで、自動でソルトを作成させることも可能です。その場合、
pbkdf2.Salt
でソルトのbyte配列を取得可能です。文字列で取り出すなら、BitConverter.ToString(pbkdf2.Salt)
ですね。で、出力はこんな風です。
契約書だよ。そこに名前を書きな。 千尋 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6だ。いいかい、19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6だよ。分かったら返事をするんだ、19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6!! そして、これがソルトだよ!:AF-70-37-73-C6-E3-22-A9せっかくなので、ハッシュ値を突合させる処理も書いてみました。
色々とハードコーディングしていますが、例なので、ご容赦下さい。using System; using System.Security.Cryptography; string name = "千尋"; //前回決めたソルト string salt = "AF-70-37-73-C6-E3-22-A9"; //前回求めたハッシュ値 string prevhash = "19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6"; //ソルトをbyte配列に変換 string[] saltstrings = salt.Split("-"); byte[] saltbytes = new byte[saltstrings.Length]; for (int i=0; i < saltstrings.Length; i++) { saltbytes[i] = Convert.ToByte(saltstrings[i], 16); } //ハッシュ値を算出 string hashstring = string.Empty; using (Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(name, saltbytes, 1000, HashAlgorithmName.SHA256)) { byte[] hashbyte = pbkdf2.GetBytes(32); hashstring = BitConverter.ToString(hashbyte); } //結果判定を出力 if (prevhash.Equals(hashstring)) { Console.WriteLine("OK"); } else { Console.WriteLine("NG"); }以上です。
よりパスワードクラッキングされにくい、安全な湯婆婆ができあがりました。BCryptにも対応させてみる
以下、2020/11/16追記分です。
せっかくなので、パスワード保管に適しているとされる他のハッシュ関数も実装してみます。.NETに標準で使用可能なのは上に書いたPBKDF2ですが、BCryptについてはNugetから入手します。
いくつかあるようですが、今回は↓を使用します(Nugetからのインストールの仕方については…お手数ですが、ググってください)。
早速、コードです。
すっきりしていますね。
cost = 11
はストレッチングの回数ですが、例の場合、$2^{11}$回数分処理します。最大31まで指定可能ですが、あまり大きくすると負荷が高くなります(未指定の場合はデフォルトの11)。using System; using Bcrypt = BCrypt.Net.BCrypt; Console.WriteLine("契約書だよ。そこに名前を書きな。"); var name = Console.ReadLine(); Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ハッシュ値を算出 int cost = 11; //ストレッチングの回数 (2^11回) string hashstring = Bcrypt.EnhancedHashPassword(name, cost); //出力 Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!");出力結果は以下のようになります。
千尋 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は$2a$11$Z0h6Wc9xrSyRVntye6WecuIdurJzvNP7UvfY4yvZlKBBZPIjOA9W2だ。いいかい、$2a$11$Z0h6Wc9xrSyRVntye6WecuIdurJzvNP7UvfY4yvZlKBBZPIjOA9W2だよ。分かったら返事をするんだ、$2a$11$Z0h6Wc9xrSyRVntye6WecuIdurJzvNP7UvfY4yvZlKBBZPIjOA9W2!!詳しくはググって頂きたいですが、出力した文字列の中にcost、ソルト、ハッシュ値が全て含まれています(ソルトは自動生成)。DBへの格納が簡単ですみます。
ちなみに、検証は以下のようにします(trueが返されたら、OK)。
bool validate = Bcrypt.EnhancedVerify(name, hashstring); Console.WriteLine(validate);さらに、Argon2を実装してみる
なんでも、Password Hashing Competitionなる協議会で優勝したものなのだとか。
使用にはパラメータの操作が必要です(実行時間、使用メモリ、並列処理数)。上のOWASPサイトでは、パラメータ設定のやり方が分からない場合、Bcryptがよりよい方法かもしれないと説明されています。
参考までに、PHPのArgon2のデフォルトパラメータは以下のようになっています。
memory_cost = 1024 KiB time_cost = 2 threads = 2どのパラメータが最適かについては複雑そうなので、今は触れないことにします(また分かったら追記します)。
今回は、以下のライブラリを使用してみます。
パラメータは
Argon2Config
クラスをインスタンス化して値をセットし、Argon2のコンストラクタの引数として与えてやりますが、一応デフォルト値がセットされているようです(今回は何にしません)。using System; using Isopoh.Cryptography.Argon2; Console.WriteLine("契約書だよ。そこに名前を書きな。"); var name = Console.ReadLine(); Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ハッシュ値を算出 string hashstring = Argon2.Hash(name); //出力 Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!");結果は以下の通りです。
契約書だよ。そこに名前を書きな。 千尋 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は$argon2id$v=19$m=65536,t=3,p=1$h0XiPFdMvZzkWt5YePRd8Q$+GPj6ztOfXuaKsjbLED4dy0sYmOgzE1SCnDADzUe0Ooだ。いいかい、$argon2id$v=19$m=65536,t=3,p=1$h0XiPFdMvZzkWt5YePRd8Q$+GPj6ztOfXuaKsjbLED4dy0sYmOgzE1SCnDADzUe0Ooだよ。分かったら返事をするんだ、$argon2id$v=19$m=65536,t=3,p=1$h0XiPFdMvZzkWt5YePRd8Q$+GPj6ztOfXuaKsjbLED4dy0sYmOgzE1SCnDADzUe0Oo!!BCryptと同様、1つの文字列にパラメータ値やハッシュ値がまとめられているようです。
検証は、以下のようにします。
trueが返されたら、OK。bool validate = Argon2.Verify(hashstring, name); Console.WriteLine(validate);以上です。
調子に乗って、結構、長い記事になってしまいました。
- 投稿日:2020-11-14T23:43:41+09:00
ハッシュ値を出力する湯婆婆をC#で実装してみる
はじめに
先日、.NET 5.0がリリースされました。業務ではがっつりC#は使いませんが、でも、せっかくなので触ってみたいと思ったのと、加えて、私が情報処理安全確保支援士(SC)の学習をしているのもあり、このような記事になりました。
でも、他の記事と同じように、これもネタ記事です(指摘は歓迎しますが)。
基本的なコード
最低限のコードは以下のような感じでしょうか。
C# 9.0から可能になったトップレベルステートメントで記述しています。
アルゴリズムにはSHA256を使用。using System; using System.Security.Cryptography; using System.Text; Console.WriteLine("契約書だよ。そこに名前を書きな。"); var name = Console.ReadLine(); Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ハッシュ値を算出 SHA256 Hash = SHA256.Create(); byte[] namebyte = Encoding.UTF8.GetBytes(name); byte[] hashbyte = Hash.ComputeHash(namebyte); string hashstring = BitConverter.ToString(hashbyte); Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!");実行結果は、こうなります。
ハッシュ値の基となる入力データは「千尋」です。契約書だよ。そこに名前を書きな。 千尋 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6Bだ。いいかい、05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6Bだよ。分かったら返事をするんだ、05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6B!!解説
.NETでは、ハッシュアルゴリズムとして以下が使用可能です。
- SHA256
- SHA384
- SHA512
実は、SHA1とMD5も使用可能ですが、この2つは現在では安全ではないとされており、使用は推奨されていません。元データが異なるのに同一ハッシュ値が得られることを「衝突」と呼び、発生は避けられるべきものなのですが、この2つは現在では現実的な時間で衝突が発見可能(同一ハッシュ値を得ることが可能)とされています。
で、上の例ではSHA256を使用しています。「電子政府における調達のために参照すべき暗号のリスト(CRYPTREC暗号リスト)」にもあるもので、安全であると見なされています(SHA384、SHA512についても同様)。
ComputeHashに渡す値はbyte配列ですので、文字列から変換しています。
また、得られたハッシュ値もbyte配列なので、BitConverterで16進数の文字列に変換しています。「そもそも、ハッシュ関数って何ですか?」という方のために
まぁ、ここで説明するよりググって頂いたほうが詳しい説明が見つかりますが、念のため書きますと、パスワード認証に使用したり、改ざんの検知にも使用されるもので、値(上の例で言えば「千尋」)をある関数に通すことで、不可逆な固定長の値が出力されるものです。SHAの後ろの数字は、それぞれ出力されるビットの長さを表します。
パスワード認証の場面だと、DB内にパスワードをプレーンテキストで保管するのを避けるため、ハッシュ値に変換するという使い方をします。ですので、衝突の発見が容易だとされているアルゴリズムを使用した場合、認証突破の危険性を持つことになります。
また、改ざんの検知では、ダウンロードしたファイルが改ざんされていないかを検知するのに利用されます。サイトのダウンロードページなどにハッシュ値が掲載されていることがあるので、たまに見るかな…と。
あと、Webサイト(アプリ)制作の際、CDNリソースを参照する際にintegrity属性を付けることがありますが、ここで指定する値もハッシュ値です。SRI(サブリソース完全性)で検索すると出てくると思います。.NETだとパスワード認証を実装する必要がある時は、この機能は使用するかもしれませんね。
アルゴリズムを選べるようにしてみる
上の例だとSHA256でのみ出力するので、選べるようにしてみましょう。
using System; using System.Security.Cryptography; using System.Text; Console.WriteLine("契約書だよ。そこに名前を書いてハッシュアルゴリズムを選びな。"); //名前(ハッシュコード計算の基となる値) Console.Write("名前:"); var name = Console.ReadLine(); //ハッシュアルゴリズム選択 Console.Write("アルゴリズム[MD5/SHA1/SHA256/SHA384/SHA512]:"); var algorism = Console.ReadLine(); algorism = algorism.ToUpper(); HashAlgorithm Hash; switch (algorism) { case "MD5": Hash = MD5.Create(); break; case "SHA1": Hash = SHA1.Create(); break; case "SHA256": Hash = SHA256.Create(); break; case "SHA384": Hash = SHA384.Create(); break; case "SHA512": Hash = SHA512.Create(); break; default: Console.WriteLine("アルゴリズムの選択間違ってるよ!"); Hash = SHA256.Create(); Environment.Exit(0); break; } Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ハッシュ値を算出 byte[] namebyte = Encoding.UTF8.GetBytes(name); byte[] hashbyte = Hash.ComputeHash(namebyte); string hashstring = BitConverter.ToString(hashbyte); Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!");行がちょっと長いですが、実行すると以下のようになります。
契約書だよ。そこに名前を書いてハッシュアルゴリズムを選びな。 名前:千尋 アルゴリズム[MD5/SHA1/SHA256/SHA384/SHA512]:SHA512 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2だ。いいかい、68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2だよ。分かったら返事をするんだ、68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2!!MD1、SHA1、SHA256、SHA384、SHA512は、いずれも基底クラスがHashAlgorithmです。どれも、Createメソッドでインスタンスを作成し、ComputeHashでハッシュ値を作成します。
アルゴリズムの指定を間違うと、怒られて終了します。
以上、C#で実装する「セキュアな湯婆婆」でした。
- 投稿日:2020-11-14T23:43:41+09:00
ハッシュ値を出力する湯婆婆をC#で実装してみる(プラスPBKDF2)
はじめに
先日、.NET 5.0がリリースされました。業務ではがっつりC#は使いませんが、でも、せっかくなので触ってみたいと思ったのと、加えて、私が情報処理安全確保支援士(SC)の学習をしているのもあり、このような記事になりました。
でも、他の記事と同じように、これもネタ記事です。
(勉強も兼ねていますので、指摘は歓迎です)なお、本記事では.NET 5.0(C# 9.0)で実装していますが、基本的なコードの部分に関しては、以下の記事を参考にさせて頂きました(ありがとうございます)。
更新履歴
2020/11/14 記事公開
2020/11/15 パスワード保存に適したハッシュ関数(PBKDF2)を追記基本的なコード
最低限のコードは以下のような感じでしょうか。
C# 9.0から可能になったトップレベルステートメントで記述しています。
アルゴリズムにはSHA256を使用。using System; using System.Security.Cryptography; using System.Text; Console.WriteLine("契約書だよ。そこに名前を書きな。"); var name = Console.ReadLine(); Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ハッシュ値を算出 SHA256 Hash = SHA256.Create(); byte[] namebyte = Encoding.UTF8.GetBytes(name); byte[] hashbyte = Hash.ComputeHash(namebyte); string hashstring = BitConverter.ToString(hashbyte); Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!");実行結果は、こうなります。
ハッシュ値の基となる入力データは「千尋」です。契約書だよ。そこに名前を書きな。 千尋 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6Bだ。いいかい、05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6Bだよ。分かったら返事をするんだ、05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6B!!解説
.NETでは、ハッシュアルゴリズムとして以下が使用可能です。
- SHA256
- SHA384
- SHA512
実は、SHA1とMD5も使用可能ですが、この2つは現在では安全ではないとされており、使用は推奨されていません。元データが異なるのに同一ハッシュ値が得られることを「衝突」と呼び、発生は避けられるべきものなのですが、この2つは現在では現実的な時間で衝突が発見可能(同一ハッシュ値を得ることが可能)とされています。
で、上の例ではSHA256を使用しています。「電子政府における調達のために参照すべき暗号のリスト(CRYPTREC暗号リスト)」にもあるもので、安全であると見なされています(SHA384、SHA512についても同様)。
ComputeHashに渡す値はbyte配列ですので、文字列から変換しています。
また、得られたハッシュ値もbyte配列なので、BitConverterで16進数の文字列に変換しています。「そもそも、ハッシュ値って何?」という方のために
まぁ、ここで説明するよりググって頂いたほうが詳しい説明が見つかりますが、念のため書きますと、パスワード認証の実装で使用したり、改ざんの検知にも使用されるもので、ある元となる値(上の例で言えば「千尋」)をある関数(ハッシュ関数)に通すことで得られる、不可逆な固定長の値のことを指します。SHAの後ろの数字は、それぞれ出力されるビットの長さを表します。
ちなみに、このようなハッシュ関数を暗号学的ハッシュ関数と呼び、ハッシュ値はメッセージダイジェストとも呼ばれることもあります。
パスワード認証の場面だと、DB内にパスワードをプレーンテキストで保管するのを避けるため、ハッシュ値に変換するという使い方をします。ですので、衝突の発見が容易だとされているアルゴリズムを使用した場合、認証突破の危険性を持つことになります。
また、改ざんの検知では、ダウンロードしたファイルが改ざんされていないかを検知するのに利用されます。サイトのダウンロードページなどにハッシュ値が掲載されていることがあるので、たまに見るかな…と。
あと、Webサイト(アプリ)制作の際、CDNリソースを参照する際にintegrity属性を付けることがありますが、ここで指定する値もハッシュ値です。SRI(サブリソース完全性)で検索すると出てくると思います。.NETだとパスワード認証を実装する必要がある時は、この機能は使用するかもしれませんね。
アルゴリズムを選べるようにしてみる
上の例だとSHA256でのみ出力するので、選べるようにしてみましょう。
using System; using System.Security.Cryptography; using System.Text; Console.WriteLine("契約書だよ。そこに名前を書いてハッシュアルゴリズムを選びな。"); //名前(ハッシュコード計算の基となる値) Console.Write("名前:"); var name = Console.ReadLine(); //ハッシュアルゴリズム選択 Console.Write("アルゴリズム[MD5/SHA1/SHA256/SHA384/SHA512]:"); var algorism = Console.ReadLine(); algorism = algorism.ToUpper(); HashAlgorithm Hash; switch (algorism) { case "MD5": Hash = MD5.Create(); break; case "SHA1": Hash = SHA1.Create(); break; case "SHA256": Hash = SHA256.Create(); break; case "SHA384": Hash = SHA384.Create(); break; case "SHA512": Hash = SHA512.Create(); break; default: Console.WriteLine("アルゴリズムの選択間違ってるよ!"); Hash = SHA256.Create(); Environment.Exit(0); break; } Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ハッシュ値を算出 byte[] namebyte = Encoding.UTF8.GetBytes(name); byte[] hashbyte = Hash.ComputeHash(namebyte); string hashstring = BitConverter.ToString(hashbyte); Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!");行がちょっと長いですが、実行すると以下のようになります。
契約書だよ。そこに名前を書いてハッシュアルゴリズムを選びな。 名前:千尋 アルゴリズム[MD5/SHA1/SHA256/SHA384/SHA512]:SHA512 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2だ。いいかい、68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2だよ。分かったら返事をするんだ、68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2!!MD1、SHA1、SHA256、SHA384、SHA512は、いずれも基底クラスがHashAlgorithmです。どれも、Createメソッドでインスタンスを作成し、ComputeHashでハッシュ値を生成します。
アルゴリズムの指定を間違うと、怒られて終了します。
以上、C#で実装する「セキュアな湯婆婆」でした。
ソルトとストレッチングを実装して、より安全な湯婆婆にする(PBKDF2対応)
以下、2020/11/15追記分です。
ハッシュ値から元のパスワードを解析する手法もないわけではない
MD5とSHA1は現在では安全性が担保されていないことは上でも書きましたが、他にも、レインボーテーブルを使用した攻撃とか、攻撃対象のシステムにアカウントを作成して何らかの方法(SQLインジェクションとか)でハッシュ値を盗み出し、同一ハッシュ値を発見することでパスワードを特定するという方法があり得ます。この辺りは、↓の書籍が詳しいのでご興味がある方はどうぞ。
- 徳丸浩著 『体系的に学ぶ安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践』 SBクリエイティブ、2018
特に後者の方法をされてしまうと、上で説明した、単にSHA256でハッシュ値を求めただけの方法だと心許ない気がしますね。
で、ハッシュ解読を困難にする対策としては、以下のソルトを付加することと、ストレッチングがあります。
ソルト(salt)
ハッシュ値の元データ(パスワードなど)に付加する文字列です。レインボーテーブルを使用した攻撃の対策としてはある程度の長さのパスワードが必求められるので、元の文字列にソルトを付けて長さを確保します。加えて、ソルトをユーザー毎に異なるものにすることで、別々のユーザーが作成したパスワードが仮に同一だった場合でも、同じハッシュ値を生成させないようにできます。
ストレッチング(stretching)
ハッシュ計算を繰り返し実行することで計算をより困難にさせる、という方法です。
で、これらは、わざわざ自分でゼロから実装する必要はありません。このようなパスワード保管に適したハッシュ関数として、BCrypt、Argon2、PBKDF2があります。.NETでは最初からPBKDF2が使用可能です(BCryptとArgon2はNugetから入手できる)。
湯婆婆にもっと安全なハッシュ値を出力させてみる
前置きが長くなりましたが、PFKDF2を実装した湯婆婆が以下になります。
using System; using System.Security.Cryptography; Console.WriteLine("契約書だよ。そこに名前を書きな。"); var name = Console.ReadLine(); Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ソルトを作成(最低8byte必要) byte[] salt1 = new byte[8]; using (RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider()) { rngCsp.GetBytes(salt1); } //ハッシュ値を算出 string hashstring = string.Empty; //ストレッチングの反復処理回数1000、アルゴリズムにSHA256 using (Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(name, salt1, 1000, HashAlgorithmName.SHA256)) { byte[] hashbyte = pbkdf2.GetBytes(32); hashstring = BitConverter.ToString(hashbyte); } //出力 Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!"); Console.WriteLine($"そして、これがソルトだよ!:{BitConverter.ToString(salt1)}");こんな風に出力されました。
契約書だよ。そこに名前を書きな。 千尋 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6だ。いいかい、19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6だよ。分かったら返事をするんだ、19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6!! そして、これがソルトだよ!:AF-70-37-73-C6-E3-22-A9せっかくなので、ハッシュ値を突合させる処理も書いてみました。
色々とハードコーディングしていますが、例なので、ご容赦下さい。using System; using System.Security.Cryptography; string name = "千尋"; //前回決めたソルト string salt = "AF-70-37-73-C6-E3-22-A9"; //前回求めたハッシュ値 string prevhash = "19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6"; //ソルトをbyte配列に変換 string[] saltstrings = salt.Split("-"); byte[] saltbytes = new byte[saltstrings.Length]; for (int i=0; i < saltstrings.Length; i++) { saltbytes[i] = Convert.ToByte(saltstrings[i], 16); } //ハッシュ値を算出 string hashstring = string.Empty; using (Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(name, saltbytes, 1000, HashAlgorithmName.SHA256)) { byte[] hashbyte = pbkdf2.GetBytes(32); hashstring = BitConverter.ToString(hashbyte); } //結果判定を出力 if (prevhash.Equals(hashstring)) { Console.WriteLine("OK"); } else { Console.WriteLine("NG"); }以上です。
よりパスワードクラッキングされにくい、安全な湯婆婆ができあがりました。
- 投稿日:2020-11-14T23:43:41+09:00
ハッシュ値を出力する湯婆婆をC#で実装してみる(PBKDF2対応)
はじめに
先日、.NET 5.0がリリースされました。業務ではがっつりC#は使いませんが、でも、せっかくなので触ってみたいと思ったのと、加えて、私が情報処理安全確保支援士(SC)の学習をしているのもあり、このような記事になりました。
でも、他の記事と同じように、これもネタ記事です。
(勉強も兼ねていますので、指摘は歓迎です)なお、本記事では.NET 5.0(C# 9.0)で実装していますが、基本的なコードの部分に関しては、以下の記事を参考にさせて頂きました(ありがとうございます)。
更新履歴
2020/11/14 記事公開
2020/11/15 パスワード保存に適したハッシュ関数(PBKDF2)を追記基本的なコード
最低限のコードは以下のような感じでしょうか。
C# 9.0から可能になったトップレベルステートメントで記述しています。
アルゴリズムにはSHA256を使用。using System; using System.Security.Cryptography; using System.Text; Console.WriteLine("契約書だよ。そこに名前を書きな。"); var name = Console.ReadLine(); Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ハッシュ値を算出 SHA256 Hash = SHA256.Create(); byte[] namebyte = Encoding.UTF8.GetBytes(name); byte[] hashbyte = Hash.ComputeHash(namebyte); string hashstring = BitConverter.ToString(hashbyte); Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!");実行結果は、こうなります。
ハッシュ値の基となる入力データは「千尋」です。契約書だよ。そこに名前を書きな。 千尋 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6Bだ。いいかい、05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6Bだよ。分かったら返事をするんだ、05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6B!!解説
.NETでは、ハッシュアルゴリズムとして以下が使用可能です。
- SHA256
- SHA384
- SHA512
実は、SHA1とMD5も使用可能ですが、この2つは現在では安全ではないとされており、使用は推奨されていません。元データが異なるのに同一ハッシュ値が得られることを「衝突」と呼び、発生は避けられるべきものなのですが、この2つは現在では現実的な時間で衝突が発見可能(同一ハッシュ値を得ることが可能)とされています。
で、上の例ではSHA256を使用しています。「電子政府における調達のために参照すべき暗号のリスト(CRYPTREC暗号リスト)」にもあるもので、安全であると見なされています(SHA384、SHA512についても同様)。
ComputeHashに渡す値はbyte配列ですので、文字列から変換しています。
また、得られたハッシュ値もbyte配列なので、BitConverterで16進数の文字列に変換しています。「そもそも、ハッシュ値って何?」という方のために
まぁ、ここで説明するよりググって頂いたほうが詳しい説明が見つかりますが、念のため書きますと、パスワード認証の実装で使用したり、改ざんの検知にも使用されるもので、ある元となる値(上の例で言えば「千尋」)をある関数(ハッシュ関数)に通すことで得られる、不可逆な固定長の値のことを指します。SHAの後ろの数字は、それぞれ出力されるビットの長さを表します。
ちなみに、このようなハッシュ関数を暗号学的ハッシュ関数と呼び、ハッシュ値はメッセージダイジェストとも呼ばれることもあります。
パスワード認証の場面だと、DB内にパスワードをプレーンテキストで保管するのを避けるため、ハッシュ値に変換するという使い方をします。ですので、衝突の発見が容易だとされているアルゴリズムを使用した場合、認証突破の危険性を持つことになります。
また、改ざんの検知では、ダウンロードしたファイルが改ざんされていないかを検知するのに利用されます。サイトのダウンロードページなどにハッシュ値が掲載されていることがあるので、たまに見るかな…と。
あと、Webサイト(アプリ)制作の際、CDNリソースを参照する際にintegrity属性を付けることがありますが、ここで指定する値もハッシュ値です。SRI(サブリソース完全性)で検索すると出てくると思います。.NETだとパスワード認証を実装する必要がある時は、この機能は使用するかもしれませんね。
アルゴリズムを選べるようにしてみる
上の例だとSHA256でのみ出力するので、選べるようにしてみましょう。
using System; using System.Security.Cryptography; using System.Text; Console.WriteLine("契約書だよ。そこに名前を書いてハッシュアルゴリズムを選びな。"); //名前(ハッシュコード計算の基となる値) Console.Write("名前:"); var name = Console.ReadLine(); //ハッシュアルゴリズム選択 Console.Write("アルゴリズム[MD5/SHA1/SHA256/SHA384/SHA512]:"); var algorism = Console.ReadLine(); algorism = algorism.ToUpper(); HashAlgorithm Hash; switch (algorism) { case "MD5": Hash = MD5.Create(); break; case "SHA1": Hash = SHA1.Create(); break; case "SHA256": Hash = SHA256.Create(); break; case "SHA384": Hash = SHA384.Create(); break; case "SHA512": Hash = SHA512.Create(); break; default: Console.WriteLine("アルゴリズムの選択間違ってるよ!"); Hash = SHA256.Create(); Environment.Exit(0); break; } Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ハッシュ値を算出 byte[] namebyte = Encoding.UTF8.GetBytes(name); byte[] hashbyte = Hash.ComputeHash(namebyte); string hashstring = BitConverter.ToString(hashbyte); Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!");行がちょっと長いですが、実行すると以下のようになります。
契約書だよ。そこに名前を書いてハッシュアルゴリズムを選びな。 名前:千尋 アルゴリズム[MD5/SHA1/SHA256/SHA384/SHA512]:SHA512 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2だ。いいかい、68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2だよ。分かったら返事をするんだ、68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2!!MD1、SHA1、SHA256、SHA384、SHA512は、いずれも基底クラスがHashAlgorithmです。どれも、Createメソッドでインスタンスを作成し、ComputeHashでハッシュ値を生成します。
アルゴリズムの指定を間違うと、怒られて終了します。
以上、C#で実装する「セキュアな湯婆婆」でした。
ソルトとストレッチングを実装して、より安全な湯婆婆にする(PBKDF2対応)
以下、2020/11/15追記分です。
ハッシュ値から元のパスワードを解析する手法もないわけではない
MD5とSHA1は現在では安全性が担保されていないことは上でも書きましたが、他にも、レインボーテーブルを使用した攻撃とか、攻撃対象のシステムにアカウントを作成して何らかの方法(SQLインジェクションとか)でハッシュ値を盗み出し、同一ハッシュ値を発見することでパスワードを特定するという方法があり得ます。この辺りは、↓の書籍が詳しいのでご興味がある方はどうぞ。
- 徳丸浩著 『体系的に学ぶ安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践』 SBクリエイティブ、2018
特に後者の方法をされてしまうと、上で説明した、単にSHA256でハッシュ値を求めただけの方法だと心許ない気がしますね。
で、ハッシュ解読を困難にする対策としては、以下のソルトを付加することと、ストレッチングがあります。
ソルト(salt)
ハッシュ値の元データ(パスワードなど)に付加する文字列です。レインボーテーブルを使用した攻撃の対策としてはある程度の長さのパスワードが必求められるので、元の文字列にソルトを付けて長さを確保します。加えて、ソルトをユーザー毎に異なるものにすることで、別々のユーザーが作成したパスワードが仮に同一だった場合でも、同じハッシュ値を生成させないようにできます。
ストレッチング(stretching)
ハッシュ計算を繰り返し実行することで計算をより困難にさせる、という方法です。
で、これらは、わざわざ自分でゼロから実装する必要はありません。このようなパスワード保管に適したハッシュ関数として、BCrypt、Argon2、PBKDF2があります。.NETでは最初からPBKDF2 (Password-Based Key Derivation Function 2)が使用可能です(BCryptとArgon2はNugetから入手できる)。
湯婆婆にもっと安全なハッシュ値を出力させてみる
前置きが長くなりましたが、PFKDF2を実装した湯婆婆が以下になります。
using System; using System.Security.Cryptography; Console.WriteLine("契約書だよ。そこに名前を書きな。"); var name = Console.ReadLine(); Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ソルトを作成(最低8byte必要) byte[] salt1 = new byte[8]; using (RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider()) { rngCsp.GetBytes(salt1); } //ハッシュ値を算出 string hashstring = string.Empty; //ストレッチングの反復処理回数1000、アルゴリズムにSHA256 using (Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(name, salt1, 1000, HashAlgorithmName.SHA256)) { byte[] hashbyte = pbkdf2.GetBytes(32); hashstring = BitConverter.ToString(hashbyte); } //出力 Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!"); Console.WriteLine($"そして、これがソルトだよ!:{BitConverter.ToString(salt1)}");こんな風に出力されました。
契約書だよ。そこに名前を書きな。 千尋 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6だ。いいかい、19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6だよ。分かったら返事をするんだ、19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6!! そして、これがソルトだよ!:AF-70-37-73-C6-E3-22-A9せっかくなので、ハッシュ値を突合させる処理も書いてみました。
色々とハードコーディングしていますが、例なので、ご容赦下さい。using System; using System.Security.Cryptography; string name = "千尋"; //前回決めたソルト string salt = "AF-70-37-73-C6-E3-22-A9"; //前回求めたハッシュ値 string prevhash = "19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6"; //ソルトをbyte配列に変換 string[] saltstrings = salt.Split("-"); byte[] saltbytes = new byte[saltstrings.Length]; for (int i=0; i < saltstrings.Length; i++) { saltbytes[i] = Convert.ToByte(saltstrings[i], 16); } //ハッシュ値を算出 string hashstring = string.Empty; using (Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(name, saltbytes, 1000, HashAlgorithmName.SHA256)) { byte[] hashbyte = pbkdf2.GetBytes(32); hashstring = BitConverter.ToString(hashbyte); } //結果判定を出力 if (prevhash.Equals(hashstring)) { Console.WriteLine("OK"); } else { Console.WriteLine("NG"); }以上です。
よりパスワードクラッキングされにくい、安全な湯婆婆ができあがりました。
- 投稿日:2020-11-14T23:43:41+09:00
ハッシュ値を出力する湯婆婆をC#で実装してみる(PBKDF2にも対応)
はじめに
先日、.NET 5.0がリリースされました。業務でがっつりC#を使うわけではありませんが、でも、せっかくなので触ってみたいと思ったのと、加えて、私が情報処理安全確保支援士(SC)の学習をしているのもあり、このような記事になりました。
でも、他の記事と同じように、これもネタ記事です。
(勉強も兼ねていますので、指摘は歓迎です)なお、本記事では.NET 5.0(C# 9.0)で実装していますが、基本的なコードの部分に関しては、以下の記事を参考にさせて頂きました(ありがとうございます)。
更新履歴
2020/11/14 記事公開
2020/11/15 パスワード保存に適したハッシュ関数(PBKDF2)を追記基本的なコード
最低限のコードは以下のような感じでしょうか。
C# 9.0から可能になったトップレベルステートメントで記述しています。
アルゴリズムにはSHA256を使用。using System; using System.Security.Cryptography; using System.Text; Console.WriteLine("契約書だよ。そこに名前を書きな。"); var name = Console.ReadLine(); Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ハッシュ値を算出 SHA256 Hash = SHA256.Create(); byte[] namebyte = Encoding.UTF8.GetBytes(name); byte[] hashbyte = Hash.ComputeHash(namebyte); string hashstring = BitConverter.ToString(hashbyte); Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!");実行結果は、こうなります。
ハッシュ値の基となる入力データは「千尋」です。契約書だよ。そこに名前を書きな。 千尋 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6Bだ。いいかい、05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6Bだよ。分かったら返事をするんだ、05-FA-F2-ED-C0-4D-95-28-79-2E-AB-C5-EE-80-33-A5-52-24-73-09-92-54-06-70-A3-E9-B9-A5-90-D7-22-6B!!解説
以下はハッシュ関数のアルゴリズムとしてよく知られているものだと思いますが、.NETでも標準で使用可能です。
- SHA256
- SHA384
- SHA512
実は、SHA1とMD5も使用できますが、この2つは現在では安全ではないとされており、使用は推奨されていません。というのも、この2つは現在では現実的な時間で同一ハッシュ値(もしくは元データ)を得ることが可能であることが分かっているためです。ちなみに、元データが異なるのにハッシュ値が同一になることを「衝突」と呼びますが、ハッシュ関数には衝突への耐性が求められます(強衝突耐性)。
で、上の例ではSHA256を使用しています。「電子政府における調達のために参照すべき暗号のリスト(CRYPTREC暗号リスト)」にもあるもので、安全であると見なされています(SHA384、SHA512についても同様)。
ComputeHashに渡す値はbyte配列ですので、文字列から変換しています。
また、得られたハッシュ値もbyte配列なので、BitConverterで16進数の文字列に変換しています。「そもそも、ハッシュ値って何?」という方のために
まぁ、ここで説明するよりググって頂いたほうが詳しい説明が見つかりますが、念のため書きますと、パスワード認証の実装で使用したり、改ざんの検知にも使用されるもので、ある元となる値ないしデータ(上の例で言えば「千尋」)をある関数(ハッシュ関数)に通すことで得られる、不可逆な固定長の値のことを指します。SHAの後ろの数字は、それぞれ出力されるビットの長さを表します。
ちなみに、このようなハッシュ関数を暗号学的ハッシュ関数と呼び、ハッシュ値はメッセージダイジェストとも呼ばれることもあります。
パスワード認証の場面だと、DB内にパスワードをプレーンテキストで保管するのを避けるため、ハッシュ値に変換するという使い方をします。ですので、衝突の発見が容易だったり、ブルートフォースで元データが現実的な時間で特定可能だったりすると、認証突破の危険性を持つことになります。
また、改ざんの検知では、ダウンロードしたファイルが改ざんされていないかを検知するのに利用されます。サイトのダウンロードページなどにハッシュ値が掲載されていることがあるので、たまに見るかな…と。
あと、Webサイト(アプリ)制作の際、CDNリソースを参照する際にintegrity属性を付けることがありますが、ここで指定する値もハッシュ値です。SRI(サブリソース完全性)で検索すると出てくると思います。.NETだとパスワード認証を実装する必要がある時は、この機能は使用するかもしれませんね。
アルゴリズムを選べるようにしてみる
上の例だとSHA256でのみ出力するので、選べるようにしてみましょう。
(※追記:条件分岐の箇所はタプルとC# 8.0から追加されたswitch式で、実はもう少しすっきり書けます。具体的には、コメント欄をご覧ください)using System; using System.Security.Cryptography; using System.Text; Console.WriteLine("契約書だよ。そこに名前を書いてハッシュアルゴリズムを選びな。"); //名前(ハッシュコード計算の基となる値) Console.Write("名前:"); var name = Console.ReadLine(); //ハッシュアルゴリズム選択 Console.Write("アルゴリズム[MD5/SHA1/SHA256/SHA384/SHA512]:"); var algorism = Console.ReadLine(); algorism = algorism.ToUpper(); HashAlgorithm Hash; switch (algorism) { case "MD5": Hash = MD5.Create(); break; case "SHA1": Hash = SHA1.Create(); break; case "SHA256": Hash = SHA256.Create(); break; case "SHA384": Hash = SHA384.Create(); break; case "SHA512": Hash = SHA512.Create(); break; default: Console.WriteLine("アルゴリズムの選択間違ってるよ!"); Hash = SHA256.Create(); //dummy Environment.Exit(0); break; } Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ハッシュ値を算出 byte[] namebyte = Encoding.UTF8.GetBytes(name); byte[] hashbyte = Hash.ComputeHash(namebyte); string hashstring = BitConverter.ToString(hashbyte); Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!");行がちょっと長いですが、実行すると以下のようになります。
契約書だよ。そこに名前を書いてハッシュアルゴリズムを選びな。 名前:千尋 アルゴリズム[MD5/SHA1/SHA256/SHA384/SHA512]:SHA512 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2だ。いいかい、68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2だよ。分かったら返事をするんだ、68-C3-EC-FC-8C-B7-F2-C9-42-DF-AC-B0-5F-DA-EC-2F-C3-81-0A-89-87-21-F1-55-D1-F5-CE-AC-DF-23-46-1D-25-29-D8-50-75-3D-F5-60-55-F6-FA-D1-CD-EB-14-DD-E0-3F-C0-09-E0-9D-99-A5-24-83-7D-8A-46-25-8B-A2!!MD1、SHA1、SHA256、SHA384、SHA512は、いずれも基底クラスがHashAlgorithmです。どれも、Createメソッドでインスタンスを作成し、ComputeHashでハッシュ値を生成します。
アルゴリズムの指定を間違うと、怒られて終了します。
以上、C#で実装する「セキュアな湯婆婆」でした。
ソルトとストレッチングを実装して、より安全な湯婆婆にする(PBKDF2対応)
以下、2020/11/15追記分です。
ハッシュ値から元のパスワードを解析する手法もないわけではない
MD5とSHA1は現在では安全性が担保されていないことは上でも書きましたが、他にも、レインボーテーブルを使用した攻撃とか、攻撃対象のシステムにアカウントを作成して何らかの方法(SQLインジェクションとか)でハッシュ値を盗み出し、同一ハッシュ値を発見することでパスワードを特定するという方法があり得ます。この辺りは、↓の書籍が詳しいのでご興味がある方はどうぞ。
- 徳丸浩著 『体系的に学ぶ安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践』 SBクリエイティブ、2018
特に後者の方法をされてしまうと、上で説明した、単にSHA256でハッシュ値を求めただけの方法だと心許ない気がしますね。
で、ハッシュ解読を困難にする対策としては、以下のソルトを付加することと、ストレッチングがあります。
ソルト(salt)
ハッシュ値の元データ(パスワードなど)に付加する文字列です。レインボーテーブルを使用した攻撃の対策としてはある程度の長さのパスワードが求められるので、元の文字列にソルトを付けて長さを確保します。加えて、ソルトをユーザー毎に異なるものにすることで、別々のユーザーが作成したパスワードが仮に同一だった場合でも、同じハッシュ値を生成させないようにできます。
ストレッチング(stretching)
ハッシュ計算を繰り返し実行することで計算をより困難にさせる、という方法です。
これらは、わざわざ自分でゼロから実装する必要はありません。
OWASPのサイト(Password Storage Cheat Sheet - Custom Algorithms)で独自にアルゴリズムを組むことはやめるべきと書いてありますが(わざわざreally hardとかDo not do thisと太字で書いてある)、完成されたものがあるなら、わざわざ作るのもリスクになる気がします。ソルトやストレッチングの仕組みを備えたパスワード保管により適したハッシュ関数は既に考案されていて、BCrypt、Argon2、PBKDF2があります。.NETでは最初からPBKDF2 (Password-Based Key Derivation Function 2)が使用可能です。ちなみに、BCryptとArgon2はNugetから入手できます。
湯婆婆にもっと安全なハッシュ値を出力させてみる
前置きが長くなりましたが、PFKDF2を実装した湯婆婆が以下になります。
Rfc2898DeriveBytesというクラスを使用します。using System; using System.Security.Cryptography; Console.WriteLine("契約書だよ。そこに名前を書きな。"); var name = Console.ReadLine(); Console.WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。"); //ソルトを作成(最低8byte必要) byte[] salt1 = new byte[8]; using (RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider()) { rngCsp.GetBytes(salt1); } //ハッシュ値を算出 string hashstring = string.Empty; //ストレッチングの反復処理回数1000、アルゴリズムにSHA256 using (Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(name, salt1, 1000, HashAlgorithmName.SHA256)) { byte[] hashbyte = pbkdf2.GetBytes(32); hashstring = BitConverter.ToString(hashbyte); } //出力 Console.WriteLine($"今からお前の名前は{hashstring}だ。いいかい、{hashstring}だよ。分かったら返事をするんだ、{hashstring}!!"); Console.WriteLine($"そして、これがソルトだよ!:{BitConverter.ToString(salt1)}");ちなみに、ソルトをRNGCryptoServiceProviderクラスを使用して取得していますが、Rfc2898DeriveBytesのインスタンス作成時の第2引数(上の例で言えばsalt1)を整数にすることで、自動でソルトを作成させることも可能です。その場合、
pbkdf2.Salt
でソルトのbyte配列を取得可能です。文字列で取り出すなら、BitConverter.ToString(pbkdf2.Salt)
ですね。で、出力はこんな風です。
契約書だよ。そこに名前を書きな。 千尋 フン。千尋というのかい。贅沢な名だねぇ。 今からお前の名前は19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6だ。いいかい、19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6だよ。分かったら返事をするんだ、19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6!! そして、これがソルトだよ!:AF-70-37-73-C6-E3-22-A9せっかくなので、ハッシュ値を突合させる処理も書いてみました。
色々とハードコーディングしていますが、例なので、ご容赦下さい。using System; using System.Security.Cryptography; string name = "千尋"; //前回決めたソルト string salt = "AF-70-37-73-C6-E3-22-A9"; //前回求めたハッシュ値 string prevhash = "19-A3-4B-9E-D8-25-4E-A7-12-02-86-D7-AB-6D-D7-7B-8D-C6-CB-B9-0C-C8-70-18-44-64-6F-DF-2D-63-AF-C6"; //ソルトをbyte配列に変換 string[] saltstrings = salt.Split("-"); byte[] saltbytes = new byte[saltstrings.Length]; for (int i=0; i < saltstrings.Length; i++) { saltbytes[i] = Convert.ToByte(saltstrings[i], 16); } //ハッシュ値を算出 string hashstring = string.Empty; using (Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(name, saltbytes, 1000, HashAlgorithmName.SHA256)) { byte[] hashbyte = pbkdf2.GetBytes(32); hashstring = BitConverter.ToString(hashbyte); } //結果判定を出力 if (prevhash.Equals(hashstring)) { Console.WriteLine("OK"); } else { Console.WriteLine("NG"); }以上です。
よりパスワードクラッキングされにくい、安全な湯婆婆ができあがりました。
- 投稿日:2020-11-14T16:35:39+09:00
Unity オブジェクトの移動
- 投稿日:2020-11-14T11:47:43+09:00
メモ SQLserverのVarchar型のOutputパラメータの文字数について
ストアドのOutputパラメータの文字数について
ストアドでVarchar型でOutputのパラメータを受け取ろうとしていましたが、
桁数の指定が漏れていたため1桁しか返してこなかった話。試そうとしたコードは、以下になります。
Dim conn As SqlClient.SqlConnection Dim comm As New SqlClient.SqlCommand comm.Connection = conn Dim sSQL As String = "ストアド" comm.CommandText = sSQL comm.CommandType = CommandType.StoredProcedure comm.Parameters.Clear() comm.Parameters.Add("@Input変数", SqlDbType.Int).Direction = ParameterDirection.ReturnValue comm.Parameters.Add("@Output変数", SqlDbType.Varchar).Direction = ParameterDirection.Output10桁ある文字数の1桁目しか返していないことから
色々調べたところ桁数の指定が必要だとのことでした。桁数を指定して受け取ろう
以下のコードに修正しました。
comm.Parameters.Add("@Output変数", SqlDbType.Varchar, 10).Direction = ParameterDirection.Output10という桁数を指定することで正しい文字列が返ってきました。
- 投稿日:2020-11-14T01:31:34+09:00
Unity + VSCodeで関数ジャンプができなくなった
はじめに
Unityでスクリプトを書く際のエディターは普段VSCodeを使っています。しかし突然、原因はわからないのですが、関数ジャンプができなくなりました。
その時解決した方法をメモします。環境
macOS Catalina
Version: 10.15.7Visual Studio Code
Version: 1.51.0解決方法
結論から言うと、以下の手順で解決。
1. VSCodeにインストール済みの拡張機能C# Extensions
を開く。
2.Settings
>User
>OmniSharp: Use Global Mono
のタブをalways
にする。解決のために他に試したこと
VSCode Extensionの確認
関数ジャンプのために必要な拡張機能が入っているかなどを確認。
UnityとVSCodeの紐付けを再設定。
そういえば過去に似たような事例に遭遇してました。
- UnityでVSCodeを使用中、関数ジャンプや参照ができない!の解決方法VSCodeを入れ直す
VSCodeを一度アンインストールしてインストールし直しました。
VSCodeのバージョンをグレードダウン
公式サイトからVSCodeのバージョンを下げてみた。
VSCodeの再起動
VSCodeを立ち上げ直してみる。
PC本体の再起動
本体を立ち上げ直してみる。
参考