はじめに
パスワード管理ツール KeePass は非常に便利ですが、毎回マスターパスワードを手入力するのは手間がかかります。
そこで、PowerShell と UIAutomation を利用して 自動で KeePass を起動し、マスターパスワードを入力してログインするスクリプト を作成しました。
この記事では、以下の内容を紹介します:
- スクリプトの概要
- セキュリティ上の注意点
- コード全文と解説
- 応用方法
セキュリティに関する注意
- このスクリプトは SecureString を平文に展開して入力 します。
そのため、利用環境の安全性を十分に考慮する必要があります。 - 公共PCやセキュリティが担保できない環境では使用しないでください。
スクリプトの主な機能
- STAスレッドでの実行を保証
UIAutomation を利用するためには STA(Single Threaded Apartment)が必要です。
もし MTA で実行されていた場合、自動的に再起動して STA モードに切り替えます。 - KeePassを起動/既存プロセス利用
すでに KeePass が起動していればそのウィンドウを利用。
起動していなければ指定パスから KeePass を立ち上げます。 - UIAutomation でパスワード欄を特定
ウィンドウ内のControlType.Editコントロールから「Password」「Master password」などのラベルを探し出し、自動入力します。 - OKボタンを押下
ボタンのラベル(OK, Open, 開く など)を探索し、Invoke または SendKeys で押下します。 - 平文パスワードを上書きクリア
入力後は、可能な限り平文を破棄し、SecureString のメモリも解放します。
コード全文
以下がスクリプトの全コードです。必要に応じて $KeePassPath などの設定値を変更してください。
<#
.SYNOPSIS
KeePass(例: KeePass 2.x)を起動してマスターパスワードを自動入力、OK ボタンを押すスクリプト。
.NOTES
- スクリプトは STA スレッドで実行する必要があります。STA でなければ自動で再起動します。
- KeePass のウィンドウタイトルやコントロール名はバージョン・ロケールにより異なるため、うまく行かない場合は $WindowTitlePattern やコントロール探索ロジックを調整してください。
- セキュリティ注意:SecureString を一時的に平文に展開して入力するため、実行環境の安全性に注意してください。
#>
# --- 設定 (必要なら変更) ---
$KeePassPath = "C:\Program Files\KeePass Password Safe 2\KeePass.exe" # KeePass 実行ファイルのフルパス
$KeePassArgs = "" # 引数が必要なら
$WindowTitlePattern = "KeePass" # KeePass ウィンドウを見つけるためのパターン(部分一致)
# --- STA チェック & 自動再起動 ---
if ([System.Threading.Thread]::CurrentThread.ApartmentState -ne 'STA') {
# 再起動して STA にする
$myInvocation = (Get-Variable MyInvocation -Scope 0).Value
$scriptPath = $myInvocation.MyCommand.Definition
$argsList = @()
if ($PSBoundParameters.Count -gt 0) {
foreach ($k in $PSBoundParameters.Keys) {
$argsList += "-$k"
$argsList += $PSBoundParameters[$k]
}
}
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = (Get-Command pwsh -ErrorAction SilentlyContinue).Source
if (-not $psi.FileName) { $psi.FileName = (Get-Command powershell -ErrorAction SilentlyContinue).Source }
if (-not $psi.FileName) { throw "pwsh / powershell が見つかりません。手動で STA にして実行してください。" }
$psi.Arguments = "-STA -NoProfile -File `"$scriptPath`""
if ($argsList) { $psi.Arguments += " " + ($argsList -join " ") }
$psi.UseShellExecute = $false
Write-Host "現在のスレッドは STA ではありません。STA で再起動します..."
[System.Diagnostics.Process]::Start($psi) | Out-Null
exit
}
# --- 必要なアセンブリ読み込み ---
Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes
# --- ヘルパー関数 ---
function Get-PlainTextFrom-SecureString {
param([System.Security.SecureString]$s)
if (-not $s) { return $null }
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($s)
try {
return [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
} finally {
if ($bstr -ne [IntPtr]::Zero) {
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
}
}
}
function Wait-ForProcessWindow {
param(
[int]$pid,
[int]$timeoutSec = 15
)
$sw = [Diagnostics.Stopwatch]::StartNew()
while ($sw.Elapsed.TotalSeconds -lt $timeoutSec) {
try {
$p = Get-Process -Id $pid -ErrorAction SilentlyContinue
if ($p -and $p.MainWindowHandle -ne 0) {
return $p
}
} catch {}
Start-Sleep -Milliseconds 300
}
return $null
}
function Find-ElementByPredicate {
param(
[System.Windows.Automation.AutomationElement]$root,
[ScriptBlock]$predicate,
[int]$timeoutSec = 5
)
$sw = [Diagnostics.Stopwatch]::StartNew()
while ($sw.Elapsed.TotalSeconds -lt $timeoutSec) {
$all = $root.FindAll(
[System.Windows.Automation.TreeScope]::Descendants,
[System.Windows.Automation.Condition]::TrueCondition
)
for ($i = 0; $i -lt $all.Count; $i++) {
# ❌ $el = $all.Get($i)
# ⭕ 下記いずれかに変更
# $el = $all.Item($i)
$el = $all[$i]
try {
if (& $predicate $el) { return $el }
} catch {}
}
Start-Sleep -Milliseconds 200
}
return $null
}
# --- ユーザにパスワードを聞く(安全入力) ---
$secure = Read-Host "Enter KeePass master password (input hidden)" -AsSecureString
if (-not $secure) {
Write-Error "パスワードが入力されませんでした。終了します。"
exit 1
}
$plain = Get-PlainTextFrom-SecureString -s $secure
if (-not $plain) {
Write-Error "SecureString の処理に失敗しました。"
exit 1
}
# --- KeePass プロセス起動(既に起動していれば既存を利用) ---
$existing = Get-Process | Where-Object { $_.MainWindowTitle -and $_.MainWindowTitle -like "*$WindowTitlePattern*" } | Select-Object -First 1
if ($existing) {
Write-Host "既存の KeePass プロセスを使用します (PID=$($existing.Id))"
$proc = $existing
} else {
if (-not (Test-Path $KeePassPath)) {
Write-Error "KeePass 実行ファイルが見つかりません: $KeePassPath"
exit 1
}
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
$startInfo.FileName = $KeePassPath
if ($KeePassArgs) { $startInfo.Arguments = $KeePassArgs }
$startInfo.UseShellExecute = $true
$procObj = [Diagnostics.Process]::Start($startInfo)
if (-not $procObj) { Write-Error "KeePass の起動に失敗しました"; exit 1 }
$proc = Wait-ForProcessWindow -pid $procObj.Id -timeoutSec 15
if (-not $proc) { Write-Error "KeePass のウィンドウ検出に失敗しました"; exit 1 }
}
# --- AutomationElement のルート (ウィンドウ) を取得 ---
$hwnd = $proc.MainWindowHandle
if (-not $hwnd -or $hwnd -eq 0) {
Write-Error "MainWindowHandle が取得できませんでした。"
exit 1
}
$root = [System.Windows.Automation.AutomationElement]::FromHandle([IntPtr]$hwnd)
if (-not $root) {
Write-Error "AutomationElement が取得できませんでした。"
exit 1
}
# --- パスワード入力欄を探す ---
# 方法:
# 1) Name プロパティに "Password" / "Master password" 等が含まれるコントロール(Edit)を探す
# 2) 見つからなければ最初の Edit (テキスト入力) を使用する
$predicate = {
param($el)
try {
$controlType = $el.Current.ControlType
if ($controlType -and $controlType.ProgrammaticName -eq "ControlType.Edit") {
$name = $el.Current.Name
if ($name -and ($name -like "*Password*" -or $name -like "*master*" -or $name -like "*マスタ*")) { return $true }
# AutomationId や ClassName で判定したい場合はここに追加
}
} catch {}
return $false
}
$pwElement = Find-ElementByPredicate -root $root -predicate $predicate -timeoutSec 6
if (-not $pwElement) {
# フォールバック: 最初の Edit 要素
$pwElement = Find-ElementByPredicate -root $root -predicate { param($e) try { $e.Current.ControlType.ProgrammaticName -eq "ControlType.Edit" } catch { $false } } -timeoutSec 4
}
if (-not $pwElement) {
Write-Error "パスワード入力欄を自動検出できませんでした。UI 構成が想定と異なる可能性があります。"
exit 1
}
# --- ValuePattern で値を設定する ---
$valuePattern = $null
try {
$valuePattern = $pwElement.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)
} catch {
# 取得失敗
}
if ($valuePattern) {
$vp = [System.Windows.Automation.ValuePattern]$valuePattern
try {
$vp.SetValue($plain)
Write-Host "パスワードを入力しました。"
} catch {
Write-Warning "ValuePattern.SetValue に失敗しました: $_"
}
} else {
# 代替: SendKeys でフォーカスして送信
try {
$pwElement.SetFocus()
Start-Sleep -Milliseconds 150
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait($plain)
Write-Host "SendKeys でパスワードを送信しました。"
} catch {
Write-Error "パスワード入力に失敗しました: $_"
exit 1
}
}
# --- OK ボタンを探して押す ---
$btnPredicate = {
param($el)
try {
$ct = $el.Current.ControlType
if ($ct -and $ct.ProgrammaticName -eq "ControlType.Button") {
$n = $el.Current.Name
if ($n -and ($n -like "OK" -or $n -like "Open" -or $n -like "開く" -or $n -like "OK(&O)" -or $n -like "*OK*")) { return $true }
}
} catch {}
return $false
}
$okBtn = Find-ElementByPredicate -root $root -predicate $btnPredicate -timeoutSec 4
if (-not $okBtn) {
# フォールバック: 最初の Button
$okBtn = Find-ElementByPredicate -root $root -predicate { param($e) try { $e.Current.ControlType.ProgrammaticName -eq "ControlType.Button" } catch { $false } } -timeoutSec 3
}
if ($okBtn) {
try {
$invoke = $okBtn.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern)
[System.Windows.Automation.InvokePattern]$invoke.Invoke()
Write-Host "OK ボタンをクリックしました。"
} catch {
# フォーカス → Enter の代替
try {
$okBtn.SetFocus()
Start-Sleep -Milliseconds 150
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait("{ENTER}")
Write-Host "Enter で送信しました(フォールバック)。"
} catch {
Write-Warning "ボタン押下に失敗しました: $_"
}
}
} else {
Write-Warning "OK ボタンが見つかりませんでした。手動で確認してください。"
}
# --- 後片付け: 平文をクリア ---
if ($plain) {
$plain = $plain -replace '.', ' ' # 可能な限り上書き(簡易)
$plain = $null
}
# SecureString は既に安全に解放済み(Get-PlainTextFrom-SecureString 内で ZeroFreeBSTR を実施)
Write-Host "完了。"
応用方法
- KeePass 以外にも、任意のWindowsアプリに対して自動入力を行うことが可能です。
例: VPNクライアント、業務用ツールのログイン画面など。 - 判定条件(
$predicate)を調整することで、特定のラベルやボタンを柔軟に探し出せます。
まとめ
このスクリプトを使えば、KeePass の起動からログインまでを自動化し、手入力の手間を省くことができます。
ただし、セキュリティリスクがあるため、利用環境に注意しつつ、安全な場面でのみ利用することをおすすめします。