| /* |
| * 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 { |
| |
| StringBuilder digestString = new StringBuilder(); |
| |
| for (int i = 0; i < digest.length; i ++) { |
| if ((digest[i] & 0x000000ff) < 0x10) { |
| digestString.append('0').append(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); |
| StringBuilder pad = new StringBuilder(); |
| |
| 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}", |
| 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}", |
| 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 |
| |
| 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. |
| */ |
| private static void setParityBit(byte[] key) { |
| for (int i = 0; i < key.length; i++) { |
| int b = key[i] & 0xfe; |
| b |= (Integer.bitCount(b) & 1) ^ 1; |
| key[i] = (byte) b; |
| } |
| } |
| |
| /** |
| * 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. |
| * |
| * @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); |
| switch (desStrength) { |
| case "des": |
| 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))); |
| } |
| break; |
| case "desede": |
| // 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))); |
| } |
| break; |
| default: |
| throw new IllegalArgumentException("Invalid DES strength:" + |
| desStrength); |
| } |
| return desFactory.generateSecret(spec); |
| } |
| } |