/*
* Copyright (c) 2000, 2012, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.security.sasl.digest;
import java.util.Map;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.math.BigInteger;
import java.util.Random;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
import java.security.spec.KeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.InvalidAlgorithmParameterException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.Mac;
import javax.crypto.SecretKeyFactory;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.DESedeKeySpec;
import javax.security.sasl.*;
import com.sun.security.sasl.util.AbstractSaslImpl;
import javax.security.auth.callback.CallbackHandler;
/**
* Utility class for DIGEST-MD5 mechanism. Provides utility methods
* and contains two inner classes which implement the SecurityCtx
* interface. The inner classes provide the funtionality to allow
* for quality-of-protection (QOP) with integrity checking and
* privacy.
*
* @author Jonathan Bruce
* @author Rosanna Lee
*/
abstract class DigestMD5Base extends AbstractSaslImpl {
/* ------------------------- Constants ------------------------ */
// Used for logging
private static final String DI_CLASS_NAME = DigestIntegrity.class.getName();
private static final String DP_CLASS_NAME = DigestPrivacy.class.getName();
/* Constants - defined in RFC2831 */
protected static final int MAX_CHALLENGE_LENGTH = 2048;
protected static final int MAX_RESPONSE_LENGTH = 4096;
protected static final int DEFAULT_MAXBUF = 65536;
/* Supported ciphers for 'auth-conf' */
protected static final int DES3 = 0;
protected static final int RC4 = 1;
protected static final int DES = 2;
protected static final int RC4_56 = 3;
protected static final int RC4_40 = 4;
protected static final String[] CIPHER_TOKENS = { "3des",
"rc4",
"des",
"rc4-56",
"rc4-40" };
private static final String[] JCE_CIPHER_NAME = {
"DESede/CBC/NoPadding",
"RC4",
"DES/CBC/NoPadding",
};
/*
* If QOP is set to 'auth-conf', a DIGEST-MD5 mechanism must have
* support for the DES and Triple DES cipher algorithms (optionally,
* support for RC4 [128/56/40 bit keys] ciphers) to provide for
* confidentiality. See RFC 2831 for details. This implementation
* provides support for DES, Triple DES and RC4 ciphers.
*
* The value of strength effects the strength of cipher used. The mappings
* of 'high', 'medium', and 'low' give the following behaviour.
*
* HIGH_STRENGTH - Triple DES
* - RC4 (128bit)
* MEDIUM_STRENGTH - DES
* - RC4 (56bit)
* LOW_SRENGTH - RC4 (40bit)
*/
protected static final byte DES_3_STRENGTH = HIGH_STRENGTH;
protected static final byte RC4_STRENGTH = HIGH_STRENGTH;
protected static final byte DES_STRENGTH = MEDIUM_STRENGTH;
protected static final byte RC4_56_STRENGTH = MEDIUM_STRENGTH;
protected static final byte RC4_40_STRENGTH = LOW_STRENGTH;
protected static final byte UNSET = (byte)0;
protected static final byte[] CIPHER_MASKS = { DES_3_STRENGTH,
RC4_STRENGTH,
DES_STRENGTH,
RC4_56_STRENGTH,
RC4_40_STRENGTH };
private static final String SECURITY_LAYER_MARKER =
":00000000000000000000000000000000";
protected static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
/* ------------------- Variable Fields ----------------------- */
/* Used to track progress of authentication; step numbers from RFC 2831 */
protected int step;
/* Used to get username/password, choose realm for client */
/* Used to obtain authorization, pw info, canonicalized authzid for server */
protected CallbackHandler cbh;
protected SecurityCtx secCtx;
protected byte[] H_A1; // component of response-value
protected byte[] nonce; // server generated nonce
/* Variables set when parsing directives in digest challenge/response. */
protected String negotiatedStrength;
protected String negotiatedCipher;
protected String negotiatedQop;
protected String negotiatedRealm;
protected boolean useUTF8 = false;
protected String encoding = "8859_1"; // default unless server specifies utf-8
protected String digestUri;
protected String authzid; // authzid or canonicalized authzid
/**
* Constucts an instance of DigestMD5Base. Calls super constructor
* to parse properties for mechanism.
*
* @param props A map of property/value pairs
* @param className name of class to use for logging
* @param firstStep number of first step in authentication state machine
* @param digestUri digestUri used in authentication
* @param cbh callback handler used to get info required for auth
*
* @throws SaslException If invalid value found in props.
*/
protected DigestMD5Base(Map<String, ?> props, String className,
int firstStep, String digestUri, CallbackHandler cbh)
throws SaslException {
super(props, className); // sets QOP, STENGTH and BUFFER_SIZE
step = firstStep;
this.digestUri = digestUri;
this.cbh = cbh;
}
/**
* Retrieves the SASL mechanism IANA name.
*
* @return The String "DIGEST-MD5"
*/
public String getMechanismName() {
return "DIGEST-MD5";
}
/**
* Unwrap the incoming message using the wrap method of the secCtx object
* instance.
*
* @param incoming The byte array containing the incoming bytes.
* @param start The offset from which to read the byte array.
* @param len The number of bytes to read from the offset.
* @return The unwrapped message according to either the integrity or
* privacy quality-of-protection specifications.
* @throws SaslException if an error occurs when unwrapping the incoming
* message
*/
public byte[] unwrap(byte[] incoming, int start, int len) throws SaslException {
if (!completed) {
throw new IllegalStateException(
"DIGEST-MD5 authentication not completed");
}
if (secCtx == null) {
throw new IllegalStateException(
"Neither integrity nor privacy was negotiated");
}
return (secCtx.unwrap(incoming, start, len));
}
/**
* Wrap outgoing bytes using the wrap method of the secCtx object
* instance.
*
* @param outgoing The byte array containing the outgoing bytes.
* @param start The offset from which to read the byte array.
* @param len The number of bytes to read from the offset.
* @return The wrapped message according to either the integrity or
* privacy quality-of-protection specifications.
* @throws SaslException if an error occurs when wrapping the outgoing
* message
*/
public byte[] wrap(byte[] outgoing, int start, int len) throws SaslException {
if (!completed) {
throw new IllegalStateException(
"DIGEST-MD5 authentication not completed");
}
if (secCtx == null) {
throw new IllegalStateException(
"Neither integrity nor privacy was negotiated");
}
return (secCtx.wrap(outgoing, start, len));
}
public void dispose() throws SaslException {
if (secCtx != null) {
secCtx = null;
}
}
public Object getNegotiatedProperty(String propName) {
if (completed) {
if (propName.equals(Sasl.STRENGTH)) {
return negotiatedStrength;
} else if (propName.equals(Sasl.BOUND_SERVER_NAME)) {
return digestUri.substring(digestUri.indexOf('/') + 1);
} else {
return super.getNegotiatedProperty(propName);
}
} else {
throw new IllegalStateException(
"DIGEST-MD5 authentication not completed");
}
}
/* ----------------- Digest-MD5 utilities ---------------- */
/**
* Generate random-string used for digest-response.
* This method uses Random to get random bytes and then
* base64 encodes the bytes. Could also use binaryToHex() but this
* is slightly faster and a more compact representation of the same info.
* @return A non-null byte array containing the nonce value for the
* digest challenge or response.
* Could use SecureRandom to be more secure but it is very slow.
*/
/** This array maps the characters to their 6 bit values */
private final static char pem_array[] = {
// 0 1 2 3 4 5 6 7
'A','B','C','D','E','F','G','H', // 0
'I','J','K','L','M','N','O','P', // 1
'Q','R','S','T','U','V','W','X', // 2
'Y','Z','a','b','c','d','e','f', // 3
'g','h','i','j','k','l','m','n', // 4
'o','p','q','r','s','t','u','v', // 5
'w','x','y','z','0','1','2','3', // 6
'4','5','6','7','8','9','+','/' // 7
};
// Make sure that this is a multiple of 3
private static final int RAW_NONCE_SIZE = 30;
// Base 64 encoding turns each 3 bytes into 4
private static final int ENCODED_NONCE_SIZE = RAW_NONCE_SIZE*4/3;
protected static final byte[] generateNonce() {
// SecureRandom random = new SecureRandom();
Random random = new Random();
byte[] randomData = new byte[RAW_NONCE_SIZE];
random.nextBytes(randomData);
byte[] nonce = new byte[ENCODED_NONCE_SIZE];
// Base64-encode bytes
byte a, b, c;
int j = 0;
for (int i = 0; i < randomData.length; i += 3) {
a = randomData[i];
b = randomData[i+1];
c = randomData[i+2];
nonce[j++] = (byte)(pem_array[(a >>> 2) & 0x3F]);
nonce[j++] = (byte)(pem_array[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]);
nonce[j++] = (byte)(pem_array[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]);
nonce[j++] = (byte)(pem_array[c & 0x3F]);
}
return nonce;
// %%% For testing using RFC 2831 example, uncomment the following 2 lines
// System.out.println("!!!Using RFC 2831's cnonce for testing!!!");
// return "OA6MHXh6VqTrRk".getBytes();
}
/**
* Checks if a byte[] contains characters that must be quoted
* and write the resulting, possibly escaped, characters to out.
*/
protected static void writeQuotedStringValue(ByteArrayOutputStream out,
byte[] buf) {
int len = buf.length;
byte ch;
for (int i = 0; i < len; i++) {
ch = buf[i];
if (needEscape((char)ch)) {
out.write('\\');
}
out.write(ch);
}
}
// See Section 7.2 of RFC 2831; double-quote character is not allowed
// unless escaped; also escape the escape character and CTL chars except LWS
private static boolean needEscape(String str) {
int len = str.length();
for (int i = 0; i < len; i++) {
if (needEscape(str.charAt(i))) {
return true;
}
}
return false;
}
// Determines whether a character needs to be escaped in a quoted string
private static boolean needEscape(char ch) {
return ch == '"' || // escape char
ch == '\\' || // quote
ch == 127 || // DEL
// 0 <= ch <= 31 except CR, HT and LF
(ch >= 0 && ch <= 31 && ch != 13 && ch != 9 && ch != 10);
}
protected static String quotedStringValue(String str) {
if (needEscape(str)) {
int len = str.length();
char[] buf = new char[len+len];
int j = 0;
char ch;
for (int i = 0; i < len; i++) {
ch = str.charAt(i);
if (needEscape(ch)) {
buf[j++] = '\\';
}
buf[j++] = ch;
}
return new String(buf, 0, j);
} else {
return str;
}
}
/**
* Convert a byte array to hexadecimal string.
*
* @param a non-null byte array
* @return a non-null String contain the HEX value
*/
protected byte[] binaryToHex(byte[] digest) throws
UnsupportedEncodingException {
StringBuffer digestString = new StringBuffer();
for (int i = 0; i < digest.length; i ++) {
if ((digest[i] & 0x000000ff) < 0x10) {
digestString.append("0"+
Integer.toHexString(digest[i] & 0x000000ff));
} else {
digestString.append(
Integer.toHexString(digest[i] & 0x000000ff));
}
}
return digestString.toString().getBytes(encoding);
}
/**
* Used to convert username-value, passwd or realm to 8859_1 encoding
* if all chars in string are within the 8859_1 (Latin 1) encoding range.
*
* @param a non-null String
* @return a non-nuill byte array containing the correct character encoding
* for username, paswd or realm.
*/
protected byte[] stringToByte_8859_1(String str) throws SaslException {
char[] buffer = str.toCharArray();
try {
if (useUTF8) {
for( int i = 0; i< buffer.length; i++ ) {
if( buffer[i] > '\u00FF' ) {
return str.getBytes("UTF8");
}
}
}
return str.getBytes("8859_1");
} catch (UnsupportedEncodingException e) {
throw new SaslException(
"cannot encode string in UTF8 or 8859-1 (Latin-1)", e);
}
}
protected static byte[] getPlatformCiphers() {
byte[] ciphers = new byte[CIPHER_TOKENS.length];
for (int i = 0; i < JCE_CIPHER_NAME.length; i++) {
try {
// Checking whether the transformation is available from the
// current installed providers.
Cipher.getInstance(JCE_CIPHER_NAME[i]);
logger.log(Level.FINE, "DIGEST01:Platform supports {0}", JCE_CIPHER_NAME[i]);
ciphers[i] |= CIPHER_MASKS[i];
} catch (NoSuchAlgorithmException e) {
// no implementation found for requested algorithm.
} catch (NoSuchPaddingException e) {
// no implementation found for requested algorithm.
}
}
if (ciphers[RC4] != UNSET) {
ciphers[RC4_56] |= CIPHER_MASKS[RC4_56];
ciphers[RC4_40] |= CIPHER_MASKS[RC4_40];
}
return ciphers;
}
/**
* Assembles response-value for digest-response.
*
* @param authMethod "AUTHENTICATE" for client-generated response;
* "" for server-generated response
* @return A non-null byte array containing the repsonse-value.
* @throws NoSuchAlgorithmException if the platform does not have MD5
* digest support.
* @throws UnsupportedEncodingException if a an error occurs
* encoding a string into either Latin-1 or UTF-8.
* @throws IOException if an error occurs writing to the output
* byte array buffer.
*/
protected byte[] generateResponseValue(
String authMethod,
String digestUriValue,
String qopValue,
String usernameValue,
String realmValue,
char[] passwdValue,
byte[] nonceValue,
byte[] cNonceValue,
int nonceCount,
byte[] authzidValue
) throws NoSuchAlgorithmException,
UnsupportedEncodingException,
IOException {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] hexA1, hexA2;
ByteArrayOutputStream A2, beginA1, A1, KD;
// A2
// --
// A2 = { "AUTHENTICATE:", digest-uri-value,
// [:00000000000000000000000000000000] } // if auth-int or auth-conf
//
A2 = new ByteArrayOutputStream();
A2.write((authMethod + ":" + digestUriValue).getBytes(encoding));
if (qopValue.equals("auth-conf") ||
qopValue.equals("auth-int")) {
logger.log(Level.FINE, "DIGEST04:QOP: {0}", qopValue);
A2.write(SECURITY_LAYER_MARKER.getBytes(encoding));
}
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "DIGEST05:A2: {0}", A2.toString());
}
md5.update(A2.toByteArray());
byte[] digest = md5.digest();
hexA2 = binaryToHex(digest);
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "DIGEST06:HEX(H(A2)): {0}", new String(hexA2));
}
// A1
// --
// H(user-name : realm-value : passwd)
//
beginA1 = new ByteArrayOutputStream();
beginA1.write(stringToByte_8859_1(usernameValue));
beginA1.write(':');
// if no realm, realm will be an empty string
beginA1.write(stringToByte_8859_1(realmValue));
beginA1.write(':');
beginA1.write(stringToByte_8859_1(new String(passwdValue)));
md5.update(beginA1.toByteArray());
digest = md5.digest();
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "DIGEST07:H({0}) = {1}",
new Object[]{beginA1.toString(), new String(binaryToHex(digest))});
}
// A1
// --
// A1 = { H ( {user-name : realm-value : passwd } ),
// : nonce-value, : cnonce-value : authzid-value
//
A1 = new ByteArrayOutputStream();
A1.write(digest);
A1.write(':');
A1.write(nonceValue);
A1.write(':');
A1.write(cNonceValue);
if (authzidValue != null) {
A1.write(':');
A1.write(authzidValue);
}
md5.update(A1.toByteArray());
digest = md5.digest();
H_A1 = digest; // Record H(A1). Use for integrity & privacy.
hexA1 = binaryToHex(digest);
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "DIGEST08:H(A1) = {0}", new String(hexA1));
}
//
// H(k, : , s);
//
KD = new ByteArrayOutputStream();
KD.write(hexA1);
KD.write(':');
KD.write(nonceValue);
KD.write(':');
KD.write(nonceCountToHex(nonceCount).getBytes(encoding));
KD.write(':');
KD.write(cNonceValue);
KD.write(':');
KD.write(qopValue.getBytes(encoding));
KD.write(':');
KD.write(hexA2);
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "DIGEST09:KD: {0}", KD.toString());
}
md5.update(KD.toByteArray());
digest = md5.digest();
byte[] answer = binaryToHex(digest);
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "DIGEST10:response-value: {0}",
new String(answer));
}
return (answer);
}
/**
* Takes 'nonceCount' value and returns HEX value of the value.
*
* @return A non-null String representing the current NONCE-COUNT
*/
protected static String nonceCountToHex(int count) {
String str = Integer.toHexString(count);
StringBuffer pad = new StringBuffer();
if (str.length() < 8) {
for (int i = 0; i < 8-str.length(); i ++) {
pad.append("0");
}
}
return pad.toString() + str;
}
/**
* Parses digest-challenge string, extracting each token
* and value(s)
*
* @param buf A non-null digest-challenge string.
* @param multipleAllowed true if multiple qop or realm or QOP directives
* are allowed.
* @throws SaslException if the buf cannot be parsed according to RFC 2831
*/
protected static byte[][] parseDirectives(byte[] buf,
String[]keyTable, List<byte[]> realmChoices, int realmIndex) throws SaslException {
byte[][] valueTable = new byte[keyTable.length][];
ByteArrayOutputStream key = new ByteArrayOutputStream(10);
ByteArrayOutputStream value = new ByteArrayOutputStream(10);
boolean gettingKey = true;
boolean gettingQuotedValue = false;
boolean expectSeparator = false;
byte bch;
int i = skipLws(buf, 0);
while (i < buf.length) {
bch = buf[i];
if (gettingKey) {
if (bch == ',') {
if (key.size() != 0) {
throw new SaslException("Directive key contains a ',':" +
key);
}
// Empty element, skip separator and lws
i = skipLws(buf, i+1);
} else if (bch == '=') {
if (key.size() == 0) {
throw new SaslException("Empty directive key");
}
gettingKey = false; // Termination of key
i = skipLws(buf, i+1); // Skip to next nonwhitespace
// Check whether value is quoted
if (i < buf.length) {
if (buf[i] == '"') {
gettingQuotedValue = true;
++i; // Skip quote
}
} else {
throw new SaslException(
"Valueless directive found: " + key.toString());
}
} else if (isLws(bch)) {
// LWS that occurs after key
i = skipLws(buf, i+1);
// Expecting '='
if (i < buf.length) {
if (buf[i] != '=') {
throw new SaslException("'=' expected after key: " +
key.toString());
}
} else {
throw new SaslException(
"'=' expected after key: " + key.toString());
}
} else {
key.write(bch); // Append to key
++i; // Advance
}
} else if (gettingQuotedValue) {
// Getting a quoted value
if (bch == '\\') {
// quoted-pair = "\" CHAR ==> CHAR
++i; // Skip escape
if (i < buf.length) {
value.write(buf[i]);
++i; // Advance
} else {
// Trailing escape in a quoted value
throw new SaslException(
"Unmatched quote found for directive: "
+ key.toString() + " with value: " + value.toString());
}
} else if (bch == '"') {
// closing quote
++i; // Skip closing quote
gettingQuotedValue = false;
expectSeparator = true;
} else {
value.write(bch);
++i; // Advance
}
} else if (isLws(bch) || bch == ',') {
// Value terminated
extractDirective(key.toString(), value.toByteArray(),
keyTable, valueTable, realmChoices, realmIndex);
key.reset();
value.reset();
gettingKey = true;
gettingQuotedValue = expectSeparator = false;
i = skipLws(buf, i+1); // Skip separator and LWS
} else if (expectSeparator) {
throw new SaslException(
"Expecting comma or linear whitespace after quoted string: \""
+ value.toString() + "\"");
} else {
value.write(bch); // Unquoted value
++i; // Advance
}
}
if (gettingQuotedValue) {
throw new SaslException(
"Unmatched quote found for directive: " + key.toString() +
" with value: " + value.toString());
}
// Get last pair
if (key.size() > 0) {
extractDirective(key.toString(), value.toByteArray(),
keyTable, valueTable, realmChoices, realmIndex);
}
return valueTable;
}
// Is character a linear white space?
// LWS = [CRLF] 1*( SP | HT )
// %%% Note that we're checking individual bytes instead of CRLF
private static boolean isLws(byte b) {
switch (b) {
case 13: // US-ASCII CR, carriage return
case 10: // US-ASCII LF, linefeed
case 32: // US-ASCII SP, space
case 9: // US-ASCII HT, horizontal-tab
return true;
}
return false;
}
// Skip all linear white spaces
private static int skipLws(byte[] buf, int start) {
int i;
for (i = start; i < buf.length; i++) {
if (!isLws(buf[i])) {
return i;
}
}
return i;
}
/**
* Processes directive/value pairs from the digest-challenge and
* fill out the challengeVal array.
*
* @param key A non-null String challenge token name.
* @param value A non-null String token value.
* @throws SaslException if a either the key or the value is null
*/
private static void extractDirective(String key, byte[] value,
String[] keyTable, byte[][] valueTable,
List<byte[]> realmChoices, int realmIndex) throws SaslException {
for (int i = 0; i < keyTable.length; i++) {
if (key.equalsIgnoreCase(keyTable[i])) {
if (valueTable[i] == null) {
valueTable[i] = value;
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "DIGEST11:Directive {0} = {1}",
new Object[]{
keyTable[i],
new String(valueTable[i])});
}
} else if (realmChoices != null && i == realmIndex) {
// > 1 realm specified
if (realmChoices.isEmpty()) {
realmChoices.add(valueTable[i]); // add existing one
}
realmChoices.add(value); // add new one
} else {
throw new SaslException(
"DIGEST-MD5: peer sent more than one " +
key + " directive: " + new String(value));
}
break; // end search
}
}
}
/**
* Implementation of the SecurityCtx interface allowing for messages
* between the client and server to be integrity checked. After a
* successful DIGEST-MD5 authentication, integtrity checking is invoked
* if the SASL QOP (quality-of-protection) is set to 'auth-int'.
* <p>
* Further details on the integrity-protection mechanism can be found
* at section 2.3 - Integrity protection in the
* <a href="http://www.ietf.org/rfc/rfc2831.txt">RFC2831</a> definition.
*
* @author Jonathan Bruce
*/
class DigestIntegrity implements SecurityCtx {
/* Used for generating integrity keys - specified in RFC 2831*/
static final private String CLIENT_INT_MAGIC = "Digest session key to " +
"client-to-server signing key magic constant";
static final private String SVR_INT_MAGIC = "Digest session key to " +
"server-to-client signing key magic constant";
/* Key pairs for integrity checking */
protected byte[] myKi; // == Kic for client; == Kis for server
protected byte[] peerKi; // == Kis for client; == Kic for server
protected int mySeqNum = 0;
protected int peerSeqNum = 0;
// outgoing messageType and sequenceNum
protected final byte[] messageType = new byte[2];
protected final byte[] sequenceNum = new byte[4];
/**
* Initializes DigestIntegrity implementation of SecurityCtx to
* enable DIGEST-MD5 integrity checking.
*
* @throws SaslException if an error is encountered generating the
* key-pairs for integrity checking.
*/
DigestIntegrity(boolean clientMode) throws SaslException {
/* Initialize magic strings */
try {
generateIntegrityKeyPair(clientMode);
} catch (UnsupportedEncodingException e) {
throw new SaslException(
"DIGEST-MD5: Error encoding strings into UTF-8", e);
} catch (IOException e) {
throw new SaslException("DIGEST-MD5: Error accessing buffers " +
"required to create integrity key pairs", e);
} catch (NoSuchAlgorithmException e) {
throw new SaslException("DIGEST-MD5: Unsupported digest " +
"algorithm used to create integrity key pairs", e);
}
/* Message type is a fixed value */
intToNetworkByteOrder(1, messageType, 0, 2);
}
/**
* Generate client-server, server-client key pairs for DIGEST-MD5
* integrity checking.
*
* @throws UnsupportedEncodingException if the UTF-8 encoding is not
* supported on the platform.
* @throws IOException if an error occurs when writing to or from the
* byte array output buffers.
* @throws NoSuchAlgorithmException if the MD5 message digest algorithm
* cannot loaded.
*/
private void generateIntegrityKeyPair(boolean clientMode)
throws UnsupportedEncodingException, IOException,
NoSuchAlgorithmException {
byte[] cimagic = CLIENT_INT_MAGIC.getBytes(encoding);
byte[] simagic = SVR_INT_MAGIC.getBytes(encoding);
MessageDigest md5 = MessageDigest.getInstance("MD5");
// Both client-magic-keys and server-magic-keys are the same length
byte[] keyBuffer = new byte[H_A1.length + cimagic.length];
// Kic: Key for protecting msgs from client to server.
System.arraycopy(H_A1, 0, keyBuffer, 0, H_A1.length);
System.arraycopy(cimagic, 0, keyBuffer, H_A1.length, cimagic.length);
md5.update(keyBuffer);
byte[] Kic = md5.digest();
// Kis: Key for protecting msgs from server to client
// No need to recopy H_A1
System.arraycopy(simagic, 0, keyBuffer, H_A1.length, simagic.length);
md5.update(keyBuffer);
byte[] Kis = md5.digest();
if (logger.isLoggable(Level.FINER)) {
traceOutput(DI_CLASS_NAME, "generateIntegrityKeyPair",
"DIGEST12:Kic: ", Kic);
traceOutput(DI_CLASS_NAME, "generateIntegrityKeyPair",
"DIGEST13:Kis: ", Kis);
}
if (clientMode) {
myKi = Kic;
peerKi = Kis;
} else {
myKi = Kis;
peerKi = Kic;
}
}
/**
* Append MAC onto outgoing message.
*
* @param outgoing A non-null byte array containing the outgoing message.
* @param start The offset from which to read the byte array.
* @param len The non-zero number of bytes for be read from the offset.
* @return The message including the integrity MAC
* @throws SaslException if an error is encountered converting a string
* into a UTF-8 byte encoding, or if the MD5 message digest algorithm
* cannot be found or if there is an error writing to the byte array
* output buffers.
*/
public byte[] wrap(byte[] outgoing, int start, int len)
throws SaslException {
if (len == 0) {
return EMPTY_BYTE_ARRAY;
}
/* wrapped = message, MAC, message type, sequence number */
byte[] wrapped = new byte[len+10+2+4];
/* Start with message itself */
System.arraycopy(outgoing, start, wrapped, 0, len);
incrementSeqNum();
/* Calculate MAC */
byte[] mac = getHMAC(myKi, sequenceNum, outgoing, start, len);
if (logger.isLoggable(Level.FINEST)) {
traceOutput(DI_CLASS_NAME, "wrap", "DIGEST14:outgoing: ",
outgoing, start, len);
traceOutput(DI_CLASS_NAME, "wrap", "DIGEST15:seqNum: ",
sequenceNum);
traceOutput(DI_CLASS_NAME, "wrap", "DIGEST16:MAC: ", mac);
}
/* Add MAC[0..9] to message */
System.arraycopy(mac, 0, wrapped, len, 10);
/* Add message type [0..1] */
System.arraycopy(messageType, 0, wrapped, len+10, 2);
/* Add sequence number [0..3] */
System.arraycopy(sequenceNum, 0, wrapped, len+12, 4);
if (logger.isLoggable(Level.FINEST)) {
traceOutput(DI_CLASS_NAME, "wrap", "DIGEST17:wrapped: ", wrapped);
}
return wrapped;
}
/**
* Return verified message without MAC - only if the received MAC
* and re-generated MAC are the same.
*
* @param incoming A non-null byte array containing the incoming
* message.
* @param start The offset from which to read the byte array.
* @param len The non-zero number of bytes to read from the offset
* position.
* @return The verified message or null if integrity checking fails.
* @throws SaslException if an error is encountered converting a string
* into a UTF-8 byte encoding, or if the MD5 message digest algorithm
* cannot be found or if there is an error writing to the byte array
* output buffers
*/
public byte[] unwrap(byte[] incoming, int start, int len)
throws SaslException {
if (len == 0) {
return EMPTY_BYTE_ARRAY;
}
// shave off last 16 bytes of message
byte[] mac = new byte[10];
byte[] msg = new byte[len - 16];
byte[] msgType = new byte[2];
byte[] seqNum = new byte[4];
/* Get Msg, MAC, msgType, sequenceNum */
System.arraycopy(incoming, start, msg, 0, msg.length);
System.arraycopy(incoming, start+msg.length, mac, 0, 10);
System.arraycopy(incoming, start+msg.length+10, msgType, 0, 2);
System.arraycopy(incoming, start+msg.length+12, seqNum, 0, 4);
/* Calculate MAC to ensure integrity */
byte[] expectedMac = getHMAC(peerKi, seqNum, msg, 0, msg.length);
if (logger.isLoggable(Level.FINEST)) {
traceOutput(DI_CLASS_NAME, "unwrap", "DIGEST18:incoming: ",
msg);
traceOutput(DI_CLASS_NAME, "unwrap", "DIGEST19:MAC: ",
mac);
traceOutput(DI_CLASS_NAME, "unwrap", "DIGEST20:messageType: ",
msgType);
traceOutput(DI_CLASS_NAME, "unwrap", "DIGEST21:sequenceNum: ",
seqNum);
traceOutput(DI_CLASS_NAME, "unwrap", "DIGEST22:expectedMAC: ",
expectedMac);
}
/* First, compare MAC's before updating any of our state */
if (!Arrays.equals(mac, expectedMac)) {
// Discard message and do not increment sequence number
logger.log(Level.INFO, "DIGEST23:Unmatched MACs");
return EMPTY_BYTE_ARRAY;
}
/* Ensure server-sequence numbers are correct */
if (peerSeqNum != networkByteOrderToInt(seqNum, 0, 4)) {
throw new SaslException("DIGEST-MD5: Out of order " +
"sequencing of messages from server. Got: " +
networkByteOrderToInt(seqNum, 0, 4) +
" Expected: " + peerSeqNum);
}
if (!Arrays.equals(messageType, msgType)) {
throw new SaslException("DIGEST-MD5: invalid message type: " +
networkByteOrderToInt(msgType, 0, 2));
}
// Increment sequence number and return message
peerSeqNum++;
return msg;
}
/**
* Generates MAC to be appended onto out-going messages.
*
* @param Ki A non-null byte array containing the key for the digest
* @param SeqNum A non-null byte array contain the sequence number
* @param msg The message to be digested
* @param start The offset from which to read the msg byte array
* @param len The non-zero number of bytes to be read from the offset
* @return The MAC of a message.
*
* @throws SaslException if an error occurs when generating MAC.
*/
protected byte[] getHMAC(byte[] Ki, byte[] seqnum, byte[] msg,
int start, int len) throws SaslException {
byte[] seqAndMsg = new byte[4+len];
System.arraycopy(seqnum, 0, seqAndMsg, 0, 4);
System.arraycopy(msg, start, seqAndMsg, 4, len);
try {
SecretKey keyKi = new SecretKeySpec(Ki, "HmacMD5");
Mac m = Mac.getInstance("HmacMD5");
m.init(keyKi);
m.update(seqAndMsg);
byte[] hMAC_MD5 = m.doFinal();
/* First 10 bytes of HMAC_MD5 digest */
byte macBuffer[] = new byte[10];
System.arraycopy(hMAC_MD5, 0, macBuffer, 0, 10);
return macBuffer;
} catch (InvalidKeyException e) {
throw new SaslException("DIGEST-MD5: Invalid bytes used for " +
"key of HMAC-MD5 hash.", e);
} catch (NoSuchAlgorithmException e) {
throw new SaslException("DIGEST-MD5: Error creating " +
"instance of MD5 digest algorithm", e);
}
}
/**
* Increment own sequence number and set answer in NBO sequenceNum field.
*/
protected void incrementSeqNum() {
intToNetworkByteOrder(mySeqNum++, sequenceNum, 0, 4);
}
}
/**
* Implementation of the SecurityCtx interface allowing for messages
* between the client and server to be integrity checked and encrypted.
* After a successful DIGEST-MD5 authentication, privacy is invoked if the
* SASL QOP (quality-of-protection) is set to 'auth-conf'.
* <p>
* Further details on the integrity-protection mechanism can be found
* at section 2.4 - Confidentiality protection in
* <a href="http://www.ietf.org/rfc/rfc2831.txt">RFC2831</a> definition.
*
* @author Jonathan Bruce
*/
final class DigestPrivacy extends DigestIntegrity implements SecurityCtx {
/* Used for generating privacy keys - specified in RFC 2831 */
static final private String CLIENT_CONF_MAGIC =
"Digest H(A1) to client-to-server sealing key magic constant";
static final private String SVR_CONF_MAGIC =
"Digest H(A1) to server-to-client sealing key magic constant";
private Cipher encCipher;
private Cipher decCipher;
/**
* Initializes the cipher object instances for encryption and decryption.
*
* @throws SaslException if an error occurs with the Key
* initialization, or a string cannot be encoded into a byte array
* using the UTF-8 encoding, or an error occurs when writing to a
* byte array output buffers or the mechanism cannot load the MD5
* message digest algorithm or invalid initialization parameters are
* passed to the cipher object instances.
*/
DigestPrivacy(boolean clientMode) throws SaslException {
super(clientMode); // generate Kic, Kis keys for integrity-checking.
try {
generatePrivacyKeyPair(clientMode);
} catch (SaslException e) {
throw e;
} catch (UnsupportedEncodingException e) {
throw new SaslException(
"DIGEST-MD5: Error encoding string value into UTF-8", e);
} catch (IOException e) {
throw new SaslException("DIGEST-MD5: Error accessing " +
"buffers required to generate cipher keys", e);
} catch (NoSuchAlgorithmException e) {
throw new SaslException("DIGEST-MD5: Error creating " +
"instance of required cipher or digest", e);
}
}
/**
* Generates client-server and server-client keys to encrypt and
* decrypt messages. Also generates IVs for DES ciphers.
*
* @throws IOException if an error occurs when writing to or from the
* byte array output buffers.
* @throws NoSuchAlgorithmException if the MD5 message digest algorithm
* cannot loaded.
* @throws UnsupportedEncodingException if an UTF-8 encoding is not
* supported on the platform.
* @throw SaslException if an error occurs initializing the keys and
* IVs for the chosen cipher.
*/
private void generatePrivacyKeyPair(boolean clientMode)
throws IOException, UnsupportedEncodingException,
NoSuchAlgorithmException, SaslException {
byte[] ccmagic = CLIENT_CONF_MAGIC.getBytes(encoding);
byte[] scmagic = SVR_CONF_MAGIC.getBytes(encoding);
/* Kcc = MD5{H(A1)[0..n], "Digest ... client-to-server"} */
MessageDigest md5 = MessageDigest.getInstance("MD5");
int n;
if (negotiatedCipher.equals(CIPHER_TOKENS[RC4_40])) {
n = 5; /* H(A1)[0..5] */
} else if (negotiatedCipher.equals(CIPHER_TOKENS[RC4_56])) {
n = 7; /* H(A1)[0..7] */
} else { // des and 3des and rc4
n = 16; /* H(A1)[0..16] */
}
/* {H(A1)[0..n], "Digest ... client-to-server..."} */
// Both client-magic-keys and server-magic-keys are the same length
byte[] keyBuffer = new byte[n + ccmagic.length];
System.arraycopy(H_A1, 0, keyBuffer, 0, n); // H(A1)[0..n]
/* Kcc: Key for encrypting messages from client->server */
System.arraycopy(ccmagic, 0, keyBuffer, n, ccmagic.length);
md5.update(keyBuffer);
byte[] Kcc = md5.digest();
/* Kcs: Key for decrypting messages from server->client */
// No need to copy H_A1 again since it hasn't changed
System.arraycopy(scmagic, 0, keyBuffer, n, scmagic.length);
md5.update(keyBuffer);
byte[] Kcs = md5.digest();
if (logger.isLoggable(Level.FINER)) {
traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
"DIGEST24:Kcc: ", Kcc);
traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
"DIGEST25:Kcs: ", Kcs);
}
byte[] myKc;
byte[] peerKc;
if (clientMode) {
myKc = Kcc;
peerKc = Kcs;
} else {
myKc = Kcs;
peerKc = Kcc;
}
try {
SecretKey encKey;
SecretKey decKey;
/* Initialize cipher objects */
if (negotiatedCipher.indexOf(CIPHER_TOKENS[RC4]) > -1) {
encCipher = Cipher.getInstance("RC4");
decCipher = Cipher.getInstance("RC4");
encKey = new SecretKeySpec(myKc, "RC4");
decKey = new SecretKeySpec(peerKc, "RC4");
encCipher.init(Cipher.ENCRYPT_MODE, encKey);
decCipher.init(Cipher.DECRYPT_MODE, decKey);
} else if ((negotiatedCipher.equals(CIPHER_TOKENS[DES])) ||
(negotiatedCipher.equals(CIPHER_TOKENS[DES3]))) {
// DES or 3DES
String cipherFullname, cipherShortname;
// Use "NoPadding" when specifying cipher names
// RFC 2831 already defines padding rules for producing
// 8-byte aligned blocks
if (negotiatedCipher.equals(CIPHER_TOKENS[DES])) {
cipherFullname = "DES/CBC/NoPadding";
cipherShortname = "des";
} else {
/* 3DES */
cipherFullname = "DESede/CBC/NoPadding";
cipherShortname = "desede";
}
encCipher = Cipher.getInstance(cipherFullname);
decCipher = Cipher.getInstance(cipherFullname);
encKey = makeDesKeys(myKc, cipherShortname);
decKey = makeDesKeys(peerKc, cipherShortname);
// Set up the DES IV, which is the last 8 bytes of Kcc/Kcs
IvParameterSpec encIv = new IvParameterSpec(myKc, 8, 8);
IvParameterSpec decIv = new IvParameterSpec(peerKc, 8, 8);
// Initialize cipher objects
encCipher.init(Cipher.ENCRYPT_MODE, encKey, encIv);
decCipher.init(Cipher.DECRYPT_MODE, decKey, decIv);
if (logger.isLoggable(Level.FINER)) {
traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
"DIGEST26:" + negotiatedCipher + " IVcc: ",
encIv.getIV());
traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
"DIGEST27:" + negotiatedCipher + " IVcs: ",
decIv.getIV());
traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
"DIGEST28:" + negotiatedCipher + " encryption key: ",
encKey.getEncoded());
traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
"DIGEST29:" + negotiatedCipher + " decryption key: ",
decKey.getEncoded());
}
}
} catch (InvalidKeySpecException e) {
throw new SaslException("DIGEST-MD5: Unsupported key " +
"specification used.", e);
} catch (InvalidAlgorithmParameterException e) {
throw new SaslException("DIGEST-MD5: Invalid cipher " +
"algorithem parameter used to create cipher instance", e);
} catch (NoSuchPaddingException e) {
throw new SaslException("DIGEST-MD5: Unsupported " +
"padding used for chosen cipher", e);
} catch (InvalidKeyException e) {
throw new SaslException("DIGEST-MD5: Invalid data " +
"used to initialize keys", e);
}
}
// -------------------------------------------------------------------
/**
* Encrypt out-going message.
*
* @param outgoing A non-null byte array containing the outgoing message.
* @param start The offset from which to read the byte array.
* @param len The non-zero number of bytes to be read from the offset.
* @return The encrypted message.
*
* @throws SaslException if an error occurs when writing to or from the
* byte array output buffers or if the MD5 message digest algorithm
* cannot loaded or if an UTF-8 encoding is not supported on the
* platform.
*/
public byte[] wrap(byte[] outgoing, int start, int len)
throws SaslException {
if (len == 0) {
return EMPTY_BYTE_ARRAY;
}
/* HMAC(Ki, {SeqNum, msg})[0..9] */
incrementSeqNum();
byte[] mac = getHMAC(myKi, sequenceNum, outgoing, start, len);
if (logger.isLoggable(Level.FINEST)) {
traceOutput(DP_CLASS_NAME, "wrap", "DIGEST30:Outgoing: ",
outgoing, start, len);
traceOutput(DP_CLASS_NAME, "wrap", "seqNum: ",
sequenceNum);
traceOutput(DP_CLASS_NAME, "wrap", "MAC: ", mac);
}
// Calculate padding
int bs = encCipher.getBlockSize();
byte[] padding;
if (bs > 1 ) {
int pad = bs - ((len + 10) % bs); // add 10 for HMAC[0..9]
padding = new byte[pad];
for (int i=0; i < pad; i++) {
padding[i] = (byte)pad;
}
} else {
padding = EMPTY_BYTE_ARRAY;
}
byte[] toBeEncrypted = new byte[len+padding.length+10];
/* {msg, pad, HMAC(Ki, {SeqNum, msg}[0..9])} */
System.arraycopy(outgoing, start, toBeEncrypted, 0, len);
System.arraycopy(padding, 0, toBeEncrypted, len, padding.length);
System.arraycopy(mac, 0, toBeEncrypted, len+padding.length, 10);
if (logger.isLoggable(Level.FINEST)) {
traceOutput(DP_CLASS_NAME, "wrap",
"DIGEST31:{msg, pad, KicMAC}: ", toBeEncrypted);
}
/* CIPHER(Kc, {msg, pad, HMAC(Ki, {SeqNum, msg}[0..9])}) */
byte[] cipherBlock;
try {
// Do CBC (chaining) across packets
cipherBlock = encCipher.update(toBeEncrypted);
if (cipherBlock == null) {
// update() can return null
throw new IllegalBlockSizeException(""+toBeEncrypted.length);
}
} catch (IllegalBlockSizeException e) {
throw new SaslException(
"DIGEST-MD5: Invalid block size for cipher", e);
}
byte[] wrapped = new byte[cipherBlock.length+2+4];
System.arraycopy(cipherBlock, 0, wrapped, 0, cipherBlock.length);
System.arraycopy(messageType, 0, wrapped, cipherBlock.length, 2);
System.arraycopy(sequenceNum, 0, wrapped, cipherBlock.length+2, 4);
if (logger.isLoggable(Level.FINEST)) {
traceOutput(DP_CLASS_NAME, "wrap", "DIGEST32:Wrapped: ", wrapped);
}
return wrapped;
}
/*
* Decrypt incoming messages and verify their integrity.
*
* @param incoming A non-null byte array containing the incoming
* encrypted message.
* @param start The offset from which to read the byte array.
* @param len The non-zero number of bytes to read from the offset
* position.
* @return The decrypted, verified message or null if integrity
* checking
* fails.
* @throws SaslException if there are the SASL buffer is empty or if
* if an error occurs reading the SASL buffer.
*/
public byte[] unwrap(byte[] incoming, int start, int len)
throws SaslException {
if (len == 0) {
return EMPTY_BYTE_ARRAY;
}
byte[] encryptedMsg = new byte[len - 6];
byte[] msgType = new byte[2];
byte[] seqNum = new byte[4];
/* Get cipherMsg; msgType; sequenceNum */
System.arraycopy(incoming, start,
encryptedMsg, 0, encryptedMsg.length);
System.arraycopy(incoming, start+encryptedMsg.length,
msgType, 0, 2);
System.arraycopy(incoming, start+encryptedMsg.length+2,
seqNum, 0, 4);
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST,
"DIGEST33:Expecting sequence num: {0}",
new Integer(peerSeqNum));
traceOutput(DP_CLASS_NAME, "unwrap", "DIGEST34:incoming: ",
encryptedMsg);
}
// Decrypt message
/* CIPHER(Kc, {msg, pad, HMAC(Ki, {SeqNum, msg}[0..9])}) */
byte[] decryptedMsg;
try {
// Do CBC (chaining) across packets
decryptedMsg = decCipher.update(encryptedMsg);
if (decryptedMsg == null) {
// update() can return null
throw new IllegalBlockSizeException(""+encryptedMsg.length);
}
} catch (IllegalBlockSizeException e) {
throw new SaslException("DIGEST-MD5: Illegal block " +
"sizes used with chosen cipher", e);
}
byte[] msgWithPadding = new byte[decryptedMsg.length - 10];
byte[] mac = new byte[10];
System.arraycopy(decryptedMsg, 0,
msgWithPadding, 0, msgWithPadding.length);
System.arraycopy(decryptedMsg, msgWithPadding.length,
mac, 0, 10);
if (logger.isLoggable(Level.FINEST)) {
traceOutput(DP_CLASS_NAME, "unwrap",
"DIGEST35:Unwrapped (w/padding): ", msgWithPadding);
traceOutput(DP_CLASS_NAME, "unwrap", "DIGEST36:MAC: ", mac);
traceOutput(DP_CLASS_NAME, "unwrap", "DIGEST37:messageType: ",
msgType);
traceOutput(DP_CLASS_NAME, "unwrap", "DIGEST38:sequenceNum: ",
seqNum);
}
int msgLength = msgWithPadding.length;
int blockSize = decCipher.getBlockSize();
if (blockSize > 1) {
// get value of last octet of the byte array
msgLength -= (int)msgWithPadding[msgWithPadding.length - 1];
if (msgLength < 0) {
// Discard message and do not increment sequence number
if (logger.isLoggable(Level.INFO)) {
logger.log(Level.INFO,
"DIGEST39:Incorrect padding: {0}",
new Byte(msgWithPadding[msgWithPadding.length - 1]));
}
return EMPTY_BYTE_ARRAY;
}
}
/* Re-calculate MAC to ensure integrity */
byte[] expectedMac = getHMAC(peerKi, seqNum, msgWithPadding,
0, msgLength);
if (logger.isLoggable(Level.FINEST)) {
traceOutput(DP_CLASS_NAME, "unwrap", "DIGEST40:KisMAC: ",
expectedMac);
}
/**代码未完, 请加载全部代码(NowJava.com).**/