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