「クリップボードのテキストをGnuPGで復号化」の代替品を作る

問題編

仕事でGnuPGを使用している。Windows上で使うこともあり、今までは古いWinPT*1を使用していた。鍵の管理はWinPT経由で行い、メールの暗号化/復号化にはEnigmailを使用し、ファイルの暗号化/復号化は内製のツールを使用していた。

古いWinPTを使っていた為、これまでGnuPGは1.2系だった。これがEnigmail 1.4よりGnuPG 1.4系でないと動作しないようになってしまったので、GnuPGのバージョンを上げることになった。

で、ここでWinPTの後継であるGnuPTにするか別アプリのGpg4winにするか迷ったのだけど、思い切ってGnuPG本体のみ*2に切り替えてしまうことにした。幸いにもEnigmailも内製のファイル暗号化/復号化ツールもGnuPG 1.4系で動作するので、鍵の管理はコンソールで行うことになるもののそれ以外の作業は従来通りに行える。

ただ1点だけ困ったことがある。ごく稀に二重に暗号化されたメールが届くことがある。Enigmailで復号化してもGnuPGで暗号化された文章が表示されるだけなので、更に復号化しなくてはならない。今まではここで暗号化された文章をクリップボードにコピーし、WinPTの「クリップボードの内容の復号化」の機能を使用して復号化していた。WinPTを使わなくなるので、代替案を考えなくてはならない。

解決編:構想

クリップボードを使う」という方法自体は便利なので、そのアイデアは生かしたい。gpg.exeは標準入力から読み込んだデータを復号化できるので、クリップボードからテキストを取り出して標準出力に垂れ流すツールを作ってしまえば何とかなりそうだ。

ただクリップボードを経由すると何処かで文字コードの変換が発生するように思うのだが*3、暗号化されたメールの中身を見る限りASCIIの範囲の文字を使っていそうなので*4、CF_TEXTかCF_OEMTEXTでテキストを取得すればセーフだろう。

厄介なのは復号化したテキストの表示で、というのも復号後の文字コードISO-2022-JPかもしれないしUTF-8かもしれないしShift_JISかもしれないのだ。ここは全くの他力本願で、「復号化したデータを一時ファイルに書き出し、適当なテキストエディタ/ビューアで表示する」という方法で誤魔化すことにした。

一旦は「復号化したデータをクリップボードにコピーする」というのも考えたのだけど、文字コードの判定と変換が必要そうだったので止めた。

解決編:実装

クリップボードからテキストを取り出すツールをC言語で書いた。Windows APIを叩くコンソールアプリだ。

/* ********************************************************************** */
/**
 * @brief    rtcb; Read Text from ClipBoard
 * @author   eel3 @ TRASH BOX
 * @date     2012/05/05
 *
 * @par 動作確認済み環境:
 *   - Microsoft Windows XP Professional (32bit) SP3
 *
 * @par 確認済みコンパイラ:
 *   - Microsoft(R) Visual Studio 2005 SP1
 *   - Microsoft(R) Visual Studio 2010
 *   - TDM-GCC 4.5.1
 */
/* ********************************************************************** */


#ifndef __MINGW32__
    #define _CRT_SECURE_NO_WARNINGS
    #pragma comment(lib, "User32.lib")
#endif

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

#include <fcntl.h>
#include <io.h>
#include <tchar.h>
#include <windows.h>

#ifndef STDIN_FILENO
    #define STDIN_FILENO 0
#endif
#ifndef STDOUT_FILENO
    #define STDOUT_FILENO 1
#endif

#ifdef __MINGW32__
    #ifndef _tperror
        #ifdef UNICODE
            #define _tperror _wperror
        #else
            #define _tperror perror
        #endif
    #endif
    typedef DWORD GS_SIZE_T;
#else
    typedef SIZE_T GS_SIZE_T;
#endif


/* ---------------------------------------------------------------------- */
/* 関数の定義 */
/* ---------------------------------------------------------------------- */

