しばたテックブログ

気分で書いている技術ブログです。

続・Get-Randomで使われている乱数生成アルゴリズムについて調べてみた

PowerShell Advent Calendar 2017、13日目です。

qiita.com

以前に書いた、

blog.shibata.tech

の続きになります。

本エントリではPowerShell 6.0でGet-Randomで使われている乱数生成アルゴリズムがどう変化したか調べた結果について説明します。

はじめに

これまでのバージョンのPowerShellとPowerShell 6.0で大きく異なる点は、

  • PowerShell Coreとして.NET Core製になったこと
  • クロスプラットフォーム化

になります。

PowerShellはオープンソースになったので実際のソースを確認しながら上記の点を中心に変化を調べていきます。

PowerShell 6.0におけるGet-Randomの実装

現時点で最新のPowerShell 6.0-RC(.NET Core 2.0.0)におけるGet-Randomの実装は、これまで通りGetRandomCommandクラスにあります。

ソースの基本的な構成はPowerShell 5.0~5.1の構成と大きく変わっておらず、乱数生成にはPolymorphicRandomNumberGeneratorクラスが使われています。

このクラスでは、-SetSeedパラメーターを指定してシードを指定した場合はSystem.Randomクラスを、シードを指定しない場合はSystem.Security.Cryptography.RandomNumberGeneratorクラスのRandomNumberGenerator.Create()メソッドから生成されるインスタンスを使って乱数生成を行います。

ここまでは前回と同様なのですが、それぞれのクラスは.NET Core版なので.NET Framework版のそれとは実装が異なります。

System.Randomクラスの実装について

System.Randomクラスの実装は.NET Coreでも変わっておらず、乱数生成にはKnuthの減算法に基づいたアルゴリズムが使用されます。
こちらについては変化はなく全てのプラットフォームにおいて共通です。

The Art of Computer Programming (2) 日本語版 Seminumerical algorithms Ascii Addison Wesley programming series

The Art of Computer Programming (2) 日本語版 Seminumerical algorithms Ascii Addison Wesley programming series

  • 作者: Donald E.Knuth,有沢誠,和田英一,斎藤博昭,長尾高弘,松井祥悟,松井孝雄,山内斉
  • 出版社/メーカー: アスキー
  • 発売日: 2004/10
  • メディア: 単行本
  • 購入: 1人 クリック: 80回
  • この商品を含むブログ (44件) を見る

System.Security.Cryptography.RandomNumberGeneratorクラスの実装について

System.Security.Cryptography.RandomNumberGenerator.Create()メソッドの実装は.NET Coreでは大きく変わっています。

前回の場合ではこのメソッドによりSystem.Security.Cryptography.RNGCryptoServiceProvideクラスのインスタンスが生成されていましたが、.NET CoreではSystem.Security.Cryptography.RandomNumberGeneratorImplementationクラスのインスタンスが生成され、このRandomNumberGeneratorImplementationの実装はプラットフォーム毎に異なります。

WindowsにおけるRandomNumberGeneratorImplementationクラスの実装

RandomNumberGeneratorImplementationクラスのWindowsに固有の実装はこちらにあり、以下の様になっています。

using System.Diagnostics;

namespace System.Security.Cryptography
{
    partial class RandomNumberGeneratorImplementation
    {
        private unsafe void GetBytes(byte* pbBuffer, int count)
        {
            Debug.Assert(pbBuffer != null);
            Debug.Assert(count > 0);

            Interop.BCrypt.NTSTATUS status = Interop.BCrypt.BCryptGenRandom(pbBuffer, count);
            if (status != Interop.BCrypt.NTSTATUS.STATUS_SUCCESS)
                throw Interop.BCrypt.CreateCryptographicException(status);
        }
    }
}

乱数生成には

Interop.BCrypt.BCryptGenRandom(pbBuffer, count);

System.Interop.BCryptクラスが使われ以下の様に定義されています。

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

