コンソールからのパスワード入力

作成:

C言語プログラミングの最初期に学ぶであろうscanf()を使用した入力方法。 これ一つでコンソールからの様々な入力を行うことができる。 しかし、CUIコンソールで使ったことのあるプログラム群は、 scanf()だけでは実現できなさそうな機能を実装しているだろう。 今回はそのうちの一つ、パスワード入力について解説する。

標準入力

パスワード入力とはいえ、利用者が入力したものをプログラムから取得するということになるので、 scanf()を使って入門書通りに実現するとすると以下のようになるだろう。

#include <stdio.h>

int main(int argc, char **argv) {
  char password[128];
  scanf("%s", password);
  printf("%s\n", password);
  return 0;
}

実行し"password"と入力したとすると以下のように表示されるだろう。



この方法の問題点は、入力されたパスワードがコンソールにそのまま表示されてしまうという点だ。 最近は、画面の前にいる人物に対して表示されてしまうことは、それほど問題ではないという考え方もあるようだが、 コンソールでは履歴が残ることもあり、あまり気持ちのいいものではない。

エコーの停止

コンソールからの入力を行う際、利用者が入力した内容がそのまま画面に出力されるのだが、 誰が表示しているのかを考えたことはあるだろうか? これを表示しているのはコンソール自身だ。 この利用者の入力をコンソールに表示させる機能をエコーという。 エコーはいつなんどきでも有効というわけではなく、デフォルトで有効になっているだけで、 設定を変更すれば無効にすることができる。

UNIX ではコンソールの属性・設定値を取得変更できる API が存在する。 これを利用することで、エコーを無効にする。

ここではパスワード入力という観点からの変更についてのみ解説するが、 他のパラメータの意味などは MAN などを参考にして欲しい。

現在の値の取得には以下の関数を利用する。

#include <termios.h>
int tcgetattr(int fd, struct termios *termios_p);

fdに指定したファイルディスクリプタに結び付けられたターミナル情報を取得する。 今回の場合は、標準入力、標準出力、標準エラー出力、のどれを指定してもよい。 ただし、例えば標準入力はstdinというシンボルで利用していると思うが、これは FILE 構造体のポインタであり、 ファイルディスクリプタではない。 POSIXでは標準入出力のファイルディスクリプタの値は決まっており、以下のシンボルを参照する。

#include <unistd.h>
#define STDIN_FILENO    0       /* standard input file descriptor */
#define STDOUT_FILENO   1       /* standard output file descriptor */
#define STDERR_FILENO   2       /* standard error file descriptor */

今回は関係ないが、一般的なFILE構造体からファイルディスクリプタを取り出したい場合は、以下の関数を利用する。

#include <stdio.h>
int fileno(FILE *stream);

値の設定は以下の関数を利用する。

#include <termios.h>
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);

optional_actionsは設定値をいつ反映させるかを指定する。

取得設定で使用される属性値は、以下の構造体となっており、属性は各フィールドのビット和で表現されている。

struct termios
{
  tcflag_t      c_iflag;
  tcflag_t      c_oflag;
  tcflag_t      c_cflag;
  tcflag_t      c_lflag;
  char          c_line;
  cc_t          c_cc[NCCS];
  speed_t       c_ispeed;
  speed_t       c_ospeed;
};

複数のフィールドがあり、一つのフィールドに複数のビットで設定値が表現されているため、 特定の値だけを変更する場合は、一度現在の値を取得し、変更したいビット値のみを変更するという手順が必要だ。

また、ここで行う処理は、コンソールの設定を変更するという動作なので、 実行プログラムが終了しても自動的に元に戻ることはない。 そのため、プログラム終了前には変更前の値を書き戻すという処理も必要となる。 それを忘れていたり、バグなどでその処理に至る前に終了してしまった場合、 変更した値によっては操作不能に陥る可能性があることも注意して欲しい。

さて、話を戻して、この属性値のうち、エコーを無効にする場合は c_lflagsの値のECHOビットを倒せば良い。

あるビットフィールドの特定のビットを倒すという処理を、ビット演算で表現すると、 そのビットのみが倒れている値、すなわち対象ビットのみが立っている値のNOT値と、 対象ビットフィールドのANDを取れば良い。

それを実装したのが以下になる。

#include <stdio.h>
#include <termios.h>
#include <unistd.h>

