blob: f231369a0a9fe2338c2581c25e92782079c64c4b [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 2000-2006 Sun Microsystems, Inc. All Rights Reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25
26package com.sun.security.sasl.digest;
27
28import java.util.Map;
29import java.util.Arrays;
30import java.util.List;
31import java.util.Set;
32import java.util.logging.Logger;
33import java.util.logging.Level;
34import java.math.BigInteger;
35import java.util.Random;
36import java.security.Provider;
37
38import java.io.ByteArrayInputStream;
39import java.io.ByteArrayOutputStream;
40import java.io.UnsupportedEncodingException;
41import java.io.IOException;
42
43import java.security.MessageDigest;
44import java.security.AccessController;
45import java.security.PrivilegedAction;
46import java.security.NoSuchAlgorithmException;
47import java.security.InvalidKeyException;
48import java.security.spec.KeySpec;
49import java.security.spec.InvalidKeySpecException;
50import java.security.InvalidAlgorithmParameterException;
51
52import javax.crypto.Cipher;
53import javax.crypto.SecretKey;
54import javax.crypto.Mac;
55import javax.crypto.SecretKeyFactory;
56import javax.crypto.BadPaddingException;
57import javax.crypto.NoSuchPaddingException;
58import javax.crypto.IllegalBlockSizeException;
59import javax.crypto.spec.IvParameterSpec;
60import javax.crypto.spec.SecretKeySpec;
61import javax.crypto.spec.DESKeySpec;
62import javax.crypto.spec.DESedeKeySpec;
63
64import javax.security.sasl.*;
65import com.sun.security.sasl.util.AbstractSaslImpl;
66
67import javax.security.auth.callback.CallbackHandler;
68
69/**
70 * Utility class for DIGEST-MD5 mechanism. Provides utility methods
71 * and contains two inner classes which implement the SecurityCtx
72 * interface. The inner classes provide the funtionality to allow
73 * for quality-of-protection (QOP) with integrity checking and
74 * privacy.
75 *
76 * @author Jonathan Bruce
77 * @author Rosanna Lee
78 */
79abstract class DigestMD5Base extends AbstractSaslImpl {
80 /* ------------------------- Constants ------------------------ */
81
82 // Used for logging
83 private static final String DI_CLASS_NAME = DigestIntegrity.class.getName();
84 private static final String DP_CLASS_NAME = DigestPrivacy.class.getName();
85
86 /* Constants - defined in RFC2831 */
87 protected static final int MAX_CHALLENGE_LENGTH = 2048;
88 protected static final int MAX_RESPONSE_LENGTH = 4096;
89 protected static final int DEFAULT_MAXBUF = 65536;
90
91 /* Supported ciphers for 'auth-conf' */
92 protected static final int DES3 = 0;
93 protected static final int RC4 = 1;
94 protected static final int DES = 2;
95 protected static final int RC4_56 = 3;
96 protected static final int RC4_40 = 4;
97 protected static final String[] CIPHER_TOKENS = { "3des",
98 "rc4",
99 "des",
100 "rc4-56",
101 "rc4-40" };
102 private static final String[] JCE_CIPHER_NAME = {
103 "DESede/CBC/NoPadding",
104 "RC4",
105 "DES/CBC/NoPadding",
106 };
107
108 /*
109 * If QOP is set to 'auth-conf', a DIGEST-MD5 mechanism must have
110 * support for the DES and Triple DES cipher algorithms (optionally,
111 * support for RC4 [128/56/40 bit keys] ciphers) to provide for
112 * confidentiality. See RFC 2831 for details. This implementation
113 * provides support for DES, Triple DES and RC4 ciphers.
114 *
115 * The value of strength effects the strength of cipher used. The mappings
116 * of 'high', 'medium', and 'low' give the following behaviour.
117 *
118 * HIGH_STRENGTH - Triple DES
119 * - RC4 (128bit)
120 * MEDIUM_STRENGTH - DES
121 * - RC4 (56bit)
122 * LOW_SRENGTH - RC4 (40bit)
123 */
124 protected static final byte DES_3_STRENGTH = HIGH_STRENGTH;
125 protected static final byte RC4_STRENGTH = HIGH_STRENGTH;
126 protected static final byte DES_STRENGTH = MEDIUM_STRENGTH;
127 protected static final byte RC4_56_STRENGTH = MEDIUM_STRENGTH;
128 protected static final byte RC4_40_STRENGTH = LOW_STRENGTH;
129 protected static final byte UNSET = (byte)0;
130 protected static final byte[] CIPHER_MASKS = { DES_3_STRENGTH,
131 RC4_STRENGTH,
132 DES_STRENGTH,
133 RC4_56_STRENGTH,
134 RC4_40_STRENGTH };
135
136 private static final String SECURITY_LAYER_MARKER =
137 ":00000000000000000000000000000000";
138
139 protected static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
140
141 /* ------------------- Variable Fields ----------------------- */
142
143 /* Used to track progress of authentication; step numbers from RFC 2831 */
144 protected int step;
145
146 /* Used to get username/password, choose realm for client */
147 /* Used to obtain authorization, pw info, canonicalized authzid for server */
148 protected CallbackHandler cbh;
149
150 protected SecurityCtx secCtx;
151 protected byte[] H_A1; // component of response-value
152
153 protected byte[] nonce; // server generated nonce
154
155 /* Variables set when parsing directives in digest challenge/response. */
156 protected String negotiatedStrength;
157 protected String negotiatedCipher;
158 protected String negotiatedQop;
159 protected String negotiatedRealm;
160 protected boolean useUTF8 = false;
161 protected String encoding = "8859_1"; // default unless server specifies utf-8
162
163 protected String digestUri;
164 protected String authzid; // authzid or canonicalized authzid
165
166 /**
167 * Constucts an instance of DigestMD5Base. Calls super constructor
168 * to parse properties for mechanism.
169 *
170 * @param props A map of property/value pairs
171 * @param className name of class to use for logging
172 * @param firstStep number of first step in authentication state machine
173 * @param digestUri digestUri used in authentication
174 * @param cbh callback handler used to get info required for auth
175 *
176 * @throws SaslException If invalid value found in props.
177 */
178 protected DigestMD5Base(Map props, String className, int firstStep,
179 String digestUri, CallbackHandler cbh) throws SaslException {
180 super(props, className); // sets QOP, STENGTH and BUFFER_SIZE
181
182 step = firstStep;
183 this.digestUri = digestUri;
184 this.cbh = cbh;
185 }
186
187 /**
188 * Retrieves the SASL mechanism IANA name.
189 *
190 * @return The String "DIGEST-MD5"
191 */
192 public String getMechanismName() {
193 return "DIGEST-MD5";
194 }
195
196 /**
197 * Unwrap the incoming message using the wrap method of the secCtx object
198 * instance.
199 *
200 * @param incoming The byte array containing the incoming bytes.
201 * @param start The offset from which to read the byte array.
202 * @param len The number of bytes to read from the offset.
203 * @return The unwrapped message according to either the integrity or
204 * privacy quality-of-protection specifications.
205 * @throws SaslException if an error occurs when unwrapping the incoming
206 * message
207 */
208 public byte[] unwrap(byte[] incoming, int start, int len) throws SaslException {
209 if (!completed) {
210 throw new IllegalStateException(
211 "DIGEST-MD5 authentication not completed");
212 }
213
214 if (secCtx == null) {
215 throw new IllegalStateException(
216 "Neither integrity nor privacy was negotiated");
217 }
218
219 return (secCtx.unwrap(incoming, start, len));
220 }
221
222 /**
223 * Wrap outgoing bytes using the wrap method of the secCtx object
224 * instance.
225 *
226 * @param outgoing The byte array containing the outgoing bytes.
227 * @param start The offset from which to read the byte array.
228 * @param len The number of bytes to read from the offset.
229 * @return The wrapped message according to either the integrity or
230 * privacy quality-of-protection specifications.
231 * @throws SaslException if an error occurs when wrapping the outgoing
232 * message
233 */
234 public byte[] wrap(byte[] outgoing, int start, int len) throws SaslException {
235 if (!completed) {
236 throw new IllegalStateException(
237 "DIGEST-MD5 authentication not completed");
238 }
239
240 if (secCtx == null) {
241 throw new IllegalStateException(
242 "Neither integrity nor privacy was negotiated");
243 }
244
245 return (secCtx.wrap(outgoing, start, len));
246 }
247
248 public void dispose() throws SaslException {
249 if (secCtx != null) {
250 secCtx = null;
251 }
252 }
253
254 public Object getNegotiatedProperty(String propName) {
255 if (completed) {
256 if (propName.equals(Sasl.STRENGTH)) {
257 return negotiatedStrength;
258 } else {
259 return super.getNegotiatedProperty(propName);
260 }
261 } else {
262 throw new IllegalStateException(
263 "DIGEST-MD5 authentication not completed");
264 }
265 }
266
267 /* ----------------- Digest-MD5 utilities ---------------- */
268 /**
269 * Generate random-string used for digest-response.
270 * This method uses Random to get random bytes and then
271 * base64 encodes the bytes. Could also use binaryToHex() but this
272 * is slightly faster and a more compact representation of the same info.
273 * @return A non-null byte array containing the nonce value for the
274 * digest challenge or response.
275 * Could use SecureRandom to be more secure but it is very slow.
276 */
277
278 /** This array maps the characters to their 6 bit values */
279 private final static char pem_array[] = {
280 // 0 1 2 3 4 5 6 7
281 'A','B','C','D','E','F','G','H', // 0
282 'I','J','K','L','M','N','O','P', // 1
283 'Q','R','S','T','U','V','W','X', // 2
284 'Y','Z','a','b','c','d','e','f', // 3
285 'g','h','i','j','k','l','m','n', // 4
286 'o','p','q','r','s','t','u','v', // 5
287 'w','x','y','z','0','1','2','3', // 6
288 '4','5','6','7','8','9','+','/' // 7
289 };
290
291 // Make sure that this is a multiple of 3
292 private static final int RAW_NONCE_SIZE = 30;
293
294 // Base 64 encoding turns each 3 bytes into 4
295 private static final int ENCODED_NONCE_SIZE = RAW_NONCE_SIZE*4/3;
296
297 protected static final byte[] generateNonce() {
298
299 // SecureRandom random = new SecureRandom();
300 Random random = new Random();
301 byte[] randomData = new byte[RAW_NONCE_SIZE];
302 random.nextBytes(randomData);
303
304 byte[] nonce = new byte[ENCODED_NONCE_SIZE];
305
306 // Base64-encode bytes
307 byte a, b, c;
308 int j = 0;
309 for (int i = 0; i < randomData.length; i += 3) {
310 a = randomData[i];
311 b = randomData[i+1];
312 c = randomData[i+2];
313 nonce[j++] = (byte)(pem_array[(a >>> 2) & 0x3F]);
314 nonce[j++] = (byte)(pem_array[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]);
315 nonce[j++] = (byte)(pem_array[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]);
316 nonce[j++] = (byte)(pem_array[c & 0x3F]);
317 }
318
319 return nonce;
320
321 // %%% For testing using RFC 2831 example, uncomment the following 2 lines
322 // System.out.println("!!!Using RFC 2831's cnonce for testing!!!");
323 // return "OA6MHXh6VqTrRk".getBytes();
324 }
325
326 /**
327 * Checks if a byte[] contains characters that must be quoted
328 * and write the resulting, possibly escaped, characters to out.
329 */
330 protected static void writeQuotedStringValue(ByteArrayOutputStream out,
331 byte[] buf) {
332
333 int len = buf.length;
334 byte ch;
335 for (int i = 0; i < len; i++) {
336 ch = buf[i];
337 if (needEscape((char)ch)) {
338 out.write('\\');
339 }
340 out.write(ch);
341 }
342 }
343
344 // See Section 7.2 of RFC 2831; double-quote character is not allowed
345 // unless escaped; also escape the escape character and CTL chars except LWS
346 private static boolean needEscape(String str) {
347 int len = str.length();
348 for (int i = 0; i < len; i++) {
349 if (needEscape(str.charAt(i))) {
350 return true;
351 }
352 }
353 return false;
354 }
355
356 // Determines whether a character needs to be escaped in a quoted string
357 private static boolean needEscape(char ch) {
358 return ch == '"' || // escape char
359 ch == '\\' || // quote
360 ch == 127 || // DEL
361
362 // 0 <= ch <= 31 except CR, HT and LF
363 (ch >= 0 && ch <= 31 && ch != 13 && ch != 9 && ch != 10);
364 }
365
366 protected static String quotedStringValue(String str) {
367 if (needEscape(str)) {
368 int len = str.length();
369 char[] buf = new char[len+len];
370 int j = 0;
371 char ch;
372 for (int i = 0; i < len; i++) {
373 ch = str.charAt(i);
374 if (needEscape(ch)) {
375 buf[j++] = '\\';
376 }
377 buf[j++] = ch;
378 }
379 return new String(buf, 0, j);
380 } else {
381 return str;
382 }
383 }
384
385 /**
386 * Convert a byte array to hexadecimal string.
387 *
388 * @param a non-null byte array
389 * @return a non-null String contain the HEX value
390 */
391 protected byte[] binaryToHex(byte[] digest) throws
392 UnsupportedEncodingException {
393
394 StringBuffer digestString = new StringBuffer();
395
396 for (int i = 0; i < digest.length; i ++) {
397 if ((digest[i] & 0x000000ff) < 0x10) {
398 digestString.append("0"+
399 Integer.toHexString(digest[i] & 0x000000ff));
400 } else {
401 digestString.append(
402 Integer.toHexString(digest[i] & 0x000000ff));
403 }
404 }
405 return digestString.toString().getBytes(encoding);
406 }
407
408 /**
409 * Used to convert username-value, passwd or realm to 8859_1 encoding
410 * if all chars in string are within the 8859_1 (Latin 1) encoding range.
411 *
412 * @param a non-null String
413 * @return a non-nuill byte array containing the correct character encoding
414 * for username, paswd or realm.
415 */
416 protected byte[] stringToByte_8859_1(String str) throws SaslException {
417
418 char[] buffer = str.toCharArray();
419
420 try {
421 if (useUTF8) {
422 for( int i = 0; i< buffer.length; i++ ) {
423 if( buffer[i] > '\u00FF' ) {
424 return str.getBytes("UTF8");
425 }
426 }
427 }
428 return str.getBytes("8859_1");
429 } catch (UnsupportedEncodingException e) {
430 throw new SaslException(
431 "cannot encode string in UTF8 or 8859-1 (Latin-1)", e);
432 }
433 }
434
435 protected static byte[] getPlatformCiphers() {
436 byte[] ciphers = new byte[CIPHER_TOKENS.length];
437
438 for (int i = 0; i < JCE_CIPHER_NAME.length; i++) {
439 try {
440 // Checking whether the transformation is available from the
441 // current installed providers.
442 Cipher.getInstance(JCE_CIPHER_NAME[i]);
443
444 logger.log(Level.FINE, "DIGEST01:Platform supports {0}", JCE_CIPHER_NAME[i]);
445 ciphers[i] |= CIPHER_MASKS[i];
446 } catch (NoSuchAlgorithmException e) {
447 // no implementation found for requested algorithm.
448 } catch (NoSuchPaddingException e) {
449 // no implementation found for requested algorithm.
450 }
451 }
452
453 if (ciphers[RC4] != UNSET) {
454 ciphers[RC4_56] |= CIPHER_MASKS[RC4_56];
455 ciphers[RC4_40] |= CIPHER_MASKS[RC4_40];
456 }
457
458 return ciphers;
459 }
460
461 /**
462 * Assembles response-value for digest-response.
463 *
464 * @param authMethod "AUTHENTICATE" for client-generated response;
465 * "" for server-generated response
466 * @return A non-null byte array containing the repsonse-value.
467 * @throws NoSuchAlgorithmException if the platform does not have MD5
468 * digest support.
469 * @throws UnsupportedEncodingException if a an error occurs
470 * encoding a string into either Latin-1 or UTF-8.
471 * @throws IOException if an error occurs writing to the output
472 * byte array buffer.
473 */
474 protected byte[] generateResponseValue(
475 String authMethod,
476 String digestUriValue,
477 String qopValue,
478 String usernameValue,
479 String realmValue,
480 char[] passwdValue,
481 byte[] nonceValue,
482 byte[] cNonceValue,
483 int nonceCount,
484 byte[] authzidValue
485 ) throws NoSuchAlgorithmException,
486 UnsupportedEncodingException,
487 IOException {
488
489 MessageDigest md5 = MessageDigest.getInstance("MD5");
490 byte[] hexA1, hexA2;
491 ByteArrayOutputStream A2, beginA1, A1, KD;
492
493 // A2
494 // --
495 // A2 = { "AUTHENTICATE:", digest-uri-value,
496 // [:00000000000000000000000000000000] } // if auth-int or auth-conf
497 //
498 A2 = new ByteArrayOutputStream();
499 A2.write((authMethod + ":" + digestUriValue).getBytes(encoding));
500 if (qopValue.equals("auth-conf") ||
501 qopValue.equals("auth-int")) {
502
503 logger.log(Level.FINE, "DIGEST04:QOP: {0}", qopValue);
504
505 A2.write(SECURITY_LAYER_MARKER.getBytes(encoding));
506 }
507
508 if (logger.isLoggable(Level.FINE)) {
509 logger.log(Level.FINE, "DIGEST05:A2: {0}", A2.toString());
510 }
511
512 md5.update(A2.toByteArray());
513 byte[] digest = md5.digest();
514 hexA2 = binaryToHex(digest);
515
516 if (logger.isLoggable(Level.FINE)) {
517 logger.log(Level.FINE, "DIGEST06:HEX(H(A2)): {0}", new String(hexA2));
518 }
519
520 // A1
521 // --
522 // H(user-name : realm-value : passwd)
523 //
524 beginA1 = new ByteArrayOutputStream();
525 beginA1.write(stringToByte_8859_1(usernameValue));
526 beginA1.write(':');
527 // if no realm, realm will be an empty string
528 beginA1.write(stringToByte_8859_1(realmValue));
529 beginA1.write(':');
530 beginA1.write(stringToByte_8859_1(new String(passwdValue)));
531
532 md5.update(beginA1.toByteArray());
533 digest = md5.digest();
534
535 if (logger.isLoggable(Level.FINE)) {
536 logger.log(Level.FINE, "DIGEST07:H({0}) = {1}",
537 new Object[]{beginA1.toString(), new String(binaryToHex(digest))});
538 }
539
540 // A1
541 // --
542 // A1 = { H ( {user-name : realm-value : passwd } ),
543 // : nonce-value, : cnonce-value : authzid-value
544 //
545 A1 = new ByteArrayOutputStream();
546 A1.write(digest);
547 A1.write(':');
548 A1.write(nonceValue);
549 A1.write(':');
550 A1.write(cNonceValue);
551
552 if (authzidValue != null) {
553 A1.write(':');
554 A1.write(authzidValue);
555 }
556 md5.update(A1.toByteArray());
557 digest = md5.digest();
558 H_A1 = digest; // Record H(A1). Use for integrity & privacy.
559 hexA1 = binaryToHex(digest);
560
561 if (logger.isLoggable(Level.FINE)) {
562 logger.log(Level.FINE, "DIGEST08:H(A1) = {0}", new String(hexA1));
563 }
564
565 //
566 // H(k, : , s);
567 //
568 KD = new ByteArrayOutputStream();
569 KD.write(hexA1);
570 KD.write(':');
571 KD.write(nonceValue);
572 KD.write(':');
573 KD.write(nonceCountToHex(nonceCount).getBytes(encoding));
574 KD.write(':');
575 KD.write(cNonceValue);
576 KD.write(':');
577 KD.write(qopValue.getBytes(encoding));
578 KD.write(':');
579 KD.write(hexA2);
580
581 if (logger.isLoggable(Level.FINE)) {
582 logger.log(Level.FINE, "DIGEST09:KD: {0}", KD.toString());
583 }
584
585 md5.update(KD.toByteArray());
586 digest = md5.digest();
587
588 byte[] answer = binaryToHex(digest);
589
590 if (logger.isLoggable(Level.FINE)) {
591 logger.log(Level.FINE, "DIGEST10:response-value: {0}",
592 new String(answer));
593 }
594 return (answer);
595 }
596
597 /**
598 * Takes 'nonceCount' value and returns HEX value of the value.
599 *
600 * @return A non-null String representing the current NONCE-COUNT
601 */
602 protected static String nonceCountToHex(int count) {
603
604 String str = Integer.toHexString(count);
605 StringBuffer pad = new StringBuffer();
606
607 if (str.length() < 8) {
608 for (int i = 0; i < 8-str.length(); i ++) {
609 pad.append("0");
610 }
611 }
612
613 return pad.toString() + str;
614 }
615
616 /**
617 * Parses digest-challenge string, extracting each token
618 * and value(s)
619 *
620 * @param buf A non-null digest-challenge string.
621 * @param multipleAllowed true if multiple qop or realm or QOP directives
622 * are allowed.
623 * @throws SaslException if the buf cannot be parsed according to RFC 2831
624 */
625 protected static byte[][] parseDirectives(byte[] buf,
626 String[]keyTable, List<byte[]> realmChoices, int realmIndex) throws SaslException {
627
628 byte[][] valueTable = new byte[keyTable.length][];
629
630 ByteArrayOutputStream key = new ByteArrayOutputStream(10);
631 ByteArrayOutputStream value = new ByteArrayOutputStream(10);
632 boolean gettingKey = true;
633 boolean gettingQuotedValue = false;
634 boolean expectSeparator = false;
635 byte bch;
636
637 int i = skipLws(buf, 0);
638 while (i < buf.length) {
639 bch = buf[i];
640
641 if (gettingKey) {
642 if (bch == ',') {
643 if (key.size() != 0) {
644 throw new SaslException("Directive key contains a ',':" +
645 key);
646 }
647 // Empty element, skip separator and lws
648 i = skipLws(buf, i+1);
649
650 } else if (bch == '=') {
651 if (key.size() == 0) {
652 throw new SaslException("Empty directive key");
653 }
654 gettingKey = false; // Termination of key
655 i = skipLws(buf, i+1); // Skip to next nonwhitespace
656
657 // Check whether value is quoted
658 if (i < buf.length) {
659 if (buf[i] == '"') {
660 gettingQuotedValue = true;
661 ++i; // Skip quote
662 }
663 } else {
664 throw new SaslException(
665 "Valueless directive found: " + key.toString());
666 }
667 } else if (isLws(bch)) {
668 // LWS that occurs after key
669 i = skipLws(buf, i+1);
670
671 // Expecting '='
672 if (i < buf.length) {
673 if (buf[i] != '=') {
674 throw new SaslException("'=' expected after key: " +
675 key.toString());
676 }
677 } else {
678 throw new SaslException(
679 "'=' expected after key: " + key.toString());
680 }
681 } else {
682 key.write(bch); // Append to key
683 ++i; // Advance
684 }
685 } else if (gettingQuotedValue) {
686 // Getting a quoted value
687 if (bch == '\\') {
688 // quoted-pair = "\" CHAR ==> CHAR
689 ++i; // Skip escape
690 if (i < buf.length) {
691 value.write(buf[i]);
692 ++i; // Advance
693 } else {
694 // Trailing escape in a quoted value
695 throw new SaslException(
696 "Unmatched quote found for directive: "
697 + key.toString() + " with value: " + value.toString());
698 }
699 } else if (bch == '"') {
700 // closing quote
701 ++i; // Skip closing quote
702 gettingQuotedValue = false;
703 expectSeparator = true;
704 } else {
705 value.write(bch);
706 ++i; // Advance
707 }
708
709 } else if (isLws(bch) || bch == ',') {
710 // Value terminated
711
712 extractDirective(key.toString(), value.toByteArray(),
713 keyTable, valueTable, realmChoices, realmIndex);
714 key.reset();
715 value.reset();
716 gettingKey = true;
717 gettingQuotedValue = expectSeparator = false;
718 i = skipLws(buf, i+1); // Skip separator and LWS
719
720 } else if (expectSeparator) {
721 throw new SaslException(
722 "Expecting comma or linear whitespace after quoted string: \""
723 + value.toString() + "\"");
724 } else {
725 value.write(bch); // Unquoted value
726 ++i; // Advance
727 }
728 }
729
730 if (gettingQuotedValue) {
731 throw new SaslException(
732 "Unmatched quote found for directive: " + key.toString() +
733 " with value: " + value.toString());
734 }
735
736 // Get last pair
737 if (key.size() > 0) {
738 extractDirective(key.toString(), value.toByteArray(),
739 keyTable, valueTable, realmChoices, realmIndex);
740 }
741
742 return valueTable;
743 }
744
745 // Is character a linear white space?
746 // LWS = [CRLF] 1*( SP | HT )
747 // %%% Note that we're checking individual bytes instead of CRLF
748 private static boolean isLws(byte b) {
749 switch (b) {
750 case 13: // US-ASCII CR, carriage return
751 case 10: // US-ASCII LF, linefeed
752 case 32: // US-ASCII SP, space
753 case 9: // US-ASCII HT, horizontal-tab
754 return true;
755 }
756 return false;
757 }
758
759 // Skip all linear white spaces
760 private static int skipLws(byte[] buf, int start) {
761 int i;
762 for (i = start; i < buf.length; i++) {
763 if (!isLws(buf[i])) {
764 return i;
765 }
766 }
767 return i;
768 }
769
770 /**
771 * Processes directive/value pairs from the digest-challenge and
772 * fill out the challengeVal array.
773 *
774 * @param key A non-null String challenge token name.
775 * @param value A non-null String token value.
776 * @throws SaslException if a either the key or the value is null
777 */
778 private static void extractDirective(String key, byte[] value,
779 String[] keyTable, byte[][] valueTable,
780 List<byte[]> realmChoices, int realmIndex) throws SaslException {
781
782 for (int i = 0; i < keyTable.length; i++) {
783 if (key.equalsIgnoreCase(keyTable[i])) {
784 if (valueTable[i] == null) {
785 valueTable[i] = value;
786 if (logger.isLoggable(Level.FINE)) {
787 logger.log(Level.FINE, "DIGEST11:Directive {0} = {1}",
788 new Object[]{
789 keyTable[i],
790 new String(valueTable[i])});
791 }
792 } else if (realmChoices != null && i == realmIndex) {
793 // > 1 realm specified
794 if (realmChoices.size() == 0) {
795 realmChoices.add(valueTable[i]); // add existing one
796 }
797 realmChoices.add(value); // add new one
798 } else {
799 throw new SaslException(
800 "DIGEST-MD5: peer sent more than one " +
801 key + " directive: " + new String(value));
802 }
803
804 break; // end search
805 }
806 }
807 }
808
809
810 /**
811 * Implementation of the SecurityCtx interface allowing for messages
812 * between the client and server to be integrity checked. After a
813 * successful DIGEST-MD5 authentication, integtrity checking is invoked
814 * if the SASL QOP (quality-of-protection) is set to 'auth-int'.
815 * <p>
816 * Further details on the integrity-protection mechanism can be found
817 * at section 2.3 - Integrity protection in the
818 * <a href="http://www.ietf.org/rfc/rfc2831.txt">RFC2831</a> definition.
819 *
820 * @author Jonathan Bruce
821 */
822 class DigestIntegrity implements SecurityCtx {
823 /* Used for generating integrity keys - specified in RFC 2831*/
824 static final private String CLIENT_INT_MAGIC = "Digest session key to " +
825 "client-to-server signing key magic constant";
826 static final private String SVR_INT_MAGIC = "Digest session key to " +
827 "server-to-client signing key magic constant";
828
829 /* Key pairs for integrity checking */
830 protected byte[] myKi; // == Kic for client; == Kis for server
831 protected byte[] peerKi; // == Kis for client; == Kic for server
832
833 protected int mySeqNum = 0;
834 protected int peerSeqNum = 0;
835
836 // outgoing messageType and sequenceNum
837 protected final byte[] messageType = new byte[2];
838 protected final byte[] sequenceNum = new byte[4];
839
840 /**
841 * Initializes DigestIntegrity implementation of SecurityCtx to
842 * enable DIGEST-MD5 integrity checking.
843 *
844 * @throws SaslException if an error is encountered generating the
845 * key-pairs for integrity checking.
846 */
847 DigestIntegrity(boolean clientMode) throws SaslException {
848 /* Initialize magic strings */
849
850 try {
851 generateIntegrityKeyPair(clientMode);
852
853 } catch (UnsupportedEncodingException e) {
854 throw new SaslException(
855 "DIGEST-MD5: Error encoding strings into UTF-8", e);
856
857 } catch (IOException e) {
858 throw new SaslException("DIGEST-MD5: Error accessing buffers " +
859 "required to create integrity key pairs", e);
860
861 } catch (NoSuchAlgorithmException e) {
862 throw new SaslException("DIGEST-MD5: Unsupported digest " +
863 "algorithm used to create integrity key pairs", e);
864 }
865
866 /* Message type is a fixed value */
867 intToNetworkByteOrder(1, messageType, 0, 2);
868 }
869
870 /**
871 * Generate client-server, server-client key pairs for DIGEST-MD5
872 * integrity checking.
873 *
874 * @throws UnsupportedEncodingException if the UTF-8 encoding is not
875 * supported on the platform.
876 * @throws IOException if an error occurs when writing to or from the
877 * byte array output buffers.
878 * @throws NoSuchAlgorithmException if the MD5 message digest algorithm
879 * cannot loaded.
880 */
881 private void generateIntegrityKeyPair(boolean clientMode)
882 throws UnsupportedEncodingException, IOException,
883 NoSuchAlgorithmException {
884
885 byte[] cimagic = CLIENT_INT_MAGIC.getBytes(encoding);
886 byte[] simagic = SVR_INT_MAGIC.getBytes(encoding);
887
888 MessageDigest md5 = MessageDigest.getInstance("MD5");
889
890 // Both client-magic-keys and server-magic-keys are the same length
891 byte[] keyBuffer = new byte[H_A1.length + cimagic.length];
892
893 // Kic: Key for protecting msgs from client to server.
894 System.arraycopy(H_A1, 0, keyBuffer, 0, H_A1.length);
895 System.arraycopy(cimagic, 0, keyBuffer, H_A1.length, cimagic.length);
896 md5.update(keyBuffer);
897 byte[] Kic = md5.digest();
898
899 // Kis: Key for protecting msgs from server to client
900 // No need to recopy H_A1
901 System.arraycopy(simagic, 0, keyBuffer, H_A1.length, simagic.length);
902
903 md5.update(keyBuffer);
904 byte[] Kis = md5.digest();
905
906 if (logger.isLoggable(Level.FINER)) {
907 traceOutput(DI_CLASS_NAME, "generateIntegrityKeyPair",
908 "DIGEST12:Kic: ", Kic);
909 traceOutput(DI_CLASS_NAME, "generateIntegrityKeyPair",
910 "DIGEST13:Kis: ", Kis);
911 }
912
913 if (clientMode) {
914 myKi = Kic;
915 peerKi = Kis;
916 } else {
917 myKi = Kis;
918 peerKi = Kic;
919 }
920 }
921
922 /**
923 * Append MAC onto outgoing message.
924 *
925 * @param outgoing A non-null byte array containing the outgoing message.
926 * @param start The offset from which to read the byte array.
927 * @param len The non-zero number of bytes for be read from the offset.
928 * @return The message including the integrity MAC
929 * @throws SaslException if an error is encountered converting a string
930 * into a UTF-8 byte encoding, or if the MD5 message digest algorithm
931 * cannot be found or if there is an error writing to the byte array
932 * output buffers.
933 */
934 public byte[] wrap(byte[] outgoing, int start, int len)
935 throws SaslException {
936
937 if (len == 0) {
938 return EMPTY_BYTE_ARRAY;
939 }
940
941 /* wrapped = message, MAC, message type, sequence number */
942 byte[] wrapped = new byte[len+10+2+4];
943
944 /* Start with message itself */
945 System.arraycopy(outgoing, start, wrapped, 0, len);
946
947 incrementSeqNum();
948
949 /* Calculate MAC */
950 byte[] mac = getHMAC(myKi, sequenceNum, outgoing, start, len);
951
952 if (logger.isLoggable(Level.FINEST)) {
953 traceOutput(DI_CLASS_NAME, "wrap", "DIGEST14:outgoing: ",
954 outgoing, start, len);
955 traceOutput(DI_CLASS_NAME, "wrap", "DIGEST15:seqNum: ",
956 sequenceNum);
957 traceOutput(DI_CLASS_NAME, "wrap", "DIGEST16:MAC: ", mac);
958 }
959
960 /* Add MAC[0..9] to message */
961 System.arraycopy(mac, 0, wrapped, len, 10);
962
963 /* Add message type [0..1] */
964 System.arraycopy(messageType, 0, wrapped, len+10, 2);
965
966 /* Add sequence number [0..3] */
967 System.arraycopy(sequenceNum, 0, wrapped, len+12, 4);
968 if (logger.isLoggable(Level.FINEST)) {
969 traceOutput(DI_CLASS_NAME, "wrap", "DIGEST17:wrapped: ", wrapped);
970 }
971 return wrapped;
972 }
973
974 /**
975 * Return verified message without MAC - only if the received MAC
976 * and re-generated MAC are the same.
977 *
978 * @param incoming A non-null byte array containing the incoming
979 * message.
980 * @param start The offset from which to read the byte array.
981 * @param len The non-zero number of bytes to read from the offset
982 * position.
983 * @return The verified message or null if integrity checking fails.
984 * @throws SaslException if an error is encountered converting a string
985 * into a UTF-8 byte encoding, or if the MD5 message digest algorithm
986 * cannot be found or if there is an error writing to the byte array
987 * output buffers
988 */
989 public byte[] unwrap(byte[] incoming, int start, int len)
990 throws SaslException {
991
992 if (len == 0) {
993 return EMPTY_BYTE_ARRAY;
994 }
995
996 // shave off last 16 bytes of message
997 byte[] mac = new byte[10];
998 byte[] msg = new byte[len - 16];
999 byte[] msgType = new byte[2];
1000 byte[] seqNum = new byte[4];
1001
1002 /* Get Msg, MAC, msgType, sequenceNum */
1003 System.arraycopy(incoming, start, msg, 0, msg.length);
1004 System.arraycopy(incoming, start+msg.length, mac, 0, 10);
1005 System.arraycopy(incoming, start+msg.length+10, msgType, 0, 2);
1006 System.arraycopy(incoming, start+msg.length+12, seqNum, 0, 4);
1007
1008 /* Calculate MAC to ensure integrity */
1009 byte[] expectedMac = getHMAC(peerKi, seqNum, msg, 0, msg.length);
1010
1011 if (logger.isLoggable(Level.FINEST)) {
1012 traceOutput(DI_CLASS_NAME, "unwrap", "DIGEST18:incoming: ",
1013 msg);
1014 traceOutput(DI_CLASS_NAME, "unwrap", "DIGEST19:MAC: ",
1015 mac);
1016 traceOutput(DI_CLASS_NAME, "unwrap", "DIGEST20:messageType: ",
1017 msgType);
1018 traceOutput(DI_CLASS_NAME, "unwrap", "DIGEST21:sequenceNum: ",
1019 seqNum);
1020 traceOutput(DI_CLASS_NAME, "unwrap", "DIGEST22:expectedMAC: ",
1021 expectedMac);
1022 }
1023
1024 /* First, compare MAC's before updating any of our state */
1025 if (!Arrays.equals(mac, expectedMac)) {
1026 // Discard message and do not increment sequence number
1027 logger.log(Level.INFO, "DIGEST23:Unmatched MACs");
1028 return EMPTY_BYTE_ARRAY;
1029 }
1030
1031 /* Ensure server-sequence numbers are correct */
1032 if (peerSeqNum != networkByteOrderToInt(seqNum, 0, 4)) {
1033 throw new SaslException("DIGEST-MD5: Out of order " +
1034 "sequencing of messages from server. Got: " +
1035 networkByteOrderToInt(seqNum, 0, 4) +
1036 " Expected: " + peerSeqNum);
1037 }
1038
1039 if (!Arrays.equals(messageType, msgType)) {
1040 throw new SaslException("DIGEST-MD5: invalid message type: " +
1041 networkByteOrderToInt(msgType, 0, 2));
1042 }
1043
1044 // Increment sequence number and return message
1045 peerSeqNum++;
1046 return msg;
1047 }
1048
1049 /**
1050 * Generates MAC to be appended onto out-going messages.
1051 *
1052 * @param Ki A non-null byte array containing the key for the digest
1053 * @param SeqNum A non-null byte array contain the sequence number
1054 * @param msg The message to be digested
1055 * @param start The offset from which to read the msg byte array
1056 * @param len The non-zero number of bytes to be read from the offset
1057 * @return The MAC of a message.
1058 *
1059 * @throws SaslException if an error occurs when generating MAC.
1060 */
1061 protected byte[] getHMAC(byte[] Ki, byte[] seqnum, byte[] msg,
1062 int start, int len) throws SaslException {
1063
1064 byte[] seqAndMsg = new byte[4+len];
1065 System.arraycopy(seqnum, 0, seqAndMsg, 0, 4);
1066 System.arraycopy(msg, start, seqAndMsg, 4, len);
1067
1068 try {
1069 SecretKey keyKi = new SecretKeySpec(Ki, "HmacMD5");
1070 Mac m = Mac.getInstance("HmacMD5");
1071 m.init(keyKi);
1072 m.update(seqAndMsg);
1073 byte[] hMAC_MD5 = m.doFinal();
1074
1075 /* First 10 bytes of HMAC_MD5 digest */
1076 byte macBuffer[] = new byte[10];
1077 System.arraycopy(hMAC_MD5, 0, macBuffer, 0, 10);
1078
1079 return macBuffer;
1080 } catch (InvalidKeyException e) {
1081 throw new SaslException("DIGEST-MD5: Invalid bytes used for " +
1082 "key of HMAC-MD5 hash.", e);
1083 } catch (NoSuchAlgorithmException e) {
1084 throw new SaslException("DIGEST-MD5: Error creating " +
1085 "instance of MD5 digest algorithm", e);
1086 }
1087 }
1088
1089 /**
1090 * Increment own sequence number and set answer in NBO sequenceNum field.
1091 */
1092 protected void incrementSeqNum() {
1093 intToNetworkByteOrder(mySeqNum++, sequenceNum, 0, 4);
1094 }
1095 }
1096
1097 /**
1098 * Implementation of the SecurityCtx interface allowing for messages
1099 * between the client and server to be integrity checked and encrypted.
1100 * After a successful DIGEST-MD5 authentication, privacy is invoked if the
1101 * SASL QOP (quality-of-protection) is set to 'auth-conf'.
1102 * <p>
1103 * Further details on the integrity-protection mechanism can be found
1104 * at section 2.4 - Confidentiality protection in
1105 * <a href="http://www.ietf.org/rfc/rfc2831.txt">RFC2831</a> definition.
1106 *
1107 * @author Jonathan Bruce
1108 */
1109 final class DigestPrivacy extends DigestIntegrity implements SecurityCtx {
1110 /* Used for generating privacy keys - specified in RFC 2831 */
1111 static final private String CLIENT_CONF_MAGIC =
1112 "Digest H(A1) to client-to-server sealing key magic constant";
1113 static final private String SVR_CONF_MAGIC =
1114 "Digest H(A1) to server-to-client sealing key magic constant";
1115
1116 private Cipher encCipher;
1117 private Cipher decCipher;
1118
1119 /**
1120 * Initializes the cipher object instances for encryption and decryption.
1121 *
1122 * @throws SaslException if an error occurs with the Key
1123 * initialization, or a string cannot be encoded into a byte array
1124 * using the UTF-8 encoding, or an error occurs when writing to a
1125 * byte array output buffers or the mechanism cannot load the MD5
1126 * message digest algorithm or invalid initialization parameters are
1127 * passed to the cipher object instances.
1128 */
1129 DigestPrivacy(boolean clientMode) throws SaslException {
1130
1131 super(clientMode); // generate Kic, Kis keys for integrity-checking.
1132
1133 try {
1134 generatePrivacyKeyPair(clientMode);
1135
1136 } catch (SaslException e) {
1137 throw e;
1138
1139 } catch (UnsupportedEncodingException e) {
1140 throw new SaslException(
1141 "DIGEST-MD5: Error encoding string value into UTF-8", e);
1142
1143 } catch (IOException e) {
1144 throw new SaslException("DIGEST-MD5: Error accessing " +
1145 "buffers required to generate cipher keys", e);
1146 } catch (NoSuchAlgorithmException e) {
1147 throw new SaslException("DIGEST-MD5: Error creating " +
1148 "instance of required cipher or digest", e);
1149 }
1150 }
1151
1152 /**
1153 * Generates client-server and server-client keys to encrypt and
1154 * decrypt messages. Also generates IVs for DES ciphers.
1155 *
1156 * @throws IOException if an error occurs when writing to or from the
1157 * byte array output buffers.
1158 * @throws NoSuchAlgorithmException if the MD5 message digest algorithm
1159 * cannot loaded.
1160 * @throws UnsupportedEncodingException if an UTF-8 encoding is not
1161 * supported on the platform.
1162 * @throw SaslException if an error occurs initializing the keys and
1163 * IVs for the chosen cipher.
1164 */
1165 private void generatePrivacyKeyPair(boolean clientMode)
1166 throws IOException, UnsupportedEncodingException,
1167 NoSuchAlgorithmException, SaslException {
1168
1169 byte[] ccmagic = CLIENT_CONF_MAGIC.getBytes(encoding);
1170 byte[] scmagic = SVR_CONF_MAGIC.getBytes(encoding);
1171
1172 /* Kcc = MD5{H(A1)[0..n], "Digest ... client-to-server"} */
1173 MessageDigest md5 = MessageDigest.getInstance("MD5");
1174
1175 int n;
1176 if (negotiatedCipher.equals(CIPHER_TOKENS[RC4_40])) {
1177 n = 5; /* H(A1)[0..5] */
1178 } else if (negotiatedCipher.equals(CIPHER_TOKENS[RC4_56])) {
1179 n = 7; /* H(A1)[0..7] */
1180 } else { // des and 3des and rc4
1181 n = 16; /* H(A1)[0..16] */
1182 }
1183
1184 /* {H(A1)[0..n], "Digest ... client-to-server..."} */
1185 // Both client-magic-keys and server-magic-keys are the same length
1186 byte[] keyBuffer = new byte[n + ccmagic.length];
1187 System.arraycopy(H_A1, 0, keyBuffer, 0, n); // H(A1)[0..n]
1188
1189 /* Kcc: Key for encrypting messages from client->server */
1190 System.arraycopy(ccmagic, 0, keyBuffer, n, ccmagic.length);
1191 md5.update(keyBuffer);
1192 byte[] Kcc = md5.digest();
1193
1194 /* Kcs: Key for decrypting messages from server->client */
1195 // No need to copy H_A1 again since it hasn't changed
1196 System.arraycopy(scmagic, 0, keyBuffer, n, scmagic.length);
1197 md5.update(keyBuffer);
1198 byte[] Kcs = md5.digest();
1199
1200 if (logger.isLoggable(Level.FINER)) {
1201 traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
1202 "DIGEST24:Kcc: ", Kcc);
1203 traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
1204 "DIGEST25:Kcs: ", Kcs);
1205 }
1206
1207 byte[] myKc;
1208 byte[] peerKc;
1209
1210 if (clientMode) {
1211 myKc = Kcc;
1212 peerKc = Kcs;
1213 } else {
1214 myKc = Kcs;
1215 peerKc = Kcc;
1216 }
1217
1218 try {
1219 SecretKey encKey;
1220 SecretKey decKey;
1221
1222 /* Initialize cipher objects */
1223 if (negotiatedCipher.indexOf(CIPHER_TOKENS[RC4]) > -1) {
1224 encCipher = Cipher.getInstance("RC4");
1225 decCipher = Cipher.getInstance("RC4");
1226
1227 encKey = new SecretKeySpec(myKc, "RC4");
1228 decKey = new SecretKeySpec(peerKc, "RC4");
1229
1230 encCipher.init(Cipher.ENCRYPT_MODE, encKey);
1231 decCipher.init(Cipher.DECRYPT_MODE, decKey);
1232
1233 } else if ((negotiatedCipher.equals(CIPHER_TOKENS[DES])) ||
1234 (negotiatedCipher.equals(CIPHER_TOKENS[DES3]))) {
1235
1236 // DES or 3DES
1237 String cipherFullname, cipherShortname;
1238
1239 // Use "NoPadding" when specifying cipher names
1240 // RFC 2831 already defines padding rules for producing
1241 // 8-byte aligned blocks
1242 if (negotiatedCipher.equals(CIPHER_TOKENS[DES])) {
1243 cipherFullname = "DES/CBC/NoPadding";
1244 cipherShortname = "des";
1245 } else {
1246 /* 3DES */
1247 cipherFullname = "DESede/CBC/NoPadding";
1248 cipherShortname = "desede";
1249 }
1250
1251 encCipher = Cipher.getInstance(cipherFullname);
1252 decCipher = Cipher.getInstance(cipherFullname);
1253
1254 encKey = makeDesKeys(myKc, cipherShortname);
1255 decKey = makeDesKeys(peerKc, cipherShortname);
1256
1257 // Set up the DES IV, which is the last 8 bytes of Kcc/Kcs
1258 IvParameterSpec encIv = new IvParameterSpec(myKc, 8, 8);
1259 IvParameterSpec decIv = new IvParameterSpec(peerKc, 8, 8);
1260
1261 // Initialize cipher objects
1262 encCipher.init(Cipher.ENCRYPT_MODE, encKey, encIv);
1263 decCipher.init(Cipher.DECRYPT_MODE, decKey, decIv);
1264
1265 if (logger.isLoggable(Level.FINER)) {
1266 traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
1267 "DIGEST26:" + negotiatedCipher + " IVcc: ",
1268 encIv.getIV());
1269 traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
1270 "DIGEST27:" + negotiatedCipher + " IVcs: ",
1271 decIv.getIV());
1272 traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
1273 "DIGEST28:" + negotiatedCipher + " encryption key: ",
1274 encKey.getEncoded());
1275 traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
1276 "DIGEST29:" + negotiatedCipher + " decryption key: ",
1277 decKey.getEncoded());
1278 }
1279 }
1280 } catch (InvalidKeySpecException e) {
1281 throw new SaslException("DIGEST-MD5: Unsupported key " +
1282 "specification used.", e);
1283 } catch (InvalidAlgorithmParameterException e) {
1284 throw new SaslException("DIGEST-MD5: Invalid cipher " +
1285 "algorithem parameter used to create cipher instance", e);
1286 } catch (NoSuchPaddingException e) {
1287 throw new SaslException("DIGEST-MD5: Unsupported " +
1288 "padding used for chosen cipher", e);
1289 } catch (InvalidKeyException e) {
1290 throw new SaslException("DIGEST-MD5: Invalid data " +
1291 "used to initialize keys", e);
1292 }
1293 }
1294
1295 // -------------------------------------------------------------------
1296
1297 /**
1298 * Encrypt out-going message.
1299 *
1300 * @param outgoing A non-null byte array containing the outgoing message.
1301 * @param start The offset from which to read the byte array.
1302 * @param len The non-zero number of bytes to be read from the offset.
1303 * @return The encrypted message.
1304 *
1305 * @throws SaslException if an error occurs when writing to or from the
1306 * byte array output buffers or if the MD5 message digest algorithm
1307 * cannot loaded or if an UTF-8 encoding is not supported on the
1308 * platform.
1309 */
1310 public byte[] wrap(byte[] outgoing, int start, int len)
1311 throws SaslException {
1312
1313 if (len == 0) {
1314 return EMPTY_BYTE_ARRAY;
1315 }
1316
1317 /* HMAC(Ki, {SeqNum, msg})[0..9] */
1318 incrementSeqNum();
1319 byte[] mac = getHMAC(myKi, sequenceNum, outgoing, start, len);
1320
1321 if (logger.isLoggable(Level.FINEST)) {
1322 traceOutput(DP_CLASS_NAME, "wrap", "DIGEST30:Outgoing: ",
1323 outgoing, start, len);
1324 traceOutput(DP_CLASS_NAME, "wrap", "seqNum: ",
1325 sequenceNum);
1326 traceOutput(DP_CLASS_NAME, "wrap", "MAC: ", mac);
1327 }
1328
1329 // Calculate padding
1330 int bs = encCipher.getBlockSize();
1331 byte[] padding;
1332 if (bs > 1 ) {
1333 int pad = bs - ((len + 10) % bs); // add 10 for HMAC[0..9]
1334 padding = new byte[pad];
1335 for (int i=0; i < pad; i++) {
1336 padding[i] = (byte)pad;
1337 }
1338 } else {
1339 padding = EMPTY_BYTE_ARRAY;
1340 }
1341
1342 byte[] toBeEncrypted = new byte[len+padding.length+10];
1343
1344 /* {msg, pad, HMAC(Ki, {SeqNum, msg}[0..9])} */
1345 System.arraycopy(outgoing, start, toBeEncrypted, 0, len);
1346 System.arraycopy(padding, 0, toBeEncrypted, len, padding.length);
1347 System.arraycopy(mac, 0, toBeEncrypted, len+padding.length, 10);
1348
1349 if (logger.isLoggable(Level.FINEST)) {
1350 traceOutput(DP_CLASS_NAME, "wrap",
1351 "DIGEST31:{msg, pad, KicMAC}: ", toBeEncrypted);
1352 }
1353
1354 /* CIPHER(Kc, {msg, pad, HMAC(Ki, {SeqNum, msg}[0..9])}) */
1355 byte[] cipherBlock;
1356 try {
1357 // Do CBC (chaining) across packets
1358 cipherBlock = encCipher.update(toBeEncrypted);
1359
1360 if (cipherBlock == null) {
1361 // update() can return null
1362 throw new IllegalBlockSizeException(""+toBeEncrypted.length);
1363 }
1364 } catch (IllegalBlockSizeException e) {
1365 throw new SaslException(
1366 "DIGEST-MD5: Invalid block size for cipher", e);
1367 }
1368
1369 byte[] wrapped = new byte[cipherBlock.length+2+4];
1370 System.arraycopy(cipherBlock, 0, wrapped, 0, cipherBlock.length);
1371 System.arraycopy(messageType, 0, wrapped, cipherBlock.length, 2);
1372 System.arraycopy(sequenceNum, 0, wrapped, cipherBlock.length+2, 4);
1373
1374 if (logger.isLoggable(Level.FINEST)) {
1375 traceOutput(DP_CLASS_NAME, "wrap", "DIGEST32:Wrapped: ", wrapped);
1376 }
1377
1378 return wrapped;
1379 }
1380
1381 /*
1382 * Decrypt incoming messages and verify their integrity.
1383 *
1384 * @param incoming A non-null byte array containing the incoming
1385 * encrypted message.
1386 * @param start The offset from which to read the byte array.
1387 * @param len The non-zero number of bytes to read from the offset
1388 * position.
1389 * @return The decrypted, verified message or null if integrity
1390 * checking
1391 * fails.
1392 * @throws SaslException if there are the SASL buffer is empty or if
1393 * if an error occurs reading the SASL buffer.
1394 */
1395 public byte[] unwrap(byte[] incoming, int start, int len)
1396 throws SaslException {
1397
1398 if (len == 0) {
1399 return EMPTY_BYTE_ARRAY;
1400 }
1401
1402 byte[] encryptedMsg = new byte[len - 6];
1403 byte[] msgType = new byte[2];
1404 byte[] seqNum = new byte[4];
1405
1406 /* Get cipherMsg; msgType; sequenceNum */
1407 System.arraycopy(incoming, start,
1408 encryptedMsg, 0, encryptedMsg.length);
1409 System.arraycopy(incoming, start+encryptedMsg.length,
1410 msgType, 0, 2);
1411 System.arraycopy(incoming, start+encryptedMsg.length+2,
1412 seqNum, 0, 4);
1413
1414 if (logger.isLoggable(Level.FINEST)) {
1415 logger.log(Level.FINEST,
1416 "DIGEST33:Expecting sequence num: {0}",
1417 new Integer(peerSeqNum));
1418 traceOutput(DP_CLASS_NAME, "unwrap", "DIGEST34:incoming: ",
1419 encryptedMsg);
1420 }
1421
1422 // Decrypt message
1423 /* CIPHER(Kc, {msg, pad, HMAC(Ki, {SeqNum, msg}[0..9])}) */
1424 byte[] decryptedMsg;
1425
1426 try {
1427 // Do CBC (chaining) across packets
1428 decryptedMsg = decCipher.update(encryptedMsg);
1429
1430 if (decryptedMsg == null) {
1431 // update() can return null
1432 throw new IllegalBlockSizeException(""+encryptedMsg.length);
1433 }
1434 } catch (IllegalBlockSizeException e) {
1435 throw new SaslException("DIGEST-MD5: Illegal block " +
1436 "sizes used with chosen cipher", e);
1437 }
1438
1439 byte[] msgWithPadding = new byte[decryptedMsg.length - 10];
1440 byte[] mac = new byte[10];
1441
1442 System.arraycopy(decryptedMsg, 0,
1443 msgWithPadding, 0, msgWithPadding.length);
1444 System.arraycopy(decryptedMsg, msgWithPadding.length,
1445 mac, 0, 10);
1446
1447 if (logger.isLoggable(Level.FINEST)) {
1448 traceOutput(DP_CLASS_NAME, "unwrap",
1449 "DIGEST35:Unwrapped (w/padding): ", msgWithPadding);
1450 traceOutput(DP_CLASS_NAME, "unwrap", "DIGEST36:MAC: ", mac);
1451 traceOutput(DP_CLASS_NAME, "unwrap", "DIGEST37:messageType: ",
1452 msgType);
1453 traceOutput(DP_CLASS_NAME, "unwrap", "DIGEST38:sequenceNum: ",
1454 seqNum);
1455 }
1456
1457 int msgLength = msgWithPadding.length;
1458 int blockSize = decCipher.getBlockSize();
1459 if (blockSize > 1) {
1460 // get value of last octet of the byte array
1461 msgLength -= (int)msgWithPadding[msgWithPadding.length - 1];
1462 if (msgLength < 0) {
1463 // Discard message and do not increment sequence number
1464 if (logger.isLoggable(Level.INFO)) {
1465 logger.log(Level.INFO,
1466 "DIGEST39:Incorrect padding: {0}",
1467 new Byte(msgWithPadding[msgWithPadding.length - 1]));
1468 }
1469 return EMPTY_BYTE_ARRAY;
1470 }
1471 }
1472
1473 /* Re-calculate MAC to ensure integrity */
1474 byte[] expectedMac = getHMAC(peerKi, seqNum, msgWithPadding,
1475 0, msgLength);
1476
1477 if (logger.isLoggable(Level.FINEST)) {
1478 traceOutput(DP_CLASS_NAME, "unwrap", "DIGEST40:KisMAC: ",
1479 expectedMac);
1480 }
1481
1482 // First, compare MACs before updating state
1483 if (!Arrays.equals(mac, expectedMac)) {
1484 // Discard message and do not increment sequence number
1485 logger.log(Level.INFO, "DIGEST41:Unmatched MACs");
1486 return EMPTY_BYTE_ARRAY;
1487 }
1488
1489 /* Ensure sequence number is correct */
1490 if (peerSeqNum != networkByteOrderToInt(seqNum, 0, 4)) {
1491 throw new SaslException("DIGEST-MD5: Out of order " +
1492 "sequencing of messages from server. Got: " +
1493 networkByteOrderToInt(seqNum, 0, 4) + " Expected: " +
1494 peerSeqNum);
1495 }
1496
1497 /* Check message type */
1498 if (!Arrays.equals(messageType, msgType)) {
1499 throw new SaslException("DIGEST-MD5: invalid message type: " +
1500 networkByteOrderToInt(msgType, 0, 2));
1501 }
1502
1503 // Increment sequence number and return message
1504 peerSeqNum++;
1505
1506 if (msgLength == msgWithPadding.length) {
1507 return msgWithPadding; // no padding
1508 } else {
1509 // Get a copy of the message without padding
1510 byte[] clearMsg = new byte[msgLength];
1511 System.arraycopy(msgWithPadding, 0, clearMsg, 0, msgLength);
1512 return clearMsg;
1513 }
1514 }
1515 }
1516
1517 // ---------------- DES and 3 DES key manipulation routines
1518
1519 /* Mask used to check for parity adjustment */
1520 private static final byte[] PARITY_BIT_MASK = {
1521 (byte)0x80, (byte)0x40, (byte)0x20, (byte)0x10,
1522 (byte)0x08, (byte)0x04, (byte)0x02
1523 };
1524 private static final BigInteger MASK = new BigInteger("7f", 16);
1525
1526 /**
1527 * Sets the parity bit (0th bit) in each byte so that each byte
1528 * contains an odd number of 1's.
1529 */
1530 private static void setParityBit(byte[] key) {
1531 for (int i = 0; i < key.length; i++) {
1532 int bitCount = 0;
1533 for (int maskIndex = 0;
1534 maskIndex < PARITY_BIT_MASK.length; maskIndex++) {
1535 if ((key[i] & PARITY_BIT_MASK[maskIndex])
1536 == PARITY_BIT_MASK[maskIndex]) {
1537 bitCount++;
1538 }
1539 }
1540 if ((bitCount & 0x01) == 1) {
1541 // Odd number of 1 bits in the top 7 bits. Set parity bit to 0
1542 key[i] = (byte)(key[i] & (byte)0xfe);
1543 } else {
1544 // Even number of 1 bits in the top 7 bits. Set parity bit to 1
1545 key[i] = (byte)(key[i] | 1);
1546 }
1547 }
1548 }
1549
1550 /**
1551 * Expands a 7-byte array into an 8-byte array that contains parity bits
1552 * The binary format of a cryptographic key is:
1553 * (B1,B2,...,B7,P1,B8,...B14,P2,B15,...,B49,P7,B50,...,B56,P8)
1554 * where (B1,B2,...,B56) are the independent bits of a DES key and
1555 * (PI,P2,...,P8) are reserved for parity bits computed on the preceding
1556 * seven independent bits and set so that the parity of the octet is odd,
1557 * i.e., there is an odd number of "1" bits in the octet.
1558 */
1559 private static byte[] addDesParity(byte[] input, int offset, int len) {
1560 if (len != 7)
1561 throw new IllegalArgumentException(
1562 "Invalid length of DES Key Value:" + len);
1563
1564 byte[] raw = new byte[7];
1565 System.arraycopy(input, offset, raw, 0, len);
1566
1567 byte[] result = new byte[8];
1568 BigInteger in = new BigInteger(raw);
1569
1570 // Shift 7 bits each time into a byte
1571 for (int i=result.length-1; i>=0; i--) {
1572 result[i] = in.and(MASK).toByteArray()[0];
1573 result[i] <<= 1; // make room for parity bit
1574 in = in.shiftRight(7);
1575 }
1576 setParityBit(result);
1577 return result;
1578 }
1579
1580 /**
1581 * Create parity-adjusted keys suitable for DES / DESede encryption.
1582 *
1583 * @param input A non-null byte array containing key material for
1584 * DES / DESede.
1585 * @param desStrength A string specifying eithe a DES or a DESede key.
1586 * @return SecretKey An instance of either DESKeySpec or DESedeKeySpec.
1587 *
1588 * @throws NoSuchAlgorithmException if the either the DES or DESede
1589 * algorithms cannote be lodaed by JCE.
1590 * @throws InvalidKeyException if an invalid array of bytes is used
1591 * as a key for DES or DESede.
1592 * @throws InvalidKeySpecException in an invalid parameter is passed
1593 * to either te DESKeySpec of the DESedeKeySpec constructors.
1594 */
1595 private static SecretKey makeDesKeys(byte[] input, String desStrength)
1596 throws NoSuchAlgorithmException, InvalidKeyException,
1597 InvalidKeySpecException {
1598
1599 // Generate first subkey using first 7 bytes
1600 byte[] subkey1 = addDesParity(input, 0, 7);
1601
1602 KeySpec spec = null;
1603 SecretKeyFactory desFactory =
1604 SecretKeyFactory.getInstance(desStrength);
1605
1606 if (desStrength.equals("des")) {
1607 spec = new DESKeySpec(subkey1, 0);
1608 if (logger.isLoggable(Level.FINEST)) {
1609 traceOutput(DP_CLASS_NAME, "makeDesKeys",
1610 "DIGEST42:DES key input: ", input);
1611 traceOutput(DP_CLASS_NAME, "makeDesKeys",
1612 "DIGEST43:DES key parity-adjusted: ", subkey1);
1613 traceOutput(DP_CLASS_NAME, "makeDesKeys",
1614 "DIGEST44:DES key material: ", ((DESKeySpec)spec).getKey());
1615 logger.log(Level.FINEST, "DIGEST45: is parity-adjusted? {0}",
1616 Boolean.valueOf(DESKeySpec.isParityAdjusted(subkey1, 0)));
1617 }
1618
1619 } else if (desStrength.equals("desede")) {
1620
1621 // Generate second subkey using second 7 bytes
1622 byte[] subkey2 = addDesParity(input, 7, 7);
1623
1624 // Construct 24-byte encryption-decryption-encryption sequence
1625 byte[] ede = new byte[subkey1.length*2+subkey2.length];
1626 System.arraycopy(subkey1, 0, ede, 0, subkey1.length);
1627 System.arraycopy(subkey2, 0, ede, subkey1.length, subkey2.length);
1628 System.arraycopy(subkey1, 0, ede, subkey1.length+subkey2.length,
1629 subkey1.length);
1630
1631 spec = new DESedeKeySpec(ede, 0);
1632 if (logger.isLoggable(Level.FINEST)) {
1633 traceOutput(DP_CLASS_NAME, "makeDesKeys",
1634 "DIGEST46:3DES key input: ", input);
1635 traceOutput(DP_CLASS_NAME, "makeDesKeys",
1636 "DIGEST47:3DES key ede: ", ede);
1637 traceOutput(DP_CLASS_NAME, "makeDesKeys",
1638 "DIGEST48:3DES key material: ",
1639 ((DESedeKeySpec)spec).getKey());
1640 logger.log(Level.FINEST, "DIGEST49: is parity-adjusted? ",
1641 Boolean.valueOf(DESedeKeySpec.isParityAdjusted(ede, 0)));
1642 }
1643 } else {
1644 throw new IllegalArgumentException("Invalid DES strength:" +
1645 desStrength);
1646 }
1647 return desFactory.generateSecret(spec);
1648 }
1649}