gnome-keyring を利用してコマンドライン上のパスワードを置換する

今日はサーバ向けじゃなくて Linux クライアント向け(かつ GNOME 上)な話です。

コマンドラインのオプションからパスワードを指定できるコマンドがありますよね。例えば,リモートデスクトップに接続するコマンド rdesktop

% rdesktop -u dayflower -p hogehoge machine

とか。mysql とか psql とかもそうですね。

この手のコマンドをふつうにシェルから実行すると,ヒストリに残ったりしていやな感じです。シェルから実行するのではなくとも,自動実行のためにスクリプト等を書くと,どうしてもそのスクリプトに生パスワードが残ることになります。

なので,パスワードを gnome-keyring に保管してもらうことにして,保管したパスワードを使ってコマンドを実行できるようなプログラムを書いてみました。一度(ログイン時などに)keyring を解除すれば,その後パスワードを聞かれることなくプログラムを実行できるというわけです。ssh-agent みたいな感じです。

使い方

使い方ですが,まず

% pwexec -r rdesktop set

Password: ********
Retype Password: ********
Successfully stored password.

のように(今後コマンドラインで利用する)パスワードを登録します。-r オプションの部分は一意であればなんでもよいです(パスワードのラベルのようなものです)。今回はわかりやすく rdesktop にしました。

もしこれまでの操作でまだ keyring を解除していない場合,keyring のパスワードを聞かれるかもしれません。

さて,いざ実行する段ですが,

% pwexec -r rdesktop exec rdesktop -u dayflower -p %PASSWORD% machine

のように,exec というアクションを指定します。そしてそのあとに,通常実行する場合のコマンドラインを書いていきます。%PASSWORD% の部分は例示ではなく,実際にその値を指定します。このトークンで示された部分がパスワードで置換されます。


おっと。いままでの例だとセッション keyring を利用していたので,一度ログオフするとパスワードが消えてしまいます。永続化して保存したい場合,

% pwexec -r rdesktop -k default set

のように,-k default として keyring の名前を明示的に指定します。default とすると,デフォルトの keyring になります。なにも指定しない場合,-k session のように指定したのと同じ意味をもつことになります。

セッション keyring でない場合,seahorse(アクセサリの「パスワードと暗号鍵」)の「パスワード」タブに表示され*1,そちらで変更とか削除とかもできます。このプログラムでも変更や削除もできるようにしてますが。


ちなみに,上記の例を実行して ps を実行すると,

% ps -efw| grep rdesktop

dayflower 6245 6235 1 15:32 pts/0 00:00:00 rdesktop -u dayflower -p XXXXXXXX

のようになります。これはこのプログラムがパスワードを XXXXXXXX で隠したわけではなく,rdesktop がメモリ上のコマンドラインのパスワード部分を隠すようにインプリメントしてあるからです。コマンドラインからパスワードを指定できるプログラムはたいていこのようになっています((mysql コマンドもこのように隠します。))。ですが,このパスワード隠蔽にいたるまでの一瞬のタイミングでパスワードを読み取れることがありうるので,セキュリティ上厳密な環境ではコマンドラインからパスワードを与えることは避けたほうがよいでしょう。えーっと何がいいたいかというと,このプログラムではパスワードを置換して execvp() でなげているだけので,シェルのヒストリ等には残らなくても理論上はパスワードが漏洩しうる,ということです。まして上記のようなインプリメントを行っていない insecure なアプリケーションの場合,ps コマンドや /proc/* インタフェースから生パスワードを得ることが可能になってしまいます。構造上このプログラムでは対策のしようは(たぶん)ありません。

内部構造

GNOME Keyring の Simple Password Storage API を利用しています。なのでわりと最近(2.21 以降?)の GNOME でしかビルドできないと思います。Ubuntu 8.10 (Intrepid) で動作確認をおこないました。

Simple Password Storage API はかなり単純な API です*2ので下記サイトを読めば同じようなプログラムを書くことは難しくありません。なお,一般的には非同期インタフェースを利用するほうがよいということになっていますが,面倒だったので同期インタフェース - *_sync()API を利用しました。

より深く

Simple Password Storage API では「スキーマ」というものを指定します。デフォルトでGNOME_KEYRING_NETWORK_PASSWORD というスキーマが用意されていますが,自分でスキーマを定義するのもそんなに難しくありません。本プログラムでも独自のスキーマを定義しています。

スキーマとパスワードの関係についてちょっと説明します。

さきほどあげた GNOME_KEYRING_NETWORK_PASSWORD には,下記の attributes が存在します。

  • user
  • server
  • protocol
  • domain
  • port

パスワードを store する際,これらの attributes をすべて指定する必要はありません。たとえば,

  • user = dayflower
  • server = foo
  • protocol = http

だけを指定して password = bar のようにパスワードを store できます。

そしてパスワードを find する際 attributes を指定してマッチしたパスワードを得るわけですが,たとえば

  • user = dayflower

という attribute だけを指定しても*3上記の password がひっかかります。同一の user が指定されたパスワードがいくつかある場合,どれが返されるかはわかりません。たとえていうなら,

SELECT password FROM network_passwords WHERE user = dayflower LIMIT 1;

のようにしてパスワードを取得しているようなものです。しかも Simple Password Storage API では取得したパスワードに関連付けられた他の attributes を得る手段はありません*4

なお,スキーマが異なる場合,該当するパスワードがあっても取得できません。たとえば,

  • user
  • group

というスキーマが存在しており,このスキーム下で user = dayflower のパスワードを登録してあったとしましょう。上記のように GNOME_KEYRING_NETWORK_PASSWORD というスキーマのもとで user = dayflower という attributes のパスワードを取得しようとしても,こちらのスキーマのパスワードは取得対象にはならないということです。


今回のプログラムでは,

  • pwexec_ver (UINT32 型)
  • realm (STRING 型)

という attributes をもつスキーマを定義して使用しています。本当は realm attribute 1つで十分なのですが,上記のようにたまたま realm attribute 1つのスキーマを他のアプリケーションで利用していた場合にバッティングがおこってしまうので,あえてプログラムのバージョンをいれて基本的にバッティングが発生しないようにしています。

ソース

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <getopt.h>
#include <glib.h>
#include <gnome-keyring.h>

static const char  APPVER_KEY[] = "pwexec_ver";
static const guint APPVER_VALUE = 1;
static const char  REALM[]      = "realm";

static GnomeKeyringPasswordSchema pwexec_schema = {
    GNOME_KEYRING_ITEM_GENERIC_SECRET,
    {
        { APPVER_KEY, GNOME_KEYRING_ATTRIBUTE_TYPE_UINT32 },
        { REALM,      GNOME_KEYRING_ATTRIBUTE_TYPE_STRING },
        { NULL, 0 },
    }
};

static const gchar *action  = NULL;
static const gchar *keyring = GNOME_KEYRING_SESSION;
static const gchar *realm   = NULL;

static void
usage(void)
{
    fputs("usage: pwexec [OPTIONS] <action> [<command> <args> ...]\n", stderr);
}

int
main(int argc, char *argv[])
{
    static struct option options[] = {
        /* options */
        { "keyring", required_argument, NULL, 'k' },
        { "realm",   required_argument, NULL, 'r' },

        /* command, can be set as option form */
        { "set",     no_argument,       NULL, 'S' },
        { "delete",  no_argument,       NULL, 'D' },
        { "exec",    no_argument,       NULL, 'X' },

        /* help */
        { "help",    no_argument,       NULL, '?' },

        { NULL, 0, NULL, 0 }
    };
    GnomeKeyringResult keyres;

    g_set_application_name("pwexec");

    for (;;) {
        int c, final = 0;

        c = getopt_long(argc, argv, "-k:r:", options, NULL);
        if (c < 0)
            break;

        switch (c) {

        case 0:
        case 1:
            if (optind <= 1)
                return 1;
            action = argv[optind - 1];
            final = 1;
            break;

        case 'S':
        case 'D':
        case 'X':
            if (optind <= 1)
                return 1;
            action = argv[optind - 1] + 2;  /* skip prefix '--' */
            final = 1;
            break;

        case 'k':
            keyring = optarg;
            break;
        case 'r':
            realm = optarg;
            break;

        case '?':
            usage();
            return 1;
        }

        if (final)
            break;
    }

    if (! action
     || (strcmp(action, "exec")
      && strcmp(action, "set")
      && strcmp(action, "delete"))) {
        fputs("error: ", stderr);
        if (action)
            fputs("unsupported action was specified; ", stderr);
        else
            fputs("action was not specified; ", stderr);
        fputs("action can be one of 'exec', 'set', 'delete'.\n", stderr);
        usage();
        return 1;
    }

    if (! realm) {
        fputs("error: realm was not specified.\n", stderr);
        return 1;
    }

    if (! strcmp(action, "delete")) {
        keyres = gnome_keyring_delete_password_sync(&pwexec_schema,
                                                    APPVER_KEY, APPVER_VALUE,
                                                    REALM,      realm,
                                                    NULL);
        if (keyres != GNOME_KEYRING_RESULT_OK) {
            fprintf(stderr, "failed to delete password: %s\n",
                            gnome_keyring_result_to_message(keyres));

            return keyres;
        }

        fputs("Successfully deleted password.\n", stderr);
        return 0;
    }
    else if (! strcmp(action, "set")) {
        gchar *password = NULL, *retype = NULL;
        gchar *name = NULL;

        /* you must specify keyring */
        if (! keyring) {
            fputs("error: kerring was not specified.\n", stderr);
            return 1;
        }
        if (keyring && ! strcasecmp(keyring, "default"))
            keyring = GNOME_KEYRING_DEFAULT;

        /* overwrite check */
        keyres = gnome_keyring_find_password_sync(&pwexec_schema,
                                                  &password,
                                                  APPVER_KEY, APPVER_VALUE,
                                                  REALM,      realm,
                                                  NULL);
        if (keyres == GNOME_KEYRING_RESULT_OK) {
            char buf[16];

            gnome_keyring_free_password(password);
            fputs("password already exists in specified realm.\n"
                  "Overwrite? [Y/n]", stderr);
            fflush(stderr);

            if (! fgets(buf, 16, stdin))
                return 0;
            g_strstrip(buf);

            /* not 'YES' => don't advance */
            if (*buf != 0 && strcasecmp(buf, "yes") && strcasecmp(buf, "y"))
                return 0;
        }

        password = g_strdup(getpass("Password: "));
        retype   =          getpass("Retype Password: ");
        if (! password || ! retype || strcmp(password, retype)) {
            fputs("error: password error\n", stderr);
            gnome_keyring_free_password(password);
            return 1;
        }

        name = g_strdup_printf("pwexec key for %s", realm);

        keyres = gnome_keyring_store_password_sync(&pwexec_schema,
                                                   keyring,
                                                   name,
                                                   password,
                                                   APPVER_KEY, APPVER_VALUE,
                                                   REALM,      realm,
                                                   NULL);
        gnome_keyring_free_password(password);
        g_free(name);
        if (keyres != GNOME_KEYRING_RESULT_OK) {
            fprintf(stderr, "failed to store password: %s\n",
                            gnome_keyring_result_to_message(keyres));

            return keyres;
        }

        fputs("Successfully stored password.\n", stderr);
        return 0;
    }
    else if (! strcmp(action, "exec")) {
        gchar *password = NULL;
        int i, pindex = -1, r;

        if (! argv[optind]) {
            fputs("error: command line was not specified.\n", stderr);
            usage();
            return 1;
        }

        for (i = optind; argv[i]; i ++) {
            if (! strcmp(argv[i], "%PASSWORD%"))
                pindex = i;
        }
        if (pindex < 0) {
            fputs("error: '%PASSWORD%' template was not specified"
                  " in command line.\n", stderr);
            usage();
            return 1;
        }

        keyres = gnome_keyring_find_password_sync(&pwexec_schema,
                                                  &password,
                                                  APPVER_KEY, APPVER_VALUE,
                                                  REALM,      realm,
                                                  NULL);
        if (keyres != GNOME_KEYRING_RESULT_OK) {
            fprintf(stderr, "failed to find password: %s\n",
                            gnome_keyring_result_to_message(keyres));

            return keyres;
        }

        /* overwrite password template */
        argv[pindex] = password;

        /* now, execute! */
        r = execvp(argv[optind], argv + optind);
        /* in successful condition, it doesn't reach */

        perror("failed to execute");

        gnome_keyring_free_password(password);

        return r;
    }

    return 0;
}

