/*
* Copyright (c) 2000, 2011, 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, Rosanna Lee
/**
* 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.
Params: - props – A map of property/value pairs
- className – name of class to use for logging
- firstStep – number of first step in authentication state machine
- digestUri – digestUri used in authentication
- cbh – callback handler used to get info required for auth
Throws: - SaslException – If invalid value found in props.
/**
* 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.
Returns: The String "DIGEST-MD5"
/**
* 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.
Params: - incoming – The byte array containing the incoming bytes.
- start – The offset from which to read the byte array.
- len – The number of bytes to read from the offset.
Throws: - SaslException – if an error occurs when unwrapping the incoming
message
Returns: The unwrapped message according to either the integrity or
privacy quality-of-protection specifications.
/**
* 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.
Params: - outgoing – The byte array containing the outgoing bytes.
- start – The offset from which to read the byte array.
- len – The number of bytes to read from the offset.
Throws: - SaslException – if an error occurs when wrapping the outgoing
message
Returns: The wrapped message according to either the integrity or
privacy quality-of-protection specifications.
/**
* 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 {
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 /** 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.
/**
* 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.
Params: - a – non-null byte array
Returns: a non-null String contain the HEX value
/**
* 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.
Params: - a – non-null String
Returns: a non-nuill byte array containing the correct character encoding
for username, paswd or realm.
/**
* 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.
Params: - authMethod – "AUTHENTICATE" for client-generated response;
"" for server-generated response
Throws: - NoSuchAlgorithmException – if the platform does not have MD5
digest support.
- UnsupportedEncodingException – if a an error occurs
encoding a string into either Latin-1 or UTF-8.
- IOException – if an error occurs writing to the output
byte array buffer.
Returns: A non-null byte array containing the repsonse-value.
/**
* 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.
Returns: A non-null String representing the current NONCE-COUNT
/**
* 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)
Params: - buf – A non-null digest-challenge string.
- multipleAllowed – true if multiple qop or realm or QOP directives
are allowed.
Throws: - SaslException – if the buf cannot be parsed according to RFC 2831
/**
* 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.
Params: - key – A non-null String challenge token name.
- value – A non-null String token value.
Throws: - SaslException – if a either the key or the value is null
/**
* 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'.
Further details on the integrity-protection mechanism can be found
at section 2.3 - Integrity protection in the
RFC2831 definition.
Author: Jonathan Bruce
/**
* 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.
/**
* 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.
- IOException – if an error occurs when writing to or from the
byte array output buffers.
- NoSuchAlgorithmException – if the MD5 message digest algorithm
cannot loaded.
/**
* 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.
Params: - outgoing – A non-null byte array containing the outgoing message.
- start – The offset from which to read the byte array.
- len – The non-zero number of bytes for be read from the offset.
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.
Returns: The message including the integrity MAC
/**
* 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.
Params: - incoming – A non-null byte array containing the incoming
message.
- start – The offset from which to read the byte array.
- len – The non-zero number of bytes to read from the offset
position.
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
Returns: The verified message or null if integrity checking fails.
/**
* 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.
Params: - Ki – A non-null byte array containing the key for the digest
- SeqNum – A non-null byte array contain the sequence number
- msg – The message to be digested
- start – The offset from which to read the msg byte array
- len – The non-zero number of bytes to be read from the offset
Throws: - SaslException – if an error occurs when generating MAC.
Returns: The MAC of a message.
/**
* 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.
/**
* 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'.
Further details on the integrity-protection mechanism can be found
at section 2.4 - Confidentiality protection in
RFC2831 definition.
Author: Jonathan Bruce
/**
* 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.
/**
* 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.
- NoSuchAlgorithmException – if the MD5 message digest algorithm
cannot loaded.
- 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.
/**
* 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.
Params: - outgoing – A non-null byte array containing the outgoing message.
- start – The offset from which to read the byte array.
- len – The non-zero number of bytes to be read from the offset.
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.
Returns: The encrypted message.
/**
* 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);
}
// First, compare MACs before updating state
if (!Arrays.equals(mac, expectedMac)) {
// Discard message and do not increment sequence number
logger.log(Level.INFO, "DIGEST41:Unmatched MACs");
return EMPTY_BYTE_ARRAY;
}
/* Ensure sequence number is 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);
}
/* Check message type */
if (!Arrays.equals(messageType, msgType)) {
throw new SaslException("DIGEST-MD5: invalid message type: " +
networkByteOrderToInt(msgType, 0, 2));
}
// Increment sequence number and return message
peerSeqNum++;
if (msgLength == msgWithPadding.length) {
return msgWithPadding; // no padding
} else {
// Get a copy of the message without padding
byte[] clearMsg = new byte[msgLength];
System.arraycopy(msgWithPadding, 0, clearMsg, 0, msgLength);
return clearMsg;
}
}
}
// ---------------- DES and 3 DES key manipulation routines
/* Mask used to check for parity adjustment */
private static final byte[] PARITY_BIT_MASK = {
(byte)0x80, (byte)0x40, (byte)0x20, (byte)0x10,
(byte)0x08, (byte)0x04, (byte)0x02
};
private static final BigInteger MASK = new BigInteger("7f", 16);
Sets the parity bit (0th bit) in each byte so that each byte
contains an odd number of 1's.
/**
* Sets the parity bit (0th bit) in each byte so that each byte
* contains an odd number of 1's.
*/
private static void setParityBit(byte[] key) {
for (int i = 0; i < key.length; i++) {
int bitCount = 0;
for (int maskIndex = 0;
maskIndex < PARITY_BIT_MASK.length; maskIndex++) {
if ((key[i] & PARITY_BIT_MASK[maskIndex])
== PARITY_BIT_MASK[maskIndex]) {
bitCount++;
}
}
if ((bitCount & 0x01) == 1) {
// Odd number of 1 bits in the top 7 bits. Set parity bit to 0
key[i] = (byte)(key[i] & (byte)0xfe);
} else {
// Even number of 1 bits in the top 7 bits. Set parity bit to 1
key[i] = (byte)(key[i] | 1);
}
}
}
Expands a 7-byte array into an 8-byte array that contains parity bits
The binary format of a cryptographic key is:
(B1,B2,...,B7,P1,B8,...B14,P2,B15,...,B49,P7,B50,...,B56,P8)
where (B1,B2,...,B56) are the independent bits of a DES key and
(PI,P2,...,P8) are reserved for parity bits computed on the preceding
seven independent bits and set so that the parity of the octet is odd,
i.e., there is an odd number of "1" bits in the octet.
/**
* Expands a 7-byte array into an 8-byte array that contains parity bits
* The binary format of a cryptographic key is:
* (B1,B2,...,B7,P1,B8,...B14,P2,B15,...,B49,P7,B50,...,B56,P8)
* where (B1,B2,...,B56) are the independent bits of a DES key and
* (PI,P2,...,P8) are reserved for parity bits computed on the preceding
* seven independent bits and set so that the parity of the octet is odd,
* i.e., there is an odd number of "1" bits in the octet.
*/
private static byte[] addDesParity(byte[] input, int offset, int len) {
if (len != 7)
throw new IllegalArgumentException(
"Invalid length of DES Key Value:" + len);
byte[] raw = new byte[7];
System.arraycopy(input, offset, raw, 0, len);
byte[] result = new byte[8];
BigInteger in = new BigInteger(raw);
// Shift 7 bits each time into a byte
for (int i=result.length-1; i>=0; i--) {
result[i] = in.and(MASK).toByteArray()[0];
result[i] <<= 1; // make room for parity bit
in = in.shiftRight(7);
}
setParityBit(result);
return result;
}
Create parity-adjusted keys suitable for DES / DESede encryption.
Params: - input – A non-null byte array containing key material for
DES / DESede.
- desStrength – A string specifying eithe a DES or a DESede key.
Throws: - NoSuchAlgorithmException – if the either the DES or DESede
algorithms cannote be lodaed by JCE.
- InvalidKeyException – if an invalid array of bytes is used
as a key for DES or DESede.
- InvalidKeySpecException – in an invalid parameter is passed
to either te DESKeySpec of the DESedeKeySpec constructors.
Returns: SecretKey An instance of either DESKeySpec or DESedeKeySpec.
/**
* Create parity-adjusted keys suitable for DES / DESede encryption.
*
* @param input A non-null byte array containing key material for
* DES / DESede.
* @param desStrength A string specifying eithe a DES or a DESede key.
* @return SecretKey An instance of either DESKeySpec or DESedeKeySpec.
*
* @throws NoSuchAlgorithmException if the either the DES or DESede
* algorithms cannote be lodaed by JCE.
* @throws InvalidKeyException if an invalid array of bytes is used
* as a key for DES or DESede.
* @throws InvalidKeySpecException in an invalid parameter is passed
* to either te DESKeySpec of the DESedeKeySpec constructors.
*/
private static SecretKey makeDesKeys(byte[] input, String desStrength)
throws NoSuchAlgorithmException, InvalidKeyException,
InvalidKeySpecException {
// Generate first subkey using first 7 bytes
byte[] subkey1 = addDesParity(input, 0, 7);
KeySpec spec = null;
SecretKeyFactory desFactory =
SecretKeyFactory.getInstance(desStrength);
if ("des".equals(desStrength)) {
spec = new DESKeySpec(subkey1, 0);
if (logger.isLoggable(Level.FINEST)) {
traceOutput(DP_CLASS_NAME, "makeDesKeys",
"DIGEST42:DES key input: ", input);
traceOutput(DP_CLASS_NAME, "makeDesKeys",
"DIGEST43:DES key parity-adjusted: ", subkey1);
traceOutput(DP_CLASS_NAME, "makeDesKeys",
"DIGEST44:DES key material: ", ((DESKeySpec)spec).getKey());
logger.log(Level.FINEST, "DIGEST45: is parity-adjusted? {0}",
Boolean.valueOf(DESKeySpec.isParityAdjusted(subkey1, 0)));
}
} else if ("desede".equals(desStrength)) {
// Generate second subkey using second 7 bytes
byte[] subkey2 = addDesParity(input, 7, 7);
// Construct 24-byte encryption-decryption-encryption sequence
byte[] ede = new byte[subkey1.length*2+subkey2.length];
System.arraycopy(subkey1, 0, ede, 0, subkey1.length);
System.arraycopy(subkey2, 0, ede, subkey1.length, subkey2.length);
System.arraycopy(subkey1, 0, ede, subkey1.length+subkey2.length,
subkey1.length);
spec = new DESedeKeySpec(ede, 0);
if (logger.isLoggable(Level.FINEST)) {
traceOutput(DP_CLASS_NAME, "makeDesKeys",
"DIGEST46:3DES key input: ", input);
traceOutput(DP_CLASS_NAME, "makeDesKeys",
"DIGEST47:3DES key ede: ", ede);
traceOutput(DP_CLASS_NAME, "makeDesKeys",
"DIGEST48:3DES key material: ",
((DESedeKeySpec)spec).getKey());
logger.log(Level.FINEST, "DIGEST49: is parity-adjusted? ",
Boolean.valueOf(DESedeKeySpec.isParityAdjusted(ede, 0)));
}
} else {
throw new IllegalArgumentException("Invalid DES strength:" +
desStrength);
}
return desFactory.generateSecret(spec);
}
}