int main(int argc, char **argv) {
  char password[128];
  struct termios term;
  struct termios save;
  tcgetattr(STDIN_FILENO, &term);
  save = term;
  term.c_lflag &= ~ECHO;
  tcsetattr(STDIN_FILENO, TCSANOW, &term);
  scanf("%s", password);
  printf("%s\n", password);
  tcsetattr(STDIN_FILENO, TCSANOW, &save);
  return 0;
}

一旦現在の設定値を取得し、その構造体のコピーを保存しておき、値を変更して反映、 そして処理終了後に保存しておいた値を反映させ、属性値を戻すという流れである。

実行すると、以下のようにキー入力中には何も画面に反映されず、 エンターキーを押下した段階でscanfによる入力が行われているのが分かるだろう。


このようにすることで、利用者の入力を画面に表示させないで入力を行うことができるようになる。

非カノニカルモード

エコーを無効にすることで利用者の入力を画面に反映する機能を停止することはできた。 ただ、多くのパスワード入力では入力された文字数だけは分かるような表現を行っていると思う。 それを実現するにはどうすればよいだろうか。

それにはコンソールを非カノニカルモードにする必要がある。 通常のコンソールの状態はカノニカルモードと呼ばれる状態で以下の様な違いがある。

カノニカルモード
入力は行単位に行われ、行区切りが入力された段階で有効となる。 また、行区切りが入力されるまでの間は行単位の編集が可能
非カノニカルモード
利用者の入力は即座に有効となる、また編集機能は無効となる

通常、あまり意識することはないかもしれないが、 標準入力からの入力を取得しようとすると、キーボードの一文字一文字の入力単位では取得できず、 エンターキーを押下した段階で、一行まとめてプログラム側で取得することができるようになっている。 また、利用者側で入力ミスがあった場合、 バックスペースやカーソルキーなどを使って入力した内容を修正することができるはずだ。 ここで行われている操作は、入力待ちをしているプログラム側ではうかがい知ることはできない。 最終的にエンターキーを押下された段階で、編集後の値がまとめて受け渡されるのみである。

このような状態をカノニカルモードという。 通常であれば編集機能等についてプログラム側でケアをする必要がないなど便利なのだが、 直接利用者のキーボード入力を受け取ることができないため、詳細な制御ができないという制約が出てくる。 逆に言えば非カノニカルモードにすればこれらの機能が無効になり、詳細な制御が可能になるということである。

非カノニカルモードにするには c_lflagsの値のICANONビットを倒せば良い。 併せて、利用者の入力が表示されないように、エコーも無効にする。 その処理を実装したのが以下になる。

#include <stdio.h>
#include <ctype.h>
#include <termios.h>
#include <unistd.h>

int main(int argc, char **argv) {
  int i;
  int tmp;
  char password[128];
  struct termios term;
  struct termios save;
  tcgetattr(STDIN_FILENO, &term);
  save = term;
  term.c_lflag &= ~ICANON;
  term.c_lflag &= ~ECHO;
  tcsetattr(STDIN_FILENO, TCSANOW, &term);
  for(i = 0; i < sizeof(password) - 1; i++) {
    tmp = fgetc(stdin);
    if (tmp < 0 || iscntrl(tmp)) {
      fprintf(stderr, "\n");
      break;
    }
    password[i] = tmp;
    fprintf(stderr, "*");
  }
  password[i] = 0;
  printf("%s\n", password);
  tcsetattr(STDIN_FILENO, TCSANOW, &save);
  return 0;
}

一文字ずつ入力を取得するため、fgetcを利用している。 利用者からの入力を受け取るたびに、その文字の代わりにアスタリスク*を表示する。 そしてエンターキーの入力をうけて終了させるが、判定としてはEOFもしくは制御文字を受け取ったら終了としている。 通常はケアする必要のない、逆に言えばプログラム側から取得できないキーボードの制御コードも、 すべてプログラム側で受け取り、受けとった後の処理を実装する必要がある。

実行すると以下のようになる。



これで概ね、「パスワード入力」に求められる機能を実現できたことと思う。 もうひとつ課題を上げるとすれば、非カノニカルモードにしたために編集機能が無効になってしまい、 利用者がカーソルキーやバックスペースキーを押しても編集できなくなっている点だろうか。

編集機能を提供する場合は、バックスペースキーなどの制御コードを判別して、入力された文字内容を変更し、 また、すでに出力済みのアスタリスク*の数を増減させるなどの対応を自前で実装する必要がある。 その辺りについては今後の課題として、ここでの解説は以上とする。