/* ====================================================================== */
/**
 * @brief  エラー出力して終了する
 *
 * @param[in] s           エラー出力のヘッダとなる文字列
 * @param[in] last_error  エラーコード
 */
/* ====================================================================== */
static void error_exit(const LPCTSTR s, const DWORD last_error)
{
    LPTSTR buf;

    assert(s != NULL);

    FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER
                  | FORMAT_MESSAGE_IGNORE_INSERTS
                  | FORMAT_MESSAGE_FROM_SYSTEM,
                  NULL,
                  last_error,
                  MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                  (LPTSTR) &buf,
                  0,
                  NULL);

    _ftprintf(stderr, _T("%s: %s"), s, buf);
    LocalFree((HLOCAL) buf);

    exit(EXIT_FAILURE);
}

/* ********************************************************************** */
/**
 * @brief  メインルーチン
 *
 * @retval EXIT_SUCCESS  正常終了
 * @retval EXIT_FAILURE  異常終了
 */
/* ********************************************************************** */
int main(void)
{
#define ERREXIT(s)                  \
    DWORD err = GetLastError();     \
    (void) CloseClipboard();        \
    error_exit(_T(s), err);

    HANDLE hMem;
    LPVOID p;
    GS_SIZE_T size;

    errno = 0;
    if (_setmode(STDOUT_FILENO, O_BINARY) == -1) {
        _tperror(_T("_setmode"));
        return EXIT_FAILURE;
    }

    if (!OpenClipboard(NULL)) {
        error_exit(_T("OpenClipboard"), GetLastError());
    }

    if (!IsClipboardFormatAvailable(CF_OEMTEXT)) {
        ERREXIT("IsClipboardFormatAvailable");
    }

    if ((hMem = GetClipboardData(CF_OEMTEXT)) == NULL) {
        ERREXIT("GetClipboardData");
    }

    if ((p = GlobalLock(hMem)) == NULL) {
        ERREXIT("GlobalLock");
    }

    if ((size = GlobalSize(hMem)) > 1) {
        /* ヌル文字は出力しない */
        (void) fwrite(p, size - 1, 1, stdout);
    }

    (void) GlobalUnlock(hMem);
    (void) CloseClipboard();

    return EXIT_SUCCESS;

#undef ERREXIT
}

MinGW(TDM-GCC)とVisual Studio 2005 or 2010でビルドできる。不要かもしれないが、念の為_setmode()を使用してバイナリデータでも問題なく標準出力に流し込めるようにしている。

このアプリをrtcb.exeという名前でビルドした上で、次のようなバッチファイルでラッピングした。

@echo off
setlocal

for /f "usebackq tokens=1-7 delims=/:." %%A in (`echo %DATE%/%TIME: =0%`) do (
    set TMPNAM="C:\tmp\__rtcb_tmp_file_%%A%%B%%C%%D%%E%%F%%G.dat"
)

%~dp0rtcb.exe | gpg.exe -d > %TMPNAM%
ttpage %TMPNAM%
del %TMPNAM%

endlocal

このバッチファイルは以下の環境を想定している。

  • このバッチファイルをrtcb.exeと同じフォルダに配置している。
  • 一時作業用フォルダとしてC:\tmpを使用している。
  • 環境変数PATHにgpg.exeの置いてあるフォルダが登録されている。
  • テキストビューアとしてttpageが使用可能で、且つ環境変数PATHに登録済み。

一時ファイルの名前のつけ方は適当。安全な方法ではない。

あとクリップボードにテキストが無かった場合や暗号化データ以外のテキストだった場合の処理を全く考慮していない。あくまで自分用のツールなので運用でカバーする腹積もりだ。

*1:日本語化されていた。

*2:実はftp.gnupg.orgからコンソールアプリのみのWindows用バイナリを取得できる。但し1.4系のみ。

*3:可能性が高いのは、例えばThunderbirdのウィンドウからテキストをコピーする時点。

*4:この辺り、本来なら仕様を調べるべきだけど……。