ビルド方法

libglib や libgnome-keyring を使用しているのでいろいろインクルードディレクトリ等を指定しなきゃいけないんですが,pkg-config を使用すると取得できます。なので,下記のような Makefile を書いてビルドしました。

TARGET:=	pwexec

GLIB:=		glib-2.0
GNOME_KEYRING:=	gnome-keyring-1

CFLAGS+=	-O2 -Wall

CFLAGS+=	$(shell pkg-config --cflags $(GLIB))
CFLAGS+=	$(shell pkg-config --cflags $(GNOME_KEYRING))
LIBS+=		$(shell pkg-config --libs   $(GLIB))
LIBS+=		$(shell pkg-config --libs   $(GNOME_KEYRING))

all:	build

build:	$(TARGET)

clean:
	rm -f $(TARGET) *.o

$(TARGET):	$(TARGET).o
	$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LIBS)

余談

コマンドラインのパースに popt を使おうと思ったんですが,今回のように「後続する引数を使ってコマンドを実行する」場合にうまくいかない(実行するコマンドのオプションも解釈してしまう)ので,結局 getopt_long を使いました。なので help が充実していません。残念。

コマンドライン解析部分で結構な行数をとってしまったのも残念。コアロジックはたいしたことないのですが。

*1:かつての gnome-keyring-manager ではセッション keyring も管理できたような気がするのですが。当時全然使ったことがなかったので記憶違いかもしれません。

*2:そのぶん利用できる機能も制限されていますが。

*3:もちろんすべての attibutes の値を指定してパスワードを取り出すこともできます。

*4:legacy でより低レベルな API でできるかどうかはわかりません。が Seahorse で attributes を見ることができるのでたぶんできるでしょう。