コマンドプロンプトでパスワード入力文字を見えなくする方法

# はじめに

仕事をしているとパスワードを入力する場面が何度もあります。共用サーバーの接続スクリプトなど自組織内で作成したスクリプトやバッチファイルを使う人も多いかと思いますが，入力したパスワードが平文のまま表示されてしまうバッチファイルに遭遇すると

<font color=red><b>こ　の　バ　ッ　チ　を　作　っ　た　の　は　誰　だ　！　？</b></font>

と作者を呼び出したくなります。とはいえ，**Windows** のバッチファイルで入力パスワードの隠匿化を行うのはとても難しいようです。

ということで自作することにしました。良いアイデアが思い浮かんだので。

# 最初に結論を書く

それはエスケープシーケンスを使うというものです。**Windows10** の途中で可能になったテクニックです。要は文字の色を背景色と同じにしてしまえば入力した文字は見えなくなります。サンプルコードを以下に示します。注意事項としてはパスワード入力後に文字の色と背景色を元に戻す必要があります。この際に改行なしでエスケープシーケンスを出力する必要があるので，ちょっとしたテクニック[^1]を使っています。

```plantuml
@startuml
<style>
root {
  margin 0
  padding 0
}
title {
  roundCorner 16
  padding 0 16 16 16
  horizontalAlignment left
  backgroundColor #202020
  fontColor #e3e3e3
  fontStyle normal
  fontSize 14
  fontName monospaced
}
</style>
sprite TITLE <svg width="640" height="28">
<path d="M0 0 v28 a4 4 0 0 0 4 4 h220 a4 4 0 0 0 4 -4 v-28" fill="#606060">
<text x="8" y="24" font-size="16" fill="white">TEST.CMD（サンプルコード）</text>
</svg>
title
<$TITLE>

@echo off
setlocal
set /P PASSWORD=Password: <color:#EBD247>^[</color>[30m<color:#EBD247>^[</color>[40m
set /P DUMMY=<color:#EBD247>^[</color>[0m<NUL
if defined PASSWORD echo Your entered password is %PASSWORD%
end title
@enduml
```

# エスケープシーケンス概説

エスケープシーケンスについての詳しい情報は **Microsoft** 公式サイト[^2] を参照下さい。以下は上記のバッチファイルで使った機能に関する部分のみを抜粋したものです。

<table>
<caption>表１　テキストの書式設定を行うエスケープシーケンス</caption>
<thead>
<tr><th>シーケンス</th>
    <th>コード</th>
    <th>説明</th>
    <th>動作</th></tr>
</thead>
<tbody>
<tr></tr>
<tr><td><code>ESC [ 0 m</code></td>
    <td><b>SGR</b></td>
    <td>既定値</td>
    <td>すべての属性を変更前の既定の状態に戻します</td></tr>
<tr></tr>
<tr><td><code>ESC [ 30 m</code></td>
    <td><b>SGR</b></td>
    <td>前景白</td>
    <td>非ボールドの明るい黒を前景に適用します</td></tr>
<tr></tr>
<tr><td><code>ESC [ 40 m</code></td>
    <td><b>SGR</b></td>
    <td>背景白</td>
    <td>非ボールドの明るい黒を背景に適用します</td></tr>
</tbody>
</table>


# 実行例

実行例を以下に示します。入力した文字は見えません。カーソルだけ見えます。

```plantuml
@startuml
<style>
root {
  margin 0
  padding 0
}
title {
  roundCorner 16
  padding 0 16 16 16
  horizontalAlignment left
  backgroundColor #202020
  fontColor #e3e3e3
  fontStyle normal
  fontSize 14
  fontName monospaced
}
</style>
sprite TITLE <svg width="640" height="28">
<path d="M0 0 v28 a4 4 0 0 0 4 4 h156 a4 4 0 0 0 4 -4 v-28" fill="#606060">
<text x="8" y="24" font-size="16" fill="white">TEST.CMD の実行例</text>
</svg>
sprite CURSOR <svg width="9" height="14">
<path d="M0 10 v4 h9 v-4" fill="e3e3e3">
</svg>
title
<$TITLE>

C:\Qiita>test
Password:      <$CURSOR>
end title
@enduml
```

人によってはコマンドプロンプトの背景色を変えている人がいるかもしれません。その場合は何をやっているのかバレバレです。

```plantuml
@startuml
<style>
root {
  margin 0
  padding 0
}
title {
  roundCorner 16
  padding 0 16 16 16
  horizontalAlignment left
  backgroundColor #0037da
  fontColor #e3e3e3
  fontStyle normal
  fontSize 14
  fontName monospaced
}
</style>
sprite TITLE <svg width="640" height="28">
<path d="M0 0 v28 a4 4 0 0 0 4 4 h170 a4 4 0 0 0 4 -4 v-28" fill="#606060">
<text x="8" y="24" font-size="16" fill="white">TEST.CMD の実行例２</text>
</svg>
sprite CURSOR <svg width="9" height="14">
<path d="M0 10 v4 h9 v-4" fill="e3e3e3">
</svg>
title
<$TITLE>

C:\Qiita>test
Password: <back:#202020>     </back><$CURSOR>
end title
@enduml
```

# 注意事項

注意事項としては，文字の色を背景色と同じにして見えなくしているだけですから，パスワードを入力した付近を選択してコピー＆ペーストすれば入力した文字を復元できます。なので早めに `CLS` コマンドを呼び出して画面クリアをしたほうが良いでしょう。

ちなみにパスワード入力中に `Ctrl-C` などで中断すると，それ以降文字が見えなくなってしまうのではないかと危惧したのですが，実際試してみると何故か `Ctrl-C` を二回連打しないと終了できなかったりと動作自体に謎は残るものの，既定の状態に戻すエスケープシーケンスが実行されて無事復帰できました。

# 補足）正統派な手段

おそらく一番正統派な解決手段[^3]は **PowerShell** を使うというものです。**PowerShell** だと下記のように **DOTNET** の機能を使えるので，奇をてらったテクニックを使うこともなく普通に書けます。`SecureString` オブジェクトになってしまうのは過剰包装的な感じもしますが。

```powershell:test.ps1（PowerShell スクリプトの例）
$obj = Read-Host "Password" -AsSecureString
$str = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR(sobj)
$password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($str)
Write-Host "Your entered password is $password"
```

実行画面もこれぞあるべき姿という感じです。

```:test.ps1 の実行画面
c:\Qiita>.\test.ps1
Password: ********
Your entered password is password
```

**PowerShell 7.1** 以降だともっとスッキリ書けます。

```powershell:test2.ps1（PowerShell 7.1 以降で有効な方法）
$password = Read-Host "Password" -MaskInput
Write-Host "Your entered password is $password"
```

# 補足その２）超絶技巧

バッチファイルでも技巧を凝らせばできるという参考文献[^4]のテクニックについても紹介しておきましょう。下記はオリジナルのエッセンスを自分なりに咀嚼して作り直したものです。特殊なテクニックを利用してパスワード文字列を一文字ずつ入力するほか，バックスペースによる修正にも対応しています。なおオリジナルは何も表示しませんが，アスタリスク `*` を表示するよう筆者が手を加えています。なお，パスワードの文字数も貴重な情報ですから，アスタリスクを表示してしまうのは改悪かもしれません。

```plantuml
@startuml
<style>
root {
  margin 0
  padding 0
}
title {
  roundCorner 16
  padding 0 16 16 16
  horizontalAlignment left
  backgroundColor #202020
  fontColor #e3e3e3
  fontStyle normal
  fontSize 14
  fontName monospaced
}
</style>
sprite TITLE <svg width="640" height="28">
<path d="M0 0 v28 a4 4 0 0 0 4 4 h98 a4 4 0 0 0 4 -4 v-28" fill="#606060">
<text x="8" y="24" font-size="16" fill="white">TEST2.CMD</text>
</svg>
title
<$TITLE>

@echo off
setlocal
set /P DUMMY=Password: <NUL
set PASSWORD=
:LOOP
  set KEY=
  for /F "usebackq delims=" %%# in (
    `xcopy /W "%\~f0" "%\~f0" 2^>NUL`
  ) do if not defined KEY set KEY=%%#
  set KEY=%KEY:\~-1%
  if "%KEY%"==""   goto BREAK
  if "%KEY%"=="<color:#EBD247>^H</color>" goto BACKSPACE
  set /P DUMMY=*<NUL
  set "PASSWORD=%PASSWORD%%KEY%"
  goto LOOP
:BACKSPACE
  if not defined PASSWORD goto LOOP
  set /P DUMMY=<color:#EBD247>^H</color> <color:#EBD247>^H</color><NUL
  set "PASSWORD=%PASSWORD:\~0,-1%"
  goto LOOP
:BREAK
echo.
if defined PASSWORD echo Your entered password is %PASSWORD%
end title
@enduml
```

実行例を以下に示しますが，とてもいい感じです。

```plantuml
@startuml
<style>
root {
  margin 0
  padding 0
}
title {
  roundCorner 16
  padding 0 16 16 16
  horizontalAlignment left
  backgroundColor #202020
  fontColor #e3e3e3
  fontStyle normal
  fontSize 14
  fontName monospaced
}
</style>
sprite TITLE <svg width="640" height="28">
<path d="M0 0 v28 a4 4 0 0 0 4 4 h170 a4 4 0 0 0 4 -4 v-28" fill="#606060">
<text x="8" y="24" font-size="16" fill="white">TEST2.CMD の実行例</text>
</svg>
sprite CURSOR <svg width="9" height="14">
<path d="M0 10 v4 h9 v-4" fill="e3e3e3">
</svg>
title
<$TITLE>

C:\Qiita>test2
Password: ~*~*~*~*~*~*~*~*
Your entered password is password
end title
@enduml
```

上記のプログラムの基本原理（文字入力方法）についても説明しておきましょう。これは `xcopy` コマンドの `/W` オプション，すなわち「コピーを開始する前に任意のキーを押すことを求めるメッセージを表示する」機能を用いたものです。このオプションを用いると **Enter** キーなしの１文字入力が可能で，入力した文字を取得することができます。ちなみに `pause` コマンドも **Enter** キーなしの１文字入力待ちが可能ですが，入力した文字をエコーバックしないので，このような用途に用いることができません。

こうして出力された４行のうちの先頭行の末尾文字（下記の例では `y`）が入力された文字となります。また３行目だけは標準エラー出力に出力されるので `NUL` デバイスにリダイレクトして捨てる必要があります。

なお，`xcopy` コマンドで指定するコピー元ファイルは確実に存在しなくてはなりません。存在しなければメッセージが表示される前にエラー終了となるからです。このためコピー元とコピー先のファイルに自分自身 `%0` を指定しています。

```plantuml
@startuml
<style>
root {
  margin 0
  padding 0
}
title {
  roundCorner 16
  padding 0 16 16 16
  horizontalAlignment left
  backgroundColor #202020
  fontColor #e3e3e3
  fontStyle normal
  fontSize 14
  fontName monospaced
}
</style>
sprite TITLE <svg width="640" height="28">
<path d="M0 0 v28 a4 4 0 0 0 4 4 h350 a4 4 0 0 0 4 -4 v-28" fill="#606060">
<text x="8" y="24" font-size="16" fill="white">xcopy コマンドを利用した１文字キー入力方法</text>
</svg>
title
<$TITLE>

c:\Qiita>xcopy /W test2.cmd test2.cmd
ファイルのコピーを開始するときは何かキーを押してくださいy
C:TEST2.CMD
ファイルを同じファイルにコピーすることはできません
0 個のファイルをコピーしました
end title
@enduml
```

他には `replace` コマンドを使う方法もあります。`replace` コマンドは「指定されたドライブやディレクトリから同じファイルを探して置き換える」というコマンドです[^5]。`/U` オプションは送り側ファイルよりも古いファイルのみを置き換えるというもの，`/W` オプションはファイルの検索開始前に待機する（ユーザーのキー入力を待つ）というものです。カレントディレクトリに存在する名前が一文字のファイル（ワイルドカード `?` にマッチングするファイル名）をカレントディレクトリ `.` にコピーしようとしているので仮にファイルが存在していてもタイムスタンプは必ず一致し，ファイルの置き換えは行われません。また一文字のファイルが存在しなくてもエラーにはなりません。

```plantuml
@startuml
<style>
root {
  margin 0
  padding 0
}
title {
  roundCorner 16
  padding 0 16 16 16
  horizontalAlignment left
  backgroundColor #202020
  fontColor #e3e3e3
  fontStyle normal
  fontSize 14
  fontName monospaced
}
</style>
sprite TITLE <svg width="640" height="28">
<path d="M0 0 v28 a4 4 0 0 0 4 4 h358 a4 4 0 0 0 4 -4 v-28" fill="#606060">
<text x="8" y="24" font-size="16" fill="white">replace コマンドを利用した１文字キー入力方法</text>
</svg>
title
<$TITLE>

c:\Qiita>replace ? . /U /W
続行するときは何かキーを押してください...
y
end title
@enduml
```



上記以外に驚いた点を以下述べます。

- これまで `for` コマンドのループ変数は英字一文字（大文字・小文字は区別するので全52種）だと思っていたのですが，`#` とか英字以外の文字も使えるんですね。筆者が確認したところ，これ以外に `@`，`$`，`_` などが変数名に使えるようです。

- オリジナルプログラムではバックスペース記号を取得するために `prompt` コマンドを用いています。`prompt` コマンドではバックスペースを `$H` という印字可能な文字列で指定できるからです。こうして一時的にプロンプトをバックスペース記号に置き換え，これを読み出すことでバックスペース記号を取得しています。本記事ではバッチファイル内に直接バックスペース記号を埋め込んでいるので，このテクニックを使っていません。

ちなみに通常コマンドプロンプト上ではエスケープキーを押すと，その行の入力がキャンセルされますが，残念ながら上記の `TEST2.CMD` ではエスケープキーによる入力キャンセルに対応していません。これは `xcopy` コマンドを用いた文字入力方法ではエスケープキーの入力を検知できないからです。

# おまけ

今回の記事で作成したバッチファイルです。

[20250310_Password.zip (google-drive)](https://drive.google.com/file/d/1mwwznFYNolR-tBTeAG_R7jikHdFu2tBY/view?usp=drive_link)

[^1]: [【バッチファイル】文字列を改行なしで表示【備忘録】- Qiita](https://qiita.com/koryuohproject/items/f53ace4bcd3bf0b261be)

[^2]: [コンソールの仮想ターミナルシーケンス - learn.microsoft.com](https://learn.microsoft.com/ja-jp/windows/console/console-virtual-terminal-sequences)

[^3]: [【Bat+PowerShell】バッチファイルでパスワードなどの入力をマスクする（隠す）方法！- Correct-Log](https://correct-log.com/bat_input_mask_with_powershell/)

[^4]: [コマンドライン上で文字の見えないパスワード入力を実現する - Qiita](https://qiita.com/mpyw/items/2e4bf35044a407be536d)

[^5]: [Windowsコマンド集：(replace) - 日経クロステック](https://xtech.nikkei.com/it/free/NT/WinKeyWord/20040805/1/replace.shtml)