SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine(コードジン) DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

Rubyで簡易POP3サーバを作る

RubyによるネットワークプログラミングとUNIXシステムプログラミングの基礎

APOP認証

 この節ではAPOP認証とその実装について解説します。

APOP認証の仕組み

 まず、APOP認証の流れをざっと説明しましょう。次の図を見ながら考えてください。

APOPの仕組み
APOPの仕組み

 APOPの認証は以下の手順で進みます。

  1. まずサーバは、接続ごとにユニークな文字列である「APOPスタンプ」を発行します。
  2. クライアントは渡された「APOPスタンプ」を使いパスワードを加工して「ダイジェスト」を作り、それをサーバに渡します。
  3. サーバも同様の方法でパスワードを加工してダイジェストを作り、それがクライアントが返したダイジェストと一致すれば、クライアントを認証します。

 クライアントは接続ごとに異なるAPOPスタンプを使ってパスワードを加工するので、ダイジェストも接続ごとに変化します。また、パスワードを加工するときにはMD5という仕組みが使われており、ダイジェストから元の文字列を計算するのは非常に困難です。ですから、もし通信内容をまるごと悪人に見られたとしても、パスワードを推測するのは難しいわけです。

 もっとも、APOPもいいことばかりではありません。一番の問題は、サーバがパスワードを平文で保存しなければいけないことです。通常の認証方法を使う場合には、サーバはパスワードを暗号化して保存できます。それと比較すると、APOPを使うことで通信中にパスワードを盗まれる危険は減りますが、サーバ側のセキュリティリスクが高まることになります。

認証前の流れ

 では、ここからはAPOP認証の実装について解説していきます。再度POPd#_sessionメソッドを見てください。

POPd#_sessionメソッド(2回目)
def _session
  stamp = apop_stamp()
  print_line "+OK popd #{stamp}"
  mailbox = auth(stamp)
  while cmd = read_cmd()
    mid = "cmd_#{cmd.name}"
    unless respond_to?(mid, true)
      print_line '-ERR unknown command'
      next
    end
    __send__(mid, mailbox, cmd.args)
  end
end

 このメソッドのうち、APOP認証に関係があるのは最初の3行だけです。残りは認証が済んだあとのコードなので、当面無視してください。

 最初の3行のうち、まだ説明していないメソッドはapop_stampauthです。apop_stampメソッドはAPOPスタンプを作成します。authメソッドは実際の認証を行います。

 上記のコードでは、まずapop_stampメソッドでAPOPスタンプを作り、print_lineメソッドでそれを含めた最初の反応(+OK ……)を返します。そしてauthメソッドでクライアントを認証します。

 メソッドの定義を順番に見ていきましょう。

POPd#apop_stampメソッドの定義

 POPd#apop_stampはAPOPスタンプを生成するメソッドです。POPd#apop_stampメソッドのコードを以下に示します。

POPd#apop_stampメソッド
def apop_stamp
  thread_id = sprintf('%x', Thread.current.object_id)
  "<#{Time.now.to_i}.#{Process.pid}.#{thread_id}@#{Socket.gethostname}>"
