PowerShellでKeePassのマスターパスワードを自動入力する方法

はじめに

パスワード管理ツール KeePass は非常に便利ですが、毎回マスターパスワードを手入力するのは手間がかかります。
そこで、PowerShell と UIAutomation を利用して 自動で KeePass を起動し、マスターパスワードを入力してログインするスクリプト を作成しました。

この記事では、以下の内容を紹介します:

  • スクリプトの概要
  • セキュリティ上の注意点
  • コード全文と解説
  • 応用方法

セキュリティに関する注意

  • このスクリプトは SecureString を平文に展開して入力 します。
    そのため、利用環境の安全性を十分に考慮する必要があります。
  • 公共PCやセキュリティが担保できない環境では使用しないでください。

スクリプトの主な機能

  1. STAスレッドでの実行を保証
    UIAutomation を利用するためには STA(Single Threaded Apartment)が必要です。
    もし MTA で実行されていた場合、自動的に再起動して STA モードに切り替えます。
  2. KeePassを起動/既存プロセス利用
    すでに KeePass が起動していればそのウィンドウを利用。
    起動していなければ指定パスから KeePass を立ち上げます。
  3. UIAutomation でパスワード欄を特定
    ウィンドウ内の ControlType.Edit コントロールから「Password」「Master password」などのラベルを探し出し、自動入力します。
  4. OKボタンを押下
    ボタンのラベル(OK, Open, 開く など)を探索し、Invoke または SendKeys で押下します。
  5. 平文パスワードを上書きクリア
    入力後は、可能な限り平文を破棄し、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 の起動からログインまでを自動化し、手入力の手間を省くことができます。
ただし、セキュリティリスクがあるため、利用環境に注意しつつ、安全な場面でのみ利用することをおすすめします。

スポンサーリンク

-IT関連
-, ,