はじめに
パスワード管理ツール 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 の起動からログインまでを自動化し、手入力の手間を省くことができます。
ただし、セキュリティリスクがあるため、利用環境に注意しつつ、安全な場面でのみ利用することをおすすめします。