end

 APOPスタンプは、メールで使われるメッセージID(message ID)と同じ形式の文字列で、接続のたびに違う文字列にならなければいけません。そこで、毎回値を変えるために、以下の3つの値を利用します。

  • 時刻(Time.now.to_i
  • プロセスID(Process.pid
  • スレッドID(Thread.current.object_id

 さらに、APOPスタンプは違うホスト上では違う値にならなければいけないので、POPサーバが動作しているホストのホスト名も使います。サーバが動作しているホストの名前を得るには、Socket.gethostnameメソッドが使えます。

 以上の値をすべて含む文字列を生成すれば、ホストごとにユニークで、かつ接続のたびに違う文字列が得られます。

POPd#authメソッドの定義

 次に、実際に認証を行うPOPd#authメソッドを説明します。POPd#authメソッドのコードを以下に示します。

POPd#authメソッド
# support only APOP
def auth(stamp)
  while cmd = read_cmd()
    case cmd.name
    when 'APOP'
      begin
        account = auth_APOP(stamp, cmd.args)
        print_line '+OK'
        return account.mailbox
      rescue => err
        print_line "-ERR #{err.message}"
      end
    when 'QUIT'
      print_line '+OK bye'
      terminate
    else
      print_line '-ERR use apop'
    end
  end
  terminate
end

 このように、POPd#authメソッドではwhileループでPOPコマンドを読み込みながら、クライアントが送ったコマンドを順番に処理していきます。認証前の段階で使ってよいPOPコマンドは、APOPQUIT、それから通常の認証に使うUSERだけです。ただし、POPdではUSERには対応しないので無視して-ERRを返します。それ以外のコマンドにもすべて-ERRを返します。

 実際にAPOP認証を行っているのは、「when 'APOP'」の節、さらに言えばauth_APOPメソッドです。auth_APOPメソッドは、認証が成功したら、APOPコマンドで指定されたユーザーに対応するPOPd::Accountオブジェクトを返します。認証に失敗した場合は、例外POPd::AuthErrorが発生します。

 認証が成功したら、クライアントに「+OK」を返して、authメソッド自体はユーザーのメールボックスオブジェクト(account.spool)を返します。メールボックスオブジェクトとは、具体的には、POPd::Maildirオブジェクトです。

POPd#auth_APOPメソッドの定義

 さて、APOP認証の具体的な実装はすべてauth_APOPメソッドで行われています。次にPOPd#auth_APOPメソッドを見てみましょう。

POPd#auth_APOPメソッド
def auth_APOP(stamp, args)
  raise SyntaxError, 'apop syntax error' unless args.size == 2
  username, digest = *args
  account = @accounts[username]
  unless digest == create_digest(stamp, account.password)
    raise AuthError, 'wrong password'
  end unless $DEBUG
  raise AuthError, 'no mbox' unless account.mailbox_exist?
  account
end

def create_digest(stamp, password)
  Digest::MD5.hexdigest(stamp + password)
rescue
  raise if $DEBUG
  raise ArgumentError, 'configuration error'
end

 APOPコマンドは「APOP ユーザー名 ダイジェスト」という構文で、引数は常に2つです。つまりauth_APOPメソッドの引数argsは、例えば["taro", "xxxxxxxxxxxxxxxxxxxx"]のような値です。

 まず、@accounts[username]でユーザー名に対応するPOPd::Accountオブジェクトを得ます。ここでユーザーが存在しなかったら、例外ArgumentErrorが発生します。

 次に、create_digestメソッドでサーバ側のダイジェストを作り、クライアントが送ってきたダイジェスト(digestの値)と比較します。値が同じなら認証は成功です。値が違ったら認証失敗なので、POPd::AuthErrorを発生させます。

 最後に、account.mailbox_exist?でアカウントのメールボックスが実際に存在するかどうか確認して、存在しないならやはり認証は失敗とみなします。

 ちなみに、本当はここでメールボックスをロックしなければいけません。しかし今回のコードでは「同じユーザが同時にアクセスすることはきっとないだろう」という希望的観測のもとにロックを省略しています。

アカウント情報の取得

 POPd::AccountTableクラスとPOPd::Accountクラスについては、コードを見れば内容は分かってしまうくらい単純なので、本稿では説明を省略します。ただし、アカウント情報(POPdで使用するのはホームディレクトリのみ)の取得については、やや説明が必要だと思うので、その点だけ解説します。

 POPdでは、Rubyのetcライブラリを使ってアカウントの情報を取得します。具体的には、Etc.getpwnamメソッドを使って、ユーザー名に対応するアカウント情報を取得します。例えばユーザー「taro」のアカウント情報を得るには、「Etc.getpwnam("taro")」を使います。

 Etc.getpwnamメソッドの返り値はStruct::Passwd構造体です。Struct::Passwd構造体のメンバは、OSによって微妙に違いますが、少なくとも以下のメンバは共通です。

Struct::Passwd構造体の共通メンバ
メンバ名解説
nameユーザー名
passwd暗号化されたパスワード
uidユーザーID
gidグループID
gecosGECOSフィールド
dirホームディレクトリのパス
shellログインシェル

 POPdでは、パスワードを記録したファイルの場所を得るためにdirメンバ(ホームディレクトリ)だけを使っています。

次のページ
POP3コマンドの実装

この記事は参考になりましたか?

特集記事連載記事一覧

もっと読む

この記事の著者

青木 峰郎(アオキ ミネロウ)

ふつうの文系プログラマ。Ruby界隈で活動中。主著は『ふつうのLinuxプログラミング』(ソフトバンククリエイティブ、2005)、『Rubyソースコード完全解説』(インプレス、2002)。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/687 2006/11/10 00:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー