APOPとIMAP(CRAM-MD5)をC#で使おう
作成日:2003/8/14 最終更新日: 2005/1/18



◆概要

.Net Frameworkには,SMTPを操作するためのライブラリは標準で付属しますが,POPやIMAPを扱うライブラリは付属していません.Active Mail のようなサードパーティ製のライブラリは存在しますが,フリーで利用できるものはあまり見当たりません.そこで,ここではVisual Studio .NetとC#を用いて,APOPとIMAP(CRAM-MD5)の基本的な操作を行うプログラムを紹介します.

以下のサンプルコードは,指定したメールサーバーにAPOPかIMAP(CRAM-MD5)経由でアクセスし,一定時間ごとに新着メールの数を取得して,ダイアログを表示するものです.「通信過程のメッセージを表示する」をチェックすることで,やり取りされるメッセージ(の一部)を表示することもできます.

今回はまず,APOPやIMAPを使う際に最大のポイントとなるMD5という暗号化アルゴリズムとCRAM-MD5という認証手法について簡単に解説し,次にソースコードを参照しながら実際の通信の様子を紹介していきます.

サンプルコード (zip)
実行ファイルのみ (zip)

図1: APOPとIMAP通信のサンプル




◆MD5

MD5は,簡単に言えば,復元の難しい高度な暗号化アルゴリズムです.
たとえば,"A"と"AA"という似た文字列をこのアルゴリズムで暗号化すると,以下のようになります.

  • A → 7fc56270e7a70fa81a5935b72eacbe29
  • AA → 3b98e2dffc6cb06a89dcb0d5c60a0206

このように,もとの文字が一文字違うだけで,出力結果がまったく異なるため,もとの文字列を予測するのがかなり難しくなります.詳しい記述は,RFC 1321 などを参考にしてください.

C#でMD5を利用するには,MD5CryptoServiceProviderというクラスを利用します.

     using System.Security.Cryptography;


     public byte[] CreateMD5AsBytes (string str)
		{
			byte[] data = Util.ConvertStrToBytes(str);
			MD5 md5 = new MD5CryptoServiceProvider();
			byte[] result = md5.ComputeHash(data);

			return result;
		}



◆CRAM-MD5

