【PowerShell】平文を暗号化・暗号文を復号する

# はじめに
コードを書いているとどうしてもパスワードとかを埋め込みたいときありますけれど、
平文でハードコーディングするのはやはり抵抗感があります。
そんなときに、気軽に暗号⇔復号をやり取り可能な仕組みがあればいいのだけれど...

Powershell(.NET)で暗号を扱うのにSecureStringなるクラスが用意されていました。

この仕組みを上手く使い手軽に暗号・復号ができる関数を作成してみましょう。

## SecureStringオブジェクト(クラス)
機密情報を保護するために使われるクラス。
文字列をメモリ上に平文で保持せず、暗号化された形式で保持する模様。

尚、SecureStringクラスは.NetCoreでは利用できず現在推奨されていない模様。

https://dev.classmethod.jp/articles/pscore-securestring-problem/

推奨されていない理由としては主にWindowsプラットフォーム以外での互換性の問題といったところでしょうか？
非Window環境ではバイナリ値に変換されるらしいので、セキュリティ上問題有ともいえますが、Window環境で使う分には問題ありませんね。
(このような仕様になっているので.NetCore環境でも動作はします)

# 暗号化
まず平文の暗号化からです。

```powershell:Protect-String
function Protect-String {
    param(
        [Parameter(Mandatory=$True)]
        [string]$plainTxt,
        [Validateset("clipboard","file")]$output = "",
        [string]$path = "./",
        [string]$fileName = ""
    )

    #ファイル名の設定
    if($output -eq "file"){
        while([string]::IsNullOrEmpty($fileName)){
            $fileName = Read-Host "出力ファイル名を入力してください"
        }
    }

    #平文をsecureStringオブジェクトに変換する
    $secureString = ConvertTo-SecureString -String $plainTxt -AsPlainText -Force 

    #secureStringオブジェクトを暗号文にする。
    $encryptString = ConvertFrom-SecureString -SecureString $secureString
    #-keyオプションを指定しない場合、DPAPIを使用して暗号化される。

    switch ($output) {
        "clipboard" { $encryptString | Set-Clipboard }
        "file" { $encryptString | out-file -FilePath "${path}\${fileName}" -NoNewline -NoClobber }
        default { return $encryptString }
    }

}
```

## ConvertTo-SecureString
暗号文をSecureStringオブジェクトに変換するコマンドレットです。
ポイントなのは平文をオブジェクト化するのでなく、暗号文をオブジェクト化するという点です。
なので平文をSecureStringオブジェクトにしたい場合``-AsPlainText`` オプションを付けます。
ただしこのオプションを付けても以下のような警告がが表示されます。

``` powershell
ConvertTo-SecureString : プレーンテキストの入力は保護できません。この警告を表示せず、プレーンテキストを SecureString に
変換するには、Force パラメーターを指定してコマンドを再実行してください。詳細については、「get-help ConvertTo-SecureStri
ng」と入力してヘルプを参照してください。
発生場所 行:1 文字:6
+ $var=ConvertTo-SecureString -String test -AsPlainText
+      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [ConvertTo-SecureString]、ArgumentException
    + FullyQualifiedErrorId : ImportSecureString_ForceRequired,Microsoft.PowerShell.Commands.ConvertToSecureStringComm
   and
```
この警告にあるように``-Force``オプションも必要となります。
尚、Powershell 7以降ではForceオプションは不要になったそうです。
※ただし、コードの互換性を保つため特に意味はありませんがオプション自体は残っているようです。

## ConvertFrom-SecureString
SecureStringオブジェクトを暗号化された標準文字列に変換します。
「標準文字列に変換」とあるようにSecureStringオブジェクトそのままでは文字列としてファイルやコンソールやらに内容を出力することはできないようです。

このコマンドレットを使うことで、SecureStringオブジェクトの内容を暗号文で取り出すことができます。
※尚、Powershell 7で``-AsPlainText``というオプションが追加され平文で取り出すことも可能になった模様です。

``-key``オプションなどで暗号化キーを指定した場合はそれを基にSecureStringを暗号化された標準文字列として出力します。
一方で暗号化キーを指定しなければDPAPIを使用して暗号化されます。

### DPAPI (Winodowsデータ保護API) とは
>Data Protection APIの頭文字を取ってDPAPIと表記する。

>Windows2000以降のWindowOSに組み込まれているデータ保護用のコンポーネント

>DPAPIはデータの対象暗号化を可能にする。

対象暗号ということは、共通鍵のような仕組みでしょうか...

>ログオン機密やドメインの認証機密情報から対象鍵を生成

なるほど、ということはOSユーザのログイン時に使うパスワードとかをもとに鍵を生成している感じかな？
実際にDPAPIで暗号化した暗号文はOSユーザが異なる場合復号できない仕様。
OSユーザなどのログイン機密を使うことで、「鍵」を意識させないという点がDPAPIのメリットという感じでしょうか？

APIの役割としては
- 平文を受け取って暗号文を返す
- 暗号文を受け取って平文を返す

といった感じをイメージしています。
そのフローの中で暗号・復号は同一のキー(対象鍵)で行われ、その鍵はログイン機密をベースにしているといったところで齟齬は無いといいのですが...

# 復号

```powershell:Unprotect-String
function Unprotect-String {
    param(
        [Parameter(ValueFromPipeline=$true)]
        [string]$encryptString = "",
        [string]$inputFile = "",
        [Validateset("clipboard","console")]$output = ""
    )

    while([string]::IsNullOrEmpty($encryptString)){
        if([string]::IsNullOrEmpty($inputFile)) {
            $encryptString = Read-Host "暗号文を入力してください"
            #Powershell 7.1以降では-MaskInputが使えるので値をマスクすることを推奨
        }else{
            $encryptString = Get-Content $inputFile
        }
    } 
    
    #secureStringオブジェクトを生成
    $secureString = $encryptString | ConvertTo-SecureString

    #bstrに変換
    $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureString)
    #bstrをstringに変換
    $String = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
    #アンマネージメモリの解放
    [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)

    switch ($output) {
        "clipboard" { $String | Set-Clipboard }
        "console" { Write-Host $String }
        default { return $String }
    }

}
```

## SecureStringToBSTR
**Marshallクラス**に属するメソッドのひとつ。
Marshallクラスってなに:joy_cat:？
って感じですが...

>アンマネージド コードを扱うときに使用できるさまざまなメソッドを提供します。

わからない言葉が分からない言葉で説明されている...

まぁ調べた感じ「アンマネージドコード」は.NetFlameworkのCLRに準拠していないコードを総称してこう呼ぶみたい。
CLRで管理できなくなるから、ガベージコレクションが使えない感じかな？
つまり、メモリの解放とか気にしなくちゃいけないのかも...
簡潔に言えば.NET側は面倒みれないのでユーザでちゃんとリソース管理しないとだめだよ的なコードかな？

でそのコードを扱うメソッドを提供するのがMarshallクラスみたいな。
つまり.Netでのマーシャリングに関する機能を提供していると解釈します。

https://e-words.jp/w/%E3%83%9E%E3%83%BC%E3%82%B7%E3%83%A3%E3%83%AA%E3%83%B3%E3%82%B0.html

本題

SecureStringToBSTRメソッドはBSTRを割り当てSecureStringオブジェクトの内容をそのBSTRにコピーしている。
BSTRはとりあえずポインタと考えることにします。
なので、正確にはBSTRが指し示すアドレスのメモリ上にSecureStringのデータがコピーされたといった表現が正確なのかもシレナイ...
先述にあったように、Powershell 7以降はSecureStringオブジェクトから直接平文を出力できるみたいだけど、
それ以前は不可能だったので、こういう取り出し方になったのかもしれない。

## PtrToStringBSTR
アンマネージドメモリ内のBSTRからマネージドなStringをアロケートするらしい
(一体何を言っているんだ...:joy:)

まぁ意訳するとCLRで管理されていないメモリ領域からあるデータをコピーしてきて.NETで使えるStringとして割り当てるといった感じだろうか。
このうち「あるデータ」がSelectStringからコピーしたBSTRが示すデータで、きっとそれをSystem.Stringに変換しているんだね。

## ZeroFreeBSTR
SecureStringToBSTRの公式リファレンスの注釈に以下の内容が

>always free the BSTR when finished by calling the ZeroFreeBSTR method.

使い終わったらBSTRを開放しなさい的なことが書いてある。

確かにアンマネージドだから、いい感じに勝手に開放してくれるわけではないか...

ということで、以下の``ZeroFreeBSTR``なるメソッドを記述して明示的にリソースを開放しておく。

# 留意点
DPAPIの箇所で少し触れましたが、
異なるユーザ環境間での暗号化⇔復号には対応してません。

DPAPIはユーザのログイン機密を基に対象鍵を生成しているため、
アカウントのパスワードの変更がなされた場合に復号できなくなると考えられます。

## 別ユーザーで復号した場合のエラー例
```powershell:別ユーザーで復号
ConvertTo-SecureString : 指定された状態で使用するには無効なキーです。
発生場所 C:\Users\username\powershell\function\Unprotect-String.psm1:17 文字:38
+     $secureString = $encryptString | ConvertTo-SecureString
+                                      ~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [ConvertTo-SecureString]、CryptographicException
    + FullyQualifiedErrorId : ImportSecureString_InvalidArgument_CryptographicError,Microsoft.PowerShell.Commands.Conv
   ertToSecureStringCommand

```


# ソースコード更新履歴
【2023/11/26】
- ``Protect-String``のデフォルト動作として暗号文を関数の返り値として出力する仕様に変更しました。
- ``Unprotect-String``が標準入力から暗号分を読み込む仕様を追加しました。
- ``Unprotect-String``のデフォルトの動作として復号文を返り値として出力する仕様に変更しました。

# 参考
https://github.com/senkousya/usingEncryptedStandardStringOnPowershell

https://learn.microsoft.com/ja-jp/dotnet/api/system.security.securestring?view=net-8.0

https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.marshal?view=net-8.0

https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.marshal.securestringtobstr?view=net-8.0#system-runtime-interopservices-marshal-securestringtobstr(system-security-securestring)

https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.marshal.ptrtostringbstr?view=net-8.0#system-runtime-interopservices-marshal-ptrtostringbstr(system-intptr)

https://ja.wikipedia.org/wiki/DPAPI