しばたテックブログ

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

PowerShell 6.0で範囲演算子(..演算子)が拡張される話

とあるPull RequestがきっかけでPowerShell 6.0 RC1から範囲演算子(..演算子)が拡張され、Int型だけでなくChar型も扱える様になりました。

本エントリではその内容について説明します。

範囲演算子でChar型が扱える様になります

もともと範囲演算子(..)は、

1..5

の様に[Int型の数値]..[Int型の数値]の形式をとり、指定した数値間で連続する要素を持つ配列を生成します。

# 1~5を要素にもつ配列が生成される
1..5 => (1,2,3,4,5)

f:id:stknohg:20171221213103p:plain

今回、この指定がChar型を取れる様に拡張され、以下の様な記述が可能になります。

'a'..'e'

この例の場合だとaeを要素にもつ配列が生成されます。

# a~eを要素にもつ配列が生成される
'a'..'e' => ([char]'a', [char]'b', [char]'c', [char]'d', [char]'e')

f:id:stknohg:20171221213133p:plain

ただ、PowerShellにはChar型リテラルを表現する手段はないため、'a''e'の記述は実際にはString型であり内部的にChar型へのキャストが発生しています。

# 内部的にはString型→Char型へのキャストが発生しているイメージ
[char]'a'..[char]'e'

もう少し深入りしてみる

これだけだとなんとなくそんなものかという感じですが、この機能拡張はまだバギーなうえかなり気持ち悪い挙動をします。

PowerShellにおけるChar型と本機能拡張

元のPull Requestを見てわかる様に、この機能拡張はもともと、

'a'..'z'

の様な単純なASCIIの範囲の文字の使用を想定している様です。

ただ、PowerShellは.NET Framework/.NET Core製であり.NETの世界のChar型はUnicode Characterです。

このため、

'あ'..'お'

の様な指定も可能となり、以下の様な結果を返します。

f:id:stknohg:20171221213229p:plain

これはざっくり以下と同義になります。

[int][char]'あ'..[int][char]'お' | % { [char]$_ }

現時点で発生しているバグ

現時点で以下のバグが報告されており、

github.com

'あ'..'お' | % { [int]$_ }

の様な記述がエラーとなってしまいます。

f:id:stknohg:20171221213254p:plain

いまのところ()でくくることで回避可能ですが早急に修正してほしい感じです。

# ()でくくることでバグを回避できる
('あ'..'お') | % { [int]$_ }

また、私も別件で以下のIssueを報告しています。

github.com

現状の出来を見るに、個人的には機能を拡張するには軽率だったんじゃないかと思いますし、今からでもこの拡張は取り下げた方が良いんじゃないかとさえ思えます...

型変換の仕様との演算子の整合性について

個人的な感情はとりあえず置いておいて、今回の拡張によって範囲演算子はInt型およびChar型を受け入れることができる様になりました。

ではInt型とChar型を混在させたときはどうなるかというと次の様になります。

  • [Char型] .. [Int型]の場合はOK
# OK
'a'..100

f:id:stknohg:20171221213537p:plain

  • [Int型] .. [Char型]の場合はNG
# NG
95..'a'

f:id:stknohg:20171221213552p:plain

これはPowerShellの二項演算子が左オペランドの型に合わせようとする原則があるためです。
比較演算子のケースですが過去のこちらのエントリが参考になるでしょう。

blog.shibata.tech

PowerShellではInt型からChar型へのキャストは直接行えるのですが、

# OK
[char]100

Char型からInt型へのキャストについては、記述上String型からInt型へのキャストが必要になり、PowerShellにはString型を直接Intにキャストできないため構文エラーとなります。

# NG : String型 → Int型へ直接キャストはできない
[int]'a'

# OK : Char型からInt型へのキャストなら可
[int][char]'a'

なお、Char型からInt型へのキャストは可能なので、

# OK : String型 → Char型 → Int型へのキャストとなる
95..[char]'a'

の様に記述すれば[Int型] .. [Char型]の場合でもエラーなく実行することができます。

f:id:stknohg:20171221213804p:plain

【2017/12/22】 ちょっと追記

String型からInt型へのキャストについて、

[int]'1'

の様に文字列で数値を表現している場合は可能です。
このため、

# これはPowerShell 6.0以前でも可能
'1'..'5'

# これもPowerShell 6.0以前で可能
95..'97'

の様な記述であればエラーなく可能です。

最後に

とりあえずこんな感じです。

この範囲演算子の拡張は個人的には不要とさえ思っているのですが何かしら役に立つケースもあるかもしれません。

PowerShellGetをアップデートしよう

PowerShell Advent Calendar 2017、18日目です。

qiita.com

PowerShell 5.0以降、Windows 10やWindows Server 2016では標準でインストール済みのPowerShellGetの機能について、現時点での最新バージョンはVer.1.6.0なのですが、例えばWindows 10においてはFall Creator Update(1709)を適用してもVer.1.0.0.1と古いバージョンのまま更新されません。

www.powershellgallery.com

PowerShellGetの変更履歴はこちらあり、これまでいくつかのバグフィックスや機能追加がなされています。
例えば最新版のVer.1.6.0ではプレリリースバージョンのモジュールのインストールに対応する様になっています。

一般論としてできるだけ新しいバージョンのモジュールを利用していくのが良いため、本エントリではその手順を紹介していきます。

PowerShellGetをアップデートする

本エントリではWindows 10 Fall Creators Update(1709)を検証環境として説明します。

はじめに更新前のバージョンをチェックしてみます。
PowerShellGetは内部でPackageManagementモジュールを使用するのでこちらのバージョンも確認しておきます。

Get-Module PowerShellGet,PackageManagement -ListAvailable

f:id:stknohg:20171218001908p:plain

どちらのモジュールもVer.1.0.0.1と古いままです。

通常、モジュールのアップデートにはUpdate-Moduleコマンドレットを使いますが、何も考えずに実行すると以下のエラーメッセージが出てしまいます。

Update-Module PowerShellGet 

f:id:stknohg:20171218002004p:plain

このためInstall-Moduleコマンドレットを使い新しいバージョンを別にインストールする体をとる必要があります。
Install-Moduleコマンドレットですが、インストールするモジュールを過去バージョンと併用するには-Forceオプションが必要です。
また既存コマンドの動作を上書き許可するため-AllowClobberオプションも必要になります。

Install-Module PowerShellGet -Scope CurrentUser -Force -AllowClobber 

インストール後はこんな感じになります。

f:id:stknohg:20171218002024p:plain

  • PowerShellGet : Ver.1.6.0
  • PackageManagement : Ver.1.1.7

と、両モジュールとも新しいバージョンが追加インストールされていることが分かります。

コンソールを再起動すると新しいバージョンのモジュールが読み込まれ利用可能になります。

PowerShellGetの新機能を試してみる

最初に新しいバージョンのPowerShellGetでは

プレリリースバージョンのモジュールのインストールに対応する

と説明しました。

ちょうど今PowerShellコンソールに様々な拡張機能を提供しているPSReadlineモジュールがVer.2.0 ベータをプレリリース版として公開しているので試しにインストールしてみます。

PSReadline 2.0は1.x系に対して主にPowerShell Coreへの対応とバグフィックスが主な変更点ですが、

  • 日本語入力時にキャレット位置が狂うバグの修正(#542)
  • 行頭でバックスペースキーを押したときにBeep音がならない様に修正 (#422)

などの日本語ユーザーにとっても嬉しい修正が入っているので積極的に試してみると良いと思います。

PSReadline 2.0 Betaをインストールしてみる

プレリリース版のモジュールを取り扱うには新たに追加された-AllowPrereleaseオプションを指定してやります。

モジュールを検索する場合にはFind-Module-AllowPrereleaseオプションを指定します。

Find-Module PSReadline -AllowPrerelease 

f:id:stknohg:20171218002156p:plain

-AllowPrereleaseを付けたときだけプレリリース版であるVer.2.0.0-beta1がヒットするのが見て取れます。

続けて、プレリリース版のモジュールをインストールするにはInstall-Module-AllowPrereleaseオプションを指定してやります。
過去バージョンのモジュールと併用するには-Forceオプションが必要です。
また、既存のバージョン(ここではVer.1.2)が署名済みのため、Ver.2.0でも署名を求められており、いまのところは-SkipPublisherCheckを付けてやる必要があります。

Install-Module PSReadline -AllowPrerelease -Scope CurrentUser -Force -SkipPublisherCheck

f:id:stknohg:20171218002355p:plain

エラー無く処理が終わればインストール完了です。
コンソールを再起動すると新しいバージョンのモジュールが読み込まれます。

補足 システム全体へのインストール

ここまでの手順では各コマンドを-Scope CurrentUserで実行してユーザー毎のインストールを行いましたが、もちろん管理者としてシステムワイドにインストールしても構いません。

この場合は-AllowClobberオプションは不要になります。

Install-Module PowerShellGet -Force

f:id:stknohg:20171218002432p:plain

コンソールを再起動すると新しいバージョンのモジュールが読み込まれます。

同様にPSReadlineも管理者としてインストール可能です。

Install-Module PSReadline -AllowPrerelease -Force -SkipPublisherCheck

f:id:stknohg:20171218002452p:plain

続・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:正直ちょっと自信がありません...