CRAMとは,Challenge-Response Authentication Mechanismの略で,APOPやIMAP(CRAM-MD5)の認証に利用される仕組みです.APOPの場合を例にして簡単に説明すると,だいたい以下のような流れで利用されています.(IMAPの場合は,もう少し複雑な手順を踏むことになります.C#でIMAPの項や, HMAC-MD5の項を参照してください.)

  • (クライアントがサーバーにアクセスする)
  • サーバーは,「アクセス毎に変化するランダムな文字列 (Challenge)」を生成し,クライアントに送信する.
  • クライアントは,「受信した文字列とユーザのパスワードを連結し,MD5アルゴリズムで暗号化した文字列」をサーバーに送信する.
  • サーバーは,「受信した文字列(暗号化済み)」を,「送信したChallengeコードと(サーバー側の)ユーザのパスワードをMD5で暗号化したもの」を比較し,一致すれば認証成功となる.

ポイントとしては,ネットワーク上には生のパスワードが一度も流れないことです.また前述のように,MD5で暗号化された文字列は,もとの文字列が一文字異なるだけで,大きく変化してきます.よって,「ランダムな文字列」と「パスワード」の両方を知っている場合以外,つまりサーバーとクライアント以外のコンピュータでは,非常に解読が難しくなります.
(逆に言うと,CRAM-MD5を利用してもサーバーには生のパスワードをおいておく必要があるようです.)

詳しい記述は,RFC 2095 IMAP4 and CRAM-MD5 Authentication などを参考にしてください.



◆C#でAPOP

APOPを用いた基本的な通信の流れを,C#のソースコードと出力結果をもとに解説します.

APOPの通信は,MailCheckerクラスのConnectApopServerというメソッドを中心に実装しました.

public int ConncectApopServer(string user, string pass, string hostname)
{
	TcpClient client = new TcpClient();		
	NetworkStream stream;
	int result = 0;
まず,TcpClientを用いて,APOPのポート(110番)にソケット接続します.
		client.Connect(hostname, APOP_PORT);
		stream = client.GetStream();

次にサーバーからの返答を読み取り,簡単な正規表現を用いてChallengeキー(ランダムに生成される文字列)を 抜き出します.

		string inputStr = ReadStream(stream);
		Match m = Regex.Match(inputStr, @"^.+(<.+>).*$");
		
		if(!(m.Success))
		{
			MessageBox.Show("Error: " + inputStr);
			result = -1;
			return result;
		}

サーバーから受信する文字列の例

次に,Challengeキー(上の例では<26978.1060839180@mail>)とパスワードを連結し,MD5で暗号化した文字列を生成します.
そして,"APOP user 329435e5e66be809a656af105f42401e\r\n"といった書式の文字列を サーバーに送信し,認証を行います.

		string passStr = CreateMD5AsStr(m.Groups[1].Value + pass);		
		WriteStream(stream, "APOP " + user + " " + passStr + "\r\n");
サーバーからの返答を読み取ります.認証が成功した場合,"+OK"で始まる文字列が返ってきます. ここでは,正規表現を用いて,サーバーに格納されたメールの数を抜き出しています.
		string authStr = ReadStream(stream);
		if(authStr.StartsWith("+OK"))
		{
			Match m2 = Regex.Match(authStr, @"^\+OK \w+ has (\d+) visible messages .+$");
			if(!(m2.Success))
			{
				MessageBox.Show("Error: Can't parse received Data.");
				result = -1;
				return result;
			}
			
			result = int.Parse(m2.Groups[1].Value);

認証成功時のメッセージの例

最後に,"QUIT\r\n"という文字列を送信し,APOPサーバーからログアウトします. そして,ソケット接続を終了します.

		WriteStream(stream,"QUIT\r\n");
		ReadStream(stream);
		client.Close();

おおむね,APOPの通信手順はこのようなものです. 個々のメールの読み取りなどをする場合,認証後に特定の文字列を送信することで行います. 利用できるコマンドについては, RFC 1939 や,@ITのAPOP記事 を参照してください.

 


◆C#でIMAP (CRAM-MD5)

今度は,IMAPを用いた基本的な通信の流れを,C#のソースコードと出力結果をもとに解説します.

IMAPの通信は,MailCheckerクラスのConnectImapServerというメソッドを中心に実装しました.

	public int ConnectImapServer(string user, string pass, string hostname)
	{
		TcpClient client = new TcpClient();		
		NetworkStream stream;
		int result = 0;
	  

まず,TcpClientを用いて,IMAPのポート(143番)にソケット接続します.

		client.Connect(hostname, IMAP_PORT);
		stream = client.GetStream();

次にサーバーからの返答を読み取り,認証を開始するために以下のような文字列( クライアント名 + " AUTHENTICATE CRAM-MD5\r\n")を送信します.

		ReadStream(stream);
		WriteStream(stream, "MAILTEST AUTHENTICATE CRAM-MD5\r\n");

ソケット接続時にサーバーから返される文字列の例

次のサーバーからの返答には,Base64でエンコードされたChallengeキーが含まれます.まず,Base64をデコードして,Challengeキーを取得します.次に,ChallengeキーとパスワードをHMAC-MD5というアルゴリズムを用いて,暗号化します.(HMAC-MD5については後述).その後,ユーザ名と暗号化した文字列を連結して,サーバーに送信します.実際に送信する文字列は "user 329435e5e66be809a656af105f42401e\r\n"といったものになります.

		string inputStr = ReadStream(stream);
		Match m = Regex.Match(inputStr, @"^\+\s(.+)$");
		if(!(m.Success))
		{
			MessageBox.Show("Error: " + inputStr);	
			result = -1;
			return result;
		}

		string challengeStr = DecodeBase64Str(m.Groups[1].Value);
		string keyStr = CreateHmacMD5(pass, challengeStr);
		WriteStream(stream, EncodeBase64Str(user + " " + keyStr) + "\r\n");
				
				

Base64でエンコードされた状態のChallengeキーの例

平文のChallengeキーの例

次に,サーバーからの返答を読み取ります.認証が成功した場合は,その旨を示す文字列が返されます.

			
		if(ReadStream(stream) == "")
		{
			MessageBox.Show("Error: Authentication Error.");
			result = -1;
			return result;
		}

認証成功時のサーバーからの受信文字列の例

次に,メールボックスの状態を調べるために,クライアント名 + " EXAMINE INBOX\r\n"という文字列をサーバーに送信します.サーバーから返される文字列には,メールボックス内のメール数や,新着メールの数などが含まれます.ここでは,正規表現を用いて,新着メールの数を抜き出しています.

	   WriteStream(stream, "MAILTEST EXAMINE INBOX\r\n");
		string examineStr = ReadStream(stream);		
				
		Match m2 = Regex.Match(examineStr, @"\* (\d+) RECENT");

		if(!(m2.Success))
		{
			MessageBox.Show("Error: " + examineStr);	
			result = -1;
			return result;
		}
		result = int.Parse(m2.Groups[1].Value);
				

EXAMINEコマンドによりサーバーから返される文字列

最後に,クライアント名 + "LOGOUT\r\n"という文字列を送信して,ログアウトします.

		WriteStream(stream, "MAILTEST LOGOUT\r\n");
		ReadStream(stream);
				

おおむね,IMAPの通信手順はこのようなものです. 全体の流れはAPOPとほぼ同様ですが,暗号化に使うアルゴリズム(HMAC-MD5)は結構複雑なものになっています(次の章で簡単に説明します).個々のメールの読み取りなどをする場合,認証後に特定の文字列を送信することで行います. 利用できるコマンドについては, RFC 2060 などを参照してください.




補足: HMAC-MD5

IMAPの認証では,HMAC-MD5という暗号化手法が使われています.これは,二重にMD5で暗号化を行うなどの方法で,解読をさらに難しくする手法のようです. 詳しく知りたい方は RFC 2140 などを参考にしてください.ここでは,実装の方法のみを紹介します.

サンプルプログラムでは,MailCheckerのCreateHmacMD5というメソッドで実装しています.
まず,利用する変数を定義します.HMAC-MD5の基本的なアルゴリズムは,
MD5(passChars XOR 0x5c, MD5( passChars XOR 0x36, challenge))
となります.

MD5(X, Y)というのは,文字列XとYを連結して,MD5アルゴリズムを適用する,ということです.

private string CreateHmacMD5(string pass, string challenge)
		{
			char [] passChars = new char[64];
			char [] passIpad = new char[64];
			char [] passOpad = new char[64];


まず,パスワードを64バイトのChar型配列(passChars)に格納します.パスワードの長さを越えた部分は,初期値としてNUL(0x00)が格納されます.

			for(int i=0;i<pass.Length;i++)
			{	
				passChars[i] = pass[i];
			}

次に,HMAC-MD5の計算に利用するpassIpadとpassOpadの値を設定します.passIpad[i]は passChars[i] と 0x36, passOpad[i]は passChars[i]と0x5cの排他的論理和(XOR)となります.

			for(int i=0;i<passChars.Length;i++)
			{
				passIpad[i] = passChars[i];
				passOpad[i] = passChars[i];
				passIpad[i] ^= (char)0x36;
				passOpad[i] ^= (char)0x5c;
			}

次に, passIpadとchallenge文字列を連結して,MD5を計算します.ここでの計算結果は16進の文字列ではなく,バイト配列として保持します.

			byte [] ipadBytes = CreateMD5AsBytes((new String(passIpad)) + challenge);

最後に,passOpadと上のバイト配列を連結し,もう一度MD5を計算します.この計算結果を,今度は16進の文字列として返します.

			string opadStr = CreateMD5AsStr(
				(new String(passOpad)) + Util.ConvertBytesToStr(ipadBytes,ipadBytes.Length));

			return opadStr;
		}

 






◆参考URL


[俄プログラマー心得]