001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019 020 package org.apache.geronimo.javamail.authentication; 021 022 import java.io.UnsupportedEncodingException; 023 import java.security.MessageDigest; 024 import java.security.NoSuchAlgorithmException; 025 026 import javax.mail.MessagingException; 027 028 import org.apache.geronimo.mail.util.Hex; 029 030 public class CramMD5Authenticator implements ClientAuthenticator { 031 032 // the user we're authenticating 033 protected String username; 034 035 // the user's password (the "shared secret") 036 protected String password; 037 038 // indicates whether we've gone through the entire challenge process. 039 protected boolean complete = false; 040 041 /** 042 * Main constructor. 043 * 044 * @param username 045 * The login user name. 046 * @param password 047 * The login password. 048 */ 049 public CramMD5Authenticator(String username, String password) { 050 this.username = username; 051 this.password = password; 052 } 053 054 /** 055 * Respond to the hasInitialResponse query. This mechanism does not have an 056 * initial response. 057 * 058 * @return Always returns false. 059 */ 060 public boolean hasInitialResponse() { 061 return false; 062 } 063 064 /** 065 * Indicate whether the challenge/response process is complete. 066 * 067 * @return True if the last challenge has been processed, false otherwise. 068 */ 069 public boolean isComplete() { 070 return complete; 071 } 072 073 /** 074 * Retrieve the authenticator mechanism name. 075 * 076 * @return Always returns the string "CRAM-MD5" 077 */ 078 public String getMechanismName() { 079 return "CRAM-MD5"; 080 } 081 082 /** 083 * Evaluate a CRAM-MD5 login challenge, returning the a result string that 084 * should satisfy the clallenge. 085 * 086 * @param challenge 087 * The decoded challenge data, as a byte array. 088 * 089 * @return A formatted challege response, as an array of bytes. 090 * @exception MessagingException 091 */ 092 public byte[] evaluateChallenge(byte[] challenge) throws MessagingException { 093 // we create the challenge from the userid and password information (the 094 // "shared secret"). 095 byte[] passBytes; 096 097 try { 098 // get the password in an UTF-8 encoding to create the token 099 passBytes = password.getBytes("UTF-8"); 100 // compute the password digest using the key 101 byte[] digest = computeCramDigest(passBytes, challenge); 102 103 // create a unified string using the user name and the hex encoded 104 // digest 105 String responseString = username + " " + new String(Hex.encode(digest)); 106 complete = true; 107 return responseString.getBytes(); 108 } catch (UnsupportedEncodingException e) { 109 // got an error, fail this 110 throw new MessagingException("Invalid character encodings"); 111 } 112 113 } 114 115 /** 116 * Compute a CRAM digest using the hmac_md5 algorithm. See the description 117 * of RFC 2104 for algorithm details. 118 * 119 * @param key 120 * The key (K) for the calculation. 121 * @param input 122 * The encrypted text value. 123 * 124 * @return The computed digest, as a byte array value. 125 * @exception NoSuchAlgorithmException 126 */ 127 protected byte[] computeCramDigest(byte[] key, byte[] input) throws MessagingException { 128 // CRAM digests are computed using the MD5 algorithm. 129 MessageDigest digest; 130 try { 131 digest = MessageDigest.getInstance("MD5"); 132 } catch (NoSuchAlgorithmException e) { 133 throw new MessagingException("Unable to access MD5 message digest", e); 134 } 135 136 // if the key is longer than 64 bytes, then we get a digest of the key 137 // and use that instead. 138 // this is required by RFC 2104. 139 if (key.length > 64) { 140 digest.update(key); 141 key = digest.digest(); 142 } 143 144 // now we create two 64 bit padding keys, initialized with the key 145 // information. 146 byte[] ipad = new byte[64]; 147 byte[] opad = new byte[64]; 148 149 System.arraycopy(key, 0, ipad, 0, key.length); 150 System.arraycopy(key, 0, opad, 0, key.length); 151 152 // and these versions are munged by XORing with "magic" values. 153 154 for (int i = 0; i < 64; i++) { 155 ipad[i] ^= 0x36; 156 opad[i] ^= 0x5c; 157 } 158 159 // now there are a pair of MD5 operations performed, and inner and an 160 // outer. The spec defines this as 161 // H(K XOR opad, H(K XOR ipad, text)), where H is the MD5 operation. 162 163 // inner operation 164 digest.reset(); 165 digest.update(ipad); 166 digest.update(input); // this appends the text to the pad 167 byte[] md5digest = digest.digest(); 168 169 // outer operation 170 digest.reset(); 171 digest.update(opad); 172 digest.update(md5digest); 173 return digest.digest(); // final result 174 } 175 }