internal partial class Interop
{
    internal partial class BCrypt
    {
        internal static unsafe NTSTATUS BCryptGenRandom(byte* pbBuffer, int count)
        {
            Debug.Assert(pbBuffer != null);
            Debug.Assert(count >= 0);

            return BCryptGenRandom(IntPtr.Zero, pbBuffer, count, BCRYPT_USE_SYSTEM_PREFERRED_RNG);
        }

        private const int BCRYPT_USE_SYSTEM_PREFERRED_RNG = 0x00000002;

        [DllImport(Libraries.BCrypt, CharSet = CharSet.Unicode)]
        private static extern unsafe NTSTATUS BCryptGenRandom(IntPtr hAlgorithm, byte* pbBuffer, int cbBuffer, int dwFlags);
    }
}

最終的に乱数生成にはCryptography API: Next Generation(CNG)BCryptGenRandom関数が使われます。

この関数で使われるアルゴリズムについてですが...よくわかりませんでした。
セキュリティに詳しい方がいらっしゃいましたら是非教えてください。

【ちょっと追記】

BCryptGenRandom関数の定義に、

The default random number provider implements an algorithm for generating random numbers that complies with the NIST SP800-90 standard, specifically the CTR_DRBG portion of that standard.

とあるので、デフォルトではNIST SP 800-90というアルゴリズムが使われるようです。

.NET Coreの実装はおそらくデフォルト指定なのでPowerShellにおいもこのアルゴリズムが使われていそうです。
ただ、このアルゴリズムがどんなものなのかはさっぱりです...

Unix(Linux)におけるRandomNumberGeneratorImplementationクラスの実装

RandomNumberGeneratorImplementationクラスのUnixに固有の実装はこちらにあり、以下の様になっています。

using System.Diagnostics;

namespace System.Security.Cryptography
{
    partial class RandomNumberGeneratorImplementation
    {
        private unsafe void GetBytes(byte* pbBuffer, int count)
        {
            Debug.Assert(pbBuffer != null);
            Debug.Assert(count > 0);

            if (!Interop.Crypto.GetRandomBytes(pbBuffer, count))
            {
                throw Interop.Crypto.CreateOpenSslCryptographicException();
            }
        }
    }
}

乱数生成には

Interop.Crypto.GetRandomBytes(pbBuffer, count);

とUnix固有のSystem.Interop.Cryptクラスが使われています。
このクラスの実装に関して、使用されるアルゴリズム毎に実装されるファイルが分かれているのですが、GetRandomBytesに関しては"おそらく"Interop.RAND.csファイルの実装が使われる様です。*1

この実装については以下の様になっています。

using System.Diagnostics;
using System.Runtime.InteropServices;

internal static partial class Interop
{
    internal static partial class Crypto
    {
        internal static unsafe bool GetRandomBytes(byte* pbBuffer, int count)
        {
            Debug.Assert(pbBuffer != null);
            Debug.Assert(count >= 0);

            return CryptoNative_GetRandomBytes(pbBuffer, count);
        }

