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
= dayflowerserver
= fooprotocol
= 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)