GnuPGで公開鍵暗号方式でファイルを暗号化する為のGUIラッパー

id:eel3:20130526:1369569828 の続き的なネタ。

GnuPG公開鍵暗号方式でファイルを暗号化する場合、使用する公開鍵を指定しなくてはならない。

復号化する相手が決まりきっている場合は、バッチファイルに使用する公開鍵まで含めてコマンドを記述しておき、そのショートカットを「送る」メニューに登録すればよい。

@echo off
gpg.exe -r myself@example.com -r you@example.com --encrypt-files %*

この方法なら、暗号化したいファイルを「送る」のショートカット経由で一発で暗号化できる。

しかし使用する公開鍵がケースバイケースで変化するような場合、公開鍵を選択する仕組みが必要となる。

そこでPowerShell + WPF + XAMLGUIラッパーアプリを書いてみた。GnuPTやGpg4winは、ちょっとばかりオーバースペックなので。

# You must execute this script in STA mode
# $ powershell -Sta -File <this script> [file ...]

Set-StrictMode -version Latest

Add-Type -assemblyName PresentationFramework
Add-Type -assemblyName System.Windows.Forms

Set-Variable -name APP_TITLE -value 'Encrypt files with GPG' -option Constant

function Notify-Message ([string] $message = '')
{
    [Void][System.Windows.Forms.MessageBox]::Show($message, $APP_TITLE);
}

function Get-GpgKeyList
{
    $list = @{}
    $nkeys = 0

    gpg.exe --batch --no-tty --fixed-list-mode --list-keys | %{
        if ($_ -cmatch '^uid\s+(?<name>[\w ]+) <(?<mail>\S+)>$') {
            $list[$Matches.mail] = @{
                name = $Matches.name
                mail = $Matches.mail
                checked = $False
            }
            $nkeys++
        }
    } | Out-Null

    $list, $nkeys
}

function Escape-Xml ([string] $s)
{
    [System.Security.SecurityElement]::Escape($s)
}

function ConvertTo-CheckBoxTag
{
    begin {$nr = 0}
    process {
        $nr++
        -join ('<CheckBox Name="', "checkBox$nr", '" ',
                         'Content="', (Escape-Xml $_), '" ',
                         'HorizontalAlignment="Left" ',
                         'DockPanel.Dock="Top" />')
    }
}

function Make-XamlTags ([hashtable] $keyList)
{
    $tags = $keyList[$keyList.Keys] |
            %{"$($_.name) <$($_.mail)>"} |
            Sort-Object |
            ConvertTo-CheckBoxTag

    @"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="$APP_TITLE"
    Width="400" Height="300"
>
<DockPanel>
<Button Name="encryptButton" Content="Encrypt" DockPanel.Dock="Bottom" />
<ScrollViewer HorizontalScrollBarVisibility="Auto">
<DockPanel LastChildFill="False">
$tags
</DockPanel>
</ScrollViewer>
</DockPanel>
</Window>
"@
}

function Show-MainWindow
{
    $key_list, $nkeys = Get-GpgKeyList
    if ($nkeys -le 0) {
        Notify-Message 'No GPG keys found.'
        exit 1
    }
    $xaml = Make-XamlTags $key_list
    $window = [System.Windows.Markup.XamlReader]::Parse($xaml)

    $button = $window.FindName('encryptButton')
    $button.Add_Click({
        $keys = $key_list[$key_list.Keys] |
                ?{$_.checked} |
                %{'-r', $_.mail}

        if (($keys | Measure-Object).Count -eq 0) {
            Notify-Message 'No GPG keys selected.'
            return
        }
        $script:public_keys = $keys
        $script:do_encrypt = $True
        $window.Close()
    })

    1..$nkeys | %{
        $check_box = $window.FindName("checkBox$_")
        $check_box.Add_Checked({
            param([System.Windows.Controls.CheckBox] $cb)
            if ($cb.Content -cmatch '^[\w ]+ <(?<mail>\S+)>$') {
                $key_list[$Matches.mail].checked = $True
            }
        })
        $check_box.Add_Unchecked({
            param([System.Windows.Controls.CheckBox] $cb)
            if ($cb.Content -cmatch '^[\w ]+ <(?<mail>\S+)>$') {
                $key_list[$Matches.mail].checked = $False
            }
        })
    }

    $window.ShowDialog() | Out-Null
}

if (($Args | Measure-Object).Count -eq 0) {
    Notify-Message 'No files specified.'
    exit 1
}
$Args | %{
    if (-not (Test-Path -literalPath $_  -pathType Leaf)) {
        Notify-Message "file not found: '$_'"
        exit 1
    }
}

$public_keys = $Null
$do_encrypt = $False

Show-MainWindow

if ($do_encrypt) {
    gpg.exe $public_keys --encrypt-files $Args
    exit $LastExitCode
}

GnuPGに登録されている公開鍵の情報を取り出す部分の正規表現は適当なので、他の環境では問題があると思う。名前に記号文字が含まれていなくて且つコメントを登録していない環境向け、なはず。

引数に指定されたファイルの存在チェックをしているけど、「送る」メニューから使う想定なのでワイルドカードのことは考えていない。

さて、このPowerShellスクリプトのショートカットを「送る」メニューに登録しても動かないので、こんな内容のバッチファイルを作成してPowerShellスクリプトと同じフォルダに配置して、そのショートカットを登録しておく。

@echo off
start powershell -ExecutionPolicy RemoteSigned -Sta -WindowStyle Hidden -File %~dp0encrypt.ps1 %*

WPFを使うために「-Sta」を、GUIだけを見せる為に「-WindowStyle Hidden」を指定している。あとこのバッチファイル自体も即座に終了させる為にstartコマンドを使用している。

実際に使用すると、コマンドプロンプトが一瞬表示される現象が2回発生する。ちょっと微妙かもしれない。