銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

.NET Core で暗号論的疑似乱数生成器を使ってソルトを作成し、PBKDF2でパスワードのハッシュ化を試みる

この時代、自前でパスワードの管理などしたくはないのですが、しないといけないケースもあるでしょう。最低限やらないといけないこととしては、ソルト生成した上でハッシュ化したパスワードを保存することではないでしょうか。.NET Coreでこれらの処理を行う必要があった時に調べたコードをまとめます。

まずソルトを作成する場合、.NET CoreではRNGCryptoServiceProviderクラスを使って暗号論的に乱数を生成します。GetBytesメソッドは、引数に指定したbyte配列を、生成した乱数で埋めます。

docs.microsoft.com

次にパスワードのハッシュ化ですが、PBKDF2を利用する場合、Rfc2898DeriveBytesクラスを利用できます。ハッシュ化対象の文字列、ソルト、反復回数、ハッシュアルゴリズム名を指定できます。ハッシュアルゴリズムはデフォルトがSHA1なので適宜変更しましょう。

docs.microsoft.com

以上をまとめてコードにするとこうなります。hashedPasswordはbyte配列なので適宜保存しましょう。

using System.Collections.Generic;
using System.Security.Cryptography;

var password = "<入力されたパスワード>";
//必要な長さ分の配列を先に用意する
var salt = new byte[1024];
//RNGCryptoServiceProviderはIDisposableを実装しているので適宜破棄する
using var rng = new RNGCryptoServiceProvider()
rng.GetBytes(salt);

var b = new Rfc2898DeriveBytes(password, salt, 100, HashAlgorithmName.SHA256);
var hashedPassword = b.GetBytes(256);

ソルトとハッシュ値を保存しておき、入力されたパスワードと照合する場合は、保存したソルトを使って、同じ反復回数、ハッシュアルゴリズムでハッシュ化し、両者を比べます。byte配列なのでSequenceEqualメソッドを使っています。

using System.Collections.Generic;
using System.Security.Cryptography;
using System.Linq;

var password = "<入力されたパスワード>";
var salt = "<保存していたソルト>";
var hashedPassword = "<保存していたハッシュ値>";
var b = new Rfc2898DeriveBytes(password, salt, 100, HashAlgorithmName.SHA256);
var challengePassword = b.GetBytes(256);
if (!(hashedPassword?.SequenceEqual(challengePassword) == true))
{
    throw new Exception("ログイン失敗");
}

.NET CoreでUTCのISO8601形式の時刻文字列から指定のタイムゾーンに変換する

例えば 2020-03-23T12:00:00.000Z というUTCでのISO8601形式の文字列をパースしたうえで、指定したタイムゾーンでの時間に変換したいとします。2020-03-23T12:00:00.000Zであれば、日本標準時(UTC+9)で2020年3月23日21時です。これは次のようなコードで処理できます。

var tokyoStandardTime = TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time");
var dateString = "2020-03-23T12:00:00.000Z";
var convertedDate = TimeZoneInfo.ConvertTime(DateTimeOffset.Parse(dateString), tokyoStandardTime)

.NET Coreの場合、タイムゾーンの情報を含んだ特定の日時はDateTimeOffset構造体を使うのがよいでしょう。

docs.microsoft.com

手元の環境(Windows 10およびmcr.microsoft.com/dotnet/core/sdk のLinuxコンテナ)では、ParseメソッドやTryParseメソッドでISO8601形式が処理できました。厳密に扱いたい場合はフォーマット文字列を指定するのがよいかもしれません。与えた文字列はUTCでの日時を表現しているので、この時点でDateTimeOffsetオブジェクトではUTCである情報がセットされています。これを異なるタイムゾーンでの同じ日時に変換するためにはTimeZoneInfo.ConvertTimeメソッドを使います。もしくは、TimeZoneInfoオブジェクトの生成を経由せずにConvertTimeBySystemTimeZoneIdメソッドも利用できます。

docs.microsoft.com

TimeZoneのID指定についてはWindowsとLinux環境で異なるので昨日の記事も参考にしてください。

tech.tanaka733.net

ちなみに次のようなコードは、環境により実行時例外になります。いくつか試したところ、WindowsやWSLのUbuntuでは動くのですが、mcr.microsoft.com/dotnet/core/sdk のLinuxコンテナではエラーになりました。

var tokyoStandardTime = TimeZoneInfo.FindSystemTimeZoneById("Asia/Tokyo");
var dateString = "2020-03-23T12:00:00.000Z";
var convertedDate = new DateTimeOffset(DateTime.Parse(dateString), tokyoStandardTime.BaseUtcOffset);
Exception has occurred: CLR/System.ArgumentException
型 'System.ArgumentException' のハンドルされていない例外が System.Private.CoreLib.dll で発生しました: 'The UTC Offset of the local dateTime parameter does not match the offset argument.'

素直にドキュメントでも使われているConvertTimeメソッドを使うのが無難なようです。

.NET CoreでWindowsとLinuxでタイムゾーンを識別するID表記が異なるという話

最近、ASP.NET CoreでTimeZoneまわりのコード書いてて遭遇しました。以前このブログ読んだ記憶はあるのですが、すっかり忘れていました。そして、.NET Core 3でもそのままなんだなというのがわかりました。

devblogs.microsoft.com

.NET Coreで特定のTimeZoneを取得する場合、Windowsな人はこのようなコードを書くと思います。

System.TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time");

当然Windowsでは動くのですが、Unix系システムの上(多いと思われるのがLinuxコンテナ上)で動かすとこんなエラーが発生します。

Exception has occurred: CLR/System.TimeZoneNotFoundException
An unhandled exception of type 'System.TimeZoneNotFoundException' occurred in System.Private.CoreLib.dll: 'The time zone ID 'Tokyo Standard Time' was not found on the local computer.'

これはTimZoneを識別するIDがプラットフォームで異なるという問題によるものです。ブログにある通り、Windowsでは次のように管理されている一方

docs.microsoft.com

LinuxではIANAで公開されているもので管理されています。

www.iana.org

ようは、.NET Coreの内部で情報をもたずにOS(というべきか基本パッケージ・ライブラリとよぶべきか)の持っている情報へのプロキシとして動いている状況です。このあたりは日本の元号の扱いと似たような感じかもしれません。

さて、Linuxで動かすためには次のように書けばOKです。

System.TimeZoneInfo.FindSystemTimeZoneById("Asia/Tokyo");

ただし、当然のように、このコードはWindows上では動きません。そこで最初に紹介したブログではTimeZoneConverterというNuGetライブラリを紹介しています。

github.com

これを使えば次のどちらで書いても動作します。

TimeZoneConverter.TZConvert.GetTimeZoneInfo("Tokyo Standard Time");
TimeZoneConverter.TZConvert.GetTimeZoneInfo("Asia/Tokyo");

また、タイムゾーンの情報を扱う場合、最新の変更はWindowsであればWindows Update、Linuxであればtzdataパッケージの更新などで配布されます。そのため、クライアントアプリなど実行環境が管理できない場合、かならずしも実行結果が一致しない可能性があることに加え、TimeZoneConverterは.NET Core SDKのライブラリではないため別途更新が必要となります。TimeZoneConverterは、TimeZoneの情報そのものを格納しているわけではなく、プラットフォームで異なるIDのマッピングを管理しているだけのようですので、このライブラリを使う場合は、かならずライブラリの更新とOS側の更新が必要になると思われます。