/*
* Sun Public License
*
* The contents of this file are subject to the Sun Public License Version
* 1.0 (the "License"). You may not use this file except in compliance with
* the License. A copy of the License is available at http://www.sun.com/
*
* The Original Code is the SLAMD Distributed Load Generation Engine.
* The Initial Developer of the Original Code is Neil A. Wilson.
* Portions created by Neil A. Wilson are Copyright (C) 2004-2010.
* Some preexisting portions Copyright (C) 2002-2006 Sun Microsystems, Inc.
* All Rights Reserved.
*
* Contributor(s): Neil A. Wilson
*/
package com.slamd.scripting.mail;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import com.slamd.asn1.ASN1Element;
import com.unboundid.util.Base64;
/**
* This class provides a set of methods that can be used in the process of
* authenticating to a server using CRAM-MD5 as defined in RFC 2195, which
* itself is based on HMAC as defined in RFC 2104.
*
*
* @author Neil A. Wilson
*/
public class CRAMMD5Handler
{
/**
* The block length in characters used in generating an HMAC-MD5 digest.
*/
public static final int BLOCK_LENGTH = 64;
/**
* The number of bytes contained in an MD5 digest.
*/
public static final int MD5_DIGEST_LENGTH = 16;
/**
* The inner pad byte, which will be XORed with the shared secret for the
* first digest.
*/
public static final byte IPAD_BYTE = 0x36;
/**
* The outer pad byte, which will be XORed with the shared secret for the
* second digest.
*/
public static final byte OPAD_BYTE = 0x5c;
// An array filled with the iPad byte.
private byte[] iPadArray;
// An array filled with the oPad byte.
private byte[] oPadArray;
// The message digest that will be used to create MD5 hashes.
private MessageDigest md5Digest;
/**
* Provides a means of testing this CRAM-MD5 handler by generating a CRAM-MD5
* response for the given information. The data to use to generate the
* response must be provided as arguments and the response will be written to
* standard output.
*
* @param args The command line arguments provided to this program. There
* must be exactly three arguments, and they must be the
* username, password, and challenge (in that order).
*/
public static void main(String[] args)
{
if (args.length != 3)
{
System.err.println("ERROR: There must be exactly 3 arguments " +
"(username, password, challenge)");
System.exit(1);
}
try
{
CRAMMD5Handler crammer = new CRAMMD5Handler();
System.out.println(crammer.generateCRAMMD5Response(args[0], args[1],
args[2]));
}
catch (Exception e)
{
System.err.println("Caught an exception during processing:");
e.printStackTrace();
}
}
/**
* Creates a new instance of this CRAM-MD5 handler.
*
* @throws NoSuchAlgorithmException If a problem occurs while trying to
* initialize the MD5 digest handler.
*/
public CRAMMD5Handler()
throws NoSuchAlgorithmException
{
md5Digest = MessageDigest.getInstance("MD5");
iPadArray = new byte[BLOCK_LENGTH];
oPadArray = new byte[BLOCK_LENGTH];
for (int i=0; i < BLOCK_LENGTH; i++)
{
iPadArray[i] = IPAD_BYTE;
oPadArray[i] = OPAD_BYTE;
}
}
/**
* Generates the CRAM-MD5 response that should be used for the provided
* information.
*
* @param username The username to use in the authentication process.
* @param password The password to use in the authentication process.
* @param challenge The challenge provided by the server to which
* authentication is to be performed.
*
* @return The string containing the CRAM-MD5 response.
*/
public String generateCRAMMD5Response(String username, String password,
String challenge)
{
// The resulting response will be the concatenation of the username, a
// space, and a hex string representation of the HMAC-MD5 hash of the
// password and the challenge. First, create a string buffer long enough to
// hold everything.
StringBuilder buffer = new StringBuilder(username.length() + 1 +
(2*MD5_DIGEST_LENGTH));
// Next, append the username and the space.
buffer.append(username);
buffer.append(' ');
// Finally, generate the HMAC-MD5 digest of the password and challenge,
// convert it to a string, and return it.
byte[] challengeBytes = ASN1Element.getBytes(challenge);
byte[] pwBytes = ASN1Element.getBytes(password);
byte[] hmacMD5Bytes = generateHMACMD5(challengeBytes, pwBytes);
writeToHexString(hmacMD5Bytes, buffer);
return Base64.encode(ASN1Element.getBytes(buffer.toString()));
}
/**
* Generates an HMAC-MD5 response based on the provided key and data.
*
* @param data The plain-text data to include in the response.
* @param key The secret key to use in the response.
*
* @return A byte array containing the HMAC-MD5 response.
*/
public byte[] generateHMACMD5(byte[] data, byte[] key)
{
// First, if the key is longer than BLOCK_LENGTH, then use the MD5 digest of
// the key instead of the actual key.
byte[] k;
if (key.length > BLOCK_LENGTH)
{
k = md5Digest.digest(key);
}
else
{
k = key;
}
// Create byte arrays that will hold the data we need to use in this
// process. Place the appropriate data in each array.
byte[] iPadAndData = new byte[BLOCK_LENGTH + data.length];
System.arraycopy(iPadArray, 0, iPadAndData, 0, BLOCK_LENGTH);
System.arraycopy(data, 0, iPadAndData, BLOCK_LENGTH, data.length);
byte[] oPadAndHash = new byte[BLOCK_LENGTH + MD5_DIGEST_LENGTH];
System.arraycopy(oPadArray, 0, oPadAndHash, 0, BLOCK_LENGTH);
// Iterate through the bytes in the key and XOR them with iPad and oPad as
// appropriate.
for (int i=0; i < k.length; i++)
{
iPadAndData[i] ^= k[i];
oPadAndHash[i] ^= k[i];
}
// Copy an MD5 digest of the iPad XORed key and the data into the array to
// be hashed.
System.arraycopy(md5Digest.digest(iPadAndData), 0, oPadAndHash,
BLOCK_LENGTH, MD5_DIGEST_LENGTH);
// Return an MD5 hash of the resulting combination of the iPadHash and the
// oPad XOR.
return md5Digest.digest(oPadAndHash);
}
/**
* Writes a hexadecimal representation of the contents of the provided byte
* array into the given string buffer. All hexadecimal digits greater than
* nine will use the lowercase alphabetic representation.
*
* @param byteArray The byte array to be written as a hex string.
* @param buffer The buffer to which the data should be written.
*/
public static void writeToHexString(byte[] byteArray, StringBuilder buffer)
{
for (int i=0; i < byteArray.length; i++)
{
switch ((byteArray[i] >>> 4) & 0x0F)
{
case 0x00:
buffer.append('0');
break;
case 0x01:
buffer.append('1');
break;
case 0x02:
buffer.append('2');
break;
case 0x03:
buffer.append('3');
break;
case 0x04:
buffer.append('4');
break;
case 0x05:
buffer.append('5');
break;
case 0x06:
buffer.append('6');
break;
case 0x07:
buffer.append('7');
break;
case 0x08:
buffer.append('8');
break;
case 0x09:
buffer.append('9');
break;
case 0x0a:
buffer.append('a');
break;
case 0x0b:
buffer.append('b');
break;
case 0x0c:
buffer.append('c');
break;
case 0x0d:
buffer.append('d');
break;
case 0x0e:
buffer.append('e');
break;
case 0x0f:
buffer.append('f');
break;
}
switch (byteArray[i] & 0x0F)
{
case 0x00:
buffer.append('0');
break;
case 0x01:
buffer.append('1');
break;
case 0x02:
buffer.append('2');
break;
case 0x03:
buffer.append('3');
break;
case 0x04:
buffer.append('4');
break;
case 0x05:
buffer.append('5');
break;
case 0x06:
buffer.append('6');
break;
case 0x07:
buffer.append('7');
break;
case 0x08:
buffer.append('8');
break;
case 0x09:
buffer.append('9');
break;
case 0x0a:
buffer.append('a');
break;
case 0x0b:
buffer.append('b');
break;
case 0x0c:
buffer.append('c');
break;
case 0x0d:
buffer.append('d');
break;
case 0x0e:
buffer.append('e');
break;
case 0x0f:
buffer.append('f');
break;
}
}
}
}