Knowledge Base

お知らせや身辺のことを綴っています。
目次
デジタル金庫環境の整備とPowerShell

デジタル金庫環境の整備とPowerShell

デジタル金庫環境の整備

筆者はWindows Pro版というWindowsのちょっとだけいいエディションを使っているので、これを最大限活用するべく、BitLockerによって暗号化された複数の暗号化VHDと、クロスプラットフォームスクリプト環境のPowerShell、そして運用にタスクスケジューラを利用したデジタル金庫システムを前から構築している。本稿ではこのシステムの構築においてビジネスロジックを書くのに利用した、PowerShellについて思ったことと、その実装について軽く記述したい。

PowerShellはすごい!

PowerShellは結構すごい。

NETのインターフェースを叩ける

まず、PowerShellから.NETのAPIを使える。これはPowerShellでもスタックやキューなど、C#と共通の基本なデータ構造やその他クラスを使うことができることを意味する。例えば、可変長配列を使いたいとき、System.Collections.Generic.Listとすると、.NETライブラリのSystem.Collections.Generic名前空間上にあるListクラスを実際に利用できる。筆者はこれまでC#を触ったことがなかったが、.NETのAPIはC#のほかにもVisual Basicなどからも利用できるという。こういった共通のライブラリを色々な言語から叩けるという仕組みがあるのは感心する。

$List = New-Object System.Collections.Generic.List[System.Object]
1..5 | ForEach-Object { $List.Add($_) }
Write-Host -NoNewLine $List  # 1 2 3 4 5

全部オブジェクト

また、UNIXシェルスクリプティングにおいて扱う主役のデータ形式が基本すべてプレーンテキストであるというところに美しさがあるならば、PowerShellのいいところはそれが全部オブジェクトであるところにあると思う。あるオブジェクトの出力に対して、 Get-Memberなどのコマンドをパイプでつないであげれば、なかにどんなプロパティがあるのかが一目瞭然である。この出力がどんなデータを引っ張り出せるのか、ということが簡単にわかる。

たとえば、ボリューム情報を出力するコマンドレットであるGet-Volumeの出力に対して、シンプルにオブジェクトのプロパティをリストアップしてくれるFormat-Listを適用することができる。

Get-ChildItem C:\Windows\Media\onestop.mid | Format-List
結果
    Directory: C:\Windows\Media

Name           : onestop.mid
Length         : 40075
CreationTime   : 2022/05/07 14:18:59
LastWriteTime  : 2022/05/07 14:18:59
LastAccessTime : 2024/04/24 14:03:39
Mode           : la---
LinkType       : HardLink
Target         :
VersionInfo    : File:             C:\Windows\Media\onestop.mid
                 InternalName:
                 OriginalFilename:
                 FileVersion:
                 FileDescription:
                 Product:
                 ProductVersion:
                 Debug:            False
                 Patched:          False
                 PreRelease:       False
                 PrivateBuild:     False
                 SpecialBuild:     False
                 Language:

そして、こうしたプロパティへのアクセスは、ドット記法を使って行える。

(Get-Volume C).UniqueId
# \\?\Volume{c94f1507-47ee-4df0-85db-eaa2205672da}\

また、メンバーを表示するGet-Memberコマンドレットを適用することができる。-MemberTypeオプションを指定することによってさらに絞り込むことも可能である。

Get-Volume C | Get-Member
結果
   TypeName: Microsoft.Management.Infrastructure.CimInstance#ROOT/Microsoft/Windows/Storage/MSFT_Volume

Name                      MemberType     Definition
----                      ----------     ----------
Dispose                   Method         void Dispose(), void IDisposable.Dispose()
Equals                    Method         bool Equals(System.Object obj)
GetCimSessionComputerName Method         string GetCimSessionComputerName()
GetCimSessionInstanceId   Method         guid GetCimSessionInstanceId()
GetHashCode               Method         int GetHashCode()
GetType                   Method         type GetType()
ToString                  Method         string ToString()
AllocationUnitSize        Property       uint AllocationUnitSize {get;}
DriveLetter               Property       char DriveLetter {get;}
FileSystem                Property       string FileSystem {get;}
FileSystemLabel           Property       string FileSystemLabel {get;set;}
ObjectId                  Property       string ObjectId {get;}
PassThroughClass          Property       string PassThroughClass {get;}
PassThroughIds            Property       string PassThroughIds {get;}
PassThroughNamespace      Property       string PassThroughNamespace {get;}
PassThroughServer         Property       string PassThroughServer {get;}
Path                      Property       string Path {get;}
PSComputerName            Property       string PSComputerName {get;}
Size                      Property       ulong Size {get;}
SizeRemaining             Property       ulong SizeRemaining {get;}
UniqueId                  Property       string UniqueId {get;}
DedupMode                 ScriptProperty System.Object DedupMode {get=switch ($this.psBase.CimInstanceProperties["Dedu…
DriveType                 ScriptProperty System.Object DriveType {get=switch ($this.psBase.CimInstanceProperties["Driv…
FileSystemType            ScriptProperty System.Object FileSystemType {get=switch ($this.psBase.CimInstanceProperties[…
HealthStatus              ScriptProperty System.Object HealthStatus {get=switch ($this.psBase.CimInstanceProperties["H…
OperationalStatus         ScriptProperty System.Object OperationalStatus {get=$_status = @();…

このように、シェルスクリプトでは作成がちょっと大変な中規模のスクリプトを、PowerShellではオブジェクト指向プログラミング言語のパラダイムを用いて作れるのが魅力的だと思う。ただし、暗黙の型変換が普通に起こったり、スクリプトの不具合を追跡しづらかったりするのが非常につらいところではある。あとは個人的な問題ではあるが、単純にWindowsの知識がなくて基礎知識を得るのに終始してしまい、かなり時間を浪費してしまう点も否めない。

実装

実装とCSVファイル
# Utilization of virtual disk encrypted with bitlocker as digital vault, with a support of automated mount with powershell on startup.
param (
    [Parameter(Mandatory)][string] $File,
    [switch] $Detach = $false
)

if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
    throw "This PowerShell script needs to be executed by an account with priviledges."
}

class Storage {
    [string]$Path
    [char]$DriveLetterExpected
    [string]$UniqueIdExpected

    Storage(
        [string]$Path,
        [char]$DriveLetterExpected,
        [string]$UniqueIdExpected
    ) {
        $this.Path = $Path
        $this.DriveLetterExpected = $DriveLetterExpected
        $this.UniqueIdExpected = $UniqueIdExpected
    }

    [bool] ImageAttached() {
        return (Get-DiskImage $this.Path).Attached
    }

    [bool] LetterAssigned() {
        return $this.DriveLetterExpected -in (Get-Volume).DriveLetter
    }

    [void] Mount() {
        if (!($this.ImageAttached())) {
            Mount-DiskImage -ImagePath $this.Path -NoDriveLetter
        }
    }

    [void] AssignDriveLetter() {
        $DriveLetter = [string]$this.DriveLetterExpected + ":"
        mountvol.exe $DriveLetter $this.UniqueIdExpected

        if (!($this.LetterAssigned())) {
            throw "Failed to assign the drive letter $DriveLetter"
            return
        }

        Write-Output $this
    }

    [void] Dismount() {
        Dismount-DiskImage -ImagePath $this.Path
    }

    [void] Unlock($SecureString) {
        $drive = Get-Volume $this.DriveLetterExpected
        
        if ($drive.UniqueId -ne $this.UniqueIdExpected) {
            throw "The unique id of mounted volume must be identical to {0}:, but is {1}:." -f $drive.UniqueId, $this.UniqueIdExpected
        }

        Unlock-BitLocker -MountPoint $this.DriveLetterExpected -Password $SecureString
    }
}

$Vhd = New-Object System.Collections.Generic.List[System.Object]

Import-Csv $File -Header 'Path', 'DriveLetter', 'UniqueId' | ForEach-Object {
    $Vhd.Add([Storage]::new($_.Path, $_.DriveLetter, $_.UniqueId))
}

switch ($Detach) {
    $true {
        $Vhd | ForEach-Object { $_.Dismount() }
    }
    Default {
        $Password = Read-Host "Enter Password" -AsSecureString
        $Vhd | ForEach-Object { $_.Mount() }
        $Vhd | ForEach-Object { $_.AssignDriveLetter() }
        $Vhd | ForEach-Object { $_.Unlock($Password) }
    }
}
"C:/path/to/Harddisk0.vhdx","H","\\?\Volume{00000000-0000-0000-0000-000000000000}\"
"C:/path/to/Harddisk1.vhdx","G","\\?\Volume{00000000-0000-0000-0000-000000000000}\"
"C:/path/to/Harddisk2.vhdx","I","\\?\Volume{00000000-0000-0000-0000-000000000000}\"

基本的には以下のようにして利用する。

.\Mount-VHDs.ps1 <PATH> [-Detach]

スクリプトに対して手動で-Detachみたいな引数を付与して起動すると、一括でドライブをアンマウントすることもできる機能も付けている。画面を共有するときなどに叩くとフールプルーフとして役立つ。

なお、このプログラムはディスクの暗号化パスワードがすべて同じという前提の上に成り立っているものなので、必ずしもすべての人の用途に合わない場合もあることに注意すること。

おまけ

一応、動作を知りたい人のために、PowerShellスクリプト部分の実行動作を説明する。まずこのスクリプトが動く前提は以下のようになる。

  • Windows Pro Edition
  • BitLockerによって暗号化され、ユーザーによる暗号化の解除が可能な仮想ディスク(VHD)
  • これらVHDまでのパスと、想定するドライブのUUID、そしてマウントしたいドライブレターをタプルにして格納したCSVファイル
  • PowerShell Core

そして実行動作としては以下の通り。

  1. CSVファイルまでのパスをPowerShellスクリプトに引数で渡し、ユーザーは暗号化の解除に必要なパスワードの入力を促される。
  2. パスワードを入力する。
  3. スクリプトは、設定の内容に応じてドライブをすべてマウントする。このときのマウント操作のうち、特定のUUIDを特定のドライブレターにアサインするという、マウントポジションを設定する操作は、mountvolという別のコマンドを使わないとできないので、PowerShell側では-NoDriveLetterオプションを使って実行するだけにする。
    実行順としてはMount()AssignDriveLetter()になる。自分が作ったスクリプトの中では、ドライブをクラスとしてモデリングしていて、マウントするドライブごとにこれらのメソッドが呼び出されると考えてよい。
[void] Mount() {
	if (!($this.ImageAttached())) {
		Mount-DiskImage -ImagePath $this.Path -NoDriveLetter
	}
}

[void] AssignDriveLetter() {
	$DriveLetter = [string]$this.DriveLetterExpected + ":"
	mountvol.exe $DriveLetter $this.UniqueIdExpected

	if (!($this.LetterAssigned())) {
		throw "Failed to assign the drive letter $DriveLetter"
	}

	Write-Output $this
}
  1. BitLockerによる暗号化を解除する。
  2. (任意)タスクスケジューラでこのスクリプトの実行条件をコンピューターの起動時に設定し、管理者権限のシェルで以下のコマンドが走るようにする。
pwsh.exe -Command "powershellスクリプトのフルパス CSVファイルのフルパス"

前の記事

4月10日(水) スパムコメントで学ぶ英語のイディオム

次の記事

検索エンジンのBotについて考える

コメント

0

コメントはありません。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

関連投稿

日記
「検索エンジンのBotについて考える」のサムネイル画像

検索エンジンのBotについて考える

このサイトに訪れているクローラの種類と数を集計して、ダウンタイムの謎を暴きます。 続きを読む

日記
「4月10日(水) スパムコメントで学ぶ英語のイディオム」のサムネイル画像

4月10日(水) スパムコメントで学ぶ英語のイディオム

out of thin airという英語のイディオムについて。 続きを読む

日記
「4月9日(火) シェルスクリプトであそぼう」のサムネイル画像

4月9日(火) シェルスクリプトであそぼう

今日の雨は土砂降りというレベルですごかった。AtCoder Problemsでの難易度が150~30… 続きを読む

日記
「4月8日(月) 日記」のサムネイル画像

4月8日(月) 日記

関心が変わった 最近着手しているプロジェクトもなく、最近なんとなく手持ち無沙汰に感じていたところがあ… 続きを読む