CRAM-MD5はPOP, IMAPで使用されるチャレンジ・レスポンス方式の認証方法で、HMAC-MD5ハッシュアルゴリズムを用いてパスワードを暗号化する。 IMAPのAUTHENTICATEコマンドおよびPOPのAUTHコマンドにてCRAM-MD5による認証を行う場合のシーケンスは次のようになる。

IMAP AUTHENTICATEコマンドのシーケンス
C:	0001 AUTHENTICATE CRAM-MD5
S:	+ PDQwMDEzNDQxMTIxNDM1OTQuMTI3MjQ5OTU1MEBtYWlsLmV4YW1wbGUubmV0Pg==
C:	dXNlciAxZDFiOTFiN2FkM2ZjMjYxZjljZDgyOTUzMWYyMzVlYw==
S:	0001 OK Logged in.
POP AUTHコマンドのシーケンス
C:	AUTH CRAM-MD5
S:	+ PDQwMDEzNDQxMTIxNDM1OTQuMTI3MjQ5OTU1MEBtYWlsLmV4YW1wbGUubmV0Pg==
C:	dXNlciAxZDFiOTFiN2FkM2ZjMjYxZjljZDgyOTUzMWYyMzVlYw==
S:	+OK

AUTHENTICATEコマンド/AUTHコマンドで送受信される内容はBase64エンコードされるため、上記の送受信内容をデコードすると次のようになっている。 ここではユーザー名にuser、パスワードにpassを使用している。

Base64デコードしたIMAP AUTHENTICATEコマンドのシーケンス
C:	0001 AUTHENTICATE CRAM-MD5
S:	+ <4001344112143594.1272499550@mail.example.net>
C:	user 1d1b91b7ad3fc261f9cd829531f235ec
S:	0001 OK Logged in.
Base64デコードしたPOP AUTHコマンドのシーケンス
C:	AUTH CRAM-MD5
S:	+ <4001344112143594.1272499550@mail.example.net>
C:	user 1d1b91b7ad3fc261f9cd829531f235ec
S:	+OK

AUTHENTICATEコマンド/AUTHコマンドを送信すると、サーバーからはまずタイムスタンプとホスト名を含むチャレンジコードがBase64エンコードされた上で返される。 上記の例では<4001344112143594.1272499550@mail.example.net>がチャレンジコードである。

クライアントは、サーバーから受信したチャレンジコードに対してパスワードをキーにしたHMAC-MD5ハッシュアルゴリズムを適用し、さらに認証を行うユーザ名とHMAC-MD5を適用したチャレンジコードを連結し、それをBase64エンコードしてレスポンスとして返す。 上記の例では1d1b91b7ad3fc261f9cd829531f235ecがHMAC-MD5ハッシュ化したチャレンジコードである。

この処理を図式化して表記すると以下のようになる。

AUTHENTICATEコマンド/AUTHコマンドのシーケンス
C:	AUTH CRAM-MD5
S:	+ Base64(チャレンジコード)
C:	Base64(ユーザ名 HMAC-MD5(パスワード, チャレンジコード))
S:	+OK

(ここでチャレンジコード = <タイムスタンプ@ホスト名>)

§1 C#でのCRAM-MD5認証の実装

Mono/.NET FrameworkではHMAC-MD5ハッシュ値の計算にSystem.Security.Cryptography.HMACMD5クラスのComputeHashメソッドを用いることが出来る。 次の例は受信したチャレンジコードと、ユーザ名、パスワードを引数としてCRAM-MD5認証のレスポンスを返すメソッド。

CRAM-MD5認証のレスポンスを返すメソッドの実装
public string AuthenticateCramMD5(string challenge, string username, string password)
{
  // Base64デコードしたチャレンジコードに対してパスワードをキーとしたHMAC-MD5ハッシュ値を計算する
  var keyed = (new HMACMD5(Encoding.ASCII.GetBytes(password))).ComputeHash(Convert.FromBase64String(challenge));

  // 計算したHMAC-MD5ハッシュ値のbyte[]を16進表記の文字列に変換する
  var digest = string.Empty;

  foreach (var octet in keyed) {
    digest += octet.ToString("x02");
  }

  // ユーザ名と計算したHMAC-MD5ハッシュ値をBase64エンコードしてレスポンスとして返す
  return Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Format("{0} {1}", username, digest)));
}

§2 C#でのHMAC-MD5の実装

次の例はRFC 2104で記述されている内容に基づいて作成したHMAC-MD5ハッシュ値の計算を行うメソッド。 HMACMD5.ComputeHashメソッドに相当するものを実装したもの。 (コメントはRFCより抜粋)

HMAC-MD5の実装
public byte[] HmacMd5(byte[] key, byte[] text)
{
  const int B = 64;

  // 2. Definition of HMAC

  // The definition of HMAC requires a cryptographic hash function, 
  // which we denote by H,
  var H = new MD5CryptoServiceProvider();

  // and a secret key K
  var K = new byte[B];

  // We assume H to be a cryptographic hash function where data is
  // hashed by iterating a basic compression function on blocks of data.
  // We denote by B the byte-length of such blocks
  // (B=64 for all the above mentioned examples of hash functions)

  // The authentication key K can be of any length up to B, the
  // block length of the hash function.
  if (B < key.Length)
    throw new InvalidOperationException("key length is too long");

  // We define two fixed and different strings ipad and opad as follows
  // (the 'i' and 'o' are mnemonics for inner and outer):
  var ipad  = new byte[B]; // ipad = the byte 0x36 repeated B times
  var opad  = new byte[B]; // opad = the byte 0x5C repeated B times

  // To compute HMAC over the data `text' we perform
  //      H(K XOR opad, H(K XOR ipad, text))

  // (1) append zeros to the end of K to create a B byte string
  key.CopyTo(K, 0);

  for (var i = key.Length; i < B; i++) {
    K[i] = 0x00;
  }

  for (var i = 0; i < B; i++) {
    // (2) XOR (bitwise exclusive-OR) the B byte string computed in 
    //     step (1) with ipad
    ipad[i] = (byte)(K[i] ^ 0x36);

    // (5) XOR (bitwise exclusive-OR) the B byte string computed in
    //     step (1) with opad
    opad[i] = (byte)(K[i] ^ 0x5c);
  }

  // (3) append the stream of data 'text' to the B byte string
  //     resulting from step (2)
  var hi = new byte[ipad.Length + text.Length]; // hi = K XOR ipad, text
  ipad.CopyTo(hi, 0);
  text.CopyTo(hi, ipad.Length);

  // (4) apply H to the stream generated in step (3)
  var Hi = H.ComputeHash(hi); // Hi = H(hi) = H(K XOR ipad, text)

  // (6) append the H result from step (4) to the B byte string
  //     resulting from step (5)
  var ho = new byte[opad.Length + Hi.Length]; // ho = K XOR opad, H(hi)
  opad.CopyTo(ho, 0);
  Hi  .CopyTo(ho, opad.Length);

  // (7) apply H to the stream generated in step (6) and output
  //     the result
  H.Initialize();

  return H.ComputeHash(ho); // Ho = H(ho) = H(K XOR opad, H(hi))
}