        [DllImport(Libraries.CryptoNative)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern unsafe bool CryptoNative_GetRandomBytes(byte* buf, int num);
    }
}

内部で呼ばれているCryptoNative_GetRandomBytesの実装はこちらにあり、抜粋すると以下の実装となっています。

/*
Function:
GetRandomBytes
Puts num cryptographically strong pseudo-random bytes into buf.
Return values:
Returns a bool to managed code.
1 for success
0 for failure
*/
extern "C" int32_t CryptoNative_GetRandomBytes(uint8_t* buf, int32_t num)
{
    int ret = RAND_bytes(buf, num);

    return ret == 1;
}

最終的にはOpenSSLのRAND_bytes関数が使われています。

アルゴリズムについては...こちらもよくわかりませんでした...orz

【ちょっと追記】

細かい話がWikiに載っているようなので、気になる方は調べてみるとよいかもしれません。

macOSにおけるRandomNumberGeneratorImplementationクラスの実装

最後に、RandomNumberGeneratorImplementationクラスのmacOSに固有の実装はこちらにあり、以下の様になっています。

using System.Diagnostics;

namespace System.Security.Cryptography
{
    partial class RandomNumberGeneratorImplementation
    {
        private unsafe void GetBytes(byte* pbBuffer, int count)
        {
            Debug.Assert(pbBuffer != null);
            Debug.Assert(count > 0);

            Interop.AppleCrypto.GetRandomBytes(pbBuffer, count);
        }
    }
}

乱数生成には

Interop.AppleCrypto.GetRandomBytes(pbBuffer, count);

System.Interop.AppleCryptoクラスが使われ以下の様に定義されています。

using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;

internal static partial class Interop
{
    internal static partial class AppleCrypto
    {
        internal static unsafe void GetRandomBytes(byte* pbBuffer, int count)
        {
            Debug.Assert(pbBuffer != null);
            Debug.Assert(count >= 0);

            int errorCode;
            int ret = AppleCryptoNative_GetRandomBytes(pbBuffer, count, out errorCode);

            if (ret == 0)
            {
                throw CreateExceptionForCCError(errorCode, CCRNGStatus);
            }

            if (ret != 1)
            {
                throw new CryptographicException();
            }
        }

        [DllImport(Libraries.AppleCryptoNative)]
        private static extern unsafe int AppleCryptoNative_GetRandomBytes(byte* buf, int num, out int errorCode);
    }
}

内部で呼ばれているAppleCryptoNative_GetRandomBytesの実装はこちらにあり、以下の実装となっています。

#include "pal_random.h"

#include <CommonCrypto/CommonCrypto.h>
#include <CommonCrypto/CommonRandom.h>

extern "C" int32_t AppleCryptoNative_GetRandomBytes(uint8_t* pBuf, uint32_t cbBuf, int32_t* pkCCStatus)
{
    if (pBuf == nullptr || pkCCStatus == nullptr)
        return -1;

    CCRNGStatus status = CCRandomGenerateBytes(pBuf, cbBuf);
    *pkCCStatus = status;
    return status == kCCSuccess;
}

最終的に乱数生成にはCommonCryptoCCRandomGenerateBytes関数が使われています。

アルゴリズムは、というかこのライブラリ自体がどういったものかよくわかりません。
OS標準の機能なんですよね?多分...
Macに詳しい方なら余裕でわかるものなのでしょうか?

まとめ

アルゴリズムの詳細についてはわからないことだらけでしたが、実装については明らかにできたかと思います。

前回の分と合わせてGet-Randomの実装をまとめると、

  • PowerShell 2.0 ~ 4.0

    • シード指定あり => .NET FrameworkのSystem.Randomクラスで乱数生成
    • シード指定なし => .NET FrameworkのSystem.Randomクラスで乱数生成
  • PowerShell 5.0 ~ 5.1

    • シード指定あり => .NET FrameworkのSystem.Randomクラスで乱数生成
    • シード指定なし => .NET FrameworkのSystem.Security.Cryptography.RNGCryptoServiceProviderクラスで乱数生成
  • PowerShell 6.0

    • シード指定あり => .NET CoreのSystem.Randomクラスで乱数生成
      • Randomクラスの実装は全プラットフォーム共通
    • シード指定なし => .NET CoreのSystem.Security.Cryptography.RandomNumberGeneratorImplementationクラスで乱数生成
      • RandomNumberGeneratorImplementationクラスの実装はプラットフォーム毎に異なる

となります。

*1:正直ちょっと自信がありません...

PowerShell Core 6.0をデバッグ実行する - Visual Studio Code編

PowerShell Advent Calendar 2017、11日目です。

qiita.com

PowerShell 6.0のGAリリースも間近となりビルド・デバッグ手順も固まってきたと思えたので本エントリを書きます。

はじめに

PowerShellがオープンソース化した時点でPowerShellのビルドはVisual Studio 2015で行われており、ソリューションの構成で、

  • .NET Frameworkで動作するWindows PowerShell
  • NET Coreで動作するPowerShell Core

をそれぞれ個別にビルドできる様になっていました。

その後PowerShell 6.0がPowerShell Coreのみをリリースする方針となってからは、Windows PowerShell専用の処理を削除しつつ、Visual Studio 2017とVisual Studio Codeでビルド・デバッグができる様に変更されています。
(ただ、Visual Studio 2017でのビルドについては現在も目下修正中の様で手元の環境ではうまくいきませんでした。追記しました)

細かい話はGitHub上の、

PowerShell/docs/building at master · PowerShell/PowerShell · GitHub

PowerShell/docs/debugging at master · PowerShell/PowerShell · GitHub

のドキュメントに記載されていますので興味があればご覧ください。

PowerShellをデバッグ実行する

本エントリではWindows環境、64bit Windows 10(1709)でVisual Studio Codeを使用した場合の手順を紹介します。
他のOSについては先述のGihHubのドキュメントに手順がありますのでそちらをご覧ください。

1. Gitのインストール

ソースコードをCloneするのにGitクライアントが必要です。
適当にインストールしてgitコマンドが使える様にしておいてください。

git-for-windows.github.io

2. Visual Studio Codeのインストール

Visual Studio Codeを適当にインストールしてください。

code.visualstudio.com

今回は64bit版のVer.1.18.1をインストールします。
また、以下の拡張が必要になるのでインストールしておいてください。

  • C#拡張 (現時点のバージョンは1.13.1)
  • PowerShell拡張 (現時点のバージョンは1.5.1)

f:id:stknohg:20171206162748p:plain

f:id:stknohg:20171206162759p:plain

3. ソースコードのClone

PowerShellのリポジトリよりソースコードをCloneします。

ソースコードを修正したい、Pull Requestを送りたいといった場合であればForkしてからのCloneとなるでしょうが、今回はとりあえずデバッグ実行できれば良いのでリポジトリを直接Cloneします。

任意のディレクトリで以下の様に--recursiveオプションを付けてgit cloneしてください。

git clone --recursive https://github.com/PowerShell/PowerShell.git

実行後PowerShellディレクトリが作成されソースコードがCloneされているはずです。

4. その他必要ツールのインストール

PowerShellをビルド・デバッグ実行するには上記の他に

が必要となります。

これらのツールを個別にインストールしても構わないのですが、PowerShellのソースコードにはビルトのための専用モジュールがあり、このモジュールの機能を利用することで必要なツールをまとめてインストールすることができます。

ソースコードのルートディレクトリにbuild.psm1というモジュールがあるので、これをインポートしStart-PSBootstrap関数を実行すると必要なツールをまとめてインストールしてくれます。

# cd [PowerShellのルートディレクトリ]
cd .\PowerShell\
Import-Module .\build.psm1
Start-PSBootstrap

f:id:stknohg:20171209195521p:plain

このStart-PSBootstrapでは$env:LOCALAPPDATA\Microsoft\配下のフォルダに必要なツールをインストールします。
現時点では、

  • .NET Core SDK : Ver.2.0.2
  • PowerShell 6.0 : Ver.6.0.0-rc
  • RCEdit : Ver.0.2.0

がインストールされます。
(今後インストールされるバージョンは上がっていくでしょう)

ここで、仕様なのかバグなのか不明なのですが、.NET Core SDKをインストールした後にdonetコマンドのパス($env:LOCALAPPDATA\Microsoft\dotnet)がPATH環境変数に永続して登録されませんでした。
このため、手動でPATHに登録するかビルド・デバッグ実行の都度PATHに追加しておく必要があります。

5. ビルドとデバッグ実行

最後にVisual Studio Codeを起動し、PowerShellをデバッグ実行してみます。

# dotnetコマンドのPATHを通してからVisual Studio Codeを実行
$env:PATH += ";$env:LOCALAPPDATA\Microsoft\dotnet"
code

PowerShellにはC++のソースも一部含まれているため、初回起動時に以下の様なダイアログが出ますが放置して大丈夫です。

f:id:stknohg:20171206162852p:plain

ここでデバッグの構成をみると、下図の様に、

  • .NET Core Launch
  • .NET Core Attach

が登録されており、.NET Core Launchを実行すればPowerShellのビルトとデバッグ実行が可能です。
F5キー実行でも構いません。

f:id:stknohg:20171206162911p:plain

最初のビルドにはしばらく時間がかかりますので焦らず待ちます。
ビルドエラーが出る様であれば適宜対処してください。

f:id:stknohg:20171206162944p:plain

最終的には下図の様にPowerShellをデバッグ実行することができます。

f:id:stknohg:20171206163006p:plain

【補足】PowerShellをビルドする

PowerShellをビルドするだけであればVisual Studio Codeが無くても先述のbuild.psm1モジュールを使い、Start-PSBuildコマンドで可能です。

# Start-PSBuildのパラメーターはこの例の他にもいろいろあります
Import-Module .\build.psm1
Start-PSBuild -Configuration Release

最後に

とりあえずこんな感じです。
PowerShellをデバッグ実行することでソースの修正はもちろんですが、怪しいと思った挙動の詳細を細かく追うことも可能になります。

みなさんも是非トライしてみてください。


【2018/04/12追記】

Visual Studio 2017を使った手順を書きました。

blog.shibata.tech

Redmineをアップグレードしてみた

結構前にやった作業記録なんですが、個人的な備忘録としてブログに残しておきます。

blog.shibata.tech

で構築したRedmineですが新しいバージョンがリリースされていたのでアップグレードしてみました。
3.4.2 → 3.4.3へのアップグレードをしています。

1. 公式な手順

Redmineのアップグレードについて、公式な手順はこちらにあります。

アップグレード - Redmine Guide 日本語訳

日本語訳がしっかり用意されているのは私みたいな利用者からすると非常にありがたいです。

環境によって細かい手順は変わりますが、基本的には、

  • 新バージョンのRedmineの展開
  • データベースのバックアップと(必要があれば)移行
  • 添付ファイルのバックアップと移行
  • Redmineのアップグレード
  • データベースのアップグレード
  • クリーンアップ

という手順を踏むことになります。

1. 新バージョンのRedmineの展開

最初に新しいバージョンのRedmineを展開します。

現時点ではC:\redmine-3.4.2にRedmineをインストールしていますので、C:\redmine-3.4.3に新しいバージョンのRedmineを展開してアップグレードすることにします。

手順としてはもう少し後でも構わないのですが、最初に現行のRedmineのサービスを止めておきます。

# サービス停止
$REDMINE_SERVICE_NAME = "Redmine"
$REDMINE_INSTALL_PORT = 3000
Stop-Service $REDMINE_SERVICE_NAME

サービスを止めたら新しいRedmineを展開します。
この手順についてはバージョンが違うだけで前にインストールしたときと同様になります。

# 新しいRedmineの展開
$REDMINE_VER = '3.4.3'
$REDMINE_INSTALL_ROOT = 'C:\'
$REDMINE_INSTALL_PATH = Join-Path $REDMINE_INSTALL_ROOT "redmine-$REDMINE_VER"
# Zipのダウンロード
Invoke-WebRequest -Uri "http://www.redmine.org/releases/redmine-$REDMINE_VER.zip" -OutFile "$(Get-Location -PSProvider Filesystem)\redmine-$REDMINE_VER.zip"
# PowerShell 4.0だとZipを扱えないのでRubyで頑張って解凍する
# ※いったんカレントディレクトリに解凍してから移動している
gem install rubyzip --no-rdoc --no-ri
ruby -e "require 'zip'; Zip::File.open('.\redmine-$REDMINE_VER.zip') do |zip| zip.each do |entry| puts \""entry #{entry.name}\""; zip.extract(entry, entry.name) { true } end; end"
Move-Item ".\redmine-$REDMINE_VER" $REDMINE_INSTALL_ROOT -Force

2. データベースのバックアップ(とコピー)

今回の環境ではデータベースにSQLiteを使用しています。
Redmine 3.4.2のデータベースファイルを、先ほど展開したディレクトリにコピーして移行し、コピー元のファイルをバックアップとすることにします。

これは単純にファイル(config\database.ymldb\redmine.sqlite3")をコピーするだけです。

# データベースのコピー : SQLite
$OLD_REDMINE_PATH = "C:\redmine-3.4.2"
Copy-Item -LiteralPath (Join-Path $OLD_REDMINE_PATH "config\database.yml") -Destination (Join-Path $REDMINE_INSTALL_PATH "config\database.yml")
Copy-Item -LiteralPath (Join-Path $OLD_REDMINE_PATH "db\redmine.sqlite3") -Destination (Join-Path $REDMINE_INSTALL_PATH "db\redmine.sqlite3")

3. 添付ファイルのバックアップ(とコピー)

データベースと同様に添付ファイル(filesフォルダ)もコピーすることで新しいバージョンへの移行とバックアップを行います。

# filesフォルダの移行
Copy-Item -LiteralPath (Join-Path $OLD_REDMINE_PATH "files") -Destination (Join-Path $REDMINE_INSTALL_PATH "files") -Recurse

4. Redmineのアップグレード

ここからRedmineのアップグレードをしていきます。
公式な手順ではconfig/configuration.ymlを移行すると記載されていますが、今回の環境では設定していないため何もしません。
ただし、ThinをインストールするためにGemfile.localファイルを追加しているので、このファイルをコピーして移行しておきます。

# config/configuration.yml は今回の例では設定していないのでスキップ
# ただし、他にカスタマイズしている設定があればコピーしておくこと

# 今回は Gemfile.local(Thin Service用)をコピー
Copy-Item -LiteralPath (Join-Path $OLD_REDMINE_PATH "Gemfile.local") -Destination (Join-Path $REDMINE_INSTALL_PATH "Gemfile.local")

ここからは前にインストールした時と同様にbundle installを行い新しいバージョンのRedmine環境を構築します。

# DevKitへのPATHが通ってない場合は通しておく
$RUBY_DEVKIT_INSTALL_PATH = 'C:\MinGW'
$env:PATH = "$(Join-Path $RUBY_DEVKIT_INSTALL_PATH "bin");$(Join-Path $RUBY_DEVKIT_INSTALL_PATH "mingw\bin");" + $env:PATH

# bundle install
cd $REDMINE_INSTALL_PATH
bundle install --without development test rmagick --path vendor/bundle
# セッション改ざん防止用秘密鍵の作成
bundle exec rake generate_secret_token

5. データベースのアップグレード

# データベースのマイグレーション
bundle exec rake db:migrate RAILS_ENV="production"

6. クリーンアップ

# キャッシュとセッションファイルのクリア
bundle exec rake tmp:cache:clear tmp:sessions:clear RAILS_ENV="production"

7. サービス設定の変更

ここまでの手順で新しいバージョンのRedmine環境の構築が完了しているので、Thinで起動するRedmineを新しいバージョンのものに変更することで環境の切り替えを行います。

# Thinの再登録
thin_service remove -N $REDMINE_SERVICE_NAME
thin_service install -N $REDMINE_SERVICE_NAME -c "$REDMINE_INSTALL_PATH" -p $REDMINE_INSTALL_PORT -e production
# サービス起動
Start-Service Redmine

動作確認

以上でアップグレードは完了です。
ブラウザからRedmineにログインしてバージョンが新しくなっている事、正しくデータ移行できていることが確認できればOKです。

f:id:stknohg:20171